@feigi/fleet-ctl 3.82.13 → 3.82.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,8 +60,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
60
60
 
61
61
  The `tier-roles` step runs `~/.fleet/bin/fleet-run tier-roles.mjs --check
62
62
  --in-flight <B>`, `<B>` being the `--in-flight` given, else the default
63
- **Defaults** (**Queue depth**) states. What it checks is under **Phase 2**'s
64
- tier routes. A line it prints on a pass about omp's `task.maxConcurrency`
63
+ **Defaults** (**Queue depth**) states. What it checks is in the header of `scripts/tier-roles.mjs`, under
64
+ the tier routes. A line it prints on a pass about omp's `task.maxConcurrency`
65
65
  is a notice in the step's output and never holds: read it, report it, and
66
66
  go on — the fleet never writes that setting.
67
67
 
@@ -185,7 +185,7 @@ it prints. There are no hand-run
185
185
  refill or dispatch edges left. Record first, because the tick reads the run off
186
186
  `.fleet/ledger.md` and nothing else of yours: a wake you have not written down is
187
187
  one it cannot see, and it names the same dispatch again. Every record goes
188
- through `ledger.mjs` (**Run ledger**), which `fleet-event.mjs` drives — never a
188
+ through `ledger.mjs` (references/ledger.md), which `fleet-event.mjs` drives — never a
189
189
  hand edit. What each kind writes is in the header comment of `fleet-event.mjs`.
190
190
  See references/ci-and-staleness.md.
191
191
 
@@ -219,7 +219,7 @@ owes beyond its call:
219
219
  - **Finisher reports halted** → the per-cause rule (**Resolving a finisher
220
220
  halt**, below).
221
221
  - **A finisher failed or was killed** → the **Member confirmed dead or never dispatched** wake (`member-died finisher-pr-<M>`), which settles it `failed` or `killed`; no label, and the tick prints no dispatch for it. The cockpit flags the PR `finisher:failed` / `finisher:killed` at severity 4 and you resolve it by hand (**Resolving a finisher that died**, below), then run the tick.
222
- - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`; the causes and the issue action each takes are under **Relabel causes**.
222
+ - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`; the causes and the issue action each takes are under **Relabel causes**. **Only an explicit bail report — the member itself naming the cause — reaches Implementer bails before implementing**; a died or merely-quiet member's silence never does.
223
223
  - **A review result lands** → see the result file written (by you or by the
224
224
  runner — **The reviewer** says which, per harness), then
225
225
  `fleet-event.mjs review-result <pr> <path>`: a PR with survived or unverified findings prints as
@@ -283,6 +283,28 @@ owes beyond its call:
283
283
  context exhaustion caused (references/close-out.md).
284
284
  - **The finisher-gate wakes** — a CI run reaching a terminal state, a fix-applier reporting `no-op`, a `no-ci` verdict → each is a bullet of **The finisher gate**.
285
285
 
286
+ Write each state change through `ledger.mjs` before you act on it, never by hand: a wake is done when no row, filing or ruling lives only in your context.
287
+
288
+ Compaction parks every in-flight member and nothing reports it, so on resume take the registry census first (**Member unaccounted**, in **Failure handling**) before you trust a live token.
289
+
290
+ Token additions go through `ledger.mjs token`. `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row. Re-typing the row with `row` replaces the whole line, so it drops the tokens you do not restate. Write member tokens and `review=` tokens only through `ledger.mjs`, never by hand.
291
+
292
+ Never rotate the ledger mid-run: it is phase 0's step alone.
293
+
294
+ Record each ruling with `ledger.mjs ruled <pr> <decision>`, one line with its reason. Gate on the `ledger.ok` field of `check`, not on the stderr warning. The writer policy, the gap the lock does not close, and `check`'s exits: See references/ledger.md.
295
+
296
+ **Write the ledger line before dispatching, not after.** Record the member with `ledger.mjs dispatch`, or `review` for a review, before its `task` call.
297
+
298
+ ### Dispatching the members a wake names
299
+
300
+ - **The reviewer.** **Dispatch the review, never wait on it, and act when its result lands. That is the default path.** `DISPATCH review PR#<M>`: a `task` member named `review-pr-<pr#>`, agent `fleet-review-runner`, its prompt the five args `pr`, `branch`, `worktree`, `testCmd` and `scratch`. Record `review=member:review-pr-<pr#>` on the PR's row before the call. `scratch` is the scratch root itself, never a `pr<N>` partition (`references/reviewers.md`). Write the `review=` token with `ledger.mjs review <pr> <token>`; `ledger.mjs` refuses a bad grammar. The tick names every PR owed one, oldest first, and each gets one review, once per PR. The tick counts in-flight reviews off the ledger and nowhere else, so a review you launched and never recorded is one it names again. The runner writes the result file and reports its path and `reviewed=` token.
301
+ A row with `review=` and no `reviewed=` is a review in flight, and the tick counts it as a live review unit unless gh reports its PR MERGED or CLOSED.
302
+ - **The fix-applier.** `DISPATCH fix-pr PR#<M>` on a `reviewed=` count that survived or unverified findings, with no fix-applier live: name `fix-pr-<pr#>` (the next suffix if that name is on record), never the PR's implementer; render with `dispatch-prompt.mjs render fix-applier --kind review --member fix-pr-<pr#> …`, then record `ledger.mjs dispatch <pr#> fix-pr-<pr#>` before the call. Its brief carries the PR number, the worktree abs path, the PR's branch, the same `testCmd` you passed the review, and the **path** `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's latest `reviewed=` — never the findings. Every `<scratch>` in that brief is the absolute path `scratch-root.mjs` printed. Its rulings are the fix-applier body's (`agents/fleet-fix-applier.agent.md`); you hold none. It holds every finding and you hold none, so there is nothing for you to rank, relay or pre-rule — and a report from a refuter it spawns that surfaces to you is its to retrieve, never yours to scan or pass on. It answers the hold and nothing else: survived or unverified findings of a returned review the row also carries stay unanswered through its settle, so the tick prints `DISPATCH fix-pr PR#<M>` again once it lands, and that one is a review fix-applier, dispatched as above. On a PR carrying `ready-to-merge`, the push moves the head after the label, so the next merge bot refuses it `head-moved-after-label-#<M>`: a fresh finisher, per **Failure handling**.
303
+ - **The conflict hold.** `DISPATCH fix-pr PR#<M>` on a conflict hold: the row carries an unresolved `conflict-hold:#<M>`, the merge bot's or one you recorded off the tick's `CONFLICT` line. It is the same slot with a different job, and no review file to hand over. Name it `fix-pr-<M>`, or the next suffix (`-b`, `-c` …) when that name is on record; render with `dispatch-prompt.mjs render fix-applier --kind conflict --member <name> …`; dispatch with the `agent` `ledger.mjs dispatch` printed, in the PR's existing worktree. Read the definition off that output (`fleet-fix-applier` whatever the row's `tier=`), never recall it. Settle `applied:<head>`, or `no-op` if no conflict was left, which lifts the merge hold; a dispatched-but-live one does not, and `failed`/`killed` leave the PR fix-due for a replacement.
304
+ - **Then dispatch a red-check fixer** — on a `check`-job `failure` bound to the PR's current head that is not **solely** staleness: a `rebase-check` red, or heavy jobs `skipped`, that comes only off the behind-count gets no fixer (the five-condition note in `agents/fleet-fix-applier.agent.md` establishes that). Name `fix-pr-<M>`, or the next free suffix; render with `dispatch-prompt.mjs render fix-applier --kind red-check --member <name> …`, record with `ledger.mjs dispatch <pr#> <name>` before the call, and dispatch with the printed `agent` in the PR's existing worktree. While a review or a review fix-applier on the PR is live or still owes its report — a dispositions mismatch awaiting its retry and a conflict hold awaiting its fix-applier included — the red-check fixer does not fire: their push starts a new run, and a fixer racing them would work on a SHA that is still moving. The common case, the implementer's own first CI firing turning red before any review exists, still dispatches the review against that head as normal. The tick prints no `DISPATCH` for it, so you reconcile it yourself, and it always goes after the review, never before. It is told outright not to watch, poll or wait for CI: your persistent Monitor owns the next run on the pushed head, and the finisher gate — review returned AND fix-applier report sent — applies to this member the same way. Settle it `applied:<head>` once a fix is pushed. Settle it `no-op` only when the red turns out not to be this member's to fix — a flake that passes on rerun, or staleness — with nothing pushed; `no-op` counts as landed, so it never stands for "could not fix". Settle it `failed` when it cannot reach green, which the **Failure handling** row "Reviewer, fix-applier or red-check fixer cannot reach green" covers. Its three dispatch edges are under **The finisher gate**; their rationale is in `references/member-lifecycle.md` (The red-check fixer's three edges).
305
+ - **The merge bot.** Named `merge-bot-<n>`; `ledger.mjs dispatch merge-bot` numbers it. Never two at once. Render `--member merge-bot-<n> --n <n> --scratch <scratch>`. Dispatch with the printed agent, omit `model`, and dispatch on the first `ready-to-merge` label without waiting for the finisher report.
306
+ - **The finisher.** Its render and receives are in `references/finisher.md`.
307
+
286
308
  ### Reading a review result
287
309
 
288
310
  **You read the digest; the findings are the fix-applier's.**
@@ -463,7 +485,7 @@ set it, and never otherwise.
463
485
  It prints `actual/target` and a named ACTION per role, with the whole **Queue
464
486
  depth** guard table applied in code. Act on each line as it reads:
465
487
 
466
- - `PULL #<N> …` — one Pull per named ticket, in the order printed: `pull.mjs next --head <N>`, or the `pull.mjs answer` the row names (phase 1), then phase 2's dispatch.
488
+ - `PULL #<N> …` — one Pull per named ticket, in the order printed: `pull.mjs next --head <N>`, or the `pull.mjs answer` the row names (phase 1), then the dispatch under **Dispatching the Pull's member**.
467
489
  - `REFRESHED shortlist: <n> entries; <k> lifted` — the tick refreshed the shortlist itself, and its `PULL` row already reads the result; `REFRESH FAILED` is yours, since `shortlist.mjs` could not run.
468
490
  - `SUGGEST /triage, hold idle` — nothing is admissible after the refresh: suggest `/triage` in the report, never run it.
469
491
  - `specs-dropped=#<N> …` — never actionable: log those tickets as specs awaiting to-tickets, not as no work.
@@ -488,7 +510,7 @@ depth** guard table applied in code. Act on each line as it reads:
488
510
 
489
511
  ## Phase 1 — Pull: judge, then claim or relabel
490
512
 
491
- A Pull is done when `pull.mjs next` exits 0 with its `DISPATCH` line, which phase 2 acts on, or exits 1 with the ticket gone from the Pull: answer each question it stops at, and take the next entry after an exit 1.
513
+ A Pull is done when `pull.mjs next` exits 0 with its `DISPATCH` line, which the dispatch under **Dispatching the Pull's member** acts on, or exits 1 with the ticket gone from the Pull: answer each question it stops at, and take the next entry after an exit 1.
492
514
 
493
515
  Serial, main checkout, one Pull at a time. Never parallel, never inside a member —
494
516
  concurrent `worktree add` and label writes race.
@@ -498,7 +520,7 @@ main checkout (without `--head`, the next shortlist entry). It runs the
498
520
  mechanical steps in order and stops at each judgement with a `QUESTION
499
521
  <qid>:` block and exit 10; `pull.mjs answer <N> <qid> <token> [--path <p>] [--]
500
522
  [<text>…]` records the answer and resumes it. Exit 0 prints `DISPATCH {name,
501
- agent, brief}`, the `task` call phase 2 makes; exit 1 means the ticket left the
523
+ agent, brief}`, the `task` call that the dispatch makes; exit 1 means the ticket left the
502
524
  Pull, so take the next entry; exit 2 names what could not be looked at —
503
525
  `next --head <N>` again retries a step that failed, the same `answer` again
504
526
  retries one that failed while applying an answer (the question stands), and
@@ -595,7 +617,7 @@ worktree re-created for an already-open PR is in references/isolation.md.
595
617
 
596
618
  | `pull.mjs` | At the Pull | Then |
597
619
  |---|---|---|
598
- | 0 `DISPATCH {…}` | decided, live, free, claimed, routed; row, brief and dispatch recorded | the `task` call (phase 2) |
620
+ | 0 `DISPATCH {…}` | decided, live, free, claimed, routed; row, brief and dispatch recorded | the `task` call (**Dispatching the Pull's member**) |
599
621
  | 1 `LEFT #N: taken …` | in-flight hit, an in-flight check that could not answer, or `claim-ticket.sh` exit 2 | next entry |
600
622
  | 1 `LEFT #N: relabelled elsewhere` | no longer labelled `ready-for-agent` | next entry |
601
623
  | 1 `LEFT #N: closed: fixed by …` | `close` answered `yes` | next entry |
@@ -606,508 +628,18 @@ worktree re-created for an already-open PR is in references/isolation.md.
606
628
  | 3 | a usage mistake, or a token the question does not take | correct it; nothing was recorded |
607
629
  | 10 `QUESTION <qid>:` | a question pending | answer it |
608
630
 
609
- ## Phase 2 — dispatch every member
631
+ ### Dispatching the Pull's member
610
632
 
611
633
  A dispatch is done when its `task` call is made, in the background, with the `agent` and the name `ledger.mjs dispatch` printed, and — for an implementer — the tier check has written its verdict: dispatch nothing on top of an unchecked one.
612
634
 
613
- ### The implementer
614
-
615
- **A Pull is one claim followed by one dispatch of a new member under a name no
616
- member has held; nothing holds a queue.** The In-flight bound is the fleet's
617
- own accounting rather than anything the runtime enforces, and the member is
618
- fresh and never a wake of one that already ran. Admission is per slot and
619
- never batched, so there is nothing to hand a freed slot its next ticket but
620
- the next Pull.
621
-
622
- a Pull dispatches its member with one more `task` call — `agent` the definition `ledger.mjs dispatch` printed for it (below); name `impl-<N>`; in the background, so its report arrives on its own.
623
-
624
- **No workpool, and no kernel-resident handle.** A pool's only refill virtue —
625
- handing a queued item to a freed worker without a controller turn — is exactly
626
- what a Pull forbids, and its completions never arrive per member. `eval`'s
627
- `agent()` is not the omp call either: a kernel-resident handle dies with the
628
- kernel and needs a `wait`, while a `task` job is process-level and its report
629
- auto-delivers on its own, like every other member's.
630
-
631
- **No per-call tier or effort anywhere on this path**: tier stays the agent
632
- definition's own frontmatter, whichever `fleet-implementer-<cell>`
633
- definition the Pull names, and a member's Resolved tier stays readable from its transcript.
634
-
635
- **Claim what you are about to dispatch, and dispatch it at once**: `pull.mjs`'s
636
- claim step runs `claim-ticket.sh`, its route, row, render and dispatch steps
637
- follow in the same command, and your `task` call follows its `DISPATCH` line.
638
-
639
- **The dispatch is recorded before you make it**: `pull.mjs`'s row step writes the
640
- row, its render step renders the brief and its dispatch step runs `ledger.mjs
641
- dispatch`, whose `agent` and `brief.path` its `DISPATCH` line prints as `agent`
642
- and `brief`.
643
-
644
- **The prompt carries only what varies**: `pull.mjs`'s render step fills the
645
- implementer brief's placeholders from the Pull's own read, and the agent body
646
- carries everything shared, so never re-paste it.
647
-
648
- **The prompt pastes the Pull's own read of the ticket rather than telling the
649
- member to re-fetch it, and this still costs one `gh issue view` per ticket,
650
- never one per member.** The fresh-context design's isolation rationale — "every
651
- implementer re-reads its Agent Brief... from scratch"
652
- — is about a member
653
- never inheriting another ticket's context, not about where the bytes come from.
654
- Pasting the Pull's own read into the prompt keeps that: the member still gets the
655
- brief verbatim and re-derives nothing from a sibling. What it drops is the
656
- unconditional re-fetch of an issue the controller already has open, on every
657
- dispatch — the fetch the agent body still carries is the backstop for a brief
658
- that turns out to be insufficient, not the default path.
659
-
660
- **A member settling with nothing is not, by itself, a bail.** Each member's
661
- outcome arrives as its own job status. Settle (CONTEXT.md § Coordination) is
662
- three states on omp — `completed`, `failed`, `cancelled` — and reading
663
- `failed`/`cancelled` the same as a `completed` turn that had nothing to report
664
- is the exact collapse this line blocks: a member that died never chose to bail,
665
- so demoting its ticket on its silence demotes a ticket its member never got the
666
- chance to judge. Confirm death first, exactly as the Member-killed row does
667
- (Failure handling, below) — its mtime discriminator, never silence alone.
668
- **Only an explicit bail report — the member itself naming the cause — reaches
669
- Implementer bails before implementing** (**What a wake owes beyond its call**, above); a died or
670
- merely-quiet member's silence never does. A confirmed-dead member recovers
671
- exactly as the Member-killed row says — new member, new name, the SAME ticket —
672
- never a demotion.
673
-
674
- **Dispatch every implementer with the `agent` that `ledger.mjs dispatch`
675
- printed, and still omit `model` on the `task` call.** The
676
- tier lives in that definition's frontmatter (`agents/fleet-implementer-<cell>.agent.md`;
677
- `slow-high`, the policy cell, for a row with no `tier=`), which is what an
678
- omitted `model` takes first — the session's tier applies only when the definition
679
- names none, and a member dispatched with `model` set does not get the declared
680
- tier back. Omitting `model` is therefore still the mechanism; what changed is
681
- that the tier it resolves to is now declared and pinned rather than inherited by
682
- accident. Keep `name: impl-<N>`: the name is what makes a member, and both the
683
- spend classifier and `member-outcomes.mjs` read it.
684
-
685
- ### The reviewer
686
-
687
- **Dispatch the review, never wait on it, and act when its result lands. That is the default path.** See references/reviewers.md.
688
- The tick names every PR owed one — `DISPATCH review PR#<M>`, oldest first —
689
- and each gets one review, once per PR:
690
-
691
- a `task` member named `review-pr-<pr#>`, agent `fleet-review-runner`, its prompt the five args `pr`, `branch`, `worktree`, `testCmd` and `scratch` — it loads `review-eval.mjs` through the Resolver (`~/.fleet/bin/fleet-run --path review-eval.mjs`), awaits `runReviewOnOmp` in its own kernel, writes the result file into the review's own run root and reports its path and `reviewed=` token; record `review=member:review-pr-<pr#>` on the PR's row before the dispatch call. **`scratch` is the scratch root itself, the absolute path `scratch-root.mjs` prints, never the review side's `<scratch>/pr<N>` partition:** the review creates `pr<N>/` under it, so a `pr<N>` passed in nests every run root at `pr<N>/pr<N>/`, and the review refuses a `scratch` whose last component is `pr` plus digits before dispatching anything.
692
-
693
- Write the `review=` token with `ledger.mjs review <pr> <token>`, which appends
694
- it to the **PR's row** — the ticket row once `settle impl-<N>=PR#M` has run,
695
- else `#M`, created when neither exists — and prints that row's key. `review`
696
- is the only writer of `review=`/`reviewed=` tokens, so a PR's tokens sit on
697
- one row in the order you wrote them. The tick counts in-flight reviews off the
698
- ledger and nowhere else, so a review you launched and never recorded is one it
699
- names again. `review` reads the PR's review record across every row that maps
700
- to the PR, in ledger order, and refuses a write that adds a `reviewed=` with no
701
- open launch before it, a launch identity the PR already used, a second launch
702
- while one is open, or an unparseable `review=`/`reviewed=` (the ledger's token
703
- grammar, below), or any token that is not a review token. `row` refuses a
704
- write that adds a review token (one the row holds is carried), drops an open
705
- launch, names a member twice on the line, or makes a PR map to a second row. A
706
- bad token the ledger already holds is carried, never re-judged.
707
-
708
- ### The fix-applier
709
-
710
- **Then dispatch a fix-applier** — on `DISPATCH fix-pr PR#<M>`, which the tick
711
- prints for a PR whose `reviewed=` counts survived or unverified findings that
712
- no review fix-applier
713
- has landed `applied:`/`no-op` on since, with no fix-applier live on it — one
714
- named member per PR, `fix-pr-<pr#>` (the next suffix when that name is already
715
- on record), never the PR's
716
- implementer, its brief rendered with `dispatch-prompt.mjs render fix-applier
717
- --kind review --member fix-pr-<pr#> …` and then recorded with `ledger.mjs
718
- dispatch <pr#> fix-pr-<pr#>` before the call. Its brief carries the PR number, the worktree abs path, the PR's branch,
719
- the same `testCmd` you passed the review, and the **path** `<scratch>/pr<pr>/<run>/review.json`,
720
- `<run>` off the PR's latest `reviewed=` — never the findings. Every `<scratch>`
721
- in that brief is the absolute path `scratch-root.mjs` printed, the
722
- `dispositions-<its name>.json` record's included. The fix-applier owns every per-PR ruling you once made
723
- before dispatch: both mutual-exclusion scans, the suggested-fix re-derivation,
724
- `refuted=false` ≠ apply, per-site measurement for a sibling-site extension, and
725
- `testEnvironment`/`cwdAudit` before any result of the shared test run. It
726
- holds every finding and you hold none, so there is nothing for you to rank,
727
- relay or pre-rule — and a report from a refuter it spawns that surfaces to you
728
- is its to retrieve, never yours to scan or pass on.
729
-
730
- **A `DISPATCH fix-pr PR#<M>` on a conflict hold** — the row carries an
731
- unresolved `conflict-hold:#<M>`, the merge bot's (`run-merge-bot.md` step 1's
732
- fallback) or one you recorded off the tick's `CONFLICT` line —
733
- is the same slot with a different job, and no review file to hand
734
- over. Name it `fix-pr-<M>`, or the next suffix (`-b`, `-c` …) when that name is
735
- already on record — `dispatch` refuses a reused one — render its brief with
736
- `dispatch-prompt.mjs render fix-applier --kind conflict --member <name> …`,
737
- and dispatch it with the
738
- `agent` that `ledger.mjs dispatch` printed, in the PR's existing worktree.
739
- `dispatch` prints this member's definition, `fleet-fix-applier` whatever the
740
- row's `tier=` — read it off that output, never recall it. Its `conflict` brief carries the
741
- same worktree and branch identity paragraph, but in place of the review paragraphs:
742
- bring `<branch>` current with `origin/main`, resolving every conflict by
743
- `run-merge-bot.md`'s **No-undo audit**, run `<checkCmd>`/`<testCmd>`, push, report the new
744
- head. Settle it `applied:<head>` — `no-op` if no conflict was left — which is
745
- what lifts the tick's merge hold; a dispatched-but-live one does not, and
746
- `failed`/`killed` leave the PR fix-due for a replacement. It answers the
747
- hold and nothing else: survived or unverified findings of a returned review the row also carries
748
- stay unanswered through its settle, so the tick prints `DISPATCH fix-pr
749
- PR#<M>` again once it lands, and that one is a review fix-applier, dispatched
750
- as above. On a PR carrying
751
- `ready-to-merge`, the push moves the head after the label, so the next merge
752
- bot refuses it `head-moved-after-label-#<M>`: a fresh finisher, per **Failure
753
- handling**.
754
-
755
- **Then dispatch a red-check fixer** — on a `check`-job `failure` bound to the
756
- PR's current head that is not **solely** staleness: a `rebase-check` red, or
757
- heavy jobs `skipped`, that comes only off the behind-count gets no fixer (the
758
- five-condition note in the fix-applier body states how to establish
759
- that). The tick prints no `DISPATCH` for it, so you reconcile it yourself, and
760
- it always goes after the review, never before. Take the common case, the
761
- implementer's own first CI firing turning red before any review exists: the
762
- review is still dispatched against that head as normal, because a red suite is
763
- review evidence and not a blocker. While a review or a review fix-applier on
764
- the PR is live or still owes its report — a dispositions mismatch awaiting its
765
- retry and a conflict hold awaiting its fix-applier included — the red-check
766
- fixer does not fire: their push starts a new run, and a fixer racing them
767
- would work on a SHA that is still moving. These edges dispatch it, each the
768
- finisher's own: **Monitor: CI run completes** binding a red on a head
769
- whose review has returned and whose fix-applier, if any, has reported;
770
- **A fix-applier reports `no-op`**, or a SHA you have already bound, when that
771
- head's bound conclusion is red; and **A review result lands** with no finding
772
- and nothing to file, when that head's bound conclusion is red. The Monitor is
773
- edge-keyed on `<run-id>:<attempt>:<conclusion>`, so a red that fired before the
774
- review never fires again, and with no push there is no new run — the other
775
- edges are the only route there, so do not wait for an event. Edit one of these
776
- bullets without the others and what strands is exactly the red-only PR.
777
-
778
- Name it `fix-pr-<M>`, or the next free suffix (`-b`, `-c` …) — `dispatch`
779
- refuses a reused name — render its brief with `dispatch-prompt.mjs render
780
- fix-applier --kind red-check --member <name> …`, record it with `ledger.mjs dispatch <pr#> <name>`
781
- before the call, and dispatch it with the `agent` that command printed
782
- (`fleet-fix-applier`, as for a review fix-applier), in the PR's
783
- existing worktree. Its `red-check` brief carries the same worktree and branch
784
- identity paragraph, but in place of the review paragraphs: read the failing job's
785
- log, fix the cause, run `<checkCmd>`/`<testCmd>`, push, report the new head, and **stop**.
786
- It is told outright not to watch, poll or wait for CI: your persistent Monitor
787
- owns the next run on the pushed head, and the finisher gate — review returned
788
- AND fix-applier report sent — applies to this member the same way. Settle it
789
- `applied:<head>` once a fix is pushed. Settle it `no-op` only when the red
790
- turns out not to be this member's to fix — a flake that passes on rerun, or
791
- staleness — with nothing pushed; `no-op` counts as landed, so it never stands
792
- for "could not fix". Settle it `failed` when it cannot reach green, which the
793
- **Failure handling** row "Reviewer, fix-applier or red-check fixer cannot
794
- reach green" covers.
795
-
796
- **What a fix-applier receives.** Its standing text — the worktree and
797
- session-root discipline, the review result file and every ruling on it, the
798
- claim discipline, the refuter prompt, the kill rule, the stash prohibition, the
799
- dispositions record and the report rules — is the body of
800
- `agents/fleet-fix-applier.agent.md`, which the harness injects as its system
801
- prompt, so none of it is pasted. The brief carries what varies, and the
802
- renderer prints it: `~/.fleet/bin/fleet-run dispatch-prompt.mjs render
803
- fix-applier --kind <review|conflict|dispositions-retry|red-check> --member <name>
804
- --worktree <abs-path> --branch <branch> --testCmd <testCmd> --checkCmd <checkCmd> --scratch <scratch>`,
805
- plus `--pr <pr#> --run <run>` for a review or a dispositions retry and
806
- `--mismatched-record <path> --violations <list>` for a retry. Then
807
- `ledger.mjs dispatch <pr#> <name>` records it before the call.
808
-
809
- ### The finisher
810
-
811
- **What a finisher receives.** The instrument re-check, the test-run verdict
812
- rule, duty 1's audit script and what each of its exits means, duty 3's script
813
- and the halt-cause rule are the body of `agents/fleet-finisher.agent.md`, which
814
- the harness injects as its system prompt; none of them is pasted. The
815
- instrument-set rule is stated once, in **Rules that fail silently**, which no finisher
816
- is dispatched to read, so it reaches one through that body. The brief carries
817
- the PR, the worktree, the dispatch pin, `<testCmd>` and the scratch root:
818
- `~/.fleet/bin/fleet-run dispatch-prompt.mjs render finisher --kind <fresh|repair>
819
- --member finisher-pr-<pr#> --pr <pr#> --pin <sha> --worktree <abs-path>
820
- --testCmd <testCmd> --scratch <scratch>`, which `ledger.mjs dispatch <pr#>
821
- finisher-pr-<pr#>` records. See references/finisher.md.
822
-
823
- ### The merge bot
824
-
825
- Per pass, named `merge-bot-<n>`, never two at once. `ledger.mjs dispatch
826
- merge-bot` names it before the dispatch call — `n` is 1 + the highest
827
- `merge-bot-<n>` the session holds, in the ledger's `## Dispatched` list, on a
828
- row or in `## Retired`, so it restarts at 1 only when none was carried or
829
- retired (a `dead` or `none` rotate, or a fresh start), a replacement for a
830
- dead bot takes the next `n`, and nobody counts. With no brief rendered for
831
- that name it refuses, naming the bot and the `dispatch-prompt.mjs render
832
- merge-bot --member merge-bot-<n> …` to run — render it, then run `ledger.mjs
833
- dispatch merge-bot` again. Dispatch
834
- it on the tick's `DISPATCH merge-bot`: the first `ready-to-merge` label is
835
- enough, and it never waits for the finisher's report. Its body
836
- (`agents/fleet-merge-bot.agent.md`) tells it to read
837
- `$(~/.fleet/bin/fleet-run --root)/commands/run-merge-bot.md` and run **one**
838
- pass — drain the queue, re-evaluating after each merge, hold a 15-minute grace
839
- for late labels, then report once and exit — and that you dispatched it,
840
- which is what makes it hold that grace instead of arming its own watcher. Its
841
- brief is `~/.fleet/bin/fleet-run dispatch-prompt.mjs render merge-bot --member
842
- merge-bot-<n> --n <n> --scratch <scratch>`, `<scratch>` the absolute
843
- `scratch-root.mjs` output: the bot writes only under `<scratch>/pr<N>/merge-bot-<n>/`. **Dispatch
844
- it with the `agent` that `ledger.mjs dispatch merge-bot` printed and omit
845
- `model` on the call** — the tier lives in that definition's frontmatter,
846
- `agents/fleet-merge-bot.agent.md`, as the `@smol:low` route, which resolves
847
- through the operator's `modelRoles`.
848
-
849
- Rebase, wait for green, check the
850
- label, merge is checklist work, and a bad merge still needs the label and the
851
- per-job CI state to have been read correctly, which is exactly what a wrong
852
- merge later surfaces and a human resolves; `no-undo-audit.sh` is the
853
- deterministic, script-driven backstop run before every push specifically to
854
- catch a wrongly-resolved conflict, so the tier buys nothing that backstop
855
- doesn't already cover.
856
-
857
- **The shell traps live in the merge bot's agent body, not in its brief and not
858
- in a follow-up message.** A bot already looping cannot be corrected — the loop
859
- consumes the turns a correction would land in — so
860
- `agents/fleet-merge-bot.agent.md` carries **every entry in
861
- `references/shell-traps.md`'s Shell traps**, each written out in full: the rules
862
- with the measurements from that file beside them, and the bot has them before its first
863
- turn. The bot reads an exit code off `gh pr update-branch`, `merge-gate.mjs` and
864
- `prove-merge.sh`, which is the `PIPESTATUS` and read-only-`status` pair, and its
865
- grace poll captures `gh pr list` output and loops over it, which is the
866
- assigned-variable trap. Its brief names the bot and `<scratch>`, plus the
867
- one-line no-acknowledgement notice; do not paste the traps there. An edit to
868
- `references/shell-traps.md` lands in that body in the same change.
869
-
870
- ### The tier check
871
-
872
- **After every Pull's dispatch, the tier check runs — a scripted step, never a
873
- prose reminder.** `tick --apply` runs it itself, a batch of one apiece, on every
874
- implementer it would print `HOLD (tier unchecked impl-<N>)` for, `session` off
875
- the phase-0 receipt, and prints `APPLIED tier-check impl-<N>: <verdict>`. By
876
- hand it is `~/.fleet/bin/fleet-run tier-check.mjs --batch <path-to-batch.json>`,
877
- which judges the implementer just dispatched on the ledger's terms. The definition it
878
- should have run under comes off its ticket row's `tier=` token: none means the
879
- policy cell, `fleet-implementer-slow-high`; `tier=<cell>` means
880
- `fleet-implementer-<cell>` — the same mapping `ledger.mjs dispatch` prints its
881
- `agent` from, so a call that names the printed `agent` passes this half. It
882
- fails the member two ways, one line each —
883
- `member: dispatched <agent>, expected <definition>` when its transcript says
884
- it was dispatched as anything else (a generic `task` dispatch fails even when
885
- its model happens to match), and `member: declared <m>/<l> resolved <m>/<l>`
886
- when the harness resolved a different model or level than that definition
887
- declares. The verdict lands on the ledger either way, written by the check
888
- itself: exit 0 appends `tier-ok=impl-<N>:<definition>` to the row, once
889
- however often it runs; exit 1 runs `ledger.mjs settle impl-<N> tier-mismatch`
890
- — or, for a member already settled another way, which `settle` will not
891
- re-settle, appends `tier-mismatch=impl-<N>:<definition>` instead. Exit 3 is
892
- the third verdict, `tier-unverifiable=impl-<N>:no-transcript`, appended once
893
- for a member settled `killed` or `released` anywhere on the ledger — a row
894
- or `## Dispatched` — whose `session` names an existing directory holding no
895
- `<member>.jsonl`: a dispatch that failed before a transcript was written. It
896
- says nothing about the tier and only clears the unchecked hold. Exit 2
897
- writes nothing — a usage error, which includes a LIVE member with no
898
- transcript; a member settled `PR#M`, `bailed` or `tier-mismatch` with none,
899
- which ran, so the `session` is the wrong directory — pass the one that
900
- dispatched it; a session directory that cannot be searched; and a `session`
901
- that is omitted, empty, or not an existing directory, live member or settled.
902
- A batch holding both a mismatch and an unverifiable member exits 1.
903
-
904
- **The tick holds the next Pull until that verdict exists.** Dispatching the
905
- next member on top of an unchecked or mismatched dispatch multiplies whatever
906
- silently degraded. The tick prints `HOLD (tier unchecked impl-<N>)` for the
907
- newest `impl-` member of a ticket whose row carries none of the three
908
- verdicts, whether that member is still running or already settled `bailed`,
909
- `killed`, `released` or `blocked`, and `HOLD (tier mismatch impl-<N>)` for a mismatched
910
- one. Only the newest member counts, so a replacement (`impl-<N>-b`) is what
911
- clears a mismatch — and is then owed its own check. A mismatch on a ticket
912
- whose issue is already CLOSED holds nothing: the tick probes that issue and
913
- drops it, since there is nothing left to replace. The tick reads no transcript
914
- itself: `tick --apply` runs the check, and an unchecked hold it leaves is
915
- cleared by running the check on that member by hand. A
916
- settled member's check works only when its transcript exists;
917
- without one, the verdict is `tier-unverifiable` for a member settled `killed`
918
- or `released`, and exit 2 naming the wrong session for any other. Clear a
919
- mismatch by fixing the definition or the dispatch and dispatching the
920
- replacement at the right tier.
921
-
922
- **After `/resume`, a carried implementer can hold the whole tick.** A live
923
- implementer rotate carried forward with no verdict on its row is unchecked,
924
- and once this session has been reopened with `/resume` its transcript sits
925
- under the earlier session's directory: the check given this run's `session`
926
- finds no `<member>.jsonl` and exits 2, writing nothing. `HOLD (tier unchecked
927
- impl-<N>)` then stops the whole tick, not only that member's ticket. It
928
- clears once the member settles: one settled `killed` or `released` gets
929
- `tier-unverifiable=` from the same check, and one settled any other way ran,
930
- so pass the directory that dispatched it, as above. Until it settles,
931
- follow the carried token's reconciliation in references/phase-0.md, the
932
- "Resume the claims a dead run left without a PR" section, under its
933
- `prior: ancestor` branch: `proc://` reads `running` → wait; dead → the
934
- Member-killed row.
935
-
936
- **Run the check right after the dispatch, and never reinstall the plugin or
937
- change `modelRoles` while any member is unchecked.** The check reads the
938
- installed definition and the live `modelRoles` at check time, not at
939
- dispatch time, so a late check judges a member against a tier it was never
940
- dispatched under.
941
-
942
- **The batch file is a JSON array, one entry per dispatched member** — under
943
- Pull, a batch of one, the member just dispatched: `{member}`.
944
- `member` is the AgentId the dispatch returns (`impl-<N>`). An entry that
945
- names neither `session` nor `transcript` takes the session directory phase
946
- 0's receipt `.fleet/phase0.json` of `--ledger`'s workspace (else the cwd's,
947
- resolved as the default ledger is) records, and is exit 2 naming the receipt
948
- when it records none; the check reads `<session>/<member>.jsonl`, so a
949
- member with no assistant turn yet still resolves off its dispatch-time
950
- record. A `session` the entry gives
951
- wins over the receipt's; a caller already holding the exact transcript path
952
- may give `transcript` instead. The agent type an implementer was dispatched
953
- as lives only in its transcript. `resolvedModel`
954
- **and** `resolvedThinkingLevel` together — the dispatch's own job record —
955
- may ride beside it and are preferred for the model and level. An `impl-`
956
- entry carries no `agentFile`: its definition is the row's, never the
957
- caller's. `transcript`/`session` resolve relative to `--repo` (defaults to
958
- the plugin's own root), and the check reads and writes the run's own ledger
959
- unless `--ledger` names another.
960
-
961
- **The declaration is `model: "@<role>:<level>"` — a fleet tier route, never
962
- a vendor id.** `@slow`, `@task`, `@smol` resolve through the operator's
963
- `modelRoles`, so an install with no Anthropic model still runs the fleet
964
- at whatever each role points at; the explicit `:<level>` suffix wins over
965
- the role's baked one, and the tier check judges a member against the
966
- role's own target (`modelRoles.<role>`), never a model family.
967
-
968
- Before the run's first dispatch run `~/.fleet/bin/fleet-run tier-roles.mjs
969
- --check --in-flight <B>`, `<B>` the run's In-flight bound (phase 0's
970
- `tier-roles` step runs it); exit 1 names every definition whose `model:` is not a route,
971
- every unset `modelRoles.<role>` a definition needs, every definition whose
972
- `:<level>` its role's target does not run at (omp's model catalog — omp
973
- would clamp it silently), except an implementer definition whose cell the
974
- router never selects (not in `router-table.json`'s `cells` or `rows` values,
975
- and not the policy cell), which is a notice and never stops the run; every
976
- `task.agentModelOverrides` entry that would shadow a fleet definition, and
977
- an `--agents` directory holding no `*.agent.md` definition,
978
- prints the exact remedy, and **stops the run** before any member is
979
- dispatched — the fleet reads that config and never writes it. omp's
980
- `task.maxConcurrency` is not the bound (it counts live members, not tickets,
981
- queues at its ceiling rather than refusing, and is session-wide): below
982
- 2·B+2, or when it cannot be read, the check prints a notice line and still
983
- exits 0, so the notice is the operator's to read and report, never a reason to
984
- stop the run.
985
-
986
- ### The exploration rule
987
-
988
- **Every 5th Pull by ledger count goes at an exploration cell.** See references/pull.md.
989
-
990
- **The router's arguments, and nothing past its line.** `--impl-row` is the
991
- row count the Pull creates — or, while an assignment is rolling (the latest
992
- row 5k and every row after it carry no `tier=`), that row's number;
993
- `--chain-head` marks a ticket another open ticket sequences after, which is
994
- what rolls it. `--arm` is `A` for an even ticket number and `B` for an odd
995
- one; a B Pull routes exactly as an A one until the table opens arm B.
996
- `--session` is this run's own session directory. The printed line is the
997
- whole routing decision — never re-derive `CELL` from the table, the guard or
998
- this paragraph. A missing or unreadable `.fleet/cost-guard.json` routes the
999
- default cell only (`REASON=guard-missing`), burn-in included, and `REASON`
1000
- other than `ok` goes on the row as `route=<REASON>`, nowhere else.
1001
-
1002
- **Count the rate against the ledger, because a Pull is the dispatch unit.** One
1003
- Pull is one member, so a rate counted per Pull is a rate counted per member —
1004
- one implementer in five, however the Pulls fall across the run — and the
1005
- ledger, not your memory, is what still holds the count after a compaction.
1006
-
1007
- **Do not label it anywhere — the dispatch record already carries it.** A
1008
- cell's comparisons are a query, and `cell-readout.mjs` is that query: it joins
1009
- `docs/metrics/ticket-features.tsv`, whose `chosen_cell` names the cell the
1010
- router chose, to `docs/metrics/member-outcomes.tsv` on `session`+`agent`, and
1011
- admits a row only when its `subagent_type` is that cell's
1012
- `fleet-implementer-<cell>` definition and its `effort` is the cell's level.
1013
- That column is the record of what each member was dispatched AS, scraped like
1014
- every other column, so it survives the file's regeneration and mislabels no
1015
- historical row — the pre-cutover `fleet-implementer` and
1016
- `fleet-implementer-alt` rows are no cell's, and the readout never admits them.
1017
- A hand-set column would fail both of those tests, and one derived against
1018
- today's declared tiers would mislabel every historical row.
1019
-
1020
- **The readout counts DELIBERATE comparisons.** See references/pull.md.
1021
-
1022
- **Why one in five within the run, not a week of one tier then a week of the other:**
1023
- tier would then be confounded with calendar date and therefore with prompt
1024
- evolution, which is exactly the state the rows already on disk are in, and why
1025
- they cannot answer the question they were collected for.
1026
-
1027
- The raw split still favours the top tier — 3 failures in 11 `routine`/`sonnet`
1028
- rows against 3 in 55 `routine`/`opus`. **Do not read that as a tier result.**
1029
- Tier remains largely confounded with calendar date and therefore with prompt
1030
- evolution: 8 of the 11 `sonnet` rows fall on 2026-08-13 to 08-17, before the
1031
- window in which 54 of the 55 `opus` rows were run, and the dispatch prompts
1032
- gained rules throughout. The confound is **weakened, not resolved.** Three
1033
- `sonnet` rows now break it rather than one — a maintainer-authorized
1034
- deliberate control (2026-08-20) and two more (2026-08-21) —
1035
- all three run against prompts of the same vintage as the `opus`
1036
- rows, and **all three passed**, which is the direction opposite the raw split.
1037
- Three is still not a result. **What replaces this argument going forward is the
1038
- within-run pairing above**: one implementer in five at the alternate tier makes
1039
- tier orthogonal to date by construction, so the question stops depending on
1040
- whichever rows history happened to leave. **Orthogonal to date, and to nothing
1041
- else** — the alternate member is whichever ticket the 5th Pull draws, rolling
1042
- past every ticket another open one sequences after, while
1043
- the top tier absorbs all of those, so the pairing trades the calendar confound
1044
- for a difficulty one that runs in a known direction. That is why the four covariates
1045
- exist: condition a pair comparison on `sizing`/`profile`/`loc`/`files` before
1046
- reading it as a tier result, never on the raw split.
1047
-
1048
- `minted_false_claim` **discriminates and no longer reads "always yes"** — 20 of
1049
- the 66 `routine` rows carry `no`, 18 at `opus` and 2 at `sonnet`.
1050
- Read it as a property of the TICKET before the tier: a pure code simplification
1051
- need not add prose, while a correction ticket adds prose by construction. The
1052
- honest summary is unchanged and is the reason this paragraph exists: the guard
1053
- fired on the criterion the maintainer chose in advance, not on a demonstration
1054
- that the cheaper tier is worse.
1055
-
1056
- `row` **replaces the whole line**, it does not append. To add a token to a row
1057
- that exists — `ci=`, `held-behind:#M`, `conflict-hold:#M`, `label-off=`, `ports=`
1058
- — run `ledger.mjs token <key> <token>…`, which keeps every token already on the
1059
- row and takes a PR's number as the key. A `row` over a row that already carries
1060
- `KILLED`, `ports=` or `→ PR#` must repeat those tokens or they are gone, and its
1061
- stderr, `rewrote row #N; dropped: <token>…`, names each token it lost.
1062
-
1063
- `ledger.mjs read` is safe on a pipe as well as a redirect: its payload used to
1064
- be abandoned at the pipe buffer and still exit 0, which is what left `board.mjs`
1065
- reporting `ledger read parse failed` and serving a blind cockpit for a whole run.
1066
- Every use above is a recovery path, so a payload that arrives short
1067
- lands exactly where a settled `ruled:` is unrecoverable — which is why the
1068
- script's own suite pins that `read`, `row`, `filed` and `ruled` each reach a
1069
- pipe whole.
1070
-
1071
- `check` reaches a pipe whole at both of its exits too. Its ALREADY FILED exit
1072
- sits mid-branch, where falling through would run the near-miss ranking and the
1073
- tracker search that exit exists to skip — so `check`'s branch is a function now,
1074
- where `process.exitCode` carries the code and a `return` is what skips them.
1075
- No payload this script prints is abandoned to `process.exit()` any more,
1076
- and the suite drives both of `check`'s arms through a pipe on a payload the
1077
- ledger sizes rather than argv. So trust that payload — and read `verdict`,
1078
- because a searched-and-clean tracker and one that was never read share exit 0.
1079
-
1080
- The consumer side raised its cliff rather than removing it. `board.mjs` reads
1081
- `ledger.mjs read` with an explicit `maxBuffer`, so a payload past node's
1082
- default is carried rather than killed mid-flight and reported as an
1083
- unreachable tool — which is how the blind cockpit came back at HTTP 200 even
1084
- after the pipe cut was gone. That cap is deliberately bounded, so a
1085
- payload past it lands in the same blind cockpit — and so does a ledger that
1086
- cannot be read at all, at any size. The read fails open to an empty ledger, and
1087
- `board.json`'s `ledgerState` field now distinguishes a read that failed or
1088
- didn't parse from one that succeeded (`unread`/`unparsed`/`read`) —
1089
- `spend` in the same payload carries its own error the same way. Zeroes
1090
- on the cockpit are not yet proof the pipeline is idle.
1091
-
1092
- ### The test command every gate runs
1093
-
1094
- **Where `testCmd` comes from:** the repository's Test entrypoint, out of the
1095
- Recipe cache phase 0's Recipe derivation step proved —
1096
- `~/.fleet/bin/fleet-run derive-testcmd.sh . test --at origin/main` prints it — and the one you
1097
- hand specialists per `review-and-fix.md`'s **Give specialists a stack-free test command**. Pass
1098
- the same command to the review, to the fix-applier, and to the finisher — whose
1099
- duty-2 mutation
1100
- gate runs it too — so every gate runs one command. Omit it from the review args
1101
- and the review (`review-core.mjs`, in a runner) reads the same cache itself,
1102
- refusing outright when there is none; the fix-applier has no such fallback, so
1103
- substituting `<testCmd>` with nothing leaves it no gate at all.
1104
-
1105
- The review wraps its own one run, so its args carry the bare
1106
- command; a wrapped string handed to it anyway is harmless, the inner wrapper
1107
- being the nested case. The fix-applier and the finisher run what you hand them,
1108
- so hand them the wrapped string as `<testCmd>` —
1109
- `~/.fleet/bin/fleet-run suite-slot.mjs '<command>'`, each `'` inside the command
1110
- written `'\''` — and it is still one command for every gate.
635
+ - **One claim, one dispatch, no queue.** Make one claim and then one dispatch of a new member under a name no member has held; nothing holds a queue. Claim what you dispatch and dispatch at once. Record `ledger.mjs dispatch` before the call.
636
+ - **The implementer.** Dispatch with the printed `agent`, omit `model`, keep `name: impl-<N>`. **No per-call tier or effort anywhere on this path**: tier stays the agent definition's own frontmatter, whichever `fleet-implementer-<cell>` definition the Pull names, and a member's Resolved tier stays readable from its transcript. The prompt carries only what varies, and the Pull's own read of the ticket is pasted, never a request to re-fetch. A member settling with nothing is not a bail. Confirm death first, exactly as the Member-killed row does (Failure handling, below) — its mtime discriminator, never silence alone. Only the member's own bail report counts. A confirmed-dead member recovers exactly as the Member-killed row says — new member, new name, the SAME ticket — never a demotion.
637
+ - **The tier check.** **Run the check right after the dispatch, and never reinstall the plugin or change `modelRoles` while any member is unchecked.** `tick --apply`, or `tier-check.mjs --batch <path>`, runs it. Dispatch nothing on top of an unchecked or mismatched member. **The tick holds the next Pull until that verdict exists.** Clear a mismatch by fixing the definition or the dispatch and dispatching the replacement at the right tier. A mismatch on a ticket whose issue is already CLOSED holds nothing: the tick probes that issue and drops it, since there is nothing left to replace. **After `/resume`, a carried implementer can hold the whole tick.** It holds when the check given this run's `session` finds no `<member>.jsonl` and exits 2, writing nothing. `HOLD (tier unchecked impl-<N>)` then stops the whole tick, not only that member's ticket. It clears once the member settles: one settled `killed` or `released` gets `tier-unverifiable=` from the same check, and one settled any other way ran, so pass the directory that dispatched it, as above. Until it settles, follow the carried token's reconciliation in references/phase-0.md, the "Resume the claims a dead run left without a PR" section, under its `prior: ancestor` branch: `proc://` reads `running` → wait; dead → the Member-killed row.
638
+ - **The exploration rule.** **Every 5th Pull by ledger count goes at an exploration cell.** See references/pull.md. The printed CELL line is the whole routing decision: never re-derive `CELL`, never label a cell by hand. **Do not label it anywhere — the dispatch record already carries it.** **The readout counts DELIBERATE comparisons.** See references/pull.md.
639
+
640
+ Pointers: the `/resume` rules are in `references/phase-0.md`; the tick HOLD tier rules are in `references/tick-rows.md`; shell traps are in `references/shell-traps.md`; the fix-applier's per-PR rulings are in `agents/fleet-fix-applier.agent.md`. Rationale for these rules: `references/member-lifecycle.md`.
641
+
642
+ **The test command every gate runs.** Pass the same command to the review, to the fix-applier, and to the finisher — whose duty-2 mutation gate runs it too — so every gate runs one command. The fix-applier and the finisher run what you hand them, so hand them the wrapped string as `<testCmd>` — `~/.fleet/bin/fleet-run suite-slot.mjs '<command>'`, each `'` inside the command written `'\''` — and it is still one command for every gate. The review wraps its own one run, so its args carry the bare command; a wrapped string handed to it anyway is harmless, the inner wrapper being the nested case. Omit it from the review args and the review (`review-core.mjs`, in a runner) reads the same cache itself, refusing outright when there is none; the fix-applier has no such fallback, so substituting `<testCmd>` with nothing leaves it no gate at all. See `references/isolation.md` (the stack-free test command).
1111
643
 
1112
644
  **Where `<branch>` comes from:** the PR's head branch — `gh pr view <N> --json headRefName -q .headRefName`, or the same branch you cut the worktree from. See references/reviewers.md.
1113
645
 
@@ -1156,7 +688,7 @@ Dispatch a finisher only when every test here passes at once — the review retu
1156
688
  same goes for a red one: a red that fired before the review never fires
1157
689
  again, so the **fix-applier reports `no-op`** and **review result lands**
1158
690
  bullets are also the only routes to the red-check fixer there.
1159
- Then run the tick.
691
+ Then run the tick. These edges dispatch it, each the finisher's own: **Monitor: CI run completes** binding a red on a head whose review has returned and whose fix-applier, if any, has reported; **A fix-applier reports `no-op`**, or a SHA you have already bound, when that head's bound conclusion is red; and **A review result lands** with no finding and nothing to file, when that head's bound conclusion is red.
1160
692
  - **A fix-applier reports `no-op`, or a SHA you have already bound** → dispatch
1161
693
  the finisher **now**, against the existing head — when that head's bound
1162
694
  conclusion is red, the red-check fixer instead (**Then dispatch a red-check
@@ -1172,8 +704,7 @@ Dispatch a finisher only when every test here passes at once — the review retu
1172
704
  whole route to a finisher**: a `no-op` report satisfies the gate above by
1173
705
  itself — the review has returned, because a fix-applier exists only after it
1174
706
  did, and the member has reported — so nothing further is owed and no further
1175
- event is coming. Edit either bullet without the other and what strands is
1176
- exactly the PR that needed no fixes.
707
+ event is coming. Edit either bullet without the other and what strands is exactly the PR that needed no fixes. Edit one of these bullets without the others and what strands is exactly the red-only PR.
1177
708
  - **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → no workflow run will ever
1178
709
  complete for this repo, so the CI-run-completion edge above never fires and
1179
710
  waiting for it stalls the whole PR — the same silent-stall shape reported
@@ -1306,7 +837,7 @@ merge. `review-and-fix.md` states them; the body says they apply.
1306
837
 
1307
838
  **Once the label is on, take it off before you approve any push.** Write `label-off=<latest finisher attempt>` onto the PR's row first
1308
839
  (`ledger.mjs token <PR number> label-off=<latest finisher attempt>`, which keeps
1309
- every token the row already has — **Run ledger**), then
840
+ every token the row already has, see references/ledger.md), then
1310
841
  remove `ready-to-merge`, then approve. Then
1311
842
  dispatch a **fresh** finisher against the new head; the first audit does not
1312
843
  transfer, since it verified a different tree. The merge bot refuses that head on
@@ -1586,6 +1117,8 @@ and flag the PR for a human. A new review of the PR counts again from none.
1586
1117
 
1587
1118
  These rules fail silently when they are skipped: check each before you act on a member's silence, and stop reading silence as a report or as assent.
1588
1119
 
1120
+ Zeroes on the cockpit are not yet proof the pipeline is idle.
1121
+
1589
1122
  **A report is delivered, not implied by ending a turn.** Every member owes
1590
1123
  the controller one report naming its outcome before it exits — including a
1591
1124
  member whose prompt carries no explicit report line. The "report the SHA",
@@ -1840,7 +1373,7 @@ untouched, so re-running it is safe. That append is the whole duty.
1840
1373
 
1841
1374
  The row's last four fields are the ticket's difficulty, and they are what lets a
1842
1375
  tier comparison condition on the thing that swamps it. `sizing` is the
1843
- **member's** own `light`/`heavy` verdict from its phase-2 `sizing-a-ticket`
1376
+ **member's** own `light`/`heavy` verdict from its dispatched `sizing-a-ticket`
1844
1377
  run, which `append` reads itself off the member's report file
1845
1378
  (`<scratch>/reports/impl-<ticket>[-x].json`, its `sizing.verdict`, the highest
1846
1379
  retry letter's when there are several), so you pass no `--sizing`; the member
@@ -1988,212 +1521,6 @@ hold the old text — re-brief only if it changes what they do *now*.
1988
1521
 
1989
1522
  Plan it as a PR the run's own merge queue lands, never as an edit-and-continue. See references/merge-bot.md.
1990
1523
 
1991
- ## Run ledger
1992
-
1993
- Write each state change through `ledger.mjs` before you act on it, never by hand: this section is done when no row, filing or ruling lives only in your context.
1994
-
1995
- One git-ignored `.fleet/ledger.md` per run, updated at **every** state change via
1996
- `~/.fleet/bin/fleet-run ledger.mjs` subcommands (`row`, `filed`, `ruled`,
1997
- `check`, `read`, `dispatch`, `settle`, `review`, `drain`, `rotate`). Your context is the least
1998
- durable thing in the run: it compacts, and a controller that loses the dispatch
1999
- map or the filed list redoes finished work. Two duplicate tickets shipped in one
2000
- run from exactly that.
2001
- Compaction parks every in-flight member and nothing reports it, so on resume
2002
- take the registry census first (**Member unaccounted**, in **Failure
2003
- handling**) before you trust a live token.
2004
-
2005
- **The file lives one run.** Phase 0 rotates it before anything reads it, so
2006
- no exclusion, filing, ruling or drain marker outlives the run that wrote it,
2007
- and no row does either — except, when the previous run was this session's
2008
- own, a row still carrying a live token, which rotate carries forward with
2009
- its live `## Dispatched` entries (phase 0). Never rotate mid-run: it is
2010
- phase 0's step alone.
2011
-
2012
- One line per ticket, rewritten in place (`ledger.mjs row <ticket> <text>`):
2013
-
2014
- ```
2015
- #<t1> impl-<t1>=PR#<p1> → PR#<p1> → MERGED 73b356de
2016
- #<t2> impl-<t2>=PR#<p2> → PR#<p2> · fix-pr-<p2> · ports=16<t2> · ruled:6-applies · held-behind:#<t3>
2017
- #<t3> impl-<t3> · tier=task-high
2018
- #<t4> excluded · behind-pr:#<p2>
2019
- #<t5> excluded · behind-issue:#<t3>
2020
- ```
2021
-
2022
- **A member's token is `<member>` while it is live and `<member>=<outcome>` once
2023
- it settles** — the grammar is `ledger-grammar.mjs`'s, and the tick derives every
2024
- live count from it, so nobody states one. Write neither by hand:
2025
-
2026
- - **`ledger.mjs dispatch <ticket|pr> <member>`** — before the dispatch call,
2027
- marks the member live on its row and appends it to `## Dispatched` with
2028
- `brief=<first 8 hex of its brief's sha256>`. `dispatch
2029
- merge-bot` names the next bot itself. It prints `{member, agent, ticket,
2030
- line, created, total, brief}`, `brief` the `{path, sha256}` of the rendered
2031
- brief the `task` call is handed; `agent` is the definition the `task` call names — an
2032
- implementer's comes off its row's `tier=` (phase 2's tier check states the
2033
- mapping), a finisher's and a merge bot's are their own definitions, and a
2034
- fix-applier's is `fleet-fix-applier` for every kind of work. It
2035
- refuses a member whose `briefs/<member>.json` beside the ledger is missing,
2036
- names another role or carries no sha256, a brief file beside it that is missing or does not hash to that sha256, a name this session already used — on the ledger or in its
2037
- `## Retired` — and names the replacement (`-b`, `-c` …), a
2038
- second live member on one ticket or PR, any implementer once the run is
2039
- draining, an implementer whose row's `tier=` names no single definition
2040
- or one with no file under `agents/`, and a finisher on a ledger the tick
2041
- itself refuses.
2042
- - **`ledger.mjs settle <member> <outcome>`** — rewrites the token to
2043
- `<member>=<outcome>` in both places. Outcomes: `impl-N` = `PR#M | bailed |
2044
- released | killed | tier-mismatch | blocked`; `fix-pr-M` = `applied:<head> | no-op |
2045
- failed | killed`; `finisher-pr-M` = `labelled | failed | killed |
2046
- halted:<cause>`, `<cause>` one of `live-editor | rebase | past-pin |
2047
- unreadable | missing | absent | other` (**Resolving a finisher halt**);
2048
- `merge-bot-n` = `done | killed`. A settled member stays settled. `settle
2049
- impl-N=PR#M` also folds a `#M` row a PR-bound dispatch or a `review` made
2050
- before the settle into `#N`'s row, so the PR's row is the one row naming
2051
- each PR from then on — but only when `#N`'s row named no PR before this
2052
- write (a second `PR#` on it would misattribute the fold, or bury `#N`'s own
2053
- fresher `review=` state under an older one) and no implementer claims `#M`'s
2054
- row as its own. `settle impl-N=PR#M` refuses a settle after which a PR maps
2055
- to more than one row when it did not before, judged after the fold — a PR
2056
- has one row, the PR's row; a split already on disk is carried.
2057
- - **`ledger.mjs settle <member> --from-report [--reports <dir>]`** — the
2058
- settle for a member that wrote a report: reads `<dir>/<member>.json`
2059
- (`<dir>` defaults to `<scratch>/reports`), derives the outcome from it and,
2060
- before writing, checks what that outcome claims — `PR#M`: `verify-sha.sh`
2061
- on the report's branch and head, and PR M OPEN and closing N; `bailed`: a
2062
- `bailCause`, which the receipt names; `applied:<head>`: origin's branch at
2063
- that head; `no-op`: origin's branch at the head the PR's latest
2064
- `reviewed=` names; `labelled`: `ready-to-merge` on the PR and in the
2065
- read-back; `halted:<cause>`: a cause and non-empty evidence; `done`: every
2066
- PR the bot merged reads MERGED. It then records each `unrecorded` filing in
2067
- **filed** and, for a fix-applier, each refutation the report defers in
2068
- **ruled**, once however often it runs, and prints `{member, outcome,
2069
- checks, filed, ruled}`. Exit 3: a check failed — the member's claim and the
2070
- world disagree — and nothing is written; you rule. Exit 2: the report is
2071
- absent or invalid, or a check could not look, and nothing is written; a
2072
- check that could not look is run again once `gh` and `git` answer. The bare
2073
- form above, for an outcome a report carries, warns `settled without
2074
- --from-report` on stderr.
2075
- - **`ledger.mjs review <pr> <token>…`** — the only writer of `review=` and
2076
- `reviewed=` (below): appends each to the PR's row — the ticket row once
2077
- `settle impl-<N>=PR#M` has run, else `#M`, created when neither exists —
2078
- and prints `{pr, row, line, created, next}`, `row` the key written and
2079
- `next` the runner name the PR's next launch takes, null with every letter
2080
- taken. A
2081
- `review=<launch>=failed` whose `review=<launch>` the row holds settles that
2082
- launch in place, alone or with the next launch in the same call. Any other
2083
- token is refused.
2084
- - **`ledger.mjs drain "<reason>"`** — at most one standing drain marker; supply
2085
- stops, the review and merge sides keep going. It lives in this run's
2086
- ledger, so the next run's rotation retires it with everything else.
2087
- - **`ledger.mjs undrain "<reason>"`** — clears the drain marker and refuses when
2088
- no marker is set. `closeout.mjs reopen` runs it.
2089
- - **`ledger.mjs rotate`** — phase 0's first ledger write, every run: moves
2090
- the file as-is to `ledger.<UTC YYYY-MM-DDTHHMMSSZ>.md` beside it under the
2091
- same `<file>.lock`, and the next write starts the empty skeleton — unless
2092
- the previous run was this session's own, when it first writes the rows and
2093
- `## Dispatched` entries still live into a new ledger, and every other
2094
- settled name, or name given an out-of-vocabulary outcome, into its
2095
- `## Retired` (references/phase-0.md, "Rotate the ledger", says which). Refused
2096
- while the recorded controller is alive and not this run's ancestor, ledger
2097
- or none — or, with no record and a ledger to move, while the heartbeat mark
2098
- is still beating, or a tick or ledger write is inside its window with no
2099
- beat — or when the archive name already exists, the process
2100
- table cannot be read, or `.fleet/heartbeat.json` exists but cannot be used
2101
- (unreadable, not JSON, not an object, or a `controller` it cannot judge);
2102
- with no ledger and no such refusal it moves nothing and exits 0. Then it
2103
- records this run's controller in `.fleet/heartbeat.json`. The guard is
2104
- stated in references/phase-0.md, "Rotate the ledger".
2105
-
2106
- `row` refuses a malformed member token anywhere in its text — one whose outcome
2107
- is outside the vocabulary above, e.g. `merge-bot-1=dispatched` — at exit 2,
2108
- naming the token and writing nothing, because every reader would take it as a
2109
- permanent settle. It refuses a write that adds a `review=`/`reviewed=` token
2110
- (one the row already holds is carried), and one after which a PR maps to more
2111
- than one row when it did not before — `row <M>` creating `#M` for a PR a ticket
2112
- row maps, or a rewrite adding a `PR#M` mention to another row — naming the
2113
- PR's row (`settle impl-N=PR#M` applies the same rule after its fold).
2114
-
2115
- `→ PR#M` stays as the human-readable arrow; the tick reads only the `=`
2116
- tokens. `tier=<cell>` marks an Exploration Pull's cell and `route=<REASON>` a router fallback (phase 2), and `excluded ·
2117
- behind-pr:#M | behind-issue:#M` is an Exclusion (phase 1) — a ticket row like
2118
- any other, never a section of its own.
2119
-
2120
- A PR's review is not a member, so it carries its own pair of row tokens,
2121
- written with `review` onto the PR's row (**The reviewer**): `review=wf:<runId>` |
2122
- `review=member:<name>` at launch, `<name>` being
2123
- `review-pr-<n>` for the PR's first runner and the next letter its rows do not
2124
- carry yet (`review-pr-<n>-b`, `-c`, …) for every later one,
2125
- settled dead as `…=failed`, then `reviewed=<head>:<survived>/<refuted>/<unverified>:<run>`
2126
- when the result lands, `<run>` the review's `run-XXXXXXXX` run root under
2127
- `<scratch>/pr<n>/`, which holds its `review.json`. A row with `review=` and no `reviewed=` is a review in
2128
- flight, and the tick counts it as a live review unit unless gh reports its PR
2129
- MERGED or CLOSED. `ci=<run-id>:<attempt>:<conclusion>`
2130
- and `held-behind:#<lower>` are row tokens the same way, and so is
2131
- `conflict-hold:#<pr>` — written by the merge bot itself (`run-merge-bot.md`
2132
- step 1), or by you off the tick's `CONFLICT` line, naming the row's own PR. It
2133
- is not an Exclusion: that gates a ticket's claim, this a PR's merge. `label-off=<finisher-pr-M[-x]>`
2134
- is yours, written before you take `ready-to-merge` off PR M on purpose (taking
2135
- it off before you approve a push, or clearing one left on a moved head),
2136
- naming that PR's latest finisher attempt: the tick reads a missing label as a
2137
- finisher's miss only after the last attempt a `label-off=` names, so without
2138
- it your own removal reads as one. `row` and `token` refuse a `label-off=` naming no
2139
- `finisher-pr` member that `## Dispatched` or some row already carries.
2140
-
2141
- `review` holds a PR's review record to these rules, read across every row that
2142
- maps to the PR — its settled `impl-<N>=PR#<M>` token, else its first `PR#`
2143
- mention, else its key — in ledger order. Every `reviewed=` answers the nearest
2144
- earlier launch that is neither answered nor `=failed`. A launch identity (a
2145
- workflow's runId, or a runner's name, `=failed` aside) appears once, and no launch takes a runner name the
2146
- ledger's `## Retired` holds — an earlier run of this session launched it —
2147
- naming the next free reviewer name instead. No launch appears
2148
- while an earlier one is open — unanswered and not `=failed`. Every
2149
- `review=`/`reviewed=` parses. `row` holds the rest: a launch open before the
2150
- write is still on one of the PR's rows after it, `=failed` allowed, and a
2151
- member name appears once on the line, whatever its outcome. A write that adds
2152
- a violation is refused, exit 2, nothing written; one already on the ledger and
2153
- carried forward is not. A second launch refused for an open one waits for
2154
- that one's `reviewed=`, or — once the Member-killed row confirms its runner
2155
- dead — settles it `=failed` in the same `review` call as the new launch.
2156
-
2157
- Plus two append-only lists:
2158
-
2159
- - **filed** (`ledger.mjs filed <issue> <subject>`, checked with `ledger.mjs check
2160
- <subject>` before every `gh issue create`) — so a finding already recorded
2161
- this run is not filed twice. That is not a guarantee against duplicates at
2162
- large; the tracker query below is what covers those. The FILER writes this
2163
- one, not you: `review-and-fix.md` step 5 and finisher duty 2 each run `filed`
2164
- immediately after their own `gh issue create`, with the subject they passed
2165
- to `check`, and report `filed: #N <subject>`. A filer whose `filed` failed
2166
- twice reports `unrecorded: #N <subject>` instead — an `unrecorded` entry of
2167
- its report file — and `settle <member> --from-report` runs `filed` for it.
2168
- - **ruled** (`ledger.mjs ruled <pr> <decision>`) — PR + decision + one-line reason,
2169
- so a replacement controller does not re-litigate a settled call.
2170
-
2171
- **Writer policy: the controller owns run state (`row`/`token`/`settle`/`dispatch`/`review`/`drain`/`ruled`); filers append filings with `filed`; all writes are serialized by `<file>.lock`.** See references/ledger.md.
2172
-
2173
- **The suite slots wedge the same ways, and clear the same way.**
2174
- `~/.fleet/suite-slots/<i>.lock` is built on the ledger's lock primitive, so an empty or
2175
- unparseable `<i>.lock` and a leftover `<i>.lock.reap` are permanent, a zombie
2176
- holder is transient, and a dead holder is taken over. A wait that expired with
2177
- exit 75 names each wedged file — remove it by hand once no suite run is
2178
- running, the way references/ledger.md removes `<file>.lock`.
2179
-
2180
- **The gap the lock does not close:** `check` → `gh issue create` → `filed` is three commands, and the lock covers each one alone, so two filers can both read `clean` in the seconds before either records. See references/ledger.md.
2181
-
2182
- `check` exits **0** clean, **1** already in this run's filed list, **2** usage error (no JSON on stdout), **3** the ledger is clean but open or closed tracker issues match — read those and decide. See references/ledger.md.
2183
-
2184
- The ledger half reports its own readability as its own field, `ledger.ok`, the
2185
- peer of `tracker.ok`: false when the file did not exist, and false too when the
2186
- path led to something that did not parse as a ledger — which is what tells a
2187
- ledger that was read and held nothing filed from one there was nothing to read. The absent case is the normal one for a run's first `check` — `.fleet/`
2188
- is git-ignored and created lazily by the first write — so it stays `clean` at
2189
- exit 0, and `ledger.ok` is the field that says why rather than the verdict
2190
- moving. The stderr warning is deliberately narrower than the field: it fires on
2191
- a path with no file, so a `--file` that landed on the wrong existing file is a
2192
- silent `ok:false` with nothing on stderr. Gate on the field, not on the warning.
2193
-
2194
- **Write the ledger line before dispatching, not after.** A member that dies
2195
- between spawn and write is invisible — and members die in batches.
2196
-
2197
1524
  ## Failure handling
2198
1525
 
2199
1526
  Match the failure to its row in the table below and run that response; where the row says report or flag, stop there.
@@ -2216,6 +1543,10 @@ Match the failure to its row in the table below and run that response; where the
2216
1543
  | Member **killed** (spend limit, API error, crash) | Confirm it is dead first — a frozen transcript does not establish that; see below — then run the **Member confirmed dead or never dispatched** wake, then new member, new name, prompt carries inherited state |
2217
1544
  | Member **unaccounted** (a live token the tick flags `CHECK-LIVENESS`, or any live token nothing has reported on) | Registry census first: `read history://` once. A row other than `running` → `read agent://<member>` and the artifacts (PR head, worktree, labels), settle the real outcome with the existing vocabulary, and resume a `parked` member with `write agent://<member>` when its work is worth continuing. A `running` row → the mtime discriminator below, two polls: moved means BLOCKED and alive; frozen at both polls **and** no artifact movement → `write proc://<member>/kill`, the **Member confirmed dead or never dispatched** wake, fresh name. Three independent alive-signals — the first poll, the second poll, the artifacts — must all be silent before a kill; the tick settles and kills nothing |
2218
1545
  | A claim abandoned mid-run (a collision found after the claim) | `~/.fleet/bin/fleet-run release-ticket.sh <N> impl-<N> implementer --apply`, the pair the claim took. Run it over every claimed ticket with no PR when the run ends or the maintainer drains, and on the spot for a claim abandoned mid-run (a bail before implementing, a collision found after the claim). **That refusal is the finding, never an obstacle**: a claim carrying commits or a pushed branch is not auto-released, ever — run `worktree-audit.sh` (it audits every worktree; find this claim's row in the output) and decide by hand. Update the released tickets' ledger rows in the same step. See references/reaping.md |
1546
+ | A second review launch refused for an open one | Wait for that one's `reviewed=`, or, once the **Member killed** row has confirmed its runner dead, settle it `=failed` in the same `ledger.mjs review` call as the new launch. |
1547
+ | `ledger.mjs settle <member> --from-report` exits 3 | A check failed: the member's claim and the world disagree, and nothing is written. You rule. |
1548
+ | `ledger.mjs settle <member> --from-report` exits 2 | The report is absent or invalid, or a check could not look; nothing is written. A check that could not look is run again once `gh` and `git` answer. |
1549
+ | A suite wait exits 75 naming a wedged `~/.fleet/suite-slots/<i>.lock` | Remove each named file by hand once no suite run is running, the way references/ledger.md removes `<file>.lock`, then run the suite again. |
2219
1550
 
2220
1551
  ### Recovery
2221
1552