@feigi/fleet-ctl 3.82.13 → 3.82.14

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
 
@@ -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,15 @@ 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
+ ### Dispatching the members a wake names
287
+
288
+ - **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.
289
+ - **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**.
290
+ - **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.
291
+ - **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).
292
+ - **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.
293
+ - **The finisher.** Its render and receives are in `references/finisher.md`.
294
+
286
295
  ### Reading a review result
287
296
 
288
297
  **You read the digest; the findings are the fix-applier's.**
@@ -463,7 +472,7 @@ set it, and never otherwise.
463
472
  It prints `actual/target` and a named ACTION per role, with the whole **Queue
464
473
  depth** guard table applied in code. Act on each line as it reads:
465
474
 
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.
475
+ - `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
476
  - `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
477
  - `SUGGEST /triage, hold idle` — nothing is admissible after the refresh: suggest `/triage` in the report, never run it.
469
478
  - `specs-dropped=#<N> …` — never actionable: log those tickets as specs awaiting to-tickets, not as no work.
@@ -488,7 +497,7 @@ depth** guard table applied in code. Act on each line as it reads:
488
497
 
489
498
  ## Phase 1 — Pull: judge, then claim or relabel
490
499
 
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.
500
+ 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
501
 
493
502
  Serial, main checkout, one Pull at a time. Never parallel, never inside a member —
494
503
  concurrent `worktree add` and label writes race.
@@ -498,7 +507,7 @@ main checkout (without `--head`, the next shortlist entry). It runs the
498
507
  mechanical steps in order and stops at each judgement with a `QUESTION
499
508
  <qid>:` block and exit 10; `pull.mjs answer <N> <qid> <token> [--path <p>] [--]
500
509
  [<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
510
+ agent, brief}`, the `task` call that the dispatch makes; exit 1 means the ticket left the
502
511
  Pull, so take the next entry; exit 2 names what could not be looked at —
503
512
  `next --head <N>` again retries a step that failed, the same `answer` again
504
513
  retries one that failed while applying an answer (the question stands), and
@@ -595,7 +604,7 @@ worktree re-created for an already-open PR is in references/isolation.md.
595
604
 
596
605
  | `pull.mjs` | At the Pull | Then |
597
606
  |---|---|---|
598
- | 0 `DISPATCH {…}` | decided, live, free, claimed, routed; row, brief and dispatch recorded | the `task` call (phase 2) |
607
+ | 0 `DISPATCH {…}` | decided, live, free, claimed, routed; row, brief and dispatch recorded | the `task` call (**Dispatching the Pull's member**) |
599
608
  | 1 `LEFT #N: taken …` | in-flight hit, an in-flight check that could not answer, or `claim-ticket.sh` exit 2 | next entry |
600
609
  | 1 `LEFT #N: relabelled elsewhere` | no longer labelled `ready-for-agent` | next entry |
601
610
  | 1 `LEFT #N: closed: fixed by …` | `close` answered `yes` | next entry |
@@ -606,508 +615,18 @@ worktree re-created for an already-open PR is in references/isolation.md.
606
615
  | 3 | a usage mistake, or a token the question does not take | correct it; nothing was recorded |
607
616
  | 10 `QUESTION <qid>:` | a question pending | answer it |
608
617
 
609
- ## Phase 2 — dispatch every member
618
+ ### Dispatching the Pull's member
610
619
 
611
620
  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
621
 
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.
622
+ - **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.
623
+ - **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.
624
+ - **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.
625
+ - **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.
626
+
627
+ 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`.
628
+
629
+ **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
630
 
1112
631
  **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
632
 
@@ -1156,7 +675,7 @@ Dispatch a finisher only when every test here passes at once — the review retu
1156
675
  same goes for a red one: a red that fired before the review never fires
1157
676
  again, so the **fix-applier reports `no-op`** and **review result lands**
1158
677
  bullets are also the only routes to the red-check fixer there.
1159
- Then run the tick.
678
+ 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
679
  - **A fix-applier reports `no-op`, or a SHA you have already bound** → dispatch
1161
680
  the finisher **now**, against the existing head — when that head's bound
1162
681
  conclusion is red, the red-check fixer instead (**Then dispatch a red-check
@@ -1172,8 +691,7 @@ Dispatch a finisher only when every test here passes at once — the review retu
1172
691
  whole route to a finisher**: a `no-op` report satisfies the gate above by
1173
692
  itself — the review has returned, because a fix-applier exists only after it
1174
693
  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.
694
+ 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
695
  - **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → no workflow run will ever
1178
696
  complete for this repo, so the CI-run-completion edge above never fires and
1179
697
  waiting for it stalls the whole PR — the same silent-stall shape reported
@@ -1586,6 +1104,8 @@ and flag the PR for a human. A new review of the PR counts again from none.
1586
1104
 
1587
1105
  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
1106
 
1107
+ Zeroes on the cockpit are not yet proof the pipeline is idle.
1108
+
1589
1109
  **A report is delivered, not implied by ending a turn.** Every member owes
1590
1110
  the controller one report naming its outcome before it exits — including a
1591
1111
  member whose prompt carries no explicit report line. The "report the SHA",
@@ -1840,7 +1360,7 @@ untouched, so re-running it is safe. That append is the whole duty.
1840
1360
 
1841
1361
  The row's last four fields are the ticket's difficulty, and they are what lets a
1842
1362
  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`
1363
+ **member's** own `light`/`heavy` verdict from its dispatched `sizing-a-ticket`
1844
1364
  run, which `append` reads itself off the member's report file
1845
1365
  (`<scratch>/reports/impl-<ticket>[-x].json`, its `sizing.verdict`, the highest
1846
1366
  retry letter's when there are several), so you pass no `--sizing`; the member
@@ -2009,7 +1529,7 @@ own, a row still carrying a live token, which rotate carries forward with
2009
1529
  its live `## Dispatched` entries (phase 0). Never rotate mid-run: it is
2010
1530
  phase 0's step alone.
2011
1531
 
2012
- One line per ticket, rewritten in place (`ledger.mjs row <ticket> <text>`):
1532
+ One line per ticket, rewritten in place (`ledger.mjs row <ticket> <text>`; `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row):
2013
1533
 
2014
1534
  ```
2015
1535
  #<t1> impl-<t1>=PR#<p1> → PR#<p1> → MERGED 73b356de
@@ -2029,7 +1549,7 @@ live count from it, so nobody states one. Write neither by hand:
2029
1549
  merge-bot` names the next bot itself. It prints `{member, agent, ticket,
2030
1550
  line, created, total, brief}`, `brief` the `{path, sha256}` of the rendered
2031
1551
  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
1552
+ implementer's comes off its row's `tier=` (the tier check under **Dispatching the Pull's member** states the
2033
1553
  mapping), a finisher's and a merge bot's are their own definitions, and a
2034
1554
  fix-applier's is `fleet-fix-applier` for every kind of work. It
2035
1555
  refuses a member whose `briefs/<member>.json` beside the ledger is missing,
@@ -2063,7 +1583,8 @@ live count from it, so nobody states one. Write neither by hand:
2063
1583
  that head; `no-op`: origin's branch at the head the PR's latest
2064
1584
  `reviewed=` names; `labelled`: `ready-to-merge` on the PR and in the
2065
1585
  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
1586
+ PR the bot merged reads MERGED. `PR#M` and `applied:<head>` also need a
1587
+ green Check-step record (`SETTLE`). It then records each `unrecorded` filing in
2067
1588
  **filed** and, for a fix-applier, each refutation the report defers in
2068
1589
  **ruled**, once however often it runs, and prints `{member, outcome,
2069
1590
  checks, filed, ruled}`. Exit 3: a check failed — the member's claim and the
@@ -2113,7 +1634,7 @@ row maps, or a rewrite adding a `PR#M` mention to another row — naming the
2113
1634
  PR's row (`settle impl-N=PR#M` applies the same rule after its fold).
2114
1635
 
2115
1636
  `→ 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 ·
1637
+ tokens. `tier=<cell>` marks an Exploration Pull's cell and `route=<REASON>` a router fallback (the dispatch's CELL line), and `excluded ·
2117
1638
  behind-pr:#M | behind-issue:#M` is an Exclusion (phase 1) — a ticket row like
2118
1639
  any other, never a section of its own.
2119
1640