@feigi/fleet-ctl 3.82.19 → 3.82.21

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.
@@ -47,13 +47,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
47
47
  `$ARGUMENTS` gave one, `--plugin-checkout <dir>` when this workspace tracks
48
48
  no `plugin/…` of its own, `--spend-dir <dir>` to name the directory the
49
49
  cockpit's spend panel reads, and `--workflow <name>` or `--declare-no-ci`
50
- when the repository has no workflow named CI. It runs the start sequence in
51
- its fixed order — branch, fast-forward, pin, baseline, preflight, ruleset,
52
- tier-roles, metrics, cockpit, rotate, reclaim, recipe, fold-in, stranded,
53
- shortlist — prints one `phase0: <step>: <status>` line per step with the
54
- `NEED` lines last, and records each step in `.fleet/phase0.json`, which the
55
- tick reads: it prints `PHASE0-INCOMPLETE` and holds every dispatching row
56
- until the receipt is whole. Dispatch nothing before it exits 0. Exit 4 is a
50
+ when the repository has no workflow named CI. Its steps and receipt: See
51
+ references/phase-0.md. Dispatch nothing before it exits 0. Exit 4 is a
57
52
  step that needs you: act on each `NEED` line below, then run it again. Once
58
53
  `rotate` has run, a re-run skips every `ok` step and resumes where the last
59
54
  one stopped; before it, a re-run runs every step again.
@@ -90,43 +85,12 @@ phase, or in any later one, asks the maintainer which tickets to take.
90
85
  go on. See references/phase-0.md.
91
86
 
92
87
  1. **Build the Shortlist** — `~/.fleet/bin/fleet-run shortlist.mjs`, then read
93
- `.fleet/shortlist.json`. The script runs the cheap filters and nothing else
94
- — no ticket body is read here, because a Pull reads one ticket in full at
95
- the moment it admits it:
96
-
97
- - **Candidate scan** — `candidates.mjs --require-label ready-for-agent
98
- --with-dropped`, oldest-first, never re-ranked. Its row count is
99
- `scanned`, the supply; the rows its to-spec filter removed are
100
- `dropped`, counted nowhere.
101
- - **Dependency scan**, `next-ticket` step 2, on each row's `d` array. Open
102
- blocker → drop.
103
- - **Live Exclusions** — a ticket whose ledger row reads `excluded ·
104
- behind-pr:#M` or `excluded · behind-issue:#M` stays out while its premise
105
- holds (references/pull.md, **Exclusion**).
106
- - **In-flight check** — `inflight.sh <N>` per survivor. Exit 1 is taken, and
107
- any other non-zero exit is a check that could not answer, which is never
108
- free. Both drop the ticket, logged by number.
109
-
110
- It writes the survivors, in order, to `.fleet/shortlist.json` —
111
- `{scanned, shortlist: [{n, t}], dropped: [{n, reason}]}` — resolved against the git common dir by
112
- `fleet-dir.mjs`, the one locator `ledger.mjs` resolves `.fleet/ledger.md`
113
- with too, so you, the tick and a member in its own worktree all name one
114
- file. **Exit 0 is the file written,
115
- an empty shortlist included** — the empty queue is an answer. **Exit 2 is a
116
- refusal**: the scan, the ledger read or the write did not answer, and the
117
- previous file stands untouched, because an empty file written then would
118
- read as "no work". Never read 2 as empty.
119
-
120
- Every probe fails CLOSED: an unreadable blocker counts as open, an
121
- unreadable premise leaves its exclusion standing, an unanswered in-flight
122
- check counts as taken. A ticket dropped wrongly waits one refresh; a ticket
123
- admitted wrongly costs a Pull's full read at best and two members on one
124
- ticket at worst.
125
-
126
- **`--require-label ready-for-agent` mandatory, no fallback** — the script
127
- fixes it, and you never run `candidates.mjs --allow-fallback` by hand to
128
- widen the list: `ready-for-human` needs a human to brainstorm first and you
129
- have no channel to one mid-flight.
88
+ `.fleet/shortlist.json` — `{scanned, shortlist: [{n, t}], dropped: [{n, reason}]}`:
89
+ the survivors of its cheap filters, every probe failing CLOSED
90
+ (references/phase-0.md, **Build the Shortlist**). Exit 0 is the file
91
+ written, an empty shortlist included; exit 2 is a refusal and the previous
92
+ file stands — never read 2 as empty. **`--require-label ready-for-agent`
93
+ mandatory, no fallback**: never run `candidates.mjs --allow-fallback` by hand.
130
94
 
131
95
  `candidates.mjs`'s exit 3 reaches you as an empty shortlist whose `dropped`
132
96
  names every ticket the to-spec filter took, by number and reason — and the
@@ -142,22 +106,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
142
106
  `--allow-fallback`, whose second pass is unfiltered and so reaches the
143
107
  `ready-for-human` and untriaged work you have no channel for. A partial
144
108
  drop, survivors and specs together, is named the same way.
145
- 2. **Refresh is the tick's from here on.** `fleet-tick.mjs` runs
146
- `shortlist.mjs` itself when unclaimed entries fall below the In-flight
147
- bound, when the file is missing or empty, when an `excluded` premise has
148
- lifted, and when a branch-named `behind-pr` exclusion whose PR is not yet
149
- MERGED or CLOSED, or a `behind-issue` exclusion whose issue is not yet
150
- closed, names a ticket the file still carries, and prints
151
- `REFRESHED shortlist: <n> entries; <k> lifted`. A head whose Pull ended
152
- this run is not counted, unless on a lifted exclusion. Phase 0 runs it by hand
153
- once because a dead run's shortlist is stale by definition.
154
- None of those triggers runs while `HOLD (in-flight <n>/<B>)` stands.
155
-
156
- **No multi-select, no maintainer tick.** Selection and ordering are batched —
157
- one scan, oldest first; admission never is. The invariant this replaces — one
158
- human decision per batch of admissions, zero unilateral grabs — is retired
159
- is retired: zero human decisions per admission, and every non-admission is a
160
- label or a premise the maintainer can read.
109
+ 2. **Refresh is the tick's from here on**: it prints `REFRESHED shortlist: <n>
110
+ entries; <k> lifted` (references/tick-rows.md, **Refresh is the tick's**).
161
111
 
162
112
  ## The wake table
163
113
 
@@ -180,13 +130,8 @@ evidence cannot settle**.
180
130
  notification, a label, a CI run reaching a terminal state, a `CONFLICT` line, a
181
131
  heartbeat, the maintainer's drain — is one `fleet-event.mjs` call, which makes
182
132
  the wake's writes and ends in one `fleet-tick.mjs` invocation, and you do what
183
- it prints. There are no hand-run
184
- refill or dispatch edges left. Record first, because the tick reads the run off
185
- `.fleet/ledger.md` and nothing else of yours: a wake you have not written down is
186
- one it cannot see, and it names the same dispatch again. Every record goes
187
- through `ledger.mjs` (references/ledger.md), which `fleet-event.mjs` drives — never a
188
- hand edit. What each kind writes is in the header comment of `fleet-event.mjs`.
189
- See references/ci-and-staleness.md.
133
+ it prints. There are no hand-run refill or dispatch edges left, and no hand
134
+ edit of the ledger. See references/ci-and-staleness.md.
190
135
 
191
136
  Run each as `~/.fleet/bin/fleet-run fleet-event.mjs <kind> <args>`, from the main checkout:
192
137
 
@@ -231,10 +176,7 @@ owes beyond its call:
231
176
  Read the digest as **Reading a review result** says.
232
177
  - **Monitor: `ready-to-merge` appears** → `fleet-event.mjs label <pr>`, and
233
178
  on `DISPATCH merge-bot` dispatch the next `merge-bot-<n>` (**The merge bot**).
234
- Catches hand-added labels as well as a finisher's own. The bot does not wait
235
- for that finisher's report: its rebase is server-side,
236
- `gh pr update-branch --rebase`, so nothing on the finisher's worktree has to
237
- happen first. **You own the watcher, not the bot.** Arm one yourself,
179
+ **You own the watcher, not the bot.** Arm one yourself,
238
180
  `persistent: true`, seeded before the loop so handled PRs do not re-fire.
239
181
  **One rebase per PR, at merge time.** Any behind-count you hand a bot is
240
182
  expired on arrival; say so. See references/ci-and-staleness.md.
@@ -247,36 +189,31 @@ owes beyond its call:
247
189
  pass. Read `worktreesRemoved` alongside `reaped`: a pass that
248
190
  reaps branches and removes no worktrees is a finding, not a quiet success.
249
191
  Update the reaped tickets' ledger rows in the same step.
250
- **Do not invoke `commit-commands:clean_gone`.** The skill is the maintainer's
251
- to fix; do the reap yourself. **Never reap a branch a live member is on.**
252
- Cross-check `.fleet/ledger.md` first:
253
- a row without a terminal state means someone may still be in that worktree, and
254
- the dirty check does not see a member that committed but has not pushed.
192
+ **Do not invoke `commit-commands:clean_gone`**: do the reap yourself. **Never
193
+ reap a branch a live member is on** — cross-check `.fleet/ledger.md` first
194
+ for a row without a terminal state.
255
195
  See references/reaping.md.
256
- - **The run ends, or the maintainer says drain** → `fleet-event.mjs drain --reason "<reason>"`
257
- first (below). The marker is on disk, so the tick
258
- holds the implementer row at `HOLD (draining)` from then on — for a
259
- replacement controller too — and keeps the review, fix-applier and merge rows
260
- firing until the open PRs merge. The drain starts the end of the run; it does
261
- not end it. **End of run**, in this order:
196
+ - **The run ends, or the maintainer says drain** → **End of run**, in this
197
+ order; the drain starts the end of the run, it does not end it:
262
198
  1. `~/.fleet/bin/fleet-run fleet-event.mjs drain --reason "<reason>"` — runs
263
- `closeout.mjs begin --reason "<reason>"` (references/reaping.md), then the tick. A claim it refuses prints `NEED hand-release #<N> <blocker>`: that
264
- refusal is the finding, so run `worktree-audit.sh` and decide by hand.
265
- `begin` selects a claim with no PR and a live or `bailed` implementer token
266
- on every run, so a claim you keep leaves `begin` at exit 4 on every run:
199
+ `closeout.mjs begin --reason "<reason>"` (references/reaping.md), then the
200
+ tick, which holds the implementer row at `HOLD (draining)` from then on. A
201
+ claim it refuses prints `NEED hand-release #<N> <blocker>`: that refusal is
202
+ the finding, so run `worktree-audit.sh` and decide by hand. `begin`
203
+ selects a claim with no PR and a live or `bailed` implementer token on
204
+ every run, so a claim you keep leaves `begin` at exit 4 on every run:
267
205
  release the rest by hand and run `begin` again.
268
206
  2. Act on the review, fix-applier and merge rows until the tick prints
269
207
  `ACTION: run over → fleet-heartbeat.mjs --stop "<reason>"`, which it does
270
208
  once the run is draining with no open PR and no live implementer.
271
- 3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"` — scrapes
272
- the run's member facts, checks the tier-outcomes rows, refreshes the
273
- metrics, commits the three metrics files and opens their PR, sets the
274
- phase-0 receipt aside as `.fleet/phase0.previous.json`, then records
275
- `fleet-heartbeat.mjs --stop` last, because every reader takes a recorded
276
- stop as the end of the run. A `NEED tier-outcomes append <pr>` is a PR
277
- the ledger marks reviewed with no verdict row: append it with the verdict
278
- you rule (**Guard: accumulate per PR**) and run `finish` again. A step it
279
- fails is explained in references/close-out.md. Exit 0 is the run closed.
209
+ 3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"`, which
210
+ then records `fleet-heartbeat.mjs --stop` last, because every reader takes
211
+ a recorded stop as the end of the run. A `NEED tier-outcomes append
212
+ <pr>` is a PR the ledger marks reviewed with no verdict row: append it with
213
+ the verdict you rule (**Guard: accumulate per PR**) and run `finish` again.
214
+ A reviewed PR that closes no issue takes no row and is never a NEED.
215
+ A step it fails is explained in references/close-out.md. Exit 0 is the run
216
+ closed.
280
217
  Drain only when a documented trigger fired, and name the trigger in `--reason`.
281
218
  **Undrain** only when the maintainer asks, never a drain that budget or
282
219
  context exhaustion caused (references/close-out.md).
@@ -286,7 +223,7 @@ Write each state change through `ledger.mjs` before you act on it, never by hand
286
223
 
287
224
  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.
288
225
 
289
- 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.
226
+ `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row; re-typing the row with `row` drops the tokens you do not restate. Write member tokens and `review=` tokens only through `ledger.mjs`, never by hand.
290
227
 
291
228
  Never rotate the ledger mid-run: it is phase 0's step alone.
292
229
 
@@ -296,11 +233,11 @@ Record each ruling with `ledger.mjs ruled <pr> <decision>`, one line with its re
296
233
 
297
234
  ### Dispatching the members a wake names
298
235
 
299
- - **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.
236
+ - **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, with `ledger.mjs review <pr> <token>`: the tick counts in-flight reviews off the ledger and nowhere else. `scratch` is the scratch root itself, never a `pr<N>` partition (`references/reviewers.md`). The runner writes the result file and reports its path and `reviewed=` token.
300
237
  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.
301
- - **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**.
302
- - **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.
303
- - **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).
238
+ - **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, and a refuter report that surfaces to you is its to retrieve, never yours to scan or pass on. On a PR carrying `ready-to-merge`, its push moves the head after the label: a fresh finisher, per **Failure handling**.
239
+ - **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. It answers the hold and nothing else: review findings the row also carries get a review `DISPATCH fix-pr PR#<M>` once it lands.
240
+ - **Then dispatch a red-check fixer** — on a `check`-job `failure` bound to the PR's current head that is not **solely** staleness (the five-condition note in `agents/fleet-fix-applier.agent.md`). 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. Never 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; a red before any review still gets its review first. The tick prints no `DISPATCH` for it: reconcile it yourself, always after the review. The finisher gate applies to it as to a fix-applier. Settle it `applied:<head>` once a fix is pushed; `no-op` only when the red is not its to fix — a flake that passes on rerun, or staleness — with nothing pushed, never for "could not fix"; `failed` when it cannot reach green (**Failure handling**). Its three dispatch edges are under **The finisher gate**; their rationale is in `references/member-lifecycle.md` (The red-check fixer's three edges).
304
241
  - **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.
305
242
  - **The finisher.** Its render and receives are in `references/finisher.md`.
306
243
 
@@ -312,8 +249,7 @@ A row with `review=` and no `reviewed=` is a review in flight, and the tick coun
312
249
 
313
250
  **Name every member.**
314
251
  Names follow the unit of work: `impl-<issue#>`, `fix-pr-<pr#>`,
315
- `review-pr-<pr#>`, `finisher-pr-<pr#>`, `merge-bot-<n>`. See
316
- references/member-lifecycle.md.
252
+ `review-pr-<pr#>`, `finisher-pr-<pr#>`, `merge-bot-<n>`.
317
253
 
318
254
  **Fresh context per member.** One member, one unit of work, gone. Refill =
319
255
  **new** member, **new** name. See references/member-lifecycle.md.
@@ -321,13 +257,10 @@ references/member-lifecycle.md.
321
257
  **The scratch root is computed, never invented and never recorded.** You run
322
258
  `~/.fleet/bin/fleet-run scratch-root.mjs`; a non-zero exit is an environment to
323
259
  fix, not a cue to pick another root.
324
- `<scratch>` in this file is the path it prints, resolved before you send anything: every
325
- brief that carries one carries the absolute path you passed it — an
326
- implementer's `<scratch>/impl-<N>/`, the recipe deriver's
327
- `<scratch>/derive-recipe/`, a fix-applier's, a finisher's and a merge bot's
328
- `<scratch>/pr<N>/…`, and a review runner's `scratch` argument, which is the
329
- root itself. A member whose prompt names no path asks you by name and never
330
- makes one up. See references/isolation.md.
260
+ `<scratch>` in this file is the path it prints, resolved before you send
261
+ anything: every brief that carries one carries the absolute path. A member
262
+ whose prompt names no path asks you by name and never makes one up. See
263
+ references/isolation.md.
331
264
 
332
265
  ### Reading a review result
333
266
 
@@ -337,74 +270,23 @@ is the whole of your read, and it is what you record: `reviewed=<head>:<survived
337
270
  off `head`, `counts` and the run root's name — the runner's `ledger` field — on
338
271
  the PR's row, with `ledger.mjs review <pr>`. Every later path to the review is built from that token, before a
339
272
  restart or after one: `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's
340
- latest `reviewed=`. Findings never enter your context —
341
- a result runs 26-61 KB, 7-15k tokens that used to land in yours and then again,
342
- up to ~10k characters, in every fix-applier prompt you wrote. The fix-applier
273
+ latest `reviewed=`. Findings never enter your context. The fix-applier
343
274
  reads every one off the file and makes every ruling on them
344
- (`review-and-fix.md`'s **The review result file**). What the digest tells you:
345
-
346
- **`testEnvironment` says what the review's shared test run is evidence about**,
347
- and it is present on a healthy run as well as a degraded one, so there is
348
- nothing to notice by its absence. The
349
- fix-applier reads it before acting on any count from the shared test run; you
350
- read it so a degraded review never passes for a clean one. See references/reviewers.md.
351
- `unverified` is *not* "checked and cleared" — a `suggestion` skips the pass by
352
- policy, and a finding whose refuters all crashed or abstained lands there too.
353
- All go to the fix-applier with the rest, in the file; never rule on them yourself.
354
- **`refutersDispatched`, carried on every finding, is what tells the skip
355
- apart** — zero is the policy skip, above zero with no deciding vote is a
356
- refuter pass that decided nothing — and `refutersInconclusive` splits that
357
- one: the refuters it counts abstained, every other one dispatched crashed. So
358
- the population is read off those fields rather than off severity,
359
- which records only how much a finding would matter if true. `resume` is
360
- non-null exactly when a specialist or a refuter pair crashed again after the
361
- review's own in-run retry, and **resume beats handing a crash-heavy review
362
- on**: a deferred crash is a finding nobody ever looked at.
363
- No replay exists for a review that ran in a runner's own kernel — `resume` is reported, not acted on, and the fix-applier defers what it names.
364
- `refuted` comes back deliberately as well — a refutation is itself a claim, and
365
- one has been reversed on new evidence. The fix-applier may reverse one; it
366
- records each reversal as a `refuted` entry in its disposition record, the
367
- evidence in `reason`, and you copy those entries to the ledger's `ruled` line by
368
- running `fleet-event.mjs fix-report`, whose `settle --from-report` records each
369
- refutation the report defers.
370
-
371
- **`dimensionsRun` is the dispatch; `dimensionsUnrun` is what names a gap.** A
372
- specialist that dies contributes zero findings while its key stays in
373
- `dimensionsRun`. So read the two together. A key
374
- in `dimensionsRun` and NOT in `dimensionsUnrun` returned a review; every
375
- `dimensionsUnrun` entry is `{dimension, reason}` naming a specialist that
376
- returned nothing, its `reason` the crash.
377
- The review already re-dispatched each crashed specialist once, so what
378
- `dimensionsUnrun` still names is residue: the fix-applier names it unrun in its
379
- report and re-runs nothing — an absence of findings is not coverage.
380
-
381
- **`suiteVerdict` is the review's one judgement on its shared test run:
382
- `{usable, reason}`, always present.** The review runs the test command once,
383
- and every specialist reads that run instead of running its own. `{usable: true,
384
- reason: null}` says the run executed real tests and its exit status is
385
- explained by its counts; usable is not green, so a usable run can have failing
386
- tests. `usable: false` carries `reason`, including a run that never started
387
- (install skipped, no suite slot). The verdict says nothing about whether a
388
- dimension reviewed: a specialist that returned reviewed, its findings verified
389
- as usual, whatever the suite verdict. With `usable: false` the
390
- fix-applier names `shared test run not usable: <reason>` in its final verdict
391
- and does nothing more — its own `<testCmd>` run and CI cover the suite.
392
- **Exit 75 is not a test result.** In a review it makes
393
- `suiteVerdict` not usable, `suiteVerdict.reason` beginning "no suite slot within
394
- <n> s — the host is at its suite bound; not a test failure", with no retry
395
- inside the run: read that `reason` off the digest and re-review it
396
- under the next runner letter (`review-pr-<pr#>-b` after a first runner), as for
397
- a runner that reported `failed`.
398
-
399
- **`cwdAudit` is the specialist's own inherited-checkout audit, one entry per
400
- dispatched dimension: `{dimension, state, line}`, `state` one of**
401
- `clean`/`dirty`/`unrepo`/`missing`. The prompt asks every specialist to run
402
- `git status --porcelain -uall` against the directory it inherited before
403
- returning and fold the result into a `CWD-AUDIT:` line — a convention, not
404
- schema, so a specialist that never emits it or misspells it still validates
405
- clean. This field is the runtime backstop that reads for the line regardless:
406
- `missing` is exactly as loud as `dirty`, and both name a checkout you should
407
- treat as a live scratch write until you have checked it.
275
+ (`review-and-fix.md`'s **The review result file**); never rule on one
276
+ yourself. Each digest field in full: See references/reviewers.md. What the
277
+ digest asks of you:
278
+
279
+ - **`testEnvironment` says what the review's shared test run is evidence about**: read it so a degraded review never passes for a clean one.
280
+ - **`resume`** non-null: resume beats handing a crash-heavy review on, except that no replay exists for a review that ran in a runner's own kernel — `resume` is reported, not acted on, and the fix-applier defers what it names.
281
+ - **`unverified`** is *not* "checked and cleared", and **`refuted`** may be reversed: copy the fix-applier's reversals to the ledger's `ruled` line by running `fleet-event.mjs fix-report`.
282
+ - **`suiteVerdict`** — **Exit 75 is not a test result.** In a review it makes
283
+ `suiteVerdict` not usable, `suiteVerdict.reason` beginning "no suite slot within
284
+ <n> s — the host is at its suite bound; not a test failure", with no retry
285
+ inside the run: read that `reason` off the digest and re-review it
286
+ under the next runner letter (`review-pr-<pr#>-b` after a first runner), as for
287
+ a runner that reported `failed`.
288
+ - **`cwdAudit`**: `missing` is exactly as loud as `dirty`, and both name a
289
+ checkout you should treat as a live scratch write until you have checked it.
408
290
 
409
291
  **Know the trim before you rely on it — it is wider than `docsOnly` suggests.** See references/reviewers.md.
410
292
 
@@ -421,25 +303,12 @@ its one member-outcomes row decides), and BLANK with a WARNING naming why
421
303
  when neither settles
422
304
  it; never type a tier in over that. That append is the whole duty. See references/pull.md.
423
305
 
424
- The row's last four fields are the ticket's difficulty, and they are what lets a
425
- tier comparison condition on the thing that swamps it. `sizing` is the
426
- **member's** own `light`/`heavy` verdict from its dispatched `sizing-a-ticket`
427
- run, which `append` reads itself off the member's report file
428
- (`<scratch>/reports/impl-<ticket>[-x].json`, its `sizing.verdict`, the highest
429
- retry letter's when there are several), so you pass no `--sizing`; the member
430
- also puts it in the PR body's `Sizing:` line, with the signal it turned on;
431
- `profile`, `loc` and `files` all come from `diff-stats.mjs` over the merged
432
- diff.
433
-
434
306
  **A verdict is a measurement only if it post-dates the run that produced it.** See references/reviewers.md.
435
307
 
436
308
  ### Nothing to do: arm the beat
437
309
 
438
310
  - **Nothing to do right now** → **do not end your turn.** Arm the heartbeat
439
- below. A turn that ends on a drained queue is #3's stall itself: nothing
440
- external will wake you, and the level-check you are one command away from
441
- never runs. Listed on its own because it is the only wake with no trigger —
442
- every other row above arrives; this one is the absence of one.
311
+ below: nothing external will wake you. See references/ci-and-staleness.md.
443
312
 
444
313
  **Beat when there is nothing to do, and never end your turn on a drained queue.** See references/ci-and-staleness.md.
445
314
 
@@ -453,18 +322,14 @@ It blocks, then prints one line. Which line it is, is the whole protocol:
453
322
  - `… interval elapsed (quiet=<n>) → run fleet-event.mjs heartbeat` — the level-check
454
323
  is due. Run that command and act on what the tick it ends in prints.
455
324
  Then arm the beat again.
456
- - `… Ns of Ms remain → re-issue this command now, do not end your turn` — no
457
- command blocks for a whole interval (omp backgrounds one at 60s, which is
458
- why the CI gate holds its wait in one `eval` cell polling `gh run view`).
459
- Issue it again. That is still one blocking call per turn, never an idle turn.
325
+ - `… Ns of Ms remain → re-issue this command now, do not end your turn` —
326
+ issue it again: still one blocking call per turn, never an idle turn.
460
327
 
461
328
  **Do not raise `--ceiling` past 30 minutes.** See references/ci-and-staleness.md.
462
329
 
463
- **The beat does not stop on idleness.** An empty shortlist and an empty supply are
464
- not a reason to end the run; they are the state a newly triaged ticket arrives
465
- into. The run ends when you drain it, when the budget goes, or when this
466
- context does — not because there was briefly nothing to do.
467
- Zeroes on the cockpit are not yet proof the pipeline is idle.
330
+ **The beat does not stop on idleness**: the run ends when you drain it, when
331
+ the budget goes, or when this context does, and zeroes on the cockpit are not
332
+ proof the pipeline is idle. See references/ci-and-staleness.md.
468
333
 
469
334
  **When you stop on purpose, say why — one command, and it is the last thing
470
335
  you owe the next run.** See references/ci-and-staleness.md.
@@ -476,9 +341,7 @@ you owe the next run.** See references/ci-and-staleness.md.
476
341
  It holds nothing and returns at once. Use it when the budget goes, or when
477
342
  you can see the context ceiling coming. A drained run records it as the last
478
343
  step of **End of run** (above), never at the drain itself.
479
- Skipping it is not fatal — the next reader still reports the stop, it just
480
- reports that the beat stopped *without a recorded reason*, which is all it can
481
- honestly say about a death that named nothing.
344
+ Skipping it is not fatal: the next reader reports a stop *without a recorded reason*.
482
345
 
483
346
  **A stall line names whose stall it is.** Its opening words come off the controller
484
347
  record:
@@ -523,24 +386,11 @@ For the merge queue, see references/pull.md.
523
386
  govern — refresh, and never Pull a ticket the shortlist does not hold.
524
387
  See references/pull.md.
525
388
 
526
- It reads the run itself, and nobody states a count. The In-flight count —
527
- every open PR that closes an issue off `gh`, plus every claimed ticket whose
528
- newest implementer is live or owed a replacement and has no such PR yet, off
529
- the ledger — sits beside the live review units (in-flight `review=` tokens
530
- plus unsettled `fix-pr-` ones), the merge bot, merge holds, tier mismatches
531
- and the drain marker, all off `.fleet/ledger.md`; the unclaimed heads and the
532
- supply come off `.fleet/shortlist.json`, which it refreshes itself when the
533
- unclaimed count is below the In-flight bound, when the file is missing or
534
- empty, when an Exclusion's premise has lifted, or when a branch-named
535
- `behind-pr` or a `behind-issue` Exclusion still not lifted names a ticket the
536
- file carries — never while the bound holds; the open PRs come off `gh`. A
537
- head whose Pull ended this run is not counted, unless on a lifted exclusion. A
538
- ledger token it
539
- cannot read, it refuses rather than guessing — a member counted live or gone on
540
- a guess is the over- or under-dispatch it exists to prevent — so correct that
541
- row with `ledger.mjs row` and tick again. The In-flight bound defaults to 5
542
- and is fixed for the run: pass `--in-flight` on every tick when `$ARGUMENTS`
543
- set it, and never otherwise.
389
+ The tick reads the run itself, off `.fleet/ledger.md`, `.fleet/shortlist.json`
390
+ and `gh`; nobody states a count (references/tick-rows.md, **What the tick
391
+ reads**). A ledger token it cannot read, it refuses rather than guessing:
392
+ correct that row with `ledger.mjs row` and tick again. The In-flight bound is fixed for the run:
393
+ pass `--in-flight` on every tick when `$ARGUMENTS` set it, and never otherwise.
544
394
 
545
395
  It prints `actual/target` and a named ACTION per role. Act on each line as it reads:
546
396
 
@@ -574,19 +424,15 @@ A Pull is done when `pull.mjs next` exits 0 with its `DISPATCH` line, which the
574
424
  Serial, main checkout, one Pull at a time. Never parallel, never inside a member —
575
425
  concurrent `worktree add` and label writes race.
576
426
 
577
- A Pull's entry is `~/.fleet/bin/fleet-run pull.mjs next --head <N>`, run from the
578
- main checkout (without `--head`, the next shortlist entry). It runs the
579
- mechanical steps in order and stops at each judgement with a `QUESTION
580
- <qid>:` block and exit 10; `pull.mjs answer <N> <qid> <token> [--path <p>] [--]
581
- [<text>…]` records the answer and resumes it. Exit 0 prints `DISPATCH {name,
582
- agent, brief}`, the `task` call that the dispatch makes; exit 1 means the ticket left the
583
- Pull, so take the next entry; exit 2 names what could not be looked at —
584
- `next --head <N>` again retries a step that failed, the same `answer` again
585
- retries one that failed while applying an answer (the question stands), and
586
- after a router refusal `next --head <N>` starts the ticket over; exit 3 is a
587
- usage mistake, or an answer the question does not take, and records nothing.
588
- The record is `<scratch>/impl-<N>/pull.json`. See references/pull.md for what
589
- each step does, and why.
427
+ A Pull's entry is `~/.fleet/bin/fleet-run pull.mjs next --head <N>`, run from
428
+ the main checkout (without `--head`, the next shortlist entry). It stops at
429
+ each judgement with a `QUESTION <qid>:` block and exit 10; `pull.mjs answer <N>
430
+ <qid> <token> [--path <p>] [--] [<text>…]` records the answer and resumes it.
431
+ Exit 2 names what could not be looked at: `next --head <N>` again retries a
432
+ failed step, the same `answer` again one that failed while applying an answer,
433
+ and after a router refusal `next --head <N>` starts the ticket over. Every
434
+ other exit is in **The exit table**; the record is
435
+ `<scratch>/impl-<N>/pull.json`. See references/pull.md for what each step does, and why.
590
436
 
591
437
  ### The tick's PULL rows
592
438
 
@@ -693,12 +539,12 @@ A dispatch is done when its `task` call is made, in the background, with the `ag
693
539
 
694
540
  - **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.
695
541
  - **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.
696
- - **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.
542
+ - **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. **After `/resume`, a carried implementer can hold the whole tick** with `HOLD (tier unchecked impl-<N>)` until it settles; reconcile its carried token meanwhile. See references/member-lifecycle.md.
697
543
  - **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.
698
544
 
699
545
  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`.
700
546
 
701
- **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).
547
+ **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 get the wrapped string as `<testCmd>` — `~/.fleet/bin/fleet-run suite-slot.mjs '<command>'`, each `'` inside the command written `'\''` — and the review's args the bare command. Never substitute `<testCmd>` with nothing.
702
548
 
703
549
  **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.
704
550
 
@@ -710,93 +556,55 @@ Dispatch a finisher only when every test here passes at once — the review retu
710
556
 
711
557
  - **Monitor: CI run completes** → `fleet-event.mjs ci <pr>`, which binds the run
712
558
  (`ci-state.mjs --pr <N>`) and **records it as
713
- `ci=<run-id>:<attempt>:<conclusion>`, not just "CI green"** — in the
714
- ticket's ledger row, which outlives your context. The key is what the sibling
715
- bullet below turns on: it tells an edge already spent from one that never
716
- fired. The whole key, never the conclusion alone: **a
717
- conclusion is not stable** (below), because a rerun rewrites the run in
718
- place. The diff-validating `check` job green with no heavy job (the
719
- diff-validating suites, not the `rebase-check` currency gate) in `failure`
720
- → a finisher CANDIDATE (gated below), a `check`
559
+ `ci=<run-id>:<attempt>:<conclusion>`, not just "CI green"** — the whole key,
560
+ never the conclusion alone, since **a conclusion is not stable** (below). The
561
+ diff-validating `check` job green with no heavy job (not the `rebase-check`
562
+ currency gate) in `failure` → a finisher CANDIDATE (gated below); a `check`
721
563
  **failure** that is not solely staleness → the **red-check fixer** (**Then
722
- dispatch a red-check fixer**, above), behind the same review-returned and
723
- fix-applier-reported gate as a finisher. A `check`-green board whose heavy jobs are merely
724
- `skipped` (behind-count staleness, the normal case after a merge) still labels — do NOT
725
- gate on `ci-state --quiet` exit 0, which a behind PR never reaches. **This
726
- edge fires for every CI run the PR produces, and the implementer's own
727
- firing comes first for every PR** — `ci.yml` triggers on `pull_request`, so
728
- opening the PR at the end of `next-ticket` step 7 starts a run within seconds,
729
- while the review returns 20-40 minutes later. That first firing is
730
- the ordinary case for every PR, not a rarity, and no review has run at all
731
- when it arrives. **Green is therefore not the gate: dispatch a finisher only
732
- once the PR's review has returned AND its fix-applier, if one was dispatched,
733
- has sent its report — pushed is not reported**, and a push before that report
734
- is mid-work on a SHA the member may still move, which its own prompt tells it
735
- not to hand over. A push is therefore not an exit, and a report that has not
736
- arrived is asked for by name rather than read as one. Neither shape is caught
737
- by an empty outbox: on the implementer's firing there is no fix-applier to owe
738
- anything to, so the outbox is *vacuously* empty, and on a fix-applier's
739
- mid-work push nothing has been sent since the dispatch, so it is *genuinely*
740
- empty. **The outbox is the last test, not the only one, and it stands wherever
741
- a fix-applier exists** — that is exactly where a ruling you still owe it is
742
- outstanding, so never dispatch off it while you do. Empty your outbox to it
564
+ dispatch a red-check fixer**, above), behind the same gate. Heavy jobs merely
565
+ `skipped` still label. **This edge fires for every CI run the PR produces,
566
+ and the implementer's own firing comes first for every PR** — `ci.yml`
567
+ triggers on `pull_request`, so no review has run at all when it arrives.
568
+ **Green is therefore not the gate: dispatch a finisher only once the PR's
569
+ review has returned AND its fix-applier, if one was dispatched, has sent its
570
+ report — pushed is not reported**: a push before that report is mid-work on a
571
+ SHA the member may still move, and a report that has not arrived is asked for
572
+ by name. An empty outbox catches neither — *vacuously* empty on the
573
+ implementer's firing, *genuinely* empty on a mid-work push. **The outbox is
574
+ the last test, not the only one, and it stands wherever a fix-applier
575
+ exists**, so never dispatch off it while you do owe it a ruling: empty it
743
576
  first — including any ruling you have withdrawn or reversed — then dispatch
744
577
  (below); a final report is not proof it stopped. A PR that needs nothing fixed
745
578
  reaches a finisher through the **fix-applier reports `no-op`** bullet below,
746
- never by waiting for a second CI event this head will never produce. The
747
- same goes for a red one: a red that fired before the review never fires
748
- again, so the **fix-applier reports `no-op`** and **review result lands**
749
- bullets are also the only routes to the red-check fixer there.
750
- 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.
579
+ never by waiting for a second CI event; a red that fired before the review
580
+ reaches the red-check fixer only through that bullet or **A review result
581
+ lands**. Then run the tick. See references/finisher.md.
751
582
  - **A fix-applier reports `no-op`, or a SHA you have already bound** → dispatch
752
583
  the finisher **now**, against the existing head — when that head's bound
753
584
  conclusion is red, the red-check fixer instead (**Then dispatch a red-check
754
- fixer**, above), never a finisher and never a wait. No push means no new run, and
755
- the Monitor above is edge-keyed on `<run-id>:<attempt>:<conclusion>` — that
756
- head's terminal state already fired once and will never fire again, so waiting
757
- for the CI event waits forever. This stays a common outcome on a clean PR:
758
- `suggestion` is the band a clean diff produces, and every out-of-scope or
759
- refuted one is filed as an issue rather than committed. An edge-only label path
760
- therefore strands exactly the PRs with nothing wrong with them. **Reconcile, do
761
- not wait for an event** governs here too, not only the implementer row. **This
585
+ fixer**, above), never a finisher and never a wait: the Monitor is edge-keyed
586
+ on `<run-id>:<attempt>:<conclusion>`, so no push means no CI event, ever.
587
+ **Reconcile, do not wait for an event** governs here too. **This
762
588
  bullet and the one above are a pair, and on a clean PR this one carries the
763
589
  whole route to a finisher**: a `no-op` report satisfies the gate above by
764
- itself — the review has returned, because a fix-applier exists only after it
765
- did, and the member has reported — so nothing further is owed and no further
766
- 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.
767
- - **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → no workflow run will ever
768
- complete for this repo, so the CI-run-completion edge above never fires and
769
- waiting for it stalls the whole PR — the same silent-stall shape reported
770
- before this verdict existed. Dispatch the finisher off the reviewer's final
771
- verdict instead, the moment it lands — and, same as above, never while you still
772
- owe it a ruling — or hold anything else it has not received, a ruling you have
773
- withdrawn or reversed included. The finisher's gate is then the
590
+ itself, so nothing further is owed and no further event is coming. Edit either bullet without the other and what strands is exactly the PR that needed no fixes.
591
+ - **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → the CI edge never
592
+ fires: dispatch the finisher off the reviewer's final verdict the moment it
593
+ lands, never while you still owe it a ruling. Its gate is then the
774
594
  `--declare-no-ci` declaration, not a `check` job: with it, label off the
775
595
  reviewer's verified suite run; **without it, do not label** — report that this
776
- repo has no CI configured and no declaration, and stop. Absence never reads as
777
- pass. See `review-and-fix.md` step 6.
778
-
779
- Gate on the `check` job, **not** on `ci-state --quiet` exit 0: a behind PR never
780
- reaches full green, so an exit-0 gate strands it unlabelled. The finisher reads
781
- per-job state (`ci-state.mjs` without `--quiet`, or its `jobs`), since `--quiet`
782
- drops `jobs`, `missing` and `dropped`. Normal path, not only kill-recovery.
783
-
784
- **`ci-state.mjs` reads `verdict: "no-ci"`** — no `check` job exists in this repo
785
- to gate on, and that is not a third way to skip the wait. **The
786
- `--declare-no-ci` declaration is caller-side, and reading it back out of the
787
- payload verifies nothing**: the flag is echoed into `reasons` and flips the exit
788
- code, so re-running with it and finding it there confirms only that you passed
789
- it. Passing it yourself is not a second opinion. Check the two facts it stands
790
- for instead:
791
-
792
- 1. `ci-state.mjs` **without** the flag still reads `verdict: "no-ci"`. A repo
793
- whose CI is merely misconfigured — workflow files present under other names,
794
- or `.github/workflows/` not a directory — exits 2 there rather than reading no-ci,
795
- so this is the check that separates real absence from misconfiguration, the
796
- case that must never ship silently.
596
+ repo has no CI configured and no declaration, and stop. See `review-and-fix.md` step 6.
597
+
598
+ Gate on the `check` job, **not** on `ci-state --quiet` exit 0, which a behind
599
+ PR never reaches; the finisher reads per-job state, without `--quiet`.
600
+
601
+ **The `--declare-no-ci` declaration is caller-side, and reading it back out of
602
+ the payload verifies nothing.** Check the two facts it stands for instead:
603
+
604
+ 1. `ci-state.mjs` **without** the flag still reads `verdict: "no-ci"`; a merely
605
+ misconfigured CI exits 2 there.
797
606
  2. The reviewer's own final verdict reports a green `testCmd` run on the SHA it
798
- pushed. `review-and-fix.md` step 3 requires it to, and that run is the only
799
- gate this repo actually has.
607
+ pushed.
800
608
 
801
609
  Both → add the label, and say it rests on the reviewer's suite run, not on CI.
802
610
  Either missing → halt here, same as a dirty worktree: report `no CI configured,
@@ -804,39 +612,23 @@ no verified suite run on record` and add no label.
804
612
 
805
613
  ### The dispatch and its refusals
806
614
 
807
- **The fix-applier pushes and exits — it does not hold the CI wait.** You own the
808
- persistent Monitor; a turn-based member re-reading `gh pr checks` each idle cycle
809
- rebuilds a 100k-token context for nothing the Monitor lacks. The body
810
- carries the report-last rule and the brief says to push, write the report and exit; you do not have to restate either. When the
811
- diff-validating `check` job is green **and no
812
- heavy job is in `failure`** (the heavy diff-validating suites — not the
813
- `rebase-check` currency gate; a `skipped` heavy job is behind-count staleness and
814
- fine — **solely** off that count, which is a condition to establish rather than
815
- infer, see the five-condition note in the fix-applier body) — or
816
- `ci-state.mjs` reads `verdict: "no-ci"`, see above — **and once the PR's review
817
- has returned and its fix-applier, if one was dispatched, has sent its report,
818
- which the CI-completes edge above states in full** — dispatch a
615
+ **The fix-applier pushes and exits — it does not hold the CI wait**; you own the
616
+ persistent Monitor. When the `check` job is green **and no heavy job is in
617
+ `failure`** (a `skipped` one is fine only when **solely** off the behind-count,
618
+ per the fix-applier body's five-condition note) — or `ci-state.mjs` reads
619
+ `verdict: "no-ci"` — **and once the PR's review has returned and its
620
+ fix-applier, if one was dispatched, has sent its report**, dispatch a
819
621
  **finisher** — a fresh small agent, not the fix-applier resumed. Render its brief
820
622
  with `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<pr#> …`
821
623
  (`--kind repair` once an earlier finisher reported `labelled` without the label landing), record it with
822
624
  `ledger.mjs dispatch <pr#> finisher-pr-<pr#>` before the call, then **dispatch
823
- it with the `agent` that command printed and omit `model` on the call** — the
824
- tier lives in that definition's frontmatter, `agents/fleet-finisher.agent.md`,
825
- as the `@smol:low` route, which resolves through the operator's `modelRoles`.
826
- Its four duties are a checklist — audit the
827
- worktree, confirm every deferral has a tracker home and re-run the acceptance
828
- mutation, apply one release label, report — and the
829
- merge gate downstream still catches whatever it gets wrong, the same argument
830
- that puts the merge bot on the same tier above. **Never dispatch
831
- one while you still owe the fix-applier a ruling**: the pinned SHA is only as good
832
- as the guarantee nothing else is inbound. **The test is your OUTBOX, not the
625
+ it with the `agent` that command printed and omit `model` on the call**. **Never dispatch
626
+ one while you still owe the fix-applier a ruling. The test is your OUTBOX, not the
833
627
  member's last message** — anything you have decided that it has not received,
834
- including a ruling you have since **withdrawn or reversed**. **That is the test
835
- of THIS condition only**: an empty outbox says nothing is inbound from you, and
836
- nothing about whether the member considers itself finished, which is what the
837
- report condition above is for. A withdrawal you
838
- recorded only in the ledger is undelivered: the member still holds the original
839
- and will act on it. Relay everything — reversals too — then dispatch.
628
+ including a ruling you have since **withdrawn or reversed**, a withdrawal
629
+ recorded only in the ledger included. **That is the test of THIS condition
630
+ only**: the report condition above is the other. Relay everything — reversals
631
+ too — then dispatch. See references/finisher.md.
840
632
 
841
633
  **That `dispatch` refuses a finisher — exit 2, nothing written — on a PR whose
842
634
  latest `reviewed=` counts a survived or unverified finding, unless the
@@ -882,17 +674,8 @@ finisher again.
882
674
 
883
675
  **An unanswered question from the member is an outbox item, and it blocks dispatch with the same weight as a ruling you have already made.** See references/finisher.md.
884
676
 
885
- **A final report is not proof the member stopped.** Observed twice: a ruling
886
- relayed after dispatch produced a new commit mid-audit, and a fix-applier that
887
- reported "nothing outstanding" resumed on a ledger-only withdrawal and was
888
- cycling mutants through the worktree when the finisher audited it. Both finishers
889
- correctly halted. Note what that costs to detect: two reads of the same worktree
890
- a minute apart showed *different* mutants, so a member's report and any single
891
- `git status` are each valid only at their instant.
892
-
893
- **The standing CI facts are in the fix-applier body, not in per-event messages** —
894
- otherwise you send "your red is staleness, do not rebase" once per member per
895
- merge. `review-and-fix.md` states them; the body says they apply.
677
+ **A final report is not proof the member stopped.** **The standing CI facts are
678
+ in the fix-applier body, not in per-event messages.**
896
679
 
897
680
  **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
898
681
  (`ledger.mjs token <PR number> label-off=<latest finisher attempt>`, which keeps
@@ -907,17 +690,14 @@ See references/finisher.md.
907
690
 
908
691
  ### Own the CI waits
909
692
 
910
- **Own the CI waits.** Arm a second persistent Monitor over open PRs'
911
- latest runs, keyed `<run-id>:<attempt>:<conclusion>` so each terminal state fires once, and
912
- emit the behind-count and the per-job conclusions with it: a `success` on a branch
913
- 8 behind is not actionable, and `check`-green-with-heavy-skipped is the staleness
914
- board you must not confuse with a red. Do not reinvent that read: one
915
- `~/.fleet/bin/fleet-run ci-state.mjs --pr <N>` per open PR already emits
916
- run id, attempt, conclusion, behind-count and per-job conclusions, branch derived
917
- from the PR — including the `gh api --hostname <host>` a GHE compare call needs,
918
- without which the probe 404s and `behind` comes back `null`, never 0, so every
919
- event reads as unknown-behind. Call it **without `--quiet`** — that flag drops
920
- `jobs`, `missing` and `dropped`.
693
+ **Own the CI waits.** Arm a second persistent Monitor over open PRs' latest
694
+ runs from the block in references/ci-watcher.md, verbatim, keyed
695
+ `<run-id>:<attempt>:<conclusion>` so each terminal state fires once: one
696
+ `~/.fleet/bin/fleet-run ci-state.mjs --pr <N>` per open PR emits run id,
697
+ attempt, conclusion, behind-count and per-job conclusions. Call it **without
698
+ `--quiet`** — that flag drops `jobs`, `missing` and `dropped`. A `success` on a
699
+ branch 8 behind is not actionable, and `check`-green-with-heavy-skipped is the
700
+ staleness board you must not confuse with a red.
921
701
 
922
702
  A monitor event is a wake-up, never a verdict; members re-query at labelling
923
703
  time. See references/ci-and-staleness.md.
@@ -927,144 +707,12 @@ time. See references/ci-and-staleness.md.
927
707
  **Every merge invalidates every other open PR, silently.** Behind-count is the
928
708
  only honest signal. See references/ci-and-staleness.md.
929
709
 
930
- **A probe that cannot read is a transition event, not silence.** So check the REST budget *before* polling — `gh api rate_limit` is itself
931
- unmetered (`gh api rate_limit --jq '.resources.core.remaining'`, re-confirmed
932
- 2026-09-08) — and give an unreadable `ci-state` payload the same treatment one
933
- level down. The shape is **one outer tick loop with an inner pass over the open
934
- PRs**, because the two causes live at different levels — the budget is one
935
- account, the payload is one PR:
936
-
937
- ```sh
938
- budget_out= hidden_out= list_out= cap_out= blind= # seed the latches: set -u reads each below before tick 1 ever assigns one
939
- pr_cap=200 # the open-PR read's --limit; gh stops there with exit 0 and no warning
940
- while :; do # one tick
941
- rl=$(gh api rate_limit --jq '.resources.core.remaining' 2>/dev/null || echo ERR)
942
- case "$rl" in ''|*[!0-9]*) rl=ERR;; esac # a non-numeric read is an outage, not a budget
943
- if [ "$rl" = ERR ] || [ "$rl" -lt 200 ]; then
944
- if [ -z "$budget_out" ]; then # ONE global latch: a quota outage hits every PR
945
- echo "WATCHER DEGRADED: core REST budget=$rl — CI polling paused, silence is NOT green"
946
- budget_out=1
947
- fi
948
- else # every RECOVERED waits for the pass gates below
949
- prs=$(gh pr list --state open --limit "$pr_cap" --json number --jq '.[].number' 2>/dev/null) || prs=ERR
950
- if [ "$prs" = ERR ]; then
951
- if [ -z "$list_out" ]; then # global latch: this cause is account-level too
952
- echo "WATCHER DEGRADED: cannot list open PRs — CI polling paused, silence is NOT green"
953
- list_out=1
954
- fi
955
- else # a list or cap RECOVERED waits for the pass gates too
956
- count=$(printf '%s\n' "$prs" | grep -c .) || : # grep -c exits 1 on zero matches; set -e must not take it
957
- if [ "$count" -ge "$pr_cap" ] && [ -z "$cap_out" ]; then # global latch: a full page may have been cut there
958
- echo "WATCHER DEGRADED: open-PR list hit its --limit $pr_cap — PRs past it are NOT watched, silence on them is NOT green"
959
- cap_out=1
960
- fi
961
- # gh pr list is GraphQL, so $rl still holds. The pass is budgeted at 4 core
962
- # REST units per listed PR on top of the 200 floor. See references/ci-watcher.md.
963
- # A shortfall here, or at the Actions-bucket gate below, pauses this tick's pass.
964
- if [ "$rl" -lt $((200 + 4 * count)) ]; then
965
- if [ -z "$budget_out" ]; then
966
- echo "WATCHER DEGRADED: core REST budget=$rl < $((200 + 4 * count)) needed for $count PRs — CI polling paused, silence is NOT green"
967
- budget_out=1
968
- fi
969
- else
970
- # The workflow and run lookups bill a second core bucket that
971
- # `rate_limit` does not report. Its count is only in the response
972
- # headers of a lookup that bills it: one 1-unit read per tick, 4 units
973
- # per listed PR on the same 200 floor. Recovery for every global latch
974
- # waits until this gate clears too, so no line says polling resumed
975
- # while this one still pauses it.
976
- hrl=$(gh api -i 'repos/{owner}/{repo}/actions/workflows?per_page=1' 2>/dev/null | awk 'tolower($1)=="x-ratelimit-remaining:" {sub(/\r$/, "", $2); print $2; exit}')
977
- case "$hrl" in ''|*[!0-9]*) hrl=ERR;; esac # a missing header is an outage, not a budget
978
- if [ "$hrl" = ERR ] || [ "$hrl" -lt $((200 + 4 * count)) ]; then
979
- if [ -z "$hidden_out" ]; then
980
- need=; [ "$hrl" = ERR ] || need=" < $((200 + 4 * count)) needed for $count PRs" # an unreadable header has no amount to compare
981
- echo "WATCHER DEGRADED: unreported Actions core REST bucket=$hrl$need — CI polling paused, silence is NOT green"
982
- hidden_out=1
983
- fi
984
- else
985
- if [ -n "$list_out" ]; then
986
- echo "WATCHER RECOVERED: open-PR list readable again — CI polling resumed"
987
- list_out=
988
- fi
989
- if [ -n "$cap_out" ] && [ "$count" -lt "$pr_cap" ]; then
990
- echo "WATCHER RECOVERED: open-PR list under its --limit $pr_cap — every open PR watched again"
991
- cap_out=
992
- fi
993
- if [ -n "$budget_out" ]; then
994
- echo "WATCHER RECOVERED: core budget=$rl — CI polling resumed"
995
- budget_out=
996
- fi
997
- if [ -n "$hidden_out" ]; then
998
- echo "WATCHER RECOVERED: unreported Actions core REST bucket=$hrl — CI polling resumed"
999
- hidden_out=
1000
- fi
1001
- for pr in $(printf '%s\n' "$prs"); do # inline $(...): `for pr in $prs` is ONE iteration under zsh
1002
- st=$(~/.fleet/bin/fleet-run ci-state.mjs --pr "$pr" 2>/dev/null) || : # not-green exits non-zero; the payload is the verdict
1003
- if ! printf '%s' "$st" | jq -e '.verdict and .verdict != "rate-limited"' >/dev/null 2>&1; then
1004
- case " $blind " in *" $pr "*) ;; *) # latch keyed BY PR: this cause is per-PR
1005
- echo "WATCHER DEGRADED: no usable ci-state reading for #$pr — silence is NOT green"
1006
- blind="$blind $pr" ;;
1007
- esac
1008
- continue # inner continue — still reaches the tick sleep
1009
- fi
1010
- case " $blind " in *" $pr "*)
1011
- echo "WATCHER RECOVERED: ci-state reading #$pr again — CI polling resumed"
1012
- keep=; for b in $(printf '%s\n' "$blind"); do [ "$b" = "$pr" ] || keep="$keep $b"; done; blind=$keep ;;
1013
- esac
1014
- : # your normal handling of $st for this PR; a run at a terminal state is the CI wake: fleet-event.mjs ci "$pr"
1015
- done
1016
- fi
1017
- fi
1018
- fi
1019
- fi
1020
- sleep 120 # the block's only pacing, once per tick
1021
- done
1022
- ```
1023
-
1024
- **One gate, not a gate plus a caveat.** See references/ci-watcher.md.
1025
-
1026
- **It has to latch** — one line entering the degraded state, one leaving it,
1027
- never one per tick. **Latch each cause at the level its cause lives
1028
- at**: the budget, the unreported Actions bucket, the open-PR list and that
1029
- list's cap are one global flag each — all account-level — but the payload latch is keyed by PR, and a
1030
- shared scalar for those is an unlatched line wearing a latch's clothes — one
1031
- persistently blind PR alongside one healthy PR re-clears the flag on every tick,
1032
- emitting a DEGRADED and a false RECOVERED pair forever. The recovery line is
1033
- what says the watch is alive again, so emit it on the first good pass even when
1034
- nothing about the CI state changed; without it a reader cannot tell a recovered
1035
- watcher from a dead one. See references/ci-watcher.md.
1036
-
710
+ **A probe that cannot read is a transition event, not silence.** A `WATCHER
711
+ DEGRADED` line is a paused poll or an unread PR, and silence there is NOT green
712
+ until its `WATCHER RECOVERED` line. **One gate, not a gate plus a caveat.** See references/ci-watcher.md.
1037
713
  **All pacing in exactly one place — the tick loop's own tail `sleep`.** See references/ci-watcher.md.
1038
-
1039
- **Guard every probe, not just the ones with a verdict field.** Capture its status (`prs=$(…) || prs=ERR`), latch it globally, and fall
1040
- through to the same tail sleep. **Then iterate with the inline `$(printf '%s\n'
1041
- "$prs")` form, never a bare `for pr in $prs`** — the assigned-variable trap in
1042
- **Shell traps**, in references/shell-traps.md, which carries the mechanism and its
1043
- measurements. It
1044
- applies to **every** loop in this block, the `blind`-latch removal included:
1045
- under a bare `for b in $blind` a recovered PR never leaves the list and the block
1046
- emits a fresh RECOVERED every tick thereafter — the per-tick volume the latch
1047
- exists to prevent, wearing a latch's clothes again. See references/ci-watcher.md.
1048
-
1049
- **A readable list can still be a short one.** Pass an explicit `--limit`
1050
- (`pr_cap`: one variable for the flag and the check, so the two cannot drift)
1051
- and treat a full page as its own degraded cause — the "no silent caps" rule
1052
- `candidates.mjs`'s `refuseIfCapped()` and `fleet-tick.mjs`'s `PR_LIMIT` already
1053
- enforce. It latches globally like the list failure but does not pause polling:
1054
- the PRs it did return are real, so the pass still watches them, and the
1055
- DEGRADED line names the rest as unwatched until a pass comes back under the cap.
1056
- See references/ci-watcher.md.
1057
-
1058
714
  **The budget is gated at the floor and again at the pass, and only the pass gates announce recovery.** See references/ci-watcher.md.
1059
715
 
1060
- **Judge `ci-state` on its payload, never its exit code** — the same rule as
1061
- `merge-gate.mjs` applies on the merge path, applied to the watcher. Empty
1062
- or unparseable output is the degraded case; parseable JSON reading
1063
- `verdict: "not-green"` is normal. An exhausted quota may also name itself,
1064
- `verdict: "rate-limited"` on stdout — likewise degraded, and likewise not a
1065
- reading, which is why the gate above tests the verdict rather than mere
1066
- parseability: that payload is perfectly good JSON. See references/ci-watcher.md.
1067
-
1068
716
  **Do not back off and wait.** See references/ci-watcher.md.
1069
717
 
1070
718
  **While the budget is exhausted, `ledger.mjs check` reads
@@ -1128,10 +776,9 @@ A member that bails demotes nothing itself; demotion by cause is yours
1128
776
 
1129
777
  ### Resolving a finisher halt
1130
778
 
1131
- A halt at duty 1 reads identical from a bare SHA mismatch whatever caused it,
1132
- and the causes `finisher-audit.mjs` names are the ones seen so far, not a
1133
- closed list; the finisher's agent body gives it the cause from one script
1134
- instead of asking anyone.
779
+ **A rebase never gets a `reviewed=`**, and never write a `reviewed=` you did
780
+ not read off a review result file. Why `past-pin` needs the tick: See
781
+ references/finisher.md.
1135
782
 
1136
783
  **Resolving a finisher halt.** A halt is the finisher working correctly — it
1137
784
  refused to label — so it is never `failed`, which stays for a finisher that
@@ -1150,34 +797,17 @@ decides the rest — two resolve automatically, every other one escalates:
1150
797
  | `rebase` | **Escalate** — comment and flag, nothing more. The report's `evidence.replay` reads `pure` only when `git range-diff origin/main <pin> HEAD` pairs every pinned commit with an unchanged one; `changed` (a commit dropped, edited or added, or a stale `origin/main`) or `unknown` means read `evidence.rangeDiff` before acting, and `evidence.remote.state` says whether that head is on the PR. No automatic rule until a live halt shows the case recurring. |
1151
798
  | `unreadable`, `missing`, `absent`, `other` | **Escalate** — comment and flag, no automatic retry. |
1152
799
 
1153
- ### Why a `past-pin` halt needs the tick
1154
-
1155
- `past-pin` needs the tick because the dead-review path cannot reach it: once a
1156
- review has **returned**, its `reviewed=` token holds the PR out of `review-due=`,
1157
- and settling that review `=failed` re-queues nothing — that path covers only a
1158
- review that died before returning. A head that moved past `reviewed=` with no
1159
- such halt — a fix-applier's push — stays not due: duty 2 verifies what it
1160
- applied. **A rebase never gets a `reviewed=`:** the earlier review still
1161
- counts, because dispositions verdicts are tied to its head. Never write a
1162
- `reviewed=` you did not read off a review result file.
1163
-
1164
800
  ### Resolving a finisher that died
1165
801
 
1166
- **Resolving a finisher that died.** A finisher settled `failed` or `killed` left
1167
- its PR without `ready-to-merge`, and nothing else notices: the PR is out of the
1168
- merge queue, and the tick dispatches no finisher for it. The cockpit flags a
1169
- REVIEW-column open PR without the label, whose latest finisher attempt settled
1170
- so, `finisher:failed` or `finisher:killed` at severity 4 — one token per
1171
- outcome, neither the implementer's bare `killed` flag nor `unlabelled`. You
1172
- resolve it by hand: investigate why the finisher died, or dispatch the
1173
- next-suffix finisher after the finisher gate: render its brief with
1174
- `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<M>-<x>
1175
- …` (`--kind repair` once an earlier finisher reported `labelled` without the
1176
- label landing), then `ledger.mjs dispatch <M> finisher-pr-<M>-<x>`. The flag
1177
- clears when a later attempt settles — `labelled`
1178
- (then `unlabelled` takes over while the label is still off), `halted:<cause>`,
1179
- or a live `-b` — or when the PR carries `ready-to-merge`, merges or closes.
1180
- You never add the label yourself, and the tick does not retry.
802
+ **Resolving a finisher that died.** A finisher settled `failed` or `killed`
803
+ leaves its PR without `ready-to-merge`; the cockpit flags it
804
+ `finisher:failed` or `finisher:killed`, and the tick does not retry. Resolve
805
+ it by hand: investigate why the finisher died, or dispatch the next-suffix
806
+ finisher after the finisher gate: render its brief with `dispatch-prompt.mjs
807
+ render finisher --kind fresh --member finisher-pr-<M>-<x> …` (`--kind repair`
808
+ once an earlier finisher reported `labelled` without the label landing), then
809
+ `ledger.mjs dispatch <M> finisher-pr-<M>-<x>`. You never add the label
810
+ yourself. When the flag clears: See references/finisher.md.
1181
811
 
1182
812
  ### Dispositions escalation
1183
813
 
@@ -1225,7 +855,7 @@ Match the failure to its row in the table below and run that response; where the
1225
855
  | Member idle with work outstanding | Read the PR first, *then* ping. Idle ≠ done |
1226
856
  | 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 |
1227
857
  | 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 |
1228
- | 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 |
858
+ | 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, on the spot — and over every claimed ticket with no PR when the run ends or the maintainer drains. **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 |
1229
859
  | 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. |
1230
860
  | `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. |
1231
861
  | `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. |
@@ -1245,48 +875,29 @@ yet) whose prompt states what it inherits.
1245
875
  Reviewers that went idle on CI recover as a **finisher, not a re-review** once
1246
876
  commits are pushed. See references/member-lifecycle.md.
1247
877
 
1248
- Audit every worktree before dispatching replacements with
1249
- `~/.fleet/bin/fleet-run worktree-audit.sh` — it reports each worktree's
1250
- ahead-count and uncommitted files, the committed-vs-uncommitted distinction that
1251
- decides whether a replacement redoes or destroys work.
878
+ **Audit every worktree before dispatching replacements**, with
879
+ `~/.fleet/bin/fleet-run worktree-audit.sh`. See references/failure-handling.md.
1252
880
 
1253
881
  When a report is missing, **ask by name — that is what the "unless the controller asks by name" clause in every brief is for — and do not attribute the gap to the member until it answers.** See references/member-lifecycle.md.
1254
882
 
1255
- **A throw, an empty return, or a notification whose status is not `completed`
1256
- is a failure event, not a clean review.** **Retry once
1257
- under the next runner letter, then stop:** the `review-pr-<pr#>` runner
1258
- already retried once inside its cell and reports `failed` with both errors, so
1259
- the retry is a second runner under the next letter the PR's rows do not carry
1260
- yet — `review-pr-<pr#>-b` after a first runner — recorded as
1261
- `review=member:<that name>`. A second failure is reported, and the PR stays at
1262
- review-due. A
1263
- runner member that is killed rather than failed follows **Failure
1264
- handling** (fresh name, inherited state stated).
1265
-
1266
- **Verify every reported SHA.** `~/.fleet/bin/fleet-run verify-sha.sh
1267
- <branch> <sha>` before enqueueing — a member can commit in a nested worktree,
1268
- leaving the SHA on a stray branch while its report reads normally. Exit codes as
1269
- `inflight.sh`: `# 0 reachable, 1 not reachable, 2 unanswerable`. Not reachable
1270
- (exit 1) → flag, do not enqueue, do not return the ticket to the shortlist until the
1271
- maintainer rules. **Exit 2 is not a verdict** — the question could not be
1272
- answered, and every cause of it reaches you as one shape: no JSON on stdout at
1273
- all, the cause on stderr alone. Read that stderr, fix what it names and re-run;
1274
- escalate if it repeats. Never read exit 2 as `not reachable`, and never record it
1275
- as a stray commit — the flag and the maintainer's ruling above are what exit 1
1276
- earns, and spending them on a probe that never answered costs a member its
1277
- ticket over a failure a re-run would have cleared.
883
+ A review that throws, returns empty or notifies a status not `completed` is a
884
+ failure event, not a clean review: retry it as the **Failure handling** row for
885
+ it says. See references/failure-handling.md.
886
+
887
+ **Verify every reported SHA** with `~/.fleet/bin/fleet-run verify-sha.sh
888
+ <branch> <sha>` before enqueueing. Exit 1, not reachable → flag, do not
889
+ enqueue, do not return the ticket to the shortlist until the maintainer rules.
890
+ **Exit 2 is not a verdict**: read its stderr, fix what it names and re-run;
891
+ escalate if it repeats, and never read it as `not reachable` or record it as a
892
+ stray commit. See references/failure-handling.md.
1278
893
 
1279
894
  **Never `--delete-branch` — the merge bot deletes the head branch itself.**
1280
895
  `gh pr merge <n> --merge` alone: the flag's local cleanup errors on a `main`
1281
896
  held by another worktree, or strands the feature worktree on `main`.
1282
897
 
1283
898
  **Never force a rebase to start.** No `git clean`, `git checkout .`,
1284
- `git reset --hard`, `git stash`. Uncommitted changes may exist nowhere else.
1285
- Non-empty `git status --porcelain -unormal` → stop and report — the explicit
1286
- mode, never bare `--porcelain`, or `status.showUntrackedFiles = no` reads an
1287
- empty answer here and licenses the rebase over work this check exists to
1288
- protect. Stashing empties it either way, so that check and
1289
- `no-undo-audit.sh` both go quiet on work nothing else holds.
899
+ `git reset --hard`, `git stash`. Non-empty `git status --porcelain -unormal` →
900
+ stop and report, never off bare `--porcelain`. See references/failure-handling.md.
1290
901
 
1291
902
  **Members share one filesystem, one docker stack and one process table**, and the
1292
903
  failures arrive as *wrong findings*, not errors:
@@ -1346,19 +957,12 @@ Read why that body carries the claim discipline, and its literal-path and `grep
1346
957
  **Then.** **Re-pin first** — `~/.fleet/bin/fleet-run instruments.sh
1347
958
  --pin`. See references/instruments.md. Then the ledger line, save the
1348
959
  rationale, one line to the maintainer. **Then land it from the main checkout,
1349
- if the fix was edited in it** — until you do, the next tick reads
1350
- `MAIN-CHECKOUT-DIRTY` naming your own paths. Commit the fix on a branch and
1351
- switch back to `main` before the next tick, and never `--record` while on that
1352
- branch: the switch back restores the tree the baseline recorded, so the hold
1353
- clears itself with nothing re-recorded, and the fix leaves the checkout until
1354
- its PR merges. A `HEAD …` entry is none of your paths and clears the same way.
1355
- Re-baseline only a fix you leave edited in the checkout: run
1356
- `~/.fleet/bin/fleet-run main-checkout.mjs --check` first, and only when the
1357
- paths it names are exactly the ones you just edited, run `--record` — never
1358
- before the commit, because the switch back would then read your own paths
1359
- dirty again. Any other path on that line is a stray write — leave the hold for
1360
- the maintainer, because `--record` would certify it with yours. Live members
1361
- hold the old text — re-brief only if it changes what they do *now*.
960
+ if the fix was edited in it**: commit it on a branch and switch back to `main`
961
+ before the next tick, never `--record` on that branch. Re-baseline only a fix
962
+ you leave edited in the checkout, after the commit, and only when
963
+ `main-checkout.mjs --check` names exactly your paths; any other path is a stray
964
+ write whose hold is the maintainer's. Re-brief live members only if the fix
965
+ changes what they do *now*. See references/instruments.md.
1362
966
 
1363
967
  Plan it as a PR the run's own merge queue lands, never as an edit-and-continue. See references/merge-bot.md.
1364
968
 
@@ -1410,13 +1014,8 @@ One running table, updated as events land:
1410
1014
 
1411
1015
  Plus a queue-depth line: shortlist, supply, whether triage was suggested.
1412
1016
 
1413
- **Do not read a cell early.** A cell's
1414
- comparison is one session holding an admissible row at that cell — its tier
1415
- check passed and its resolved effort is the cell's level — and one at
1416
- `slow-high` whose resolved model and effort differ. Count them per cell, never
1417
- pooled across cells, and stop until
1418
- there are at least ten of them across five or more distinct `run_date`s; below
1419
- that, a comparison count is a number, not evidence. **Report the count the per-cell readout prints,
1017
+ **Do not read a cell early** (references/pull.md, **Do not read a cell
1018
+ early**). **Report the count the per-cell readout prints,
1420
1019
  never one from any other query:** `~/.fleet/bin/fleet-run cell-readout.mjs`
1421
1020
  prints `<cell> <comparisons> <run_dates> <resolved models>` for each cell past
1422
1021
  that floor and a cell below it only as a count on stderr; a `mixed (…)` models