Bug A from the 2026-05-09 smoke run. be-dev-1 called i_will_work_on
without `plan` on a pending task; claim() ran first (transitioned to
`claimed`), then the plan check failed → tracing_gap. The natural
retry path then dispatched to `_i_will_work_on_claimed`, which had no
plan-recovery logic and called start() against a still-plan-less task,
returning `start failed` forever. The dev kept looping; the parent
escalated up; the whole slice ended `blocked`.
Two changes (Task-5 atomicity pattern applied to i_will_work_on):
1. `_i_will_work_on_pending`: move the plan precondition BEFORE
`claim()`. A missing-plan first call now returns tracing_gap with
the task untouched in `pending`, so the agent's retry-with-plan
succeeds cleanly.
2. `_i_will_work_on_claimed`: now accepts `plan` and calls set_plan
before start() if the task has no plan yet. Recovery path for any
already-stuck task (e.g. left over from the earlier image, or an
orchestrator restart that left a partial claim).
Also wires `plan` through the dispatcher to the claimed branch.
Tests: 3137 passing (3135 + 2 regression tests pinning the atomic
invariant), 100% coverage, ruff clean.
Pre-fix, submit_for_qa opened a PR (side effect) and returned OK with
next='call i_am_done' — agents read the verb name, assumed they were
done with QA handoff, never called i_am_done, and PRs ended up
orphaned (PR #12 in the 2026-05-08 trace).
Two changes:
1. Rename submit_for_qa -> open_pr so the verb name matches the
semantic. The PR opens here; the actual QA handoff happens at
i_am_done. Renamed across:
- choreographer/_impl.py (method)
- mcp/flow_server.py (tool registration + _TOOLS dict)
- api/routes/v2/flow_dev.py (route + handler)
- api/schemas/v2/flow.py (OpenPrRequest)
- services/gateway/verb_gates.py (_STATE_VERBS)
- services/gateway/role_config.py (developer flow manifest)
- services/gateway/content_actions.py (commit-success next= hint)
- agent_sdk/server.py (post-tool guidance map)
- runtime/orchestrator.py (developer prompt)
- agents/prompts/{base,roles/developer,_generated/*}.md
- tests/unit/gateway/test_submit_for_qa.py -> test_open_pr.py
- tests/unit/api/routes/v2/test_flow_dev.py
- tests/unit/gateway/test_verb_gates.py
- tests/unit/api/test_correlation_id.py
- tests/unit/mcp_servers/test_flow_server.py
- tests/integration/test_full_lifecycle_real_db.py
2. New regression test (test_open_pr_does_not_create_pr_if_no_commits)
pins the atomic invariant: preconditions (assignee, commits,
no-prior-PR) must be checked BEFORE git.create_pr/push_branch run.
Any future re-ordering breaks the test.
Tests: 3128 passing (3127 + 1 new), 100% coverage, ruff clean.
Note: TaskService.submit_for_qa() (the v1-layer service method) is
INTENTIONALLY not renamed — it's a different layer used by the v1
routes. The rename here is only the gateway verb surface.
Pre-fix, agents had no way to introspect what verbs were valid from a
task's current state — the 2026-05-08 trace showed them spamming
escalate_to_ceo/complete/unblock/resume against a `claimed` task and
racking up rejections. Pre-gateway agents could ground reasoning in
VALID_TRANSITIONS[status] from a doc; the gateway hid that.
Now every Envelope carries:
- current_state: the task's status string (or None for tool-discovery
envelopes that aren't task-bound)
- valid_next_verbs: the verbs the caller can usefully call right now,
sourced from verb_gates.valid_next_verbs(role, task)
Wired into i_will_work_on (highest-traffic verb) for both the OK path
and the wrong-state rejection path. Remaining lifecycle verbs to be
wired in subsequent commits (Task 3.9).
Two coupled fixes from the 2026-05-08 smoke-test trace:
1. pm_cannot_execute_code is now scoped to i_will_work_on (the
EXECUTION verb) only. Pre-fix it also fired on i_will_plan, which
deadlocked any code-typed parent: cell_pm couldn't plan, so couldn't
transition parent to in_progress, so couldn't delegate. PMs PLAN
code-typed parents and DELEGATE the work — that's exactly the verb
we were blocking.
2. delegate.task_type is now REQUIRED at both the HTTP boundary
(DelegateRequest) and the choreographer dataclass (DelegateInputs).
The pre-fix default of 'code' silently changed semantics whenever a
caller forgot the field — main-pm's call in the smoke trace omitted
it, schema defaulted to 'code', and the cell PM downstream was
wedged. Also drops the choreographer's task.task_type fallback
(the DB column is NOT NULL anyway).
Plus middleware coverage tests for the parallel ServiceError →
4xx handler hierarchy added in the prior session, restoring 100%
coverage across the touched files.
Tests: 3101 passing, 100% coverage, ruff clean.