Phase 1 of TODO-agent-2: pure logic plus the tables it writes to. - domain/workforce/pipeline.py: Stage/PipelineKey/Outcome, the four pipelines, TRANSITIONS, next_stage, STAGE_ROLE (role+mode), resolve_pipeline with the no-reviewer fallback, task-kind defaults, stage -> coarse tasks.status, and the TaskPipelineState record (with the optimistic version). - application/engine/protocol.py: the [[TAG attr="v"]]body[[/TAG]] parser and its self-closing form, the per-role/mode allow-list, literal bodies (a bracket inside a body is not a tag), typed readers (ASSIGN/CREATE_TASK/ team changes/KNOWLEDGE/EDIT_FILE blocks) and the per-reply limits (3 tasks, 3 team changes, 3 knowledge items). - application/engine/state.py: advance() with the optimistic compare-and-set and park_dependents() so a failed dependency never deadlocks its children. - db/models_engine.py + migration a2b3c4d5e6f2: task_pipeline_state, employee_profiles, engine_turns, gate_results as side tables (tasks and employees stay locked). Linear head; alembic check reports no drift. - repositories/engine.py + 58 new tests (test_engine_protocol, test_engine_pipeline) covering every pipeline's happy path, illegal transitions, exactly-one-winner concurrent advance, dependency parking and all parser edge cases. Suite: 594 passed, ruff clean, mypy clean.
9.0 KiB
Agent 2 — Phase 1 report: engine core (protocol, state machine, pipelines)
Date: 2026-10-14 · Branch: dev (this repo's integration branch) · Status: done, all checks green
Environment note: the TODO file assumes the ext4 worktree (
/home/mamad/work/smart-org-wt/agent2, branchagent2/ai-workforce). This checkout is thedevrepo itself and already contains the other agents' work (org wizard, invites, delivery infra), so the engine is being implemented here directly ondevat the user's instruction. No locked file was touched; noCHANGE REQUESTis needed so far.
What was done
1.1 app/domain/workforce/pipeline.py (new, pure)
Stage(13 values: the eight pipeline stages plus the side statespending_user_decision,error_review,paused,cancelled,failed).PIPELINES:plan_code_test(default),plan_code_review_test,code_test,analysis_only(no worktree, no merge). Each is the ordered tuple of stages ending incompleted.TRANSITIONS— every legal stage move, with the reasoning in comments: a paused or parked task resumes through triage (nopaused_fromcolumn exists, and re-triage is the safe route);merge_requestis platform-executed and nobody acts in it; terminal stages never move.next_stage(pipeline, current, outcome)withOutcome(ok,revise,reject,ask_user,error,resolve).okwalks the pipeline list one step; the others have fixed targets — work sent back always returns to the developer.STAGE_ROLE→(role, mode)orNonefor stages with no AI turn. Manager modes aretriage/final/error(pluschat), every other role iswork.resolve_pipeline(manager_choice, task_kind, app_default, has_active_reviewer): priority manager > kind > app default >plan_code_test; a review pipeline without an active reviewer falls back toplan_code_test.TASK_KIND_DEFAULTS: feature →plan_code_test/medium, bugfix →code_test/low, refactor →plan_code_review_test/high, docs →code_test/low, analysis →analysis_only/low.STAGE_STATUS: stage → coarsetasks.status(new/running/waiting_approval/done/failed/cancelled) so existing APIs and the old worker keep working against one truthful column.TaskPipelineStatedataclass: pipeline, kind, risk, stage, cycle/max_cycles, branch,last_diff_hash/same_diff_count,retry_count,waiver_reason, parent/depends,version(optimistic lock).
1.2 app/application/engine/protocol.py (new)
- Parser for
[[TAG attr="v" ...]]body[[/TAG]]and[[TAG attr="v"/]], tolerant of whitespace, single- or double-quoted values. - A body is literal: an outer tag wins and the scanner moves past its close, so brackets
inside a body (a source file full of
[[) are never re-parsed. TagNameenum for all 28 tags;ALLOWED_TAGSis the per-role/mode matrix from the TODO (manager triage/final/error/chat, planner, developer, reviewer, tester) plus the six tool tags granted to working roles only — the manager never edits code.- Unknown tags, disallowed tags, stray closings, unclosed tags and malformed attribute fragments are dropped and reported as one error line each, in order, for the model to read next turn.
BODY_TAGS: only tags that carry a body must be closed; a forgotten/on a bodyless tag ([[RUN cmd="pytest"]]) is read as self-closing instead of failing the reply.- Limits: max 3
CREATE_TASK, max 3 team changes across HIRE+UPDATE_EMPLOYEE+FIRE together, max 3KNOWLEDGEper reply. Extras are dropped and reported. - Typed readers:
AssignTag,TaskDraft,TeamChange,KnowledgeItem,EditBlock(<<<OLD … === … NEW>>>), plusedit_blocks_ofwhich refuses a malformed block rather than half-applying it.ASSIGNrefuses an unknown pipeline name.
1.3 app/infrastructure/db/models_engine.py (stub filled) + migration a2b3c4d5e6f2
Four tables, all 1:1/side tables — no column was added to the locked tasks / employees:
task_pipeline_state(PKtask_id), with the denormalisedorganization_idso the scheduler's hot query stays single-table.employee_profiles(PKemployee_id):ai_model_id,effort, roles CSV,hired_by,busy_task_id.engine_turns: id, task/run/employee, role, mode, stage,prompt_chars,reply_excerpt(Text, bounded on write), tags JSON, tool_calls, ok, error_kind, started/finished.gate_results: id, task, gate, ok, detail (bounded), created.
Migration alembic/versions/a2b3c4d5e6f2_ai_workforce.py:
alembic upgrade head applies cleanly and alembic check reports no drift between the models
and the migration chain.
Deviation (documented in the migration docstring): the TODO pins
down_revision = "a0f0e1d2c3b4", which was right while each agent had its own branch. On the
merged dev chain the heads already form a line (a3b4c5d6e7f4 → a1b2c3d4e5f1 →
d5e6f7a8b9c0), so this migration chains after d5e6f7a8b9c0. Result: one linear head
(a2b3c4d5e6f2), no merge revision needed.
1.4 app/application/engine/state.py (new)
advance(states, task_id, expected_from, to): checks legality in the domain first, then the repository's compare-and-set (WHERE task_id AND stage AND version) performs the move. A lost race raisesConflictError("این کار هماکنون توسط موتور دیگری در حرکت است"), an unknown task raisesNotFoundError.park_dependents(states, task_id): a failed or cancelled dependency moves every dependent topending_user_decision(agybot bug M1). The function refuses to act on a healthy task — the guard is in the function, not in the caller — and leaves dependents that already wait on a human alone. A lost race on a dependent is skipped, not fatal.app/infrastructure/repositories/engine.py(new):PipelineStateRepository(advance/patch/ list_at_stages/list_dependents),EmployeeProfileRepository,EngineTurnRepository,GateResultRepository.advance/patchverify the version actually moved rather than trustingrowcount, so the check holds on SQLite and PostgreSQL alike.
1.5 Tests
tests/test_engine_pipeline.py(24 tests): all four pipelines walk manager review → completed step by step, each step also legal; sent-back work returns to the developer; illegal transitions refused; the stage/role/mode map; stage → coarse status; pipeline resolution priority and the reviewer fallback; plus DB-backed tests for the version bump, the illegal move, exactly one winner in a concurrent advance, the 404, the dependency-parking rule (and the healthy-task guard), optimisticpatch, cascade deletes and the employee profile round trip.tests/test_engine_protocol.py(34 tests): paired/self-closing parsing, whitespace tolerance, nested brackets staying literal, tag order, unknown/disallowed/stray/unclosed/malformed rejections, the full per-role matrix, toolkit-for-working-roles-only, typed readers (assign/task draft/team changes/knowledge/edit blocks) and all three limits.
Checks (PLAN-v2 §7.4)
cd apps/api && .venv/bin/python -m pytest -q → 594 passed, 1 skipped (whole suite; 58 new)
cd apps/api && .venv/bin/ruff check . → All checks passed!
cd apps/api && .venv/bin/mypy app → Success: no issues found in 144 source files
alembic upgrade head (scratch sqlite) → applies through a2b3c4d5e6f2, single head
alembic check (scratch sqlite) → No new upgrade operations detected.
The web suite was not touched (no UI in Phase 1).
Deviations & notes for the reviewer
- Implemented on
devinstead of anagent2/ai-workforceworktree (user instruction; the worktree path in the TODO is a Linux path unavailable here). - Migration
down_revisionchained to the merged head (above) to keep one linear chain. task_pipeline_state.organization_idadded beyond the TODO's column list: it is denormalised from the task so the scheduler never joins back, and it keeps the tenant filter single-table.resolve_pipelineapplies the no-reviewer fallback to an explicit manager choice too. A review stage nobody can act on is a dead end; the manager who wants a reviewer should hire one.- Team-change limit counts HIRE + UPDATE_EMPLOYEE + FIRE together (one budget per turn), which is what "max 3 team changes per turn" means.
- New repository module
app/infrastructure/repositories/engine.py(the engine columns live besidemodels_engine.py);repositories/workforce.pywas left untouched.
Open questions / CHANGE REQUESTs
- None for Phase 1. Expected for later phases: org automation policy behind
ModelGrantPort(TODO 4.1 says to assume enabled until then) and the worker hook for memory expiry.
Next
Phase 2 — engine/prompt.py, engine/tools.py (sandbox execution, git refusal, path safety),
engine/runner.py (run_turn with the scripted fake model, error classification, usage events)
and tests/test_engine_runner.py + tests/engine_fakes.py.