Repository navigation
Loop done is a handoff, not an exit #101
Description
Proposal
Parent: autonomous-swarm (F3).
Why
When a session emits its completion signal, the run ends and nothing else happens: no one
is told, nothing is reviewed, nothing picks up the next item. In loop/loop.ts a completion
token finalizes a prompt-mode loop as completed (~L632) and a drained queue calls
finishQueue(…, "completed", …) (~L1168). loop-eternal-by-default routes an eternal plain
loop into queue mode, which helps, but the drained-queue end is still terminal and silent,
and in a crew the finishing member's completion is invisible to the lead and to the others.
Which exact shape the operator hit ("emit a done task and it halts everything") is not yet
reproduced; autonomous-swarm Phase 0.3 does that first.
Also relevant: the completion token is the model's own word. A 35B local model declared
COMPLETE on iteration 1 of 50 (2026-09-20). Queue mode already verifies against tasks.md;
prompt mode has no ground truth.
What Changes
- Done runs a pipeline (epic D4): verify evidence → trigger
review-on-done→ record
the verdict → send onenotifyto the lead (and the delegating peer, if any) → release the
claim → pick the next eligible item → else enteridle-watch. idle-watch: a non-terminal loop state with backoff, woken by inbox messages, board
changes (new/changedopenspec/changes/*, released claims) and a new lead directive.
Reusescrew-loop3.3 (empty-board backoff) so there is one implementation. Not counted
as a stall.- Terminal states narrowed to: user cancel, error, explicit operator stop, and an
opt-in bounded prompt-mode--once. A drained queue entersidle-watchrather than
completedunless--once. - Evidence for the token: in spec-backed work, a completion token with unchecked tasks
or a failing gate is rejected and the loop continues (extends the existing "verified
completion" stance of queue mode to prompt mode when a change slug is attached). - Companion (from the conductor plan): a slug-bearing delegation from the granted lead
starts a queue-mode loop for that slug, acknowledges immediately, and sends one completion
notice at the end. Implemented here, on top ofcrew-loopPhase 2/3. - Completion notice schema:
{slug, branch, headSHA, gates, reviewVerdict?}carried in
the existing envelope; the lead's brief lists recent completions.
Depends on durable loop records (project-server Phase 2) for idle-watch to survive a
restart; until then idle-watch is per process.
Existing invariants every new driver path must respect
Reported by a peer review and verified in loop.ts ~1514–1542:
- One queue-shaped driver per directory.
QueueActiveErrorat creation and the guard
inrunPromptThenMaybeQueueexist because two queue drivers would fight over one derived
cursor and one working tree. Every path this change adds that starts or continues a
queue-shaped loop (the done pipeline's "pick next", a lead-delegated slug) MUST pass the
same check, implemented once as a shared function, not copied per path. With a project
server and crew claims the unit of exclusion becomes the claim (one live claim per
change, one worktree per claim) and the directory rule applies per worktree. - Resurrection requires
eternal(L1520). - Resurrection requires an eligible backlog (L1523:
resolveQueue(...).eligible).
idle-watchis the new behaviour for the empty case and must not bypass 1 or 2 for loops
that opted out with--once/--no-eternal.
A test per entry point asserts the invariant: a second driver-creating path in an occupied
directory is refused (seen red with the guard removed).
Dependency order
"Release the claim" and the claim-based exclusion need the claims table
(crew-loop Phase 1, branch crew-loop/claims, unmerged). Until it lands, the done
pipeline records the outcome and notifies but has no claim to release, and the directory
guard remains the only exclusion. Tasks 1.2 and 3.2 are ordered after crew-loop 1.1–1.3
for the claim parts.
Non-goals
- No change to cancel/error semantics.
- No guessing the next item for a plain session with no board; it reports and waits (quietly,
with the wake sources above), as an honest outcome.
Impact
loop/loop.ts (completion and queue-drained paths, new state), loop/spec-queue/*,
loop/completion.ts, peer/delegate.ts (ack + notice), crew-loop brief, TUI state label,
tests.
Tasks
Phase 0: Reproduce
- 0.1 Failing tests for the three shapes in
autonomous-swarm0.3: prompt-mode token with
more work on the board,--no-eternal, drained queue — each asserts the loop is NOT
terminal and a notice was sent. Each must be seen red on current code first. - 0.2 Failing test: a prompt-mode token with unchecked tasks of the attached change is
currently accepted.
Phase 0b: Invariants
- 0.3 Extract the one-queue-per-directory check into one function used by
QueueActiveError,
runPromptThenMaybeQueueand every new entry point; keep behaviour identical (existing
tests stay green). - 0.4 Entry-point tests: done-pipeline "next" and lead-delegated slug are each refused in an
occupied directory, and each test is seen red with the guard removed.
Phase 1: State and pipeline
- 1.1 Add
idle-watchto the loopStatusschema and TUI labels; not a stall; backoff
reused fromcrew-loop3.3 (coordinate, do not duplicate). - 1.2 Done pipeline in
loop.ts: verify → review hook point → record → notify → release →
next → idle-watch. - 1.3 Wake sources: inbox message, board change, lead directive, persisted timer.
Phase 2: Evidence
- 2.1 Reject token with unchecked tasks or red gate when a slug is attached; continue with
a brief naming what is missing. - 2.2
--onceopt-in for a bounded run; default unchanged otherwise per eternal-by-default.
Phase 3: Notices and delegation
- 3.1 Completion notice via the existing envelope; one per completion (repeat-guard test).
- 3.2 Slug-bearing delegation from a granted lead starts a queue-mode loop, acks, notifies
at the end; delegation from a non-lead keeps today's behaviour. - 3.3 Lead brief lists recent completions.
Phase 4: Live
- 4.1 Member finishes change A with change B waiting: continues to B, lead sees one notice.
- 4.2 Empty board: member enters idle-watch, sends nothing, wakes on a new change.
Activity