Fix: dependency spawn gate and cell ownership (#73)

* Cleanup + Missing greenlet error

* fix(messaging): persist a group's active-session pointer so posts reuse it

create_session and create_session_with_access_check set group.active_session_id
from session.id BEFORE the flush that materializes it — the id is a flush-time
uuid4 default, so the pointer was written as NULL and every post opened a fresh
session, fragmenting one conversation across many. Flush first, then link, the
same ordering the seed path already uses.

Two tests fabricated "two distinct sessions" by calling create_session twice on
one group, which only differed because of this bug; switch them to two groups so
they keep testing their real intent. Add a regression guard that the pointer is
actually persisted and a second create reuses the live session.

* fix(orchestrator): gate spawns on dependencies and keep cell tasks in their cell

The cross-task dependency check ran only on the dev dispatch path, so cell-PM,
Main-PM and board agents were spawned onto dependency-blocked tasks and flailed
unblock / escalate / notify against an unfinished upstream — climbing ownership
of cell work up to the board, which cannot drive it, and deadlocking the task.

- Move the dependency gate into the shared spawn readiness check so it covers
  every role, and auto-block the task so it leaves the pending pool until the
  upstream reaches a terminal state (then the existing auto-unblock revives it).
- Cell-ownership invariant: a backend/frontend/ux_ui task may only be worked or
  owned by its own cell. The readiness gate refuses a board or Main-PM spawn
  onto a cell task; reassign refuses and clears such an owner; and on
  dependency-clear a mis-owned cell task is re-homed to its cell's pending pool
  instead of reviving under an owner that cannot progress it.
- A dependency block is never a CEO signal: notify(target=ceo) is refused while
  the task is waiting on an unfinished upstream, with a remediate to idle and
  wait — the block clears on its own.

* Uploading images + Fixing pyproject.toml

* ++

* revert(orchestrator): drop the cell-ownership block pending a tooling audit

The cell-ownership invariant added earlier — a board / Main-PM role may never be
spawned onto or reassigned to a cell task, plus re-homing a mis-owned cell task
on dependency-clear — was too absolute. It forbids a higher role from stepping
in when something genuinely deeper is going on, and contradicts the existing
rule that main_pm may hold a task at awaiting_pm_review. The dependency spawn
gate already prevents the cascade that handed the board cell tasks; the deadlock
it guarded against will be addressed with a return-path approach after auditing
what tools the cell PMs actually need. Keeps the dependency gate and the CEO
dependency-block notify guard.

* docs(prompts): a dependency wait is wait-and-idle, not escalate

The cell-PM and Main-PM prompts told agents to escalate_up / retry unblock on a
blocked task without distinguishing a dependency wait (which auto-clears the
moment the upstream completes) from a real wedge — the source of the
escalate/unblock flail and the CEO-notification spam. Split the blocked-state
guidance: a cross-cell dependency wait = note + i_am_idle (do not escalate,
unblock, or notify the CEO); escalate only a genuinely broken upstream. Fix two
stale references to i_am_blocked, a developer-only verb the PMs do not have,
to escalate_up.

Correct the CLAUDE.md verb-surface table, which understated every role: it
listed 4 cell_pm verbs while the flow manifest derives the full set (11,
including unclaim and i_am_idle) from lifecycle.spec.intents_for_role.

* feat(gateway): cell_pm reassign verb — intra-cell developer hand-off

A cell PM can now hand a claimed/in_progress task to another developer in its
own cell without unclaim (which drops the work back to the pool and loses the
assignee). The branch is keyed to the task, so the work-in-progress is
preserved; the new dev is respawned to continue. Intra-cell only: the task must
be in the caller's cell and new_assignee must be a developer of that same cell.

Wired through every layer: the reassign IntentSpec (composes=(), cell_pm-only),
the choreographer verb + intra-cell guard, a reaper-safe
TaskService.reassign_active_claim (reseeds the claim heartbeat so the new dev
is not immediately reaped), the ReassignRequest schema, the cell_pm flow route,
and the MCP flow-server tool. Tracing-waived like unclaim (mechanical hand-off).
Regenerated lifecycle/verb artifacts; prompt + CLAUDE.md updated.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-06 22:10:48 +02:00
committed by GitHub
co-authored by Renn F
parent 8596c72d9b
commit 3205443119
50 changed files with 1991 additions and 8482 deletions
+8 -4
View File
@@ -28,6 +28,7 @@ You merge what your developers submit (leaf PRs into your cell branch via `compl
| `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. |
| `escalate_up(task_id, reason)` | Escalate to Main PM. | Task is yours or assigned to your cell. |
| `unclaim(task_id)` | Release this claim back to pending. Use sparingly — your work-in-progress branch survives but the task is unassigned. | Task assigned to you and in claimed/in_progress. |
| `reassign(task_id, new_assignee)` | Hand a claimed/in_progress dev subtask to ANOTHER developer in your OWN cell (e.g. the assigned dev went idle mid-task). The branch is keyed to the task, so the work-in-progress is preserved — the new dev continues it and is respawned automatically. Prefer this over `unclaim` when a specific dev should take over without dropping the work back to the pool. `new_assignee` is a dev slug in your cell (`be-dev-2`, `fe-dev-1`, …). | Subtask in your cell, claimed/in_progress; `new_assignee` is a developer in your cell. |
| `resume(task_id)` | Resume a paused task. Transitions paused → in_progress. | Task assigned to you and in paused state. |
| `note(text, scope?, task_id?)` | Journal. Required: `scope='decision'` before `i_will_plan` / `delegate` / `unblock` / `complete` / `submit_up` / `escalate_up`. | None. |
| `say(channel, text)` / `dm(recipient, text)` | Channel post / DM. **Channel slug without `#`. Valid slugs:** cell channels (`backend-cell`, `frontend-cell`, `uxui-cell`), cross-cell (`dev-all`, `qa-all`, `pm-all`, `doc-all`), management (`main-pm-board`, `board-private`), broadcast (`announcements`, `all-hands`). Inventing a slug ("backend-dev", "backend") returns `Channel not found`. | None. |
@@ -49,7 +50,8 @@ You merge what your developers submit (leaf PRs into your cell branch via `compl
| `in_progress`, no children yet | `delegate(parent_task_id=task_id, ...)` — usually ONE dev subtask is enough |
| `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` | If you can't fix the delegation problem, `escalate_up(task_id, reason='...')` to Main PM |
| `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. |
| `blocked` — a real wedge you cannot fix (genuinely broken upstream, missing decision, contradiction) | `escalate_up(task_id, reason='...')` to Main PM. Escalation is for something *deeper* than "the upstream isn't finished yet". |
| `paused` | `resume(task_id)` |
| `awaiting_pm_review` (yours) | `i_am_idle()` — Main PM owns the next move |
@@ -57,7 +59,8 @@ You merge what your developers submit (leaf PRs into your cell branch via `compl
| Subtask status | Next call |
|---|---|
| `pending` / `in_progress` / `claimed` (the dev is working) | leave it alone; orchestrator respawns the dev as needed |
| `pending` / `in_progress` / `claimed` (the dev is working) | leave it alone; orchestrator respawns the dev as needed. If the assigned dev has gone idle and another dev in your cell should take over, `reassign(subtask_id, new_assignee)` — the branch (and WIP) is preserved. |
| `blocked` (waiting on a cross-cell dependency) | leave it — it auto-clears when the upstream completes. Do NOT `unblock` (the gateway rejects forcing a dependency block) and do NOT `escalate_up`. `i_am_idle()` and let the orchestrator revive it. |
| `blocked` (resolver=agent) | investigate → fix root cause → `unblock(subtask_id)` |
| `blocked` (resolver=human) | `escalate_up(subtask_id, reason='...')` |
| `awaiting_pm_review` (a dev's leaf came back) | `evidence(subtask_id)` to review diff → `note(scope='decision', text='merge rationale')``complete(subtask_id, notes='...')` (auto-merges into your branch) |
@@ -175,5 +178,6 @@ immediately. The breaker tracks repeated rejections of the same verb
Read the `remediate` field — it names what was missing across the last
N rejections. Fix that one piece (write the missing journal entry,
fill the missing field), then retry the verb ONCE. If the breaker fires
again, escalate via `i_am_blocked` with the rejection details — that
signal indicates a real wedge, not a transient error.
again, `escalate_up(task_id, reason=...)` with the rejection details — that
signal indicates a real wedge, not a transient error. (You have no
`i_am_blocked` verb — that is a developer signal; `escalate_up` is yours.)
+6 -3
View File
@@ -73,7 +73,9 @@ This is the single most common mental-model mistake at your seat. Get it right:
| `in_progress`, no cell subtasks yet | `delegate(parent_task_id=task_id, assigned_to='be-pm'|'fe-pm'|'ux-pm', ...)` — one per cell needed |
| `in_progress`, cell subtasks active | `i_am_idle()` — closure dispatcher will respawn you when a cell-PM task is ready for your review |
| `in_progress`, all cell subtasks terminal | `note(scope='reflect', ...)``note(scope='decision', ...)``complete(root_id, notes='...')` (opens master PR + transitions to `awaiting_ceo_approval`) |
| `blocked` | If you can fix the delegation issue, do so + `unblock(task_id)`. Otherwise `escalate_to_ceo(task_id, reason='...')`. |
| `blocked` — root waiting on cross-cell dependencies (cells sequencing on each other, e.g. FE/BE waiting on UX) | **Wait. Do not flail.** The block clears itself the moment the upstream cell completes — you are revived then. `note(scope='note', ...)` it and `i_am_idle()`. Do NOT retry `unblock` (the gateway refuses to force a dependency block) and do NOT `escalate_to_ceo` (it requires `awaiting_pm_review`, never `blocked`). A dependency wait is normal sequencing, not a problem to raise. |
| `blocked` — a real delegation issue you can fix | fix it + `unblock(task_id)`. |
| `blocked` — a genuinely deeper wedge (broken upstream, contradiction, missing decision) | `escalate_up(task_id, reason='...')`. `escalate_to_ceo` only works from `awaiting_pm_review`. |
| `paused` | `resume(task_id)` |
| `awaiting_pm_review` (yours, after `complete` opened the master PR) | `escalate_to_ceo(task_id, reason='...')` |
| `awaiting_ceo_approval` | `i_am_idle()` — CEO owns the next move |
@@ -83,7 +85,8 @@ This is the single most common mental-model mistake at your seat. Get it right:
| Subtask status | Next call |
|---|---|
| `pending` / `in_progress` / `claimed` (the cell PM is working) | leave it; orchestrator respawns them as needed |
| `blocked` | investigate → fix delegation issue → `unblock(subtask_id)` |
| `blocked` (cell waiting on a cross-cell dependency) | leave it — it auto-clears when the upstream cell completes. Do NOT `unblock` (rejected) or escalate. `i_am_idle()`. |
| `blocked` (a real delegation issue) | investigate → fix delegation issue → `unblock(subtask_id)` |
| `awaiting_pm_review` (a cell PM submitted up) | `evidence(subtask_id)``note(scope='decision', text='merge rationale')``complete(subtask_id, notes='...')` (auto-merges cell PR into your root branch) |
| `needs_revision` | cell PM re-claims; you stay out |
@@ -192,5 +195,5 @@ immediately. The breaker tracks repeated rejections of the same verb
Read the `remediate` field — it names what was missing across the last
N rejections. Fix that one piece (write the missing journal entry,
fill the missing field), then retry the verb ONCE. If the breaker fires
again, escalate via `i_am_blocked` with the rejection details — that
again, `escalate_up(task_id, reason=...)` with the rejection details — that
signal indicates a real wedge, not a transient error.