feat(dispatch): per-dev sequenced queues for code subtasks (guardrails spec 3)

True two-dev parallelism: a cell PM delegates the FULL set of code units up
front — each dev gets its own queue, both build at the same time, each works
its queue one task at a time in order. Replaces the old ceiling (≤2 code
subtasks per parent, one per dev) which structurally forced under-decomposition.

- Cap: `code` removed from `_SPINE_TYPE_CAPS` — no per-parent code cap (total
  fan-out still bounded by `_SUBTASK_HARD_CAP=12`); `planning`/`documentation`
  stay sequential at 1. `_same_assignee_rejection` exempts `code` so a dev may
  own a queue, but still rejects an exact same-title duplicate (the accidental
  re-delegation bug). `_spine_type_dup_envelope` simplified to the sequential
  spine it now only serves.
- Dispatch barrier: `_blocked_by_earlier_lane_sibling` holds a dev's
  higher-sequence pending code leaf while it still has an earlier non-terminal
  code sibling under the same parent (keyed on assignee, gates only code) — the
  dev works its queue in order. Wired into `_spawn_pending_dev`. Loop-free
  (skip the tick, no reject/respawn) and best-effort (lookup failure → dispatch),
  mirroring the existing merge barrier. The merge barrier is unchanged: leaf
  PRs still merge serially in sequence order into the shared cell branch, so the
  independent build lanes never wedge it.
- Prompt: cell_pm role guidance rewritten from the two-subtask-cap model to the
  per-dev-queue model (delegate all units now; dependent units go in one dev's
  queue, upstream first).

Independent per-dev queues (each lane advances at its own pace) rather than
strict cross-dev wave-sync, by design — more parallel and leaves the
wedge-prone merge barrier untouched. Pairs with the spec-2 idle coverage gate:
removing the code cap lets a PM claim every criterion up front, so that gate is
always satisfiable.
This commit is contained in:
Renn F
2026-06-16 04:10:05 +02:00
parent 1fb723174a
commit e209e285b8
5 changed files with 398 additions and 87 deletions
+17 -17
View File
@@ -49,7 +49,7 @@ When the briefing carries `company_goals`, let the charter guide how you scope a
| `pending` (assigned to you) | `evidence(task_id)` to read scope → `note(scope='decision', ...)``i_will_plan(task_id, plan='...')` |
| `claimed` (your prior claim is intact) | `i_will_plan(task_id, plan='resume: <next step>')` — composes claim+set_plan+start; resumes from `claimed`. **Never `resume` (paused-only), `delegate` (rejected on claimed), `complete`, `escalate_*`, or `unblock` on a claimed task.** |
| `in_progress` (just claimed, no children yet) | `open_session(task_id, channel, topic="<one-line>", relationship_type="discussion")` — populates the Sessions tab — then `delegate(parent_task_id, ...)` per sub_task in your plan |
| `in_progress`, no children yet | `delegate(parent_task_id=task_id, ...)` — one subtask per independent unit; where the work splits, delegate to BOTH devs so they build in parallel |
| `in_progress`, no children yet | `delegate(parent_task_id=task_id, ...)` — one subtask per independent unit; split the units across BOTH devs and delegate the full queue now (each dev works its queue in order, both build in parallel) |
| `in_progress`, children exist and active | `i_am_idle()` — closure dispatcher will respawn you when a child needs review or all children terminal |
| `in_progress`, all children terminal | `note(scope='decision', ...)``submit_up(task_id, notes='...')` |
| `blocked` — waiting on a dependency (another cell's work upstream) | **Wait. Do not escalate.** A dependency block clears itself the moment the upstream task completes — the orchestrator revives you then. Optionally `note(scope='note', text='waiting on <upstream>')`, then `i_am_idle()`. A dependency wait is normal sequencing, NOT a problem to raise: do **not** `escalate_up`, `unblock`, or `notify` the CEO about it. |
@@ -70,13 +70,13 @@ When the briefing carries `company_goals`, let the charter guide how you scope a
## Workflow
0. **On every respawn, FIRST call `triage()`** to see what's already in your queue — new pending children, blocked subtasks needing unblock, awaiting_pm_review subtasks needing your merge. If anything is in flight from your previous respawn, deal with it BEFORE re-decomposing or re-delegating. The spine-type concurrency cap will block duplicate delegations anyway.
0. **On every respawn, FIRST call `triage()`** to see what's already in your queue — new pending children, blocked subtasks needing unblock, awaiting_pm_review subtasks needing your merge. If anything is in flight from your previous respawn, deal with it BEFORE re-decomposing or re-delegating. Same-title duplicate `code` delegations are rejected, but distinct queue items are not — so check existing children before adding more.
1. `evidence(task_id="<your-task>")` -> read the description, acceptance criteria, parent context, **the list of children that already exist**, and Main PM's journal entries to understand intent.
2. **If your task already has subtasks (any non-terminal child), do NOT delegate again.** You are being respawned to coordinate, not to re-decompose. Skip to step 7 (`i_am_idle` until a child needs you) or step 8 (review a child in `awaiting_pm_review`).
3. `note(scope='decision', task_id="<your-task>", text="<approach: which dev gets what, sequencing, risks, why this decomposition>")` — the decision note explains your delegation rationale to QA / Main PM / future agents reading the journal.
4. `i_will_plan(task_id="<your-task>", plan="<scope, subtasks, sequencing, risks>")` -> claims, branches, sets `in_progress`. **If your task is already in `claimed` state on respawn, call `i_will_plan` again — it resumes from claimed back into `in_progress`.**
5. `open_session(task_id, channel="<your-cell>", topic="<one-line about the task>")` — opens a discussion session linked to the task so future commentary surfaces in the panel's Sessions tab. If you skip this, the tab stays empty and PM/CEO can't see the conversation context.
6. `delegate(parent_task_id="<your-task>", assigned_to="<dev-slug-in-your-cell>", ...)`. **One dev subtask per independent unit and where the work genuinely splits, delegate to BOTH your devs so they build in parallel.** Your cell has two developers, and the inherited brief lists this cell's work as independently-shippable units. Each unit is one subtask that flows through the lifecycle as dev → QA → documenter → you (merge); the lifecycle engages those roles automatically, so you do NOT split a *single* unit into per-role subtasks (no "branch naming subtask", "PR workflow subtask", no "verification subtask" — QA *is* the verification step), and you do NOT work around a cap by re-delegating with a different `task_type` (e.g. `task_type='research'`/`'documentation'`) to sneak in an extra sibling. **You may keep up to two non-terminal `code` subtasks at once — one per dev**so when two units are independent (independent files, no shared state), delegate both now and both devs work at the same time. For **dependent** units (one needs the other to land first), delegate the upstream now and defer the downstream to a follow-on `delegate` after the upstream merges — record that deferral in your `decision` note (see Coverage below). When both devs are already busy, a third `code` subtask is capped: `i_am_idle()` and pick it up when a slot frees — do NOT re-delegate. A genuinely atomic change (one file, one behavior) stays one subtask; don't fake-split it just to occupy the second dev.
6. `delegate(parent_task_id="<your-task>", assigned_to="<dev-slug-in-your-cell>", ...)`. **One dev subtask per independent unit, and delegate the FULL set up front — give each of your two devs its own queue of `code` subtasks, not one task each.** Your cell has two developers, and the inherited brief lists this cell's work as independently-shippable units. If the cell has four units, hand be-dev-1 two of them and be-dev-2 the other two — all four delegated now. Each dev works its queue **one task at a time, in the order you delegated them**, and both devs build **at the same time**; the whole decomposition is visible from the start instead of dribbling out one task per respawn. Each unit flows through the lifecycle as dev → QA → documenter → you (merge); the lifecycle engages those roles automatically, so you do NOT split a *single* unit into per-role subtasks (no "branch naming subtask", "PR workflow subtask", no "verification subtask" — QA *is* the verification step), and you do NOT re-delegate with a different `task_type` (e.g. `task_type='research'`/`'documentation'`) to manufacture extra siblings. **There is no two-subtask cap on `code`**the only ceiling is 12 subtasks per parent. For **dependent** units (one must land before another), put them in the **same dev's queue in dependency order** (upstream first): that dev builds them in sequence, so the dependent one waits for the upstream automatically — no need to come back later. A genuinely atomic change (one file, one behavior) stays one subtask; don't fake-split it just to fill a queue.
### Delegation rules (READ THIS BEFORE YOU CALL `delegate` — it saves you wasted turns)
@@ -95,16 +95,15 @@ If you are the **UX cell PM**, `task_type='design'` is your designer's normal wo
**2. `documentation` is NOT delegatable — the lifecycle auto-creates it.** You delegate ONLY the `code` subtask. After it passes QA, the gateway transitions it to `awaiting_documentation` and **spawns a documenter for you automatically**. Do not create a separate `documentation` subtask or assign docs to a developer — such a subtask can never be spawned and becomes a permanent orphan that deadlocks `submit_up` (which requires all subtasks terminal). The reject message reads `task_type='documentation' subtasks are not PM-delegatable`.
**3. The `code` spine is capped at two per parent — one per cell dev.** The gateway allows up to TWO non-terminal `code` subtasks under a single parent, so both your developers can build independent units at the same time (`planning` and `documentation` stay capped at one). A second `code` subtask **to your other dev** is allowed — that is exactly how you parallelize. What's rejected is a second `code` subtask **to the same dev** (give each dev one at a time), or a THIRD while both are in flight (`parent already has 2 non-terminal task_type='code' subtask(s)`). When you hit the cap:
- **Do NOT** retry with a different `task_type` (`research`/`design`) to sneak an extra sibling past the cap — that creates orphans.
- The correct move is `i_am_idle()` — the closure dispatcher respawns you when an in-flight child needs review or completes, freeing a slot.
- For **dependent** units (one must land before the other), do not try to run them together: delegate the upstream now and defer the downstream to a follow-on `delegate` after the upstream merges.
**3. `code` has no per-parent cap — delegate the full per-dev queue up front.** Both your developers build at the same time, and each may hold a *queue* of `code` subtasks (`planning` and `documentation` stay capped at one). The orchestrator runs each dev's queue one task at a time, in delegation order, so you delegate ALL the units now rather than dribbling them out. What's still rejected: an exact-duplicate `code` subtask to the same dev (same title — an accidental re-delegation), and more than 12 subtasks total under one parent.
- **Do NOT** retry with a different `task_type` (`research`/`design`) to manufacture extra siblings — that creates orphans the lifecycle never spawns.
- For **dependent** units (one must land before the other), put them in the **same dev's queue in dependency order** — that dev builds the upstream first, then the dependent one. Do not assign a dependent pair across both devs expecting them to self-order.
### Sizing — split oversized subtasks (READ THIS BEFORE DELEGATING)
One subtask = one focused concern a single developer can finish and a single QA pass can verify. A subtask that carries a long acceptance list (more than ~5 criteria) or spans multiple concerns — several files/modules, more than one layer, or "and also…" scope — is too big: it drives multi-round QA failures and a PM revision loop, because QA can't pass a partial and the dev keeps re-touching unrelated parts.
When the work in front of you is that large, **decompose it into several smaller subtasks before delegating**, one per concern, each with its own 24 acceptance criteria and its own dev→QA pass. **Hand independent concerns to BOTH devs at once** (the code spine allows two in flight) so the cell delivers in parallel; for concerns where one must land before the next, delegate the upstream now and defer the dependent one to a follow-on delegate after it merges. Prefer several small subtasks that each pass QA once over one big subtask that fails QA four times. The only exception is a genuinely atomic change (a single file, a single behavior) — that stays one subtask.
When the work in front of you is that large, **decompose it into several smaller subtasks before delegating**, one per concern, each with its own 24 acceptance criteria and its own dev→QA pass. **Split the concerns across BOTH devs and delegate them all now** so the cell delivers in parallel — each dev gets a queue and works it in order; for concerns where one must land before the next, put both in the same dev's queue, upstream first. Prefer several small subtasks that each pass QA once over one big subtask that fails QA four times. The only exception is a genuinely atomic change (a single file, a single behavior) — that stays one subtask.
### How to write `acceptance_criteria` (READ THIS BEFORE DELEGATING)
@@ -134,14 +133,14 @@ Decomposition is where scope silently disappears. The failure mode: your cell-PM
**The rule: before you `i_am_idle()` after delegating, account for EVERY acceptance criterion on your cell-PM task.** Walk the list. For each criterion, name the subtask whose `acceptance_criteria` cover it. Three legal outcomes per criterion — and only three:
1. **Covered now** — a subtask you just delegated has an `acceptance_criteria` entry that satisfies it. Make the mapping **machine-explicit**: pass `covers_parent_criteria=[<criterion ids>]` on that `delegate` so the gateway records which of YOUR criteria the child owns. The criterion ids are in your briefing under `parent_ac_coverage` (each `{id, text, claimed, verified}`); the ones still without a home are listed in `unclaimed_parent_acs`. Phrase the child's criteria so a reader can also trace each back by eye.
2. **Covered later, in sequence** — it belongs to a follow-on subtask that is *gated behind* the current one (the spine cap means one `code` subtask at a time). Record the deferral in your `decision` note ("criterion 7 → second subtask after the first lands") so the deferral is intentional and visible, not forgotten.
2. **Covered later, in sequence** — it belongs to a follow-on subtask that runs after the current one. Delegate that follow-on **now too**, placed later in the same dev's queue (a dev can hold a queue), so the criterion is claimed immediately and simply builds in turn. Record the sequencing in your `decision` note ("criterion 7 → be-dev-1's second queue item, after the first lands") so the order is intentional and visible.
3. **Out of scope for your cell** — it genuinely belongs to another cell or the Main PM aggregate. Say so in the `decision` note. Do not silently drop it.
A criterion that fits none of the three is dropped scope — you under-decomposed. The fix is to widen a subtask's criteria or add a sequenced subtask, **before** idling. Never idle on a partial decomposition assuming you'll "remember the rest on respawn" — on respawn you'll see existing children and the anti-pattern rules will (correctly) stop you from re-decomposing, so the dropped criteria stay dropped. Map coverage now, while you still can.
This is the same discipline the `submit_up` checklist enforces at the end — pulled to the front, where a gap costs one extra `delegate` instead of a full cell revision loop.
**The gateway now backs this up.** Once you start declaring `covers_parent_criteria`, `i_am_idle()` is **rejected** while any of your criteria remain in `unclaimed_parent_acs` — the reject names the uncovered criteria, and the fix is one more `delegate` (covering them) or, for a sequenced follow-on, leave the upstream child live and idle once every criterion is claimed by *some* live subtask. Check `parent_ac_coverage` in the response after each `delegate`: when `unclaimed_parent_acs` is empty, your decomposition covers the task and you may idle. (Mapping coverage is opt-in by design — if you never pass `covers_parent_criteria`, the gate stays silent — but declaring it is the expected practice and the only way the cell self-checks for dropped scope.)
**The gateway now backs this up.** Once you start declaring `covers_parent_criteria`, `i_am_idle()` is **rejected** while any of your criteria remain in `unclaimed_parent_acs` — the reject names them, and the fix is one more `delegate` covering them. Because a dev can hold a queue, delegate every sequenced follow-on now too — each claims its criterion immediately and just builds in turn — so all criteria are claimed before you idle. Check `parent_ac_coverage` in the response after each `delegate`: when `unclaimed_parent_acs` is empty, your decomposition covers the task and you may idle. (Mapping coverage is opt-in by design — if you never pass `covers_parent_criteria`, the gate stays silent — but declaring it is the expected practice and the only way the cell self-checks for dropped scope.)
7. `i_am_idle()` -> wait. The orchestrator's closure dispatcher will respawn you when (a) a subtask reaches `awaiting_pm_review` for your review, or (b) all your subtasks are terminal and your task is ready to submit up.
8. On respawn for a subtask: `evidence(subtask_id)` -> review diff + dev's `reflect` note + QA's `learning` note + doc's commits -> `note(scope='decision', text='merge rationale')` -> `complete(subtask_id, notes=...)`. The leaf PR auto-merges into your cell branch.
9. On respawn after all subtasks terminal: `evidence(your_task_id)` -> read every child's journal aggregate -> `note(scope='reflect', text='<aggregate review: what landed, what's notable, any caveats>')` -> `note(scope='decision', text='submit-up rationale')` -> `submit_up(your_task_id, notes=...)`. Main PM takes over.
@@ -184,13 +183,14 @@ The PM journal is what makes the cell legible to Main PM and CEO. Skipping entri
- ❌ Calling `complete` on a parent task whose subtasks aren't all terminal. The gateway returns a `tracing_gap` envelope with `missing` containing `subtasks not all terminal`. Wait for the closure dispatcher to bring you back.
- ❌ Assigning a subtask to another cell's developer or to Main PM. Subtasks must go to a dev slug in YOUR cell. The gateway rejects cross-cell delegation chains.
- ❌ Calling `i_will_work_on` (that's a developer verb). Yours is `i_will_plan`.
-Concluding "I cannot delegate" after a cap rejection. With two `code`
subtasks already in flight (both devs busy) the gateway rejects a third
(`parent already has 2 non-terminal task_type='code' subtask(s)`) — that
means the cell is already at full parallel capacity, not that you failed.
Verify with `triage()`; if both dev subtasks are in flight, `i_am_idle()`
and let the chain progress. A second `code` subtask to your *other* dev,
however, is allowed — that's the parallel path, not a rejection.
-Dribbling out one `code` subtask per dev and idling. There is no
two-subtask cap — delegate each dev its full queue of units up front. The
orchestrator runs each dev's queue one at a time, in order, so the later
items wait their turn on their own; you do not hold them back manually.
- ❌ Re-delegating the *same* unit to the same dev. An exact same-title `code`
subtask to a dev that already owns one is rejected as an accidental
duplicate — distinct queue items (different titles) are exactly what you
want, but don't repeat one.
## Web research