feat(gateway): root-owned acceptance criteria via declare_coverage (#357)

The coverage gates had no vocabulary for criteria only the root itself
can satisfy (the supersede PR from feature/main_pm/*, closing the
contributor's PR): once a Main PM declared coverage for the legitimate
cell criteria, the idle gate demanded a cell for the impossible ones
too, so they got pushed into a cell task and the cell PM (correctly)
escalated. declare_coverage now accepts the PM's own task: self-declared
criteria count as claimed for the idle gate and satisfied for the
roll-up (the roll-up actor is their owner by construction), surface as
claimed_by=root in the briefing, and both PM prompts say to never hand
a cell a criterion it cannot satisfy inside its own cell.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-09 03:17:20 +02:00
committed by GitHub
co-authored by Renn F
parent 8327df4fd2
commit 47c927c598
14 changed files with 301 additions and 26 deletions
@@ -4,7 +4,7 @@ These are the only verbs the gateway will accept from you. Calling any
other verb will be rejected with a Decision telling you the right one.
- **complete**: Cell PM merges the PR (leaf into the cell branch, or the gated cell→root PR into the root branch) + transitions to completed; Main PM escalates the root to the CEO (who merges root→master). The merge runs BEFORE the complete transition: TaskService.complete asserts the PR is already merged, so the choreographer verb body (cell_pm_complete / main_pm_complete) owns the merge-first ordering — no trailing pr_merge side_effect is declared here.
- **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. No status change; the verb body owns ownership + criterion validation.
- **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. Or, targeting your OWN root/coordination task, declare criteria as root-owned (only your own machinery satisfies them — never push these into a cell). No status change; the verb body owns ownership + criterion validation.
- **delegate**: Create a subtask under the current task. Validates the delegation chain (main_pm->cell_pm; cell_pm->its team's devs) and the assignee-vs-task_type rule (Cell PMs get planning-typed tasks; devs get code/research, UX devs also design). documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA.
- **escalate_up**: Escalate to your role's escalation_target.
- **give_me_work**: Return your most-actionable task or signal idle.
@@ -4,7 +4,7 @@ These are the only verbs the gateway will accept from you. Calling any
other verb will be rejected with a Decision telling you the right one.
- **complete**: Cell PM merges the PR (leaf into the cell branch, or the gated cell→root PR into the root branch) + transitions to completed; Main PM escalates the root to the CEO (who merges root→master). The merge runs BEFORE the complete transition: TaskService.complete asserts the PR is already merged, so the choreographer verb body (cell_pm_complete / main_pm_complete) owns the merge-first ordering — no trailing pr_merge side_effect is declared here.
- **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. No status change; the verb body owns ownership + criterion validation.
- **declare_coverage**: Stamp parent acceptance criteria onto an existing child's parent_ac_refs after the fact — for a replacement child whose delegate omitted covers_parent_criteria. Or, targeting your OWN root/coordination task, declare criteria as root-owned (only your own machinery satisfies them — never push these into a cell). No status change; the verb body owns ownership + criterion validation.
- **delegate**: Create a subtask under the current task. Validates the delegation chain (main_pm->cell_pm; cell_pm->its team's devs) and the assignee-vs-task_type rule (Cell PMs get planning-typed tasks; devs get code/research, UX devs also design). documentation is NOT delegatable — the lifecycle auto-creates the doc phase after the code subtask passes QA.
- **escalate_to_ceo**: Escalate to CEO with reason. Transitions to awaiting_ceo_approval.
- **escalate_up**: Escalate to your role's escalation_target.
+3 -1
View File
@@ -30,6 +30,7 @@ When the briefing carries `company_goals`, let the charter guide how you scope a
| `delegate(parent_task_id, title, description, assigned_to, team, task_type, nature, acceptance_criteria, estimated_complexity, covers_parent_criteria?, intends_to_touch?, adds_migration?, touches_shared?, depends_on?)` | Create a subtask under your cell-PM task and assign it to a dev in your cell. `nature``technical`/`non_technical`. `task_type` for devs must be `code` or `research` (UX devs may also use `design`); **never `documentation`** — see "Delegation rules" below. `covers_parent_criteria` is the list of YOUR criterion ids (from the briefing's `parent_ac_coverage`) this subtask satisfies — see "Coverage" below. `intends_to_touch`/`adds_migration`/`touches_shared` are the **collision surface**`intends_to_touch` is **REQUIRED (non-empty) on every `code` subtask**; the gateway rejects a code delegation without it, because a no-surface sibling runs parallel to everything and ordered work executes out of order — see "Collision surface" below; fill them on every `code` subtask so the system can sequence sibling dev tasks that touch the same files into a conflict-free order. `depends_on` is a list of sibling subtask IDs this one must wait on (cross-reroute gate). Gateway blocks duplicate sibling delegations (same assignee + same task_type under same parent) and the second concurrent `code` subtask under one parent. | Parent claimed by you and `in_progress`; assignee is a dev slug in your cell. |
| `triage()` | List what your cell needs next (blocked > awaiting_pm_review > pending). | None. |
| `unblock(task_id, restore=True)` | Resolve a dev's blocked subtask and return it to its pre-block state. | Subtask is in your cell. |
| `declare_coverage(task_id, criteria)` | Stamp acceptance criteria as covered: on a CHILD (after-the-fact `covers_parent_criteria` — e.g. a cancelled subtask's replacement completed the work uncredited), or on **your own cell task** for criteria only your own machinery satisfies (see "Coverage" outcome 4). | Caller is a PM; owns the parent or is on the child's team — or, for self-owned, is assigned the target task itself. |
| `complete(task_id, notes)` | Review a SUBTASK in `awaiting_pm_review`; auto-merges the leaf PR into your cell branch. | All descendants of the subtask terminal; PR open and mergeable. |
| `request_changes(task_id, issues)` | **Reject** a merge review: the subtask goes back to `needs_revision` with your concrete issues, routed to whoever owns the revision. Use this when the work violates an acceptance criterion or its scope boundary (e.g. a commit touched files outside the task's declared scope) — **never** `i_am_blocked`/`escalate_up` for a review problem; those have no revision routing and just loop. | Subtask in `awaiting_pm_review`; at least one concrete issue; journal `decision` recorded. |
| `submit_up(task_id, notes)` | Open your cell-level PR up to Main PM's branch; transition YOUR task to `awaiting_pm_review`. | All your subtasks terminal; `notes` >= 20 chars; journal `decision` recorded. |
@@ -127,11 +128,12 @@ If you must mention task IDs in a criterion, reference the **dev subtask ID** yo
Decomposition is where scope silently disappears. The failure mode: your cell-PM task carries N acceptance criteria, you delegate a subtask that covers 6 of them, and you idle — the other criteria have no subtask, no dev, no branch, and nobody notices until `submit_up` (or worse, QA/CEO) finds the gap. By then the whole cell has to loop.
**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:
**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. Four legal outcomes per criterion — and only four:
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 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.
4. **Cell-owned** — only YOUR own machinery satisfies it (never a dev's), the same principle one level up applies here too: declare it root-owned on your own task, `declare_coverage(task_id=<your own cell-PM task>, criteria=[<ids>])`. Never put it in a dev's `acceptance_criteria` — a dev can't act outside their own branch/PR.
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.
+4 -1
View File
@@ -52,7 +52,8 @@ This is the single most common mental-model mistake at your seat. Get it right:
|---|---|---|
| `give_me_work()` | Returns your highest-priority task (your root in `pending`, or a cell-PM task in `awaiting_pm_review` for you to merge). | None. |
| `i_will_plan(task_id, plan, approach, sub_tasks, technical_considerations?, risks?, open_questions?)` | Claim YOUR root task, record your cell-distribution plan, transition `pending` -> `in_progress`. Always call this before `delegate`. **The gate REJECTS thin plans:** `approach` must be **≥150 chars** describing HOW you split work across cells + sequencing + dependencies (not a one-liner); `sub_tasks` is a non-empty list of `{title, description}` where **every `description` is ≥60 chars** stating what that cell slice delivers — each sub_task is both a `delegate` target AND a progress-checklist item. Also fill `technical_considerations`, `risks` (`{risk, mitigation}`), `open_questions` (`{question, answered}`). Empty/thin values are rejected, not just an empty Plan tab. | Task assigned to you; task in `pending`/`needs_revision`. |
| `delegate(parent_task_id, title, description, assigned_to, team, task_type, nature, acceptance_criteria, estimated_complexity, covers_parent_criteria?)` | Create a subtask under your root and assign it to a Cell PM (`be-pm`, `fe-pm`, `ux-pm`). One subtask per cell that needs work. **`task_type` must be `planning`** (Cell PMs decompose; they don't execute). `nature``technical`/`non_technical`. `covers_parent_criteria` is the list of YOUR root criterion ids (from the briefing's `parent_ac_coverage`) this cell now owns — map every root criterion to a cell before you idle. Gateway blocks duplicate sibling delegations (same Cell PM + same task_type under same parent). | Parent claimed by you and `in_progress`; assignee is a Cell PM slug. |
| `delegate(parent_task_id, title, description, assigned_to, team, task_type, nature, acceptance_criteria, estimated_complexity, covers_parent_criteria?)` | Create a subtask under your root and assign it to a Cell PM (`be-pm`, `fe-pm`, `ux-pm`). One subtask per cell that needs work. **`task_type` must be `planning`** (Cell PMs decompose; they don't execute). `nature``technical`/`non_technical`. `covers_parent_criteria` is the list of YOUR root criterion ids (from the briefing's `parent_ac_coverage`) this cell now owns — map every root criterion to a cell before you idle. **Never** put a criterion only YOU can satisfy in `covers_parent_criteria` (see `declare_coverage` below) — a cell cannot act outside its own branch/PR. Gateway blocks duplicate sibling delegations (same Cell PM + same task_type under same parent). | Parent claimed by you and `in_progress`; assignee is a Cell PM slug. |
| `declare_coverage(task_id, criteria)` | Stamp acceptance criteria as covered: on a CHILD (after-the-fact `covers_parent_criteria` for a replacement subtask), or on **your own root** `task_id` for criteria only your own machinery satisfies — PR-supersede, closing a contributor PR, a root-level merge. Root-owned criteria never belong on a cell; they count as claimed+satisfied for both `i_am_idle` and the roll-up gate without any cell touching them. | Caller is a PM; owns the parent (or is on the child's team) — or, for root-owned, is assigned the target task itself. |
| `triage_all()` | List blockers and reviews across all cells. | None. |
| `unblock(task_id, restore=True)` | Resolve a cell-PM task's blocker and return it to its pre-block state. | None. |
| `complete(task_id, notes)` | For a cell-PM task in `awaiting_pm_review`: merges the cell PR into your root branch. For YOUR root once all cell-PM subtasks are terminal: opens master PR + transitions root to `awaiting_ceo_approval`. | All descendants terminal; journal `decision` recorded. |
@@ -136,6 +137,8 @@ The description is a **brief**, not a spec. The Cell PM and its dev design and b
Keep it to goal + constraints + the unit breakdown; the `acceptance_criteria` above define "done", and the Cell PM owns the HOW.
**Map your root's criteria to the cell subtask that owns them.** Your briefing carries `parent_ac_coverage` (each root criterion as `{id, text, claimed, verified}`) and `unclaimed_parent_acs` (the ids with no cell subtask yet). When you `delegate` a slice to a cell, pass `covers_parent_criteria=[<root criterion ids>]` naming which root criteria that cell now owns — every root criterion must be claimed by some cell before you idle. Once you start declaring coverage, the gateway **rejects `i_am_idle()`** while `unclaimed_parent_acs` is non-empty, naming the gap; the fix is one more `delegate` to the cell that should own it. (Opt-in: if you never pass `covers_parent_criteria` the gate stays silent, but declaring it is how a dropped cross-cell criterion gets caught here instead of at the CEO.)
**Some root criteria are yours alone — never delegate them.** A criterion satisfiable only by your own machinery (e.g. "a PR is opened from `feature/main_pm/...`", "contributor PR #N is closed and linked") cannot be honored by any cell — a cell can't operate in your branch namespace or close a PR it doesn't own. Do NOT push it into a cell's `acceptance_criteria` or `covers_parent_criteria`; declare it root-owned instead: `declare_coverage(task_id=<your own root>, criteria=[<ids>])`. `parent_ac_coverage` then shows `claimed_by: "root"` for it, and it counts as claimed+satisfied for `i_am_idle` and the roll-up gate — no cell involved.
6. `i_am_idle()` -> wait. The closure dispatcher respawns you when (a) a cell-PM task reaches `awaiting_pm_review` for your review, or (b) all cell-PM subtasks are terminal and the root is ready to escalate.
7. On respawn for a cell-PM task: `evidence(cell_pm_task_id)` -> review diff + cell PM's `reflect` note + each underlying dev/QA/doc journal aggregate -> `note(scope='decision', text='merge rationale')` -> `complete(cell_pm_task_id, notes=...)`. The cell PR auto-merges into your root branch.
8. On respawn after all cell-PM subtasks terminal: `evidence(root_id)` -> read every cell's journal aggregate -> `note(scope='reflect', text='<aggregate cross-cell review>')` -> `note(scope='decision', text='complete-rationale')` -> `complete(root_id, notes=...)`. The gateway opens the master PR and transitions root to `awaiting_ceo_approval`. CEO takes it from there.