@feigi/fleet-ctl 3.82.2 → 3.82.4

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.
@@ -162,7 +162,7 @@ label or a premise the maintainer can read.
162
162
 
163
163
  ## The wake table
164
164
 
165
- A wake is done when its one `fleet-event.mjs` call has run and you have acted on every line it printed, the tick's last; with nothing left to act on, arm the heartbeat and hold rather than end your turn. What a wake owes beyond its call is under **Phase 3 — event loop**.
165
+ A wake is done when its one `fleet-event.mjs` call has run and you have acted on every line it printed, the tick's last; with nothing left to act on, arm the heartbeat and hold rather than end your turn. What a wake owes beyond its call is under **What a wake owes beyond its call**, below. The loop has no exit but the run's own end: drain the run when it ends (**End of run**).
166
166
 
167
167
  React to events; never block on one.
168
168
 
@@ -187,6 +187,7 @@ refill or dispatch edges left. Record first, because the tick reads the run off
187
187
  one it cannot see, and it names the same dispatch again. Every record goes
188
188
  through `ledger.mjs` (**Run ledger**), which `fleet-event.mjs` drives — never a
189
189
  hand edit. What each kind writes is in the header comment of `fleet-event.mjs`.
190
+ See references/ci-and-staleness.md.
190
191
 
191
192
  Run each as `~/.fleet/bin/fleet-run fleet-event.mjs <kind> <args>`, from the main checkout:
192
193
 
@@ -206,6 +207,163 @@ Run each as `~/.fleet/bin/fleet-run fleet-event.mjs <kind> <args>`, from the mai
206
207
  | Undrain | `fleet-event.mjs undrain --reason "<reason>"` |
207
208
  | Member confirmed dead or never dispatched | `fleet-event.mjs member-died <member>` |
208
209
 
210
+ ### What a wake owes beyond its call
211
+
212
+ Act on every line the call prints, the tick's last, and arm the beat. What a wake
213
+ owes beyond its call:
214
+
215
+ - **Implementer report** → the PR is owed a review from here on:
216
+ the tick names it as `DISPATCH review PR#<M>`, and it names the next
217
+ shortlist head whenever the In-flight count is below the bound. Pull
218
+ nothing the tick did not name.
219
+ - **Finisher reports halted** → the per-cause rule (**Resolving a finisher
220
+ halt**, below).
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**.
223
+ - **A review result lands** → see the result file written (by you or by the
224
+ runner — **The reviewer** says which, per harness), then
225
+ `fleet-event.mjs review-result <pr> <path>`: a PR with survived or unverified findings prints as
226
+ `DISPATCH fix-pr PR#<M>`. No survived or unverified finding and nothing to file → skip the
227
+ fix-applier and dispatch the **finisher** directly — or, when that head's
228
+ bound conclusion is red and not solely staleness, the red-check fixer
229
+ instead (**Then dispatch a red-check fixer**, below): no member will touch that
230
+ PR, so no push is coming and nothing will wake you. A review that failed is
231
+ not a result — **Recovery** says who retries it and what takes over.
232
+ Read the digest as **Reading a review result** says.
233
+ - **Monitor: `ready-to-merge` appears** → `fleet-event.mjs label <pr>`, and
234
+ on `DISPATCH merge-bot` dispatch the next `merge-bot-<n>` (**The merge bot**).
235
+ Catches hand-added labels as well as a finisher's own. The bot does not wait
236
+ for that finisher's report: its rebase is server-side,
237
+ `gh pr update-branch --rebase`, so nothing on the finisher's worktree has to
238
+ happen first. **You own the watcher, not the bot.** Arm one yourself,
239
+ `persistent: true`, seeded before the loop so handled PRs do not re-fire.
240
+ **One rebase per PR, at merge time.** Any behind-count you hand a bot is
241
+ expired on arrival; say so. See references/ci-and-staleness.md.
242
+ - **Merge-bot pass reports done** → `fleet-event.mjs merge-bot-report merge-bot-<n>`. A label that
243
+ landed at the last moment reads `DISPATCH
244
+ merge-bot` again: dispatch the next bot straight away. A `rebase-fallback-#<pr>`
245
+ on a conflict needs no record from you — the bot already appended
246
+ `conflict-hold:#<pr>` to that PR's row, and the tick turns it into `DISPATCH
247
+ fix-pr PR#<pr>` (**The fix-applier**). The tick's `--apply` reaps after the
248
+ pass. Read `worktreesRemoved` alongside `reaped`: a pass that
249
+ reaps branches and removes no worktrees is a finding, not a quiet success.
250
+ Update the reaped tickets' ledger rows in the same step.
251
+ **Do not invoke `commit-commands:clean_gone`.** The skill is the maintainer's
252
+ to fix; do the reap yourself. **Never reap a branch a live member is on.**
253
+ Cross-check `.fleet/ledger.md` first:
254
+ a row without a terminal state means someone may still be in that worktree, and
255
+ the dirty check does not see a member that committed but has not pushed.
256
+ See references/reaping.md.
257
+ - **The run ends, or the maintainer says drain** → `fleet-event.mjs drain --reason "<reason>"`
258
+ first (below). The marker is on disk, so the tick
259
+ holds the implementer row at `HOLD (draining)` from then on — for a
260
+ replacement controller too — and keeps the review, fix-applier and merge rows
261
+ firing until the open PRs merge. The drain starts the end of the run; it does
262
+ not end it. **End of run**, in this order:
263
+ 1. `~/.fleet/bin/fleet-run fleet-event.mjs drain --reason "<reason>"` — runs
264
+ `closeout.mjs begin --reason "<reason>"` (references/reaping.md), then the tick. A claim it refuses prints `NEED hand-release #<N> <blocker>`: that
265
+ refusal is the finding, so run `worktree-audit.sh` and decide by hand.
266
+ `begin` selects a claim with no PR and a live or `bailed` implementer token
267
+ on every run, so a claim you keep leaves `begin` at exit 4 on every run:
268
+ release the rest by hand and run `begin` again.
269
+ 2. Act on the review, fix-applier and merge rows until the tick prints
270
+ `ACTION: run over → fleet-heartbeat.mjs --stop "<reason>"`, which it does
271
+ once the run is draining with no open PR and no live implementer.
272
+ 3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"` — scrapes
273
+ the run's member facts, checks the tier-outcomes rows, refreshes the
274
+ metrics, commits the three metrics files and opens their PR, sets the
275
+ phase-0 receipt aside as `.fleet/phase0.previous.json`, then records
276
+ `fleet-heartbeat.mjs --stop` last, because every reader takes a recorded
277
+ stop as the end of the run. A `NEED tier-outcomes append <pr>` is a PR
278
+ the ledger marks reviewed with no verdict row: append it with the verdict
279
+ you rule (**Guard: accumulate per PR**) and run `finish` again. A step it
280
+ fails is explained in references/close-out.md. Exit 0 is the run closed.
281
+ Drain only when a documented trigger fired, and name the trigger in `--reason`.
282
+ **Undrain** only when the maintainer asks, never a drain that budget or
283
+ context exhaustion caused (references/close-out.md).
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
+
286
+ ### Reading a review result
287
+
288
+ **You read the digest; the findings are the fix-applier's.**
289
+ `jq '{pr, head, resume, testEnvironment, suiteVerdict, dimensionsRun, dimensionsUnrun, cwdAudit, counts}' <scratch>/pr<pr>/<run>/review.json`
290
+ is the whole of your read, and it is what you record: `reviewed=<head>:<survived>/<refuted>/<unverified>:<run>`,
291
+ off `head`, `counts` and the run root's name — the runner's `ledger` field — on
292
+ the PR's row, with `ledger.mjs review <pr>`. Every later path to the review is built from that token, before a
293
+ restart or after one: `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's
294
+ latest `reviewed=`. Findings never enter your context —
295
+ a result runs 26-61 KB, 7-15k tokens that used to land in yours and then again,
296
+ up to ~10k characters, in every fix-applier prompt you wrote. The fix-applier
297
+ reads every one off the file and makes every ruling on them
298
+ (`review-and-fix.md`'s **The review result file**). What the digest tells you:
299
+
300
+ **`testEnvironment` says what the review's shared test run is evidence about**,
301
+ and it is present on a healthy run as well as a degraded one, so there is
302
+ nothing to notice by its absence. The
303
+ fix-applier reads it before acting on any count from the shared test run; you
304
+ read it so a degraded review never passes for a clean one. See references/reviewers.md.
305
+ `unverified` is *not* "checked and cleared" — a `suggestion` skips the pass by
306
+ policy, and a finding whose refuters all crashed or abstained lands there too.
307
+ All go to the fix-applier with the rest, in the file; never rule on them yourself.
308
+ **`refutersDispatched`, carried on every finding, is what tells the skip
309
+ apart** — zero is the policy skip, above zero with no deciding vote is a
310
+ refuter pass that decided nothing — and `refutersInconclusive` splits that
311
+ one: the refuters it counts abstained, every other one dispatched crashed. So
312
+ the population is read off those fields rather than off severity,
313
+ which records only how much a finding would matter if true. `resume` is
314
+ non-null exactly when a specialist or a refuter pair crashed again after the
315
+ review's own in-run retry, and **resume beats handing a crash-heavy review
316
+ on**: a deferred crash is a finding nobody ever looked at.
317
+ 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.
318
+ `refuted` comes back deliberately as well — a refutation is itself a claim, and
319
+ one has been reversed on new evidence. The fix-applier may reverse one; it
320
+ records each reversal as a `refuted` entry in its disposition record, the
321
+ evidence in `reason`, and you copy those entries to the ledger's `ruled` line by
322
+ running `fleet-event.mjs fix-report`, whose `settle --from-report` records each
323
+ refutation the report defers.
324
+
325
+ **`dimensionsRun` is the dispatch; `dimensionsUnrun` is what names a gap.** A
326
+ specialist that dies contributes zero findings while its key stays in
327
+ `dimensionsRun`. So read the two together. A key
328
+ in `dimensionsRun` and NOT in `dimensionsUnrun` returned a review; every
329
+ `dimensionsUnrun` entry is `{dimension, reason}` naming a specialist that
330
+ returned nothing, its `reason` the crash.
331
+ The review already re-dispatched each crashed specialist once, so what
332
+ `dimensionsUnrun` still names is residue: the fix-applier names it unrun in its
333
+ report and re-runs nothing — an absence of findings is not coverage.
334
+
335
+ **`suiteVerdict` is the review's one judgement on its shared test run:
336
+ `{usable, reason}`, always present.** The review runs the test command once,
337
+ and every specialist reads that run instead of running its own. `{usable: true,
338
+ reason: null}` says the run executed real tests and its exit status is
339
+ explained by its counts; usable is not green, so a usable run can have failing
340
+ tests. `usable: false` carries `reason`, including a run that never started
341
+ (install skipped, no suite slot). The verdict says nothing about whether a
342
+ dimension reviewed: a specialist that returned reviewed, its findings verified
343
+ as usual, whatever the suite verdict. With `usable: false` the
344
+ fix-applier names `shared test run not usable: <reason>` in its final verdict
345
+ and does nothing more — its own `<testCmd>` run and CI cover the suite.
346
+ **Exit 75 is not a test result.** In a review it makes
347
+ `suiteVerdict` not usable, `suiteVerdict.reason` beginning "no suite slot within
348
+ <n> s — the host is at its suite bound; not a test failure", with no retry
349
+ inside the run: read that `reason` off the digest and re-review it
350
+ under the next runner letter (`review-pr-<pr#>-b` after a first runner), as for
351
+ a runner that reported `failed`.
352
+
353
+ **`cwdAudit` is the specialist's own inherited-checkout audit, one entry per
354
+ dispatched dimension: `{dimension, state, line}`, `state` one of**
355
+ `clean`/`dirty`/`unrepo`/`missing`. The prompt asks every specialist to run
356
+ `git status --porcelain -uall` against the directory it inherited before
357
+ returning and fold the result into a `CWD-AUDIT:` line — a convention, not
358
+ schema, so a specialist that never emits it or misspells it still validates
359
+ clean. This field is the runtime backstop that reads for the line regardless:
360
+ `missing` is exactly as loud as `dirty`, and both name a checkout you should
361
+ treat as a live scratch write until you have checked it.
362
+
363
+ **Know the trim before you rely on it — it is wider than `docsOnly` suggests.** See references/reviewers.md.
364
+
365
+ **A refutation resting on an injection nothing proved landed is not a refutation — it is a cell that never ran.** See references/reviewers.md.
366
+
209
367
  ### Nothing to do: arm the beat
210
368
 
211
369
  - **Nothing to do right now** → **do not end your turn.** Arm the heartbeat
@@ -231,6 +389,48 @@ It blocks, then prints one line. Which line it is, is the whole protocol:
231
389
  why the CI gate holds its wait in one `eval` cell polling `gh run view`).
232
390
  Issue it again. That is still one blocking call per turn, never an idle turn.
233
391
 
392
+ **Do not raise `--ceiling` past 30 minutes.** See references/ci-and-staleness.md.
393
+
394
+ **The beat does not stop on idleness.** An empty shortlist and an empty supply are
395
+ not a reason to end the run; they are the state a newly triaged ticket arrives
396
+ into. The run ends when you drain it, when the budget goes, or when this
397
+ context does — not because there was briefly nothing to do.
398
+
399
+ **When you stop on purpose, say why — one command, and it is the last thing
400
+ you owe the next run.** See references/ci-and-staleness.md.
401
+
402
+ ```bash
403
+ ~/.fleet/bin/fleet-run fleet-heartbeat.mjs --stop "budget exhausted"
404
+ ```
405
+
406
+ It holds nothing and returns at once. Use it when the budget goes, or when
407
+ you can see the context ceiling coming. A drained run records it as the last
408
+ step of **End of run** (above), never at the drain itself.
409
+ Skipping it is not fatal — the next reader still reports the stop, it just
410
+ reports that the beat stopped *without a recorded reason*, which is all it can
411
+ honestly say about a death that named nothing.
412
+
413
+ **A stall line names whose stall it is.** Its opening words come off the controller
414
+ record:
415
+
416
+ - `heartbeat STALLED, controller gone` — the run that stranded them is dead.
417
+ Its claims without a PR come back through the next run's phase 0
418
+ stranded-claim step (references/phase-0.md, "Resume the claims a dead run
419
+ left without a PR"), and its open PRs through the fold-in.
420
+ - `heartbeat not beating, controller alive (pid N)` — a controller that
421
+ stopped beating and kept going. Nothing died, so nothing is resumed; when
422
+ that controller is you, the fix is to tick on every wake and beat when there
423
+ is nothing to do.
424
+ - `heartbeat STALLED` alone — no record says whether that controller lives.
425
+ The line is detection only.
426
+
427
+ Your first tick may print `previous run stopped deliberately at <time> (<age>
428
+ ago) — recorded reason: <reason>` instead: the stop the previous run recorded.
429
+ It is that run's end, not a verdict on yours, and no later tick prints it.
430
+
431
+ Read the line and carry on; nothing in the fleet releases, relabels or
432
+ reaps the stranded tickets for you, and nothing restarts the run that stranded them.
433
+
234
434
  ## The tick's rows
235
435
 
236
436
  Act on each line the tick prints as its row reads here, then stop: the next wake runs the tick again, and a row marked advice or not actionable asks nothing of you. The full text of each row is in references/tick-rows.md.
@@ -466,7 +666,7 @@ so demoting its ticket on its silence demotes a ticket its member never got the
466
666
  chance to judge. Confirm death first, exactly as the Member-killed row does
467
667
  (Failure handling, below) — its mtime discriminator, never silence alone.
468
668
  **Only an explicit bail report — the member itself naming the cause — reaches
469
- Implementer bails before implementing** (phase 3, below); a died or
669
+ Implementer bails before implementing** (**What a wake owes beyond its call**, above); a died or
470
670
  merely-quiet member's silence never does. A confirmed-dead member recovers
471
671
  exactly as the Member-killed row says — new member, new name, the SAME ticket —
472
672
  never a demotion.
@@ -484,7 +684,7 @@ spend classifier and `member-outcomes.mjs` read it.
484
684
 
485
685
  ### The reviewer
486
686
 
487
- **Dispatch the review, never wait on it, and act when its result lands. That is the default path.**
687
+ **Dispatch the review, never wait on it, and act when its result lands. That is the default path.** See references/reviewers.md.
488
688
  The tick names every PR owed one — `DISPATCH review PR#<M>`, oldest first —
489
689
  and each gets one review, once per PR:
490
690
 
@@ -618,7 +818,7 @@ the PR, the worktree, the dispatch pin, `<testCmd>` and the scratch root:
618
818
  `~/.fleet/bin/fleet-run dispatch-prompt.mjs render finisher --kind <fresh|repair>
619
819
  --member finisher-pr-<pr#> --pr <pr#> --pin <sha> --worktree <abs-path>
620
820
  --testCmd <testCmd> --scratch <scratch>`, which `ledger.mjs dispatch <pr#>
621
- finisher-pr-<pr#>` records.
821
+ finisher-pr-<pr#>` records. See references/finisher.md.
622
822
 
623
823
  ### The merge bot
624
824
 
@@ -889,6 +1089,28 @@ didn't parse from one that succeeded (`unread`/`unparsed`/`read`) —
889
1089
  `spend` in the same payload carries its own error the same way. Zeroes
890
1090
  on the cockpit are not yet proof the pipeline is idle.
891
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.
1111
+
1112
+ **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
+
892
1114
  ## The finisher gate
893
1115
 
894
1116
  Dispatch a finisher only when every test here passes at once — the review returned, the fix-applier's report arrived, your outbox to it is empty and no ruling is owed; hold otherwise, and on a repo with no CI and no declaration report it and add no label.
@@ -1078,406 +1300,49 @@ correctly halted. Note what that costs to detect: two reads of the same worktree
1078
1300
  a minute apart showed *different* mutants, so a member's report and any single
1079
1301
  `git status` are each valid only at their instant.
1080
1302
 
1081
- ## Judgement rules the drivers stop for
1082
-
1083
- Each rule here is a judgement a driver stops for: answer it by the cause it names, then run the tick, and report to the maintainer what the evidence cannot settle.
1084
-
1085
- ### Relabel causes
1086
-
1087
- - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`
1088
- settles `impl-<N>=bailed` and acts by `bailCause`, never by parsing `bailDetail`; no cause opens a PR:
1303
+ **The standing CI facts are in the fix-applier body, not in per-event messages** —
1304
+ otherwise you send "your red is staleness, do not rebase" once per member per
1305
+ merge. `review-and-fix.md` states them; the body says they apply.
1089
1306
 
1090
- | Cause | `bailCause` | `bailDetail` format | Issue action | Ledger |
1091
- |---|---|---|---|---|
1092
- | brief does not decide *what* to build | `undecided` | `<one line>` | relabel `needs-triage` | `=bailed` |
1093
- | genuine fork — options named, none ruled | `fork` | `<options>` | relabel `ready-for-human` | `=bailed` |
1094
- | needs human hands — external access, manual testing, judgment during the work | `hands` | `<cause>: <line>` | relabel `ready-for-human` | `=bailed` |
1095
- | already fixed on main | `fixed` | `<sha> — <how origin/main's tree meets the acceptance>` | `verify-sha.sh main <sha>` first. Exit 0: close N with the `fixed` comment (quotes `bailDetail`), drop `ready-for-agent`, release the claim (drops `in-progress`); no label. Exit 1: relabel `needs-triage`, comment `<sha> not on origin/main`. Exit 2: `NEED`, N untouched. | `=bailed` |
1096
- | covered by an open PR M | `covered` | `PR#M — <how M's diff meets N's acceptance>` | relabel `ready-for-human` with the `covered` comment: close N when M merges, or restore `ready-for-agent` if M closes unmerged. PR M's body is never edited. | `=bailed` |
1307
+ **Once the label is on, take it off before you approve any push.** Write `label-off=<latest finisher attempt>` onto the PR's row first
1308
+ (`ledger.mjs token <PR number> label-off=<latest finisher attempt>`, which keeps
1309
+ every token the row already has — **Run ledger**), then
1310
+ remove `ready-to-merge`, then approve. Then
1311
+ dispatch a **fresh** finisher against the new head; the first audit does not
1312
+ transfer, since it verified a different tree. The merge bot refuses that head on
1313
+ its own (`run-merge-bot.md`, **The labelled head**), so this is not the only
1314
+ guard — but its refusal costs a pass and leaves the label lying, which is yours
1315
+ to clear either way, the same way: `label-off=` first, then the removal.
1316
+ See references/finisher.md.
1097
1317
 
1098
- The call comments the cause and releases the claim — never when `fixed` neither closes nor relabels N — then ticks:
1099
- the head the tick names next is a *different* ticket.
1318
+ ### Own the CI waits
1100
1319
 
1101
- ### Resolving a finisher halt
1320
+ **Own the CI waits.** Arm a second persistent Monitor over open PRs'
1321
+ latest runs, keyed `<run-id>:<attempt>:<conclusion>` so each terminal state fires once, and
1322
+ emit the behind-count and the per-job conclusions with it: a `success` on a branch
1323
+ 8 behind is not actionable, and `check`-green-with-heavy-skipped is the staleness
1324
+ board you must not confuse with a red. Do not reinvent that read: one
1325
+ `~/.fleet/bin/fleet-run ci-state.mjs --pr <N>` per open PR already emits
1326
+ run id, attempt, conclusion, behind-count and per-job conclusions, branch derived
1327
+ from the PR — including the `gh api --hostname <host>` a GHE compare call needs,
1328
+ without which the probe 404s and `behind` comes back `null`, never 0, so every
1329
+ event reads as unknown-behind. Call it **without `--quiet`** — that flag drops
1330
+ `jobs`, `missing` and `dropped`.
1102
1331
 
1103
- A halt at duty 1 reads identical from a bare SHA mismatch whatever caused it,
1104
- and the causes `finisher-audit.mjs` names are the ones seen so far, not a
1105
- closed list; the finisher's agent body gives it the cause from one script
1106
- instead of asking anyone.
1332
+ A monitor event is a wake-up, never a verdict; members re-query at labelling
1333
+ time. See references/ci-and-staleness.md.
1107
1334
 
1108
- **Resolving a finisher halt.** A halt is the finisher working correctly — it
1109
- refused to label — so it is never `failed`, which stays for a finisher that
1110
- crashed or gave up. Record it `ledger.mjs settle finisher-pr-<M>=halted:<cause>`,
1111
- `<cause>` the word the report names; the ledger refuses any other. Every halt
1112
- escalates two ways: the cockpit flags the PR `halted:<cause>` at severity 4 off
1113
- that token, and you post the finisher's halt report — cause and evidence — with
1114
- `gh pr comment <M>`, so it outlives the run. **No label**: the missing
1115
- `ready-to-merge` already keeps the PR out of the merge queue. Then the cause
1116
- decides the rest — two resolve automatically, every other one escalates:
1335
+ **A conclusion is not stable, even for a fixed run id on an unchanged head.** Never cache a conclusion, and test any fallback you write against `""`, `null` AND a real conclusion. See references/ci-and-staleness.md.
1117
1336
 
1118
- | Cause | Resolution |
1119
- |---|---|
1120
- | `live-editor` | **Automatic.** Ask the live member **by name** for its report, wait for it, then dispatch `finisher-pr-<M>-b` at the current head. If the head moved in the meantime, resolve it as `past-pin` instead. |
1121
- | `past-pin` | **Automatic, through the tick: re-review.** The head carries commits no reviewer read. The tick prints `DISPATCH review PR#<M>` for a PR whose latest finisher is `halted:past-pin` and whose latest `reviewed=<head>` is not its current head, until a review of it is running or has returned; record that review's tokens as any review's, with `ledger.mjs review <M>` onto the PR's row, its runner under the next letter the PR's rows do not carry yet (`review-pr-<M>-b`, or `-c` where a retry already took `-b`) — `review` refuses a second `review-pr-<M>` and names the free one — and its result reaches a fresh finisher through the same gate. |
1122
- | `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. |
1123
- | `unreadable`, `missing`, `absent`, `other` | **Escalate** — comment and flag, no automatic retry. |
1337
+ **Every merge invalidates every other open PR, silently.** Behind-count is the
1338
+ only honest signal. See references/ci-and-staleness.md.
1124
1339
 
1125
- ### Why a `past-pin` halt needs the tick
1126
-
1127
- `past-pin` needs the tick because the dead-review path cannot reach it: once a
1128
- review has **returned**, its `reviewed=` token holds the PR out of `review-due=`,
1129
- and settling that review `=failed` re-queues nothing — that path covers only a
1130
- review that died before returning. A head that moved past `reviewed=` with no
1131
- such halt — a fix-applier's push — stays not due: duty 2 verifies what it
1132
- applied. **A rebase never gets a `reviewed=`:** the earlier review still
1133
- counts, because dispositions verdicts are tied to its head. Never write a
1134
- `reviewed=` you did not read off a review result file.
1135
-
1136
- ### Resolving a finisher that died
1137
-
1138
- **Resolving a finisher that died.** A finisher settled `failed` or `killed` left
1139
- its PR without `ready-to-merge`, and nothing else notices: the PR is out of the
1140
- merge queue, and the tick dispatches no finisher for it. The cockpit flags a
1141
- REVIEW-column open PR without the label, whose latest finisher attempt settled
1142
- so, `finisher:failed` or `finisher:killed` at severity 4 — one token per
1143
- outcome, neither the implementer's bare `killed` flag nor `unlabelled`. You
1144
- resolve it by hand: investigate why the finisher died, or dispatch the
1145
- next-suffix finisher after the finisher gate: render its brief with
1146
- `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<M>-<x>
1147
- …` (`--kind repair` once an earlier finisher reported `labelled` without the
1148
- label landing), then `ledger.mjs dispatch <M> finisher-pr-<M>-<x>`. The flag
1149
- clears when a later attempt settles — `labelled`
1150
- (then `unlabelled` takes over while the label is still off), `halted:<cause>`,
1151
- or a live `-b` — or when the PR carries `ready-to-merge`, merges or closes.
1152
- You never add the label yourself, and the tick does not retry.
1153
-
1154
- ### Dispositions escalation
1155
-
1156
- **A `DISPATCH fix-pr PR#<M>` on a dispositions mismatch** — the PR's row carries
1157
- `dispositions-mismatch=<member>:<run>` for its latest review's run, and no later
1158
- fix-applier has landed — is one automatic retry, a review fix-applier named the
1159
- next suffix (`fix-pr-<M>-b`) and dispatched like the first, its brief rendered with `--kind dispositions-retry`. Its prompt is the
1160
- first one's with the retry paragraph added: `--mismatched-record`, the mismatched member's record
1161
- `<scratch>/dispositions-<member>.json` it starts from, and `--violations`, the check's
1162
- violation list as printed — the lines
1163
- `dispositions-check.mjs` printed, each naming a bucket, an index and a rule;
1164
- re-run the check for the mismatched member to reprint them. It writes its own
1165
- record, and when it reports you run the check for it exactly as for the first
1166
- one. `ok` ends it. A second failure on the same review is written as
1167
- `dispositions-escalate=fix-pr-<M>-b:<run>` in place of a mismatch: the tick
1168
- prints no `DISPATCH fix-pr` for it, `dispatch` refuses the finisher naming
1169
- `dispositions escalate`, and you post the check's output with `gh pr comment <M>`
1170
- and flag the PR for a human. A new review of the PR counts again from none.
1171
-
1172
- ### The main-gain hit
1173
-
1174
- | Failure | Response |
1175
- |---|---|
1176
- | Merge bot's gate finds the merge would remove lines `main` gained after the PR's work began (`main-gain-removed:<path>`, exit 1) | Relay `main-gain-removed:<path>-#<pr>` to a human with the hit list and the acknowledgement keys the bot printed. PR stays queued, label untouched, nothing written to the ledger, and no member is dispatched to restore the lines — the removal may be deliberate. It clears when the PR stops removing them or its body carries `main-gain-removal: <path> <key> - <why>` for each hit |
1177
- | Merge bot's gate cannot answer the main-gain check (`main-gain-unanswerable`, exit 2) | Report `main-gain-unanswerable-#<pr>` with what it printed, PR stays queued, label untouched |
1178
-
1179
- ## Rules that fail silently
1180
-
1181
- 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.
1182
-
1183
- **A report is delivered, not implied by ending a turn.** Every member owes
1184
- the controller one report naming its outcome before it exits — including a
1185
- member whose prompt carries no explicit report line. The "report the SHA",
1186
- "report PR number" and "stop and report" instructions below *are* that
1187
- report, not a second obligation on top of it. A member that does the work
1188
- correctly and ends its turn has reported nothing: the controller learns the
1189
- outcome only by re-reading artifacts and pinging, and a finding held only in
1190
- that member's context is lost. Members did exactly this in one run —
1191
- finishers, a reviewer and merge bots alike, the work done and only the
1192
- delivery missing. Every brief's report paragraph says it. See
1193
- references/member-lifecycle.md.
1194
-
1195
- **The implementer, fix-applier, finisher, merge-bot and review-runner briefs
1196
- each tell the member you do not acknowledge reports; you never write that line
1197
- yourself.** A member cannot see whether its report arrived, so silence
1198
- from you is indistinguishable from loss and it re-sends — three members did in
1199
- one run, each burning a turn, and in all three the original had in fact arrived.
1200
- Each of those templates under `briefs/` carries the line: *"The controller does
1201
- not acknowledge reports. Send once and exit; never re-send unless the controller
1202
- asks by name."*
1203
-
1204
- So when a report is missing, **ask by name — that is what the "unless the controller asks by name" clause is for — and do not attribute the gap to the member until it answers.** See references/member-lifecycle.md.
1205
-
1206
- The ask is the controller's move, not the member's. See references/member-lifecycle.md.
1207
-
1208
- **Your OWN messages can vanish the same way, and that half is not recoverable by
1209
- a re-send.** Two controller rulings were lost in one run, both with a
1210
- `success: true` receipt: a member proceeded on its stated default and filed a
1211
- tracker issue the lost ruling had said not to file. So **never read a member's
1212
- silence as assent**, and require members to report their *confirmation state*
1213
- alongside results — "I escalated X, received no ruling, proceeded on default Y".
1214
- That sentence is the only thing that made either loss visible. After any ruling
1215
- you cannot confirm was received, re-check the artifacts it governed.
1216
-
1217
- **Name every member.** The name is the ledger token and the hub address; omit
1218
- it and the ledger cannot match the member.
1219
- Names follow the unit of work: `impl-<issue#>`, `fix-pr-<pr#>`,
1220
- `review-pr-<pr#>`, `finisher-pr-<pr#>`, `merge-bot-<n>`. See
1221
- references/member-lifecycle.md.
1222
-
1223
- **Fresh context per member.** One member, one unit of work, gone. Refill =
1224
- **new** member, **new** name.
1225
-
1226
- See references/member-lifecycle.md.
1227
-
1228
- **You read your instruments out of a tree every member can write to — re-check them before every gate decision.** See references/instruments.md.
1229
-
1230
- - **Pin once**, at phase 0 step 0 above, from the main checkout you are
1231
- running this from — never from a member's worktree, which is gitignored
1232
- and carries no baseline:
1233
- `~/.fleet/bin/fleet-run instruments.sh --pin`. On a workspace that does not
1234
- track `plugin/…`, the pin names the plugin checkout once — `--pin --audit
1235
- <plugin-checkout>` — and every check after it follows the recorded tree.
1236
- - **Re-check before you act on any instrument reading** — a SHA acceptance, a CI
1237
- verdict, the reconcile, a reap, a label gate — from that same main checkout:
1238
- `~/.fleet/bin/fleet-run instruments.sh`. **Exit 0 is the only code that
1239
- lets a gate proceed. Exit 1 (the set changed) and exit 2 (the check could not
1240
- answer) both refuse: report what it printed and do NOT re-read the
1241
- instrument** — a guard that fails open on "could not look" protects nothing.
1242
- **A label gate and a CI verdict are also taken by dispatched members, not
1243
- only by you** — the finisher labels, and the merge bot merges on its own
1244
- reading — and neither is dispatched to read this file, so neither can be
1245
- reached by stating the rule here. Each carries its own copy: the finisher's
1246
- in the instrument re-check block of `agents/fleet-finisher.agent.md`, the merge bot's inside
1247
- `merge-gate.mjs`, step 3 of `run-merge-bot.md`.
1248
- - **Re-pin only after a change you made deliberately.** The mid-run tooling fix
1249
- below is the one legitimate writer. Re-pinning to clear a refusal you cannot
1250
- explain discards the check.
1251
-
1252
- It covers every tracked file under this repo's own plugin component directories
1253
- — `plugin/commands/`, `plugin/scripts/`, `plugin/skills/`, `plugin/agents/`,
1254
- `plugin/workflows/` — the probes, the libraries they source, and these runbooks,
1255
- which you also read from that tree. Not the whole repo: `docs/metrics/` is
1256
- appended by THIS run, so covering it would refuse on the run's own bookkeeping
1257
- every time. It does not cover refs, deliberately: `claim-ticket.sh` creates a
1258
- branch per ticket and `reap.sh` deletes them in the ref store every worktree
1259
- shares, so ordinary work moves refs several times a run and a per-gate
1260
- refusal on that is noise.
1261
-
1262
- **A member runs its test suite in the foreground and blocks on it.** See references/member-lifecycle.md.
1263
-
1264
- **The scratch root is computed, never invented and never recorded.** No harness
1265
- supplies a scratch directory to a member, so a member left to name its own
1266
- invents one — and the liveness read in **The tick's rows**, which looks at the files under the
1267
- directory a member's own prompt assigned it, then watches a directory nobody
1268
- else can find. You run `~/.fleet/bin/fleet-run scratch-root.mjs`: it prints one
1269
- absolute path, `~/.fleet/scratch/<repo-key>/`, the same from the main checkout
1270
- and from every linked worktree, and the same again when a compacted or
1271
- re-invoked controller runs it, so nothing writes it down. It exits non-zero,
1272
- with its reason, when the root would sit inside a git work tree that does not
1273
- ignore it; that is an environment to fix, not a cue to pick another root.
1274
- `<scratch>` in this file is that path, resolved before you send anything: every
1275
- brief that carries one carries the absolute path you passed it — an
1276
- implementer's `<scratch>/impl-<N>/`, the recipe deriver's
1277
- `<scratch>/derive-recipe/`, a fix-applier's, a finisher's and a merge bot's
1278
- `<scratch>/pr<N>/…`, and a review runner's `scratch` argument, which is the
1279
- root itself. A member whose prompt names no path asks you by name and never
1280
- makes one up.
1281
-
1282
- ## Phase 3 — event loop
1283
-
1284
- The loop has no exit but the run's own end: act on each wake as the wake table gives it, keep the standing duties below between wakes, and drain the run when it ends (**End of run**).
1285
-
1286
- ### What a wake owes beyond its call
1287
-
1288
- Act on every line the call prints, the tick's last, and arm the beat. What a wake
1289
- owes beyond its call:
1290
-
1291
- - **Implementer report** → the PR is owed a review from here on:
1292
- the tick names it as `DISPATCH review PR#<M>`, and it names the next
1293
- shortlist head whenever the In-flight count is below the bound. Pull
1294
- nothing the tick did not name.
1295
- - **Finisher reports halted** → the per-cause rule (**Resolving a finisher
1296
- halt**, above).
1297
- - **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**, above), then run the tick.
1298
- - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`; the causes and the issue action each takes are under **Relabel causes**.
1299
- - **A review result lands** → see the result file written (by you or by the
1300
- runner — **Reviewers** says which, per harness), then
1301
- `fleet-event.mjs review-result <pr> <path>`: a PR with survived or unverified findings prints as
1302
- `DISPATCH fix-pr PR#<M>`. No survived or unverified finding and nothing to file → skip the
1303
- fix-applier and dispatch the **finisher** directly — or, when that head's
1304
- bound conclusion is red and not solely staleness, the red-check fixer
1305
- instead (**Then dispatch a red-check fixer**, above): no member will touch that
1306
- PR, so no push is coming and nothing will wake you. A review that failed is
1307
- not a result — **Reviewers** says who retries it and what takes over.
1308
- - **Monitor: `ready-to-merge` appears** → `fleet-event.mjs label <pr>`, and
1309
- on `DISPATCH merge-bot` dispatch the next `merge-bot-<n>` (**Merge bot**).
1310
- Catches hand-added labels as well as a finisher's own. The bot does not wait
1311
- for that finisher's report: its rebase is server-side,
1312
- `gh pr update-branch --rebase`, so nothing on the finisher's worktree has to
1313
- happen first.
1314
- - **Merge-bot pass reports done** → `fleet-event.mjs merge-bot-report merge-bot-<n>`. A label that
1315
- landed at the last moment reads `DISPATCH
1316
- merge-bot` again: dispatch the next bot straight away. A `rebase-fallback-#<pr>`
1317
- on a conflict needs no record from you — the bot already appended
1318
- `conflict-hold:#<pr>` to that PR's row, and the tick turns it into `DISPATCH
1319
- fix-pr PR#<pr>` (**Reviewers**).
1320
- - **The run ends, or the maintainer says drain** → `fleet-event.mjs drain --reason "<reason>"`
1321
- first (below). The marker is on disk, so the tick
1322
- holds the implementer row at `HOLD (draining)` from then on — for a
1323
- replacement controller too — and keeps the review, fix-applier and merge rows
1324
- firing until the open PRs merge. The drain starts the end of the run; it does
1325
- not end it. **End of run**, in this order:
1326
- 1. `~/.fleet/bin/fleet-run fleet-event.mjs drain --reason "<reason>"` — runs
1327
- `closeout.mjs begin --reason "<reason>"` (**Release the claims that never became
1328
- PRs**), then the tick. A claim it refuses prints `NEED hand-release #<N> <blocker>`: that
1329
- refusal is the finding, so run `worktree-audit.sh` and decide by hand.
1330
- `begin` selects a claim with no PR and a live or `bailed` implementer token
1331
- on every run, so a claim you keep leaves `begin` at exit 4 on every run:
1332
- release the rest by hand and run `begin` again.
1333
- 2. Act on the review, fix-applier and merge rows until the tick prints
1334
- `ACTION: run over → fleet-heartbeat.mjs --stop "<reason>"`, which it does
1335
- once the run is draining with no open PR and no live implementer.
1336
- 3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"` — scrapes
1337
- the run's member facts, checks the tier-outcomes rows, refreshes the
1338
- metrics, commits the three metrics files and opens their PR, sets the
1339
- phase-0 receipt aside as `.fleet/phase0.previous.json`, then records
1340
- `fleet-heartbeat.mjs --stop` last, because every reader takes a recorded
1341
- stop as the end of the run. A `NEED tier-outcomes append <pr>` is a PR
1342
- the ledger marks reviewed with no verdict row: append it with the verdict
1343
- you rule (**Guard: accumulate per PR**) and run `finish` again. A step it
1344
- fails is explained in references/close-out.md. Exit 0 is the run closed.
1345
- Drain only when a documented trigger fired, and name the trigger in `--reason`.
1346
- **Undrain** only when the maintainer asks, never a drain that budget or
1347
- context exhaustion caused (references/close-out.md).
1348
- - **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**.
1349
-
1350
- ### Tick notes
1351
-
1352
- **Tier guards under Pull.** The per-dispatch `tier-check.mjs --batch` is
1353
- phase 2's own step, run by `tick --apply`: a mismatch it finds is the
1354
- `HOLD (tier mismatch impl-<N>)` above, and a member it has no verdict on is
1355
- the `HOLD (tier unchecked impl-<N>)`. Nothing else in the loop owns it.
1356
-
1357
- **`--in-flight <B>`** is the run's In-flight bound: leave it at its default
1358
- unless `$ARGUMENTS` set it. omp's `task.maxConcurrency` is not the bound
1359
- (**Phase 2** gives the reasons) — below 2·B+2 it is a notice, never a hold.
1360
-
1361
- **Why every wake.** A merge cascade is a firehose of merges, CI greens and
1362
- rebases that holds your attention on the merge side while the implementer side
1363
- drains to 0 and stays there — 0 implementers emit no completion event, so a
1364
- refill that waited for one would never fire. A tick on every wake you are
1365
- already handling is what makes that drain visible. **Every wake is still an
1366
- edge, so none covers a fully drained queue**: no members and no open PRs is no
1367
- event at all, and the tick goes unrun. That state is the heartbeat's.
1368
-
1369
- ### The beat's standing rules
1370
-
1371
- The interval doubles while the fleet asks for nothing and stops at `--ceiling`,
1372
- so a quiet night costs ~26 wakes instead of ~96. **The ceiling is a real bound,
1373
- not a formality**: supply grows from OUTSIDE the fleet — a ticket you triage,
1374
- one a reviewer filed `needs-triage` included — and that arrival emits no
1375
- event either, so the ceiling is your worst-case latency for noticing it.
1376
- Do not raise it past 30 minutes.
1377
-
1378
- **The beat does not stop on idleness.** An empty shortlist and an empty supply are
1379
- not a reason to end the run; they are the state a newly triaged ticket arrives
1380
- into. The run ends when you drain it, when the budget goes, or when this
1381
- context does — not because there was briefly nothing to do.
1382
-
1383
- **`--fold-unchanged` belongs to the heartbeat alone** — `fleet-event.mjs heartbeat` passes it, no other kind does.
1384
- It prints one line instead of the rows when a tick asks for nothing AND says
1385
- exactly what the last one said, which is what makes an unattended night
1386
- affordable; a tick on any other wake is read by a controller that just acted and
1387
- needs the rows. An unchanged `PULL` or `DISPATCH` is never folded — unclaimed work
1388
- always prints in full, because folding it would hide the stall behind this
1389
- ticket's own remedy. A dirty or unknown main checkout still folds — clearing it
1390
- is the maintainer's, so it is nothing for you to act on — but the folded line
1391
- names it (`…; HOLD (main checkout dirty) persists — …`) for as long as it holds
1392
- dispatch.
1393
- A live member past its family's threshold folds the same way — it is advice, so
1394
- nothing for the heartbeat to act on by itself — and the folded line names each
1395
- such member (`…; CHECK-LIVENESS fix-pr-5 persists — …`) for as long as it stays
1396
- live past the threshold.
1397
- To-spec drops fold the same way, and the folded line names them
1398
- (`…; specs-dropped=#<N> (to-spec: User Stories heading) persists — …`) for as
1399
- long as the shortlist carries them.
1400
-
1401
- **When you stop on purpose, say why — one command, and it is the last thing
1402
- you owe the next run.** The beat leaves a dated mark on every hold, so a run
1403
- that dies abruptly (a crashed harness, a closed terminal, an OOM kill) is
1404
- detected by its mark going stale. A run that stops KNOWINGLY can do better
1405
- than being detected, and nothing else can write the reason afterwards:
1406
-
1407
- ```bash
1408
- ~/.fleet/bin/fleet-run fleet-heartbeat.mjs --stop "budget exhausted"
1409
- ```
1410
-
1411
- It holds nothing and returns at once. Use it when the budget goes, or when
1412
- you can see the context ceiling coming. A drained run records it as the last
1413
- step of **End of run** (above), never at the drain itself.
1414
- Skipping it is not fatal — the next reader still reports the stop, it just
1415
- reports that the beat stopped *without a recorded reason*, which is all it can
1416
- honestly say about a death that named nothing.
1417
-
1418
- **A stall line names whose stall it is.** `fleet-tick`
1419
- prints it before its own rows when this run has gone quiet: its beat overdue
1420
- against the interval it recorded with no tick or ledger write inside the
1421
- 40-minute window either — or, with no beat yet, its latest tick, ledger write
1422
- or controller record past that window. A beat, tick or ledger write older than
1423
- the controller record rotate wrote is the previous run's and counts for
1424
- nothing. The cockpit shows the same line while it is up. It names when this
1425
- run was last seen, how overdue that is, how many
1426
- tickets are still claimed and in flight, and how much supply the last read found
1427
- — because the claimed ones keep the `in-progress` label, the candidate scan
1428
- EXCLUDES that label, and those tickets are therefore invisible to your own
1429
- shortlist and to the maintainer's. Its opening words come off the controller
1430
- record:
1431
-
1432
- - `heartbeat STALLED, controller gone` — the run that stranded them is dead.
1433
- Its claims without a PR come back through the next run's phase 0
1434
- stranded-claim step (references/phase-0.md, "Resume the claims a dead run
1435
- left without a PR"), and its open PRs through the fold-in.
1436
- - `heartbeat not beating, controller alive (pid N)` — a controller that
1437
- stopped beating and kept going. Nothing died, so nothing is resumed; when
1438
- that controller is you, the fix is to tick on every wake and beat when there
1439
- is nothing to do.
1440
- - `heartbeat STALLED` alone — no record says whether that controller lives.
1441
- The line is detection only.
1442
-
1443
- Your first tick may print `previous run stopped deliberately at <time> (<age>
1444
- ago) — recorded reason: <reason>` instead: the stop the previous run recorded.
1445
- It is that run's end, not a verdict on yours, and no later tick prints it.
1446
-
1447
- Read the line and carry on; nothing in the fleet releases, relabels or
1448
- reaps the stranded tickets for you, and nothing restarts the run that stranded them.
1449
-
1450
- ### Own the CI waits
1451
-
1452
- **Own the CI waits.** Members are turn-based and cannot hold across a ten-minute
1453
- run — they rebase, push, stop. Arm a second persistent Monitor over open PRs'
1454
- latest runs, keyed `<run-id>:<attempt>:<conclusion>` so each terminal state fires once, and
1455
- emit the behind-count and the per-job conclusions with it: a `success` on a branch
1456
- 8 behind is not actionable, and `check`-green-with-heavy-skipped is the staleness
1457
- board you must not confuse with a red. Do not reinvent that read: one
1458
- `~/.fleet/bin/fleet-run ci-state.mjs --pr <N>` per open PR already emits
1459
- run id, attempt, conclusion, behind-count and per-job conclusions, branch derived
1460
- from the PR — including the `gh api --hostname <host>` a GHE compare call needs,
1461
- without which the probe 404s and `behind` comes back `null`, never 0, so every
1462
- event reads as unknown-behind. Call it **without `--quiet`** — that flag drops
1463
- `jobs`, `missing` and `dropped`.
1464
-
1465
- A monitor event is a wake-up, never a verdict; members re-query at labelling
1466
- time. See references/ci-and-staleness.md.
1467
-
1468
- **A conclusion is not stable, even for a fixed run id on an unchanged head.** Never cache a conclusion, and test any fallback you write against `""`, `null` AND a real conclusion. See references/ci-and-staleness.md.
1469
-
1470
- **A probe that cannot read is a transition event, not silence.** The natural
1471
- watcher shape — `st=$(ci-state.mjs --pr "$pr" 2>/dev/null) || continue` — fails
1472
- closed correctly and then throws the failure away, so a rate-limited probe emits
1473
- NOTHING and "no event" becomes indistinguishable from "not green yet". Every
1474
- open PR stalls unlabelled while the run reads as merely quiet; observed live
1475
- twice. So check the REST budget *before* polling — `gh api rate_limit` is itself
1476
- unmetered (`gh api rate_limit --jq '.resources.core.remaining'`, re-confirmed
1477
- 2026-09-08) — and give an unreadable `ci-state` payload the same treatment one
1478
- level down. The shape is **one outer tick loop with an inner pass over the open
1479
- PRs**, because the two causes live at different levels — the budget is one
1480
- account, the payload is one PR:
1340
+ **A probe that cannot read is a transition event, not silence.** So check the REST budget *before* polling — `gh api rate_limit` is itself
1341
+ unmetered (`gh api rate_limit --jq '.resources.core.remaining'`, re-confirmed
1342
+ 2026-09-08) — and give an unreadable `ci-state` payload the same treatment one
1343
+ level down. The shape is **one outer tick loop with an inner pass over the open
1344
+ PRs**, because the two causes live at different levels — the budget is one
1345
+ account, the payload is one PR:
1481
1346
 
1482
1347
  ```sh
1483
1348
  budget_out= hidden_out= list_out= cap_out= blind= # seed the latches: set -u reads each below before tick 1 ever assigns one
@@ -1569,9 +1434,7 @@ done
1569
1434
  **One gate, not a gate plus a caveat.** See references/ci-watcher.md.
1570
1435
 
1571
1436
  **It has to latch** — one line entering the degraded state, one leaving it,
1572
- never one per tick. An unlatched line at a 120s poll gets the Monitor
1573
- auto-stopped for volume during even a two-minute outage, reintroducing the same
1574
- blindness by another route. **Latch each cause at the level its cause lives
1437
+ never one per tick. **Latch each cause at the level its cause lives
1575
1438
  at**: the budget, the unreported Actions bucket, the open-PR list and that
1576
1439
  list's cap are one global flag each — all account-level — but the payload latch is keyed by PR, and a
1577
1440
  shared scalar for those is an unlatched line wearing a latch's clothes — one
@@ -1579,18 +1442,11 @@ persistently blind PR alongside one healthy PR re-clears the flag on every tick,
1579
1442
  emitting a DEGRADED and a false RECOVERED pair forever. The recovery line is
1580
1443
  what says the watch is alive again, so emit it on the first good pass even when
1581
1444
  nothing about the CI state changed; without it a reader cannot tell a recovered
1582
- watcher from a dead one.
1445
+ watcher from a dead one. See references/ci-watcher.md.
1583
1446
 
1584
1447
  **All pacing in exactly one place — the tick loop's own tail `sleep`.** See references/ci-watcher.md.
1585
1448
 
1586
- **Guard every probe, not just the ones with a verdict field.** The block makes
1587
- several API calls, and `gh pr list` is the one that looks like
1588
- plumbing rather than a probe. A non-quota failure there (secondary/abuse limit,
1589
- 5xx, network blip, expired token) does not lower `.resources.core.remaining`, so
1590
- the budget gate clears, and an unguarded `for pr in $(gh pr list …)` then
1591
- iterates zero times over the empty substitution: no error, no latch, no DEGRADED
1592
- line — total silence per tick, which is exactly what this section exists to
1593
- remove. Capture its status (`prs=$(…) || prs=ERR`), latch it globally, and fall
1449
+ **Guard every probe, not just the ones with a verdict field.** Capture its status (`prs=$(…) || prs=ERR`), latch it globally, and fall
1594
1450
  through to the same tail sleep. **Then iterate with the inline `$(printf '%s\n'
1595
1451
  "$prs")` form, never a bare `for pr in $prs`** — the assigned-variable trap in
1596
1452
  **Shell traps**, in references/shell-traps.md, which carries the mechanism and its
@@ -1598,482 +1454,236 @@ measurements. It
1598
1454
  applies to **every** loop in this block, the `blind`-latch removal included:
1599
1455
  under a bare `for b in $blind` a recovered PR never leaves the list and the block
1600
1456
  emits a fresh RECOVERED every tick thereafter — the per-tick volume the latch
1601
- exists to prevent, wearing a latch's clothes again.
1457
+ exists to prevent, wearing a latch's clothes again. See references/ci-watcher.md.
1602
1458
 
1603
- **A readable list can still be a short one.** `gh pr list` stops at its
1604
- `--limit` — 30 when the flag is absent — with exit 0 and no warning, so a list
1605
- that fills its limit reads exactly like one that happens to be that long, and
1606
- every open PR past it goes unwatched without a line. Pass an explicit `--limit`
1459
+ **A readable list can still be a short one.** Pass an explicit `--limit`
1607
1460
  (`pr_cap`: one variable for the flag and the check, so the two cannot drift)
1608
1461
  and treat a full page as its own degraded cause — the "no silent caps" rule
1609
1462
  `candidates.mjs`'s `refuseIfCapped()` and `fleet-tick.mjs`'s `PR_LIMIT` already
1610
1463
  enforce. It latches globally like the list failure but does not pause polling:
1611
1464
  the PRs it did return are real, so the pass still watches them, and the
1612
1465
  DEGRADED line names the rest as unwatched until a pass comes back under the cap.
1466
+ See references/ci-watcher.md.
1613
1467
 
1614
1468
  **The budget is gated at the floor and again at the pass, and only the pass gates announce recovery.** See references/ci-watcher.md.
1615
1469
 
1616
1470
  **Judge `ci-state` on its payload, never its exit code** — the same rule as
1617
- `merge-gate.mjs` applies on the merge path, applied to the watcher. `not-green` is an
1618
- ordinary, frequent state that exits non-zero, so a watcher treating any non-zero
1619
- exit as an outage misfires constantly on PRs that are merely in progress. Empty
1471
+ `merge-gate.mjs` applies on the merge path, applied to the watcher. Empty
1620
1472
  or unparseable output is the degraded case; parseable JSON reading
1621
1473
  `verdict: "not-green"` is normal. An exhausted quota may also name itself,
1622
1474
  `verdict: "rate-limited"` on stdout — likewise degraded, and likewise not a
1623
1475
  reading, which is why the gate above tests the verdict rather than mere
1624
- parseability: that payload is perfectly good JSON.
1476
+ parseability: that payload is perfectly good JSON. See references/ci-watcher.md.
1477
+
1478
+ **Do not back off and wait.** See references/ci-watcher.md.
1479
+
1480
+ **While the budget is exhausted, `ledger.mjs check` reads
1481
+ `verdict: "unverified"`** — the ledger was read and the tracker was not. That is
1482
+ correct behaviour, but `unverified` exits **0**, the same as `clean`, so a
1483
+ member treating exit 0 as "safe to file" files blind during exactly this window.
1484
+ Say so when you flag the outage: members read `verdict` explicitly until you
1485
+ report recovery, never the exit code alone.
1486
+
1487
+ ## Judgement rules the drivers stop for
1488
+
1489
+ Each rule here is a judgement a driver stops for: answer it by the cause it names, then run the tick, and report to the maintainer what the evidence cannot settle.
1490
+
1491
+ ### Relabel causes
1492
+
1493
+ - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`
1494
+ settles `impl-<N>=bailed` and acts by `bailCause`, never by parsing `bailDetail`; no cause opens a PR:
1495
+
1496
+ | Cause | `bailCause` | `bailDetail` format | Issue action | Ledger |
1497
+ |---|---|---|---|---|
1498
+ | brief does not decide *what* to build | `undecided` | `<one line>` | relabel `needs-triage` | `=bailed` |
1499
+ | genuine fork — options named, none ruled | `fork` | `<options>` | relabel `ready-for-human` | `=bailed` |
1500
+ | needs human hands — external access, manual testing, judgment during the work | `hands` | `<cause>: <line>` | relabel `ready-for-human` | `=bailed` |
1501
+ | already fixed on main | `fixed` | `<sha> — <how origin/main's tree meets the acceptance>` | `verify-sha.sh main <sha>` first. Exit 0: close N with the `fixed` comment (quotes `bailDetail`), drop `ready-for-agent`, release the claim (drops `in-progress`); no label. Exit 1: relabel `needs-triage`, comment `<sha> not on origin/main`. Exit 2: `NEED`, N untouched. | `=bailed` |
1502
+ | covered by an open PR M | `covered` | `PR#M — <how M's diff meets N's acceptance>` | relabel `ready-for-human` with the `covered` comment: close N when M merges, or restore `ready-for-agent` if M closes unmerged. PR M's body is never edited. | `=bailed` |
1503
+
1504
+ The call comments the cause and releases the claim — never when `fixed` neither closes nor relabels N — then ticks:
1505
+ the head the tick names next is a *different* ticket.
1506
+
1507
+ ### Resolving a finisher halt
1508
+
1509
+ A halt at duty 1 reads identical from a bare SHA mismatch whatever caused it,
1510
+ and the causes `finisher-audit.mjs` names are the ones seen so far, not a
1511
+ closed list; the finisher's agent body gives it the cause from one script
1512
+ instead of asking anyone.
1513
+
1514
+ **Resolving a finisher halt.** A halt is the finisher working correctly — it
1515
+ refused to label — so it is never `failed`, which stays for a finisher that
1516
+ crashed or gave up. Record it `ledger.mjs settle finisher-pr-<M>=halted:<cause>`,
1517
+ `<cause>` the word the report names; the ledger refuses any other. Every halt
1518
+ escalates two ways: the cockpit flags the PR `halted:<cause>` at severity 4 off
1519
+ that token, and you post the finisher's halt report — cause and evidence — with
1520
+ `gh pr comment <M>`, so it outlives the run. **No label**: the missing
1521
+ `ready-to-merge` already keeps the PR out of the merge queue. Then the cause
1522
+ decides the rest — two resolve automatically, every other one escalates:
1523
+
1524
+ | Cause | Resolution |
1525
+ |---|---|
1526
+ | `live-editor` | **Automatic.** Ask the live member **by name** for its report, wait for it, then dispatch `finisher-pr-<M>-b` at the current head. If the head moved in the meantime, resolve it as `past-pin` instead. |
1527
+ | `past-pin` | **Automatic, through the tick: re-review.** The head carries commits no reviewer read. The tick prints `DISPATCH review PR#<M>` for a PR whose latest finisher is `halted:past-pin` and whose latest `reviewed=<head>` is not its current head, until a review of it is running or has returned; record that review's tokens as any review's, with `ledger.mjs review <M>` onto the PR's row, its runner under the next letter the PR's rows do not carry yet (`review-pr-<M>-b`, or `-c` where a retry already took `-b`) — `review` refuses a second `review-pr-<M>` and names the free one — and its result reaches a fresh finisher through the same gate. |
1528
+ | `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. |
1529
+ | `unreadable`, `missing`, `absent`, `other` | **Escalate** — comment and flag, no automatic retry. |
1625
1530
 
1626
- **Do not back off and wait.** See references/ci-watcher.md.
1531
+ ### Why a `past-pin` halt needs the tick
1627
1532
 
1628
- **While the budget is exhausted, `ledger.mjs check` reads
1629
- `verdict: "unverified"`** — the ledger was read and the tracker was not. That is
1630
- correct behaviour, but `unverified` exits **0**, the same as `clean`, so a
1631
- member treating exit 0 as "safe to file" files blind during exactly this window.
1632
- Say so when you flag the outage: members read `verdict` explicitly until you
1633
- report recovery, never the exit code alone.
1533
+ `past-pin` needs the tick because the dead-review path cannot reach it: once a
1534
+ review has **returned**, its `reviewed=` token holds the PR out of `review-due=`,
1535
+ and settling that review `=failed` re-queues nothing — that path covers only a
1536
+ review that died before returning. A head that moved past `reviewed=` with no
1537
+ such halt — a fix-applier's push — stays not due: duty 2 verifies what it
1538
+ applied. **A rebase never gets a `reviewed=`:** the earlier review still
1539
+ counts, because dispositions verdicts are tied to its head. Never write a
1540
+ `reviewed=` you did not read off a review result file.
1634
1541
 
1635
- ### Reviewers
1542
+ ### Resolving a finisher that died
1636
1543
 
1637
- **`selectDimensions` sizes the fan-out to the diff and the verify budget
1638
- follows severity.** It cuts one immutable
1639
- snapshot, verifies every critical/important finding adversarially,
1640
- re-dispatches each crashed specialist and each crashed refuter pair once before
1641
- it assembles the result, and has `agent()` return **into the script**, so no
1642
- report can go undelivered and you relay nothing — the delivery failure that
1643
- cost one fleet five reports on one PR and four on another. None of it runs on
1644
- your turn: a review takes 20-40 minutes, and a controller that held its turn
1645
- for each one, one at a time, was what left implementer slots empty.
1544
+ **Resolving a finisher that died.** A finisher settled `failed` or `killed` left
1545
+ its PR without `ready-to-merge`, and nothing else notices: the PR is out of the
1546
+ merge queue, and the tick dispatches no finisher for it. The cockpit flags a
1547
+ REVIEW-column open PR without the label, whose latest finisher attempt settled
1548
+ so, `finisher:failed` or `finisher:killed` at severity 4 — one token per
1549
+ outcome, neither the implementer's bare `killed` flag nor `unlabelled`. You
1550
+ resolve it by hand: investigate why the finisher died, or dispatch the
1551
+ next-suffix finisher after the finisher gate: render its brief with
1552
+ `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<M>-<x>
1553
+ …` (`--kind repair` once an earlier finisher reported `labelled` without the
1554
+ label landing), then `ledger.mjs dispatch <M> finisher-pr-<M>-<x>`. The flag
1555
+ clears when a later attempt settles — `labelled`
1556
+ (then `unlabelled` takes over while the label is still off), `halted:<cause>`,
1557
+ or a live `-b` — or when the PR carries `ready-to-merge`, merges or closes.
1558
+ You never add the label yourself, and the tick does not retry.
1646
1559
 
1647
- **Know the trim before you rely on it — it is wider than `docsOnly` suggests.** See references/reviewers.md.
1560
+ ### Dispositions escalation
1648
1561
 
1649
- **Review units are counted, never capped.** A review in flight — a
1650
- `review-pr-<pr#>` member still running — is one; each
1651
- `fix-pr-<pr#>` is one; a finisher and a CI wait are zero; and a PR holds at most
1652
- one unit at a time. In-flight reviews are derived, never remembered: a row with
1653
- `review=` and no `reviewed=` is one, which is why the token goes on before
1654
- anything else. A review whose PR has left the open list is probed: when
1655
- `gh pr view` reports that PR MERGED or CLOSED, the tick drops the review,
1656
- since none runs against a finished PR, and a probe that
1657
- cannot answer keeps the unit counted. A unit is not the agents a review fans
1658
- out to — 1 snapshot + up to 6 specialists + 2 refuters per critical/important finding.
1659
-
1660
- It returns `{pr, head, resume, testEnvironment, suiteVerdict, dimensionsRun, dimensionsUnrun, cwdAudit, counts, snapshot, survived, refuted, unverified}` —
1661
- the **digest** first, then the snapshot and the findings — and it lands as
1662
- one artefact, `<scratch>/pr<pr>/<run>/review.json`, holding that bare object.
1663
- `<run>` is the review's own `run-XXXXXXXX` run root, the one its snapshot sits
1664
- in, so a second review of the PR — at the same head or another — writes a file
1665
- of its own and never replaces the first. The `review-pr-<pr#>` runner writes it
1666
- and reports the digest, the path and the `reviewed=` token naming that run.
1562
+ **A `DISPATCH fix-pr PR#<M>` on a dispositions mismatch** — the PR's row carries
1563
+ `dispositions-mismatch=<member>:<run>` for its latest review's run, and no later
1564
+ fix-applier has landed — is one automatic retry, a review fix-applier named the
1565
+ next suffix (`fix-pr-<M>-b`) and dispatched like the first, its brief rendered with `--kind dispositions-retry`. Its prompt is the
1566
+ first one's with the retry paragraph added: `--mismatched-record`, the mismatched member's record
1567
+ `<scratch>/dispositions-<member>.json` it starts from, and `--violations`, the check's
1568
+ violation list as printed — the lines
1569
+ `dispositions-check.mjs` printed, each naming a bucket, an index and a rule;
1570
+ re-run the check for the mismatched member to reprint them. It writes its own
1571
+ record, and when it reports you run the check for it exactly as for the first
1572
+ one. `ok` ends it. A second failure on the same review is written as
1573
+ `dispositions-escalate=fix-pr-<M>-b:<run>` in place of a mismatch: the tick
1574
+ prints no `DISPATCH fix-pr` for it, `dispatch` refuses the finisher naming
1575
+ `dispositions escalate`, and you post the check's output with `gh pr comment <M>`
1576
+ and flag the PR for a human. A new review of the PR counts again from none.
1667
1577
 
1668
- **You read the digest; the findings are the fix-applier's.**
1669
- `jq '{pr, head, resume, testEnvironment, suiteVerdict, dimensionsRun, dimensionsUnrun, cwdAudit, counts}' <scratch>/pr<pr>/<run>/review.json`
1670
- is the whole of your read, and it is what you record: `reviewed=<head>:<survived>/<refuted>/<unverified>:<run>`,
1671
- off `head`, `counts` and the run root's name — the runner's `ledger` field — on
1672
- the PR's row, with `ledger.mjs review <pr>`. Every later path to the review is built from that token, before a
1673
- restart or after one: `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's
1674
- latest `reviewed=`. Findings never enter your context —
1675
- a result runs 26-61 KB, 7-15k tokens that used to land in yours and then again,
1676
- up to ~10k characters, in every fix-applier prompt you wrote. The fix-applier
1677
- reads every one off the file and makes every ruling on them
1678
- (`review-and-fix.md`'s **The review result file**). What the digest tells you:
1578
+ ### The main-gain hit
1679
1579
 
1680
- **`testEnvironment` says what the review's shared test run is evidence about**,
1681
- and it is present on a healthy run as well as a degraded one, so there is
1682
- nothing to notice by its absence. The snapshot is `git archive`d and then
1683
- `git init`ed with one commit, and the block that cuts it compares that commit's
1684
- tree hash against the reviewed commit's: verified, a suite run in the
1685
- snapshot collects and runs what a checkout does, and a red is a fact about the
1686
- tree. UNVERIFIED, it is not — tests that ask git what ships decline or fail for
1687
- the environment, and this field carries which line of the cut failed. The
1688
- fix-applier reads it before acting on any count from the shared test run; you
1689
- read it so a degraded review never passes for a clean one.
1690
- `unverified` is *not* "checked and cleared" — a `suggestion` skips the pass by
1691
- policy, and a finding whose refuters all crashed or abstained lands there too.
1692
- All go to the fix-applier with the rest, in the file; never rule on them yourself.
1693
- **`refutersDispatched`, carried on every finding, is what tells the skip
1694
- apart** — zero is the policy skip, above zero with no deciding vote is a
1695
- refuter pass that decided nothing — and `refutersInconclusive` splits that
1696
- one: the refuters it counts abstained, every other one dispatched crashed. So
1697
- the population is read off those fields rather than off severity,
1698
- which records only how much a finding would matter if true. `resume` is
1699
- non-null exactly when a specialist or a refuter pair crashed again after the
1700
- review's own in-run retry, and **resume beats handing a crash-heavy review
1701
- on**: a deferred crash is a finding nobody ever looked at.
1702
- 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.
1703
- `refuted` comes back deliberately as well — a refutation is itself a claim, and
1704
- one has been reversed on new evidence. The fix-applier may reverse one; it
1705
- records each reversal as a `refuted` entry in its disposition record, the
1706
- evidence in `reason`, and you copy those entries to the ledger's `ruled` line by
1707
- running `fleet-event.mjs fix-report`, whose `settle --from-report` records each
1708
- refutation the report defers.
1580
+ | Failure | Response |
1581
+ |---|---|
1582
+ | Merge bot's gate finds the merge would remove lines `main` gained after the PR's work began (`main-gain-removed:<path>`, exit 1) | Relay `main-gain-removed:<path>-#<pr>` to a human with the hit list and the acknowledgement keys the bot printed. PR stays queued, label untouched, nothing written to the ledger, and no member is dispatched to restore the lines — the removal may be deliberate. It clears when the PR stops removing them or its body carries `main-gain-removal: <path> <key> - <why>` for each hit |
1583
+ | Merge bot's gate cannot answer the main-gain check (`main-gain-unanswerable`, exit 2) | Report `main-gain-unanswerable-#<pr>` with what it printed, PR stays queued, label untouched |
1709
1584
 
1710
- **`dimensionsRun` is the dispatch; `dimensionsUnrun` is what names a gap.** A
1711
- specialist that dies contributes zero findings while its key stays in
1712
- `dimensionsRun`. So read the two together. A key
1713
- in `dimensionsRun` and NOT in `dimensionsUnrun` returned a review; every
1714
- `dimensionsUnrun` entry is `{dimension, reason}` naming a specialist that
1715
- returned nothing, its `reason` the crash.
1716
- The review already re-dispatched each crashed specialist once, so what
1717
- `dimensionsUnrun` still names is residue: the fix-applier names it unrun in its
1718
- report and re-runs nothing — an absence of findings is not coverage.
1585
+ ## Rules that fail silently
1719
1586
 
1720
- **`suiteVerdict` is the review's one judgement on its shared test run:
1721
- `{usable, reason}`, always present.** The review runs the test command once,
1722
- and every specialist reads that run instead of running its own. `{usable: true,
1723
- reason: null}` says the run executed real tests and its exit status is
1724
- explained by its counts; usable is not green, so a usable run can have failing
1725
- tests. `usable: false` carries `reason`, including a run that never started
1726
- (install skipped, no suite slot). The verdict says nothing about whether a
1727
- dimension reviewed: a specialist that returned reviewed, its findings verified
1728
- as usual, whatever the suite verdict. With `usable: false` the
1729
- fix-applier names `shared test run not usable: <reason>` in its final verdict
1730
- and does nothing more — its own `<testCmd>` run and CI cover the suite.
1587
+ 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.
1731
1588
 
1732
- The rule this replaces — treat any dimension with nothing in
1733
- `survived`/`refuted`/`unverified` as unrun — was the workaround for having no
1734
- such field, and it over-refuses in the direction that costs work: a dimension
1735
- that ran clean has nothing in those three either.
1589
+ **A report is delivered, not implied by ending a turn.** Every member owes
1590
+ the controller one report naming its outcome before it exits — including a
1591
+ member whose prompt carries no explicit report line. The "report the SHA",
1592
+ "report PR number" and "stop and report" instructions below *are* that
1593
+ report, not a second obligation on top of it. A member that does the work
1594
+ correctly and ends its turn has reported nothing: the controller learns the
1595
+ outcome only by re-reading artifacts and pinging, and a finding held only in
1596
+ that member's context is lost. Members did exactly this in one run —
1597
+ finishers, a reviewer and merge bots alike, the work done and only the
1598
+ delivery missing. Every brief's report paragraph says it. See
1599
+ references/member-lifecycle.md.
1736
1600
 
1737
- **`cwdAudit` is the specialist's own inherited-checkout audit, one entry per
1738
- dispatched dimension: `{dimension, state, line}`, `state` one of**
1739
- `clean`/`dirty`/`unrepo`/`missing`. The prompt asks every specialist to run
1740
- `git status --porcelain -uall` against the directory it inherited before
1741
- returning and fold the result into a `CWD-AUDIT:` line — a convention, not
1742
- schema, so a specialist that never emits it or misspells it still validates
1743
- clean. This field is the runtime backstop that reads for the line regardless:
1744
- `missing` is exactly as loud as `dirty`, and both name a checkout you should
1745
- treat as a live scratch write until you have checked it.
1601
+ **The implementer, fix-applier, finisher, merge-bot and review-runner briefs
1602
+ each tell the member you do not acknowledge reports; you never write that line
1603
+ yourself.** A member cannot see whether its report arrived, so silence
1604
+ from you is indistinguishable from loss and it re-sends — three members did in
1605
+ one run, each burning a turn, and in all three the original had in fact arrived.
1606
+ Each of those templates under `briefs/` carries the line: *"The controller does
1607
+ not acknowledge reports. Send once and exit; never re-send unless the controller
1608
+ asks by name."*
1746
1609
 
1747
- **A throw, an empty return, or a notification whose status is not `completed`
1748
- is a failure event, not a clean review.** The review throws on missing
1749
- `args.pr`/`args.worktree` and on a snapshot agent that returned no tree, and a
1750
- failure surfaces mid-loop, where "react, never block" makes it easy to log and
1751
- carry on — leaving a PR that *reads* as reviewed and is not. **Retry once
1752
- under the next runner letter, then stop:** the `review-pr-<pr#>` runner
1753
- already retried once inside its cell and reports `failed` with both errors, so
1754
- the retry is a second runner under the next letter the PR's rows do not carry
1755
- yet — `review-pr-<pr#>-b` after a first runner — recorded as
1756
- `review=member:<that name>`. A second failure is reported, and the PR stays at
1757
- review-due. Settle the dead review by appending `=failed` to its `review=`
1758
- token, then append `review=member:<that name>` after it, both in one
1759
- `ledger.mjs review <pr> review=<dead launch>=failed review=member:<that name>`
1760
- call — `review` settles the dead launch in place, and refuses a new launch
1761
- while the dead one still reads open, and the tick takes the last `review=`
1762
- token on a row as the one in flight, so that order is what keeps the retry
1763
- counted as the live review unit. A
1764
- runner member that is killed rather than failed follows **Failure
1765
- handling** (fresh name, inherited state stated).
1610
+ So when a report is missing, **ask by name — that is what the "unless the controller asks by name" clause is for — and do not attribute the gap to the member until it answers.** See references/member-lifecycle.md.
1766
1611
 
1767
- **A refutation resting on an injection nothing proved landed is not a refutation — it is a cell that never ran.** See references/reviewers.md.
1612
+ The ask is the controller's move, not the member's. See references/member-lifecycle.md.
1768
1613
 
1769
- **The suite slot bound.** At most `FLEET_SUITE_SLOTS` full suite runs execute on
1770
- a host at once — 2 by default, whatever the member caps count — because each run
1771
- already spreads over most of the host's cores. The bound is a wrapper,
1772
- `suite-slot.mjs`, reached through the Resolver:
1773
- `~/.fleet/bin/fleet-run suite-slot.mjs '<entrypoint>' [args…]` runs the
1774
- entrypoint under `sh -c` with the arguments after it. It decides by arity. No
1775
- appended arguments is a full run, and waits for a free slot. Appended arguments
1776
- are a narrowed run, which takes none, so a verifier's `<testCmd> <args>` never
1777
- queues behind a full run — only its fall-back to the full command does. A
1778
- wrapper that finds `FLEET_SUITE_SLOT` set, which a slot holder exports to its
1779
- child, is nested inside a held slot and runs straight through, so a wrapped
1780
- command inside a wrapped command is one run, not a deadlock. A slot is a
1781
- pid-stamped lock file in `~/.fleet/suite-slots/` — under the host's `~/.fleet/`
1782
- and not a workspace's, because the bound is host-wide and a review's snapshot is
1783
- no repository.
1784
-
1785
- Each seat that runs the full suite is wrapped where its command is written. A
1786
- claim's runner execs the Test entrypoint through the wrapper. A repository that
1787
- tracks its own runner wraps it itself, as this repository's `agent-test` does
1788
- for a bare run. The review wraps its own one run, so its args carry the bare
1789
- command; a wrapped string handed to it anyway is harmless, the inner wrapper
1790
- being the nested case. The fix-applier and the finisher run what you hand them,
1791
- so hand them the wrapped string as `<testCmd>` —
1792
- `~/.fleet/bin/fleet-run suite-slot.mjs '<command>'`, each `'` inside the command
1793
- written `'\''` — and it is still one command for every gate.
1614
+ **Your OWN messages can vanish the same way, and that half is not recoverable by
1615
+ a re-send.** Two controller rulings were lost in one run, both with a
1616
+ `success: true` receipt: a member proceeded on its stated default and filed a
1617
+ tracker issue the lost ruling had said not to file. So **never read a member's
1618
+ silence as assent**, and require members to report their *confirmation state*
1619
+ alongside results — "I escalated X, received no ruling, proceeded on default Y".
1620
+ That sentence is the only thing that made either loss visible. After any ruling
1621
+ you cannot confirm was received, re-check the artifacts it governed.
1794
1622
 
1795
- The wait is capped and never counts against the run: `FLEET_SUITE_WAIT_S`
1796
- seconds, 1800 by default, then exit **75** with `suite-slot: no slot after <n>s`
1797
- on stderr, having launched nothing. On getting a slot the wrapper prints
1798
- `SUITE_SLOT_WAIT_MS=<n>` — the wait in milliseconds — to stderr, which the
1799
- review's test-run block captures into its log and the test-run agent reports as
1800
- `slotWaitMs`. **Exit 75 is not a test result.** In a review it makes
1801
- `suiteVerdict` not usable, `suiteVerdict.reason` beginning "no suite slot within
1802
- <n> s — the host is at its suite bound; not a test failure", with no retry
1803
- inside the run: read that `reason` off the digest and re-review it
1804
- under the next runner letter (`review-pr-<pr#>-b` after a first runner), as for
1805
- a runner that reported `failed`. The implementer, fix-applier and finisher briefs
1806
- carry the one-line rule: *Exit 75 from the suite is not a test result; run the
1807
- command again.*
1623
+ **Name every member.** The name is the ledger token and the hub address; omit
1624
+ it and the ledger cannot match the member.
1625
+ Names follow the unit of work: `impl-<issue#>`, `fix-pr-<pr#>`,
1626
+ `review-pr-<pr#>`, `finisher-pr-<pr#>`, `merge-bot-<n>`. See
1627
+ references/member-lifecycle.md.
1808
1628
 
1809
- A slot holder killed without releasing is taken over by the next waiter under
1810
- the same `.reap` discipline as the ledger's lock; the wedges that discipline
1811
- cannot clear are in the runbook under **Writer policy**.
1629
+ **Fresh context per member.** One member, one unit of work, gone. Refill =
1630
+ **new** member, **new** name.
1812
1631
 
1813
- **Where `testCmd` comes from:** the repository's Test entrypoint, out of the
1814
- Recipe cache phase 0's Recipe derivation step proved —
1815
- `~/.fleet/bin/fleet-run derive-testcmd.sh . test --at origin/main` prints it — and the one you
1816
- hand specialists per **Give specialists a stack-free test command** above. Pass
1817
- the same command to the review, to the fix-applier, and to the finisher — whose
1818
- duty-2 mutation
1819
- gate runs it too — so every gate runs one command. Omit it from the review args
1820
- and the review (`review-core.mjs`, in a runner) reads the same cache itself,
1821
- refusing outright when there is none; the fix-applier has no such fallback, so
1822
- substituting `<testCmd>` with nothing leaves it no gate at all.
1632
+ See references/member-lifecycle.md.
1823
1633
 
1824
- **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.
1634
+ **You read your instruments out of a tree every member can write to — re-check them before every gate decision.** See references/instruments.md.
1825
1635
 
1826
- **The standing CI facts are in the fix-applier body, not in per-event messages** —
1827
- otherwise you send "your red is staleness, do not rebase" once per member per
1828
- merge. `review-and-fix.md` states them; the body says they apply.
1636
+ - **Pin once**, at phase 0 step 0 above, from the main checkout you are
1637
+ running this from — never from a member's worktree, which is gitignored
1638
+ and carries no baseline:
1639
+ `~/.fleet/bin/fleet-run instruments.sh --pin`. On a workspace that does not
1640
+ track `plugin/…`, the pin names the plugin checkout once — `--pin --audit
1641
+ <plugin-checkout>` — and every check after it follows the recorded tree.
1642
+ - **Re-check before you act on any instrument reading** — a SHA acceptance, a CI
1643
+ verdict, the reconcile, a reap, a label gate — from that same main checkout:
1644
+ `~/.fleet/bin/fleet-run instruments.sh`. **Exit 0 is the only code that
1645
+ lets a gate proceed. Exit 1 (the set changed) and exit 2 (the check could not
1646
+ answer) both refuse: report what it printed and do NOT re-read the
1647
+ instrument** — a guard that fails open on "could not look" protects nothing.
1648
+ **A label gate and a CI verdict are also taken by dispatched members, not
1649
+ only by you** — the finisher labels, and the merge bot merges on its own
1650
+ reading — and neither is dispatched to read this file, so neither can be
1651
+ reached by stating the rule here. Each carries its own copy: the finisher's
1652
+ in the instrument re-check block of `agents/fleet-finisher.agent.md`, the merge bot's inside
1653
+ `merge-gate.mjs`, step 3 of `run-merge-bot.md`.
1654
+ - **Re-pin only after a change you made deliberately.** The mid-run tooling fix
1655
+ below is the one legitimate writer. Re-pinning to clear a refusal you cannot
1656
+ explain discards the check.
1829
1657
 
1830
- The finisher's duties, in this order:
1831
-
1832
- 1. **Audit the worktree** by running one script, `finisher-audit.mjs` — the row
1833
- lookup, the dirty check, the head-equality check and the cause derivation
1834
- are one command, because each of them alone reads like the whole duty. Its
1835
- exit code is the verdict of this duty:
1836
- `~/.fleet/bin/fleet-run finisher-audit.mjs --worktree <abs-path> --pin <your dispatch pin>`,
1837
- the path the one you were dispatched with, however it was spelled, and the
1838
- pin the full SHA. It runs `worktree-audit.sh` itself and finds this
1839
- worktree's row by its `realpath`, so a spelling reached through a symlink
1840
- still finds a healthy worktree's row; it reads dirty from the row and from
1841
- `git status --porcelain -unormal`; it compares the worktree's `HEAD` to your
1842
- dispatch pin; and when the head differs from the pin it derives the cause
1843
- from the reflog. It prints one JSON verdict,
1844
- `{worktree, realpath, pin, head, cause, evidence}`. Exit 0, `cause` null, is
1845
- the only result that means the worktree is clean at your pin. Exit 1 means
1846
- `cause` names why not — `live-editor`, `rebase`, `past-pin`, `unreadable`,
1847
- `missing`, `absent` or `other` — and exit 2 means the script could not
1848
- look. Every non-zero exit halts the finisher *here*, before the label: it
1849
- reports the verdict as printed and labels nothing, and an exit 2 is cause
1850
- `other` with the line the script printed on stderr as its evidence. A
1851
- finisher that cannot see the tree it labels cannot vouch for it, so none of
1852
- the script's steps is run by hand to get past a halt. A finisher that
1853
- verifies the dirt is harmless and labels anyway has substituted the rule's
1854
- purpose for the rule, and you find out at merge time. The halt-cause block in
1855
- `agents/fleet-finisher.agent.md` holds the script call and what each exit
1856
- means, and a bare SHA mismatch names no cause. **Pass `--testCmd` too** —
1857
- the same string you passed the workflow and the fix-applier — because duty
1858
- 2's mutation gate runs it and nothing else hands the finisher one; the renderer
1859
- refuses a finisher brief rendered without it.
1860
- 2. **Confirm every deferral — and every claimed APPLY — has a home that outlives the merge.** See references/finisher.md.
1861
-
1862
- **A fix-applier's "applied" is a claim like any other**, and nothing else
1863
- checks it — the review ran against a snapshot cut before those edits existed.
1864
- Confirm `git diff origin/main...HEAD` actually contains what it reported
1865
- applying. Two finishers in one run extended duty 2 this way unprompted, and
1866
- one found five applies where the controller's hand-written list named three:
1867
- a controller's list of what to verify is itself a claim.
1868
-
1869
- **That range is right for the positive check and wrong for a negative one.** See references/finisher.md.
1870
-
1871
- **Whatever you verify by RUNNING, run in a tree nobody else owns.** See references/finisher.md.
1872
-
1873
- **Remove it on every path, the failing ones included** — a mutant that comes back green, a suite that will not start, your own halt at any duty. See references/finisher.md.
1874
-
1875
- **None of this licenses a write to the owned worktree.** Duty 1 is a read;
1876
- you never restore that tree, because you never wrote to it. A controller
1877
- instruction to restore mutated paths there is an instruction to perform the
1878
- silent-destruction ordering above — report it back, do not run it. One
1879
- finisher refused exactly that instruction live, on the grounds it could not
1880
- prove the diff was its own; that was judgement, and this is the rule.
1881
- 3. Add `ready-to-merge` by running one script, `finisher-label.mjs` — the
1882
- release-label check and the label write are one command, because the check
1883
- reads like the whole duty. Its exit code is the finisher's verdict:
1884
- `~/.fleet/bin/fleet-run finisher-label.mjs --pr <pr> --pin <your dispatch pin>`,
1885
- the pin the full SHA. In order, it finds **exactly one** release label,
1886
- `patch`/`minor`/`major` — zero or more than one is exit 3 before anything is
1887
- written, naming which it found; then it records the head you audited as a PR
1888
- comment, which the merge bot reads as the head the label was applied to;
1889
- then it adds the label and reads the labels back, and exits 0 only when that
1890
- read-back carries `ready-to-merge`. A failed call is exit 2 and the read-back
1891
- without the label is exit 4; no step after a failed one runs. The count is
1892
- the backstop for a label lost wherever it was lost, a hand-created PR
1893
- included: a `gh pr create` that outran its caller's tool timeout leaves the
1894
- PR open with the flag unapplied and no exit status anywhere to notice, and
1895
- nothing downstream re-derives the release label. It counts only those three
1896
- — the PR carries other labels, `ready-to-merge` itself among them once the
1897
- script adds it, so a PR wearing one release label beside them passes
1898
- unchanged. A repo that defines none of the three does not gate on one — the
1899
- script settles that from the repo's own label list and says
1900
- `undefined-in-repo` in its receipt. Two finishers in one run stopped at the
1901
- release-label check and reported `labelled` on PRs that sat out of the merge
1902
- queue.
1903
- 4. Report you the label (the script's receipt, the JSON line it printed, as
1904
- printed), the deferral issue numbers — a `filed: #N <subject>` line for
1905
- every issue duty 2 created, and `unrecorded: #N <subject>` for one whose
1906
- `filed` failed twice — and anything
1907
- it halted on — cause and evidence, below, never a bare "head moved". The
1908
- cause is one word of the ledger's `halted:<cause>` vocabulary:
1909
- `live-editor`, `rebase`, `past-pin`, `unreadable`, `missing` or `absent` as
1910
- duty 1's verdict names it, and `other` for every other halt — that
1911
- verdict's own `other`, and a halt at duty 2, duty 3, the instrument
1912
- re-check or the CI gate. A duty 1 halt's report carries the verdict as
1913
- printed, the JSON line, so a `rebase` halt's report carries `replay`,
1914
- `rangeDiff` and `remote` from its `evidence`.
1915
-
1916
- **Once the label is on, take it off before you approve any push.** The label is a
1917
- verdict on the tree the finisher read, and a GitHub label does not follow the
1918
- branch — on one PR it survived a push and came to sit on a commit nobody had
1919
- audited. Write `label-off=<latest finisher attempt>` onto the PR's row first
1920
- (`ledger.mjs token <PR number> label-off=<latest finisher attempt>`, which keeps
1921
- every token the row already has — **Run ledger**), then
1922
- remove `ready-to-merge`, then approve. The token is how the tick tells your
1923
- removal from a finisher that never labelled — a tick run between a removal and
1924
- its token re-dispatches a finisher into your take-off window. The label goes
1925
- before the approval because removing the artifact the merge bot gates on is
1926
- the reliable stop, where messaging the bot races it, and a label has been
1927
- observed holding until after an abort message arrived. Then
1928
- dispatch a **fresh** finisher against the new head; the first audit does not
1929
- transfer, since it verified a different tree. The merge bot refuses that head on
1930
- its own (`run-merge-bot.md`, **The labelled head**), so this is not the only
1931
- guard — but its refusal costs a pass and leaves the label lying, which is yours
1932
- to clear either way, the same way: `label-off=` first, then the removal.
1658
+ It covers every tracked file under this repo's own plugin component directories
1659
+ — `plugin/commands/`, `plugin/scripts/`, `plugin/skills/`, `plugin/agents/`,
1660
+ `plugin/workflows/` — the probes, the libraries they source, and these runbooks,
1661
+ which you also read from that tree. Not the whole repo: `docs/metrics/` is
1662
+ appended by THIS run, so covering it would refuse on the run's own bookkeeping
1663
+ every time. It does not cover refs, deliberately: `claim-ticket.sh` creates a
1664
+ branch per ticket and `reap.sh` deletes them in the ref store every worktree
1665
+ shares, so ordinary work moves refs several times a run and a per-gate
1666
+ refusal on that is noise.
1933
1667
 
1934
- **Every implementer body carries the claim discipline, because tickets that restate facts ship new wrong claims** — inherited from the ticket, and minted in prose nobody asked for. See references/correction-tickets.md.
1935
-
1936
- **The settling command for "which files read X" must never be a literal-path
1937
- grep.** The path is assembled in more than one spelling here, one of them behind
1938
- a local `read` helper, so a grep for the path string answers with the files that
1939
- merely *name* it — comments included — and misses the files that build it.
1940
- Replace the file with `MUTATED` in a throwaway copy, run the suite, and read off
1941
- which suites red: that answers which ones **depend on its contents**, which is
1942
- the question a coverage claim actually needs, and it returns a different set
1943
- from any spelling of the grep.
1944
-
1945
- **A settling command over literal text uses `grep -F`, and its zero counts only after the same command finds a line known to match.** See references/correction-tickets.md.
1946
-
1947
- ### Merge bot
1948
-
1949
- **Its merge gate is one script, run twice.** Step 3 of `run-merge-bot.md` is
1950
- `merge-gate.mjs --pr <n> --pre <sha> [--post <sha>]`: read-only, it runs the
1951
- instrument re-check, the label and review-decision read and a run-bound
1952
- `ci-state`, and answers their conjunction — exit 0 mergeable, 1 blocked (the
1953
- bot reports `<reason>-#<pr>`), 2 could not evaluate. The bot runs it once
1954
- before any CI wait and again immediately before `gh pr merge`, and merges only
1955
- on that second exit 0. Nothing but the gate reads `ci-state` on the merge path,
1956
- so a bot's report names the gate's `reason`, never a field it read by hand.
1957
-
1958
- **You own the watcher, not the bot.** A dying member takes a watcher down with it
1959
- and the queue stops silently. Arm one yourself, `persistent: true`, seeded before
1960
- the loop so handled PRs do not re-fire.
1961
-
1962
- **Every merge invalidates every other open PR, silently.** The rest show *stale
1963
- green* until something re-triggers, then flip to `rebase-check: FAILURE` with the
1964
- slow jobs skipped. Behind-count is the only honest signal.
1965
- See references/ci-and-staleness.md.
1668
+ **A member runs its test suite in the foreground and blocks on it.** See references/member-lifecycle.md.
1966
1669
 
1967
- **One rebase per PR, at merge time.** The bot establishes currency once, when the
1968
- PR is the merge candidate; requiring it earlier costs a full CI cycle per sibling
1969
- merge. Any behind-count you hand a bot is expired on arrival; say so. See
1970
- references/ci-and-staleness.md.
1971
-
1972
- `run-merge-bot.md` carries the mechanics — intra-pass re-checks, run-binding, the
1973
- ancestry proof, post-rebase red triage. Do not restate them here.
1974
-
1975
- ### Reap after each merge pass
1976
-
1977
- The merge bot deletes the remote branch after each merge (`delete-merged-branch.sh`,
1978
- in `run-merge-bot.md` step 4), leaving the local branch `[gone]` with its
1979
- worktree — and its installed dependencies — still on disk. Reap after **each** merge pass,
1980
- not once at the end: a stale worktree still answers `git worktree list`, so the
1981
- in-flight probe (`inflight.sh`, run by the Shortlist and by every Pull) reads an
1982
- already-merged ticket as taken and the queue quietly shrinks. The trigger is the
1983
- bot's `merge-bot-<n>=done`, and `tick --apply` reaps once for every one — a pass
1984
- that merged nothing included (phase 3, **Merge-bot pass reports done**). See
1985
- references/reaping.md.
1986
-
1987
- `~/.fleet/bin/fleet-run reap.sh --apply` recomputes every precondition
1988
- inside the same invocation as the delete — `for-each-ref` for `[gone]`, `git
1989
- cherry origin/main` to authorize the `git update-ref -d` delete (plus a fresh
1990
- check that no worktree holds the branch), worktree removal without `--force` —
1991
- and reports reaped and kept-with-reason counts. Update the reaped tickets' ledger
1992
- rows in the same step. See references/reaping.md.
1993
-
1994
- **It sweeps detached worktrees too — the shape a `[gone]` walk structurally
1995
- cannot see.** Bounded to `.worktrees/`, so a checkout of yours outside it is
1996
- reported, never removed. Read `worktreesRemoved` alongside `reaped`: a pass that
1997
- reaps branches and removes no worktrees is a finding, not a quiet success.
1998
- See references/reaping.md.
1999
-
2000
- **Do not invoke `commit-commands:clean_gone`.** It runs `git branch -D` and
2001
- `git worktree remove --force` with no merged check at all — nothing there stops it
2002
- deleting a branch, or discarding a member's uncommitted work, that exists nowhere
2003
- else. The skill is the maintainer's to fix; do the reap yourself.
2004
- See references/reaping.md.
2005
-
2006
- **Never reap a branch a live member is on.** Cross-check `.fleet/ledger.md` first:
2007
- a row without a terminal state means someone may still be in that worktree, and
2008
- the dirty check does not see a member that committed but has not pushed.
2009
- See references/reaping.md.
2010
-
2011
- ### Reclaim scratch after each merge pass
2012
-
2013
- `tick --apply` runs it right after `reap.sh --apply`, every pass:
2014
- `~/.fleet/bin/fleet-run reclaim-scratch.mjs --root <scratch> --apply`,
2015
- `<scratch>` the path `scratch-root.mjs` prints. Reap authorizes on git's
2016
- evidence and never removes scratch; Reclaim authorizes on GitHub state and
2017
- age, reading which members are live off `.fleet/ledger.md`:
2018
-
2019
- - **`pr<M>/` and `dispositions-fix-pr-<M>*.json`** go once PR #M is MERGED
2020
- or CLOSED and no live `review-pr-<M>`, `fix-pr-<M>` or `finisher-pr-<M>`
2021
- token names it. A live `merge-bot-<n>` holds every one of them for that
2022
- sweep — its token names no PR. An OPEN PR's `pr<M>/` stays, all but its
2023
- `run-*` roots idle past 7 days.
2024
- - **`impl-<N>/`** goes once issue #N is CLOSED, or when it is OPEN, no live
2025
- `impl-<N>` token names it, no claim worktree under `.worktrees/` still stands
2026
- for it, and nothing under it has changed in 7 days.
2027
- - **Anything else at the root** — your own ad-hoc files included — goes
2028
- once nothing under it has changed in 7 days and no live token's name
2029
- appears in its name.
2030
-
2031
- The review snapshot runs the same script as `--pr <N>` on its own PR's stale
2032
- `run-*` roots. What this sweep does with no ledger, or with a `gh` that cannot
2033
- answer, is in references/phase-0.md, under **Reclaim scratch**.
2034
-
2035
- ### Release the claims that never became PRs — at end of run, and on drain
2036
-
2037
- Phase 1 is serial and claims ahead of dispatch, so a ticket can be legitimately
2038
- claimed and then never sent: a collision surfaces after the claim, or the
2039
- maintainer says drain. The claim still holds the `in-progress`
2040
- label, a worktree and a branch, and **`reap.sh` will not take them** — the branch
2041
- is not `[gone]` and has no unique commits, so it is correctly not reapable. Reaping
2042
- fires after each merge pass and nothing fires at end of run, so the claim survives
2043
- it and the in-flight probe reads a free ticket as taken next run. Same silent queue
2044
- shrink as a stale merged worktree, from the opposite end.
2045
-
2046
- `~/.fleet/bin/fleet-run release-ticket.sh <N> impl-<N> implementer --apply` —
2047
- the pair the claim took, so the two name the same branch and worktree — is the
2048
- inverse of the claim, and recomputes its preconditions inside the same
2049
- invocation as the delete: 0 commits ahead of `origin/main`, no unique commits
2050
- (`git cherry`), no branch on `origin`, and — whenever the worktree directory is
2051
- there to read — a clean worktree. Where that directory is established absent the
2052
- dirty check does not run at all, and `git worktree remove` is no backstop for it:
2053
- it gates its own clean check on the same `stat`, so the script's absence
2054
- measurement is sole arbiter there. Two of the four are recomputed a second time
2055
- at the delete itself: the dirty check by git, which is what `worktree remove`
2056
- without `--force` is; and the ahead count, re-run against `origin/main`
2057
- immediately before the branch delete, because a commit can land across the
2058
- `gh issue view` between the checks and the delete. The `git cherry` and
2059
- pushed-branch checks are **not** re-run there — a commit landing in that window
2060
- is ahead of `origin/main` by construction, so the recount covers it. All clear →
2061
- drops the label, removes the worktree without `--force`, deletes the branch with
2062
- `git update-ref -d` on the tip it read once before the recount — a
2063
- compare-and-swap that refuses if the branch has moved off that tip — authorized
2064
- by those two commit checks, the recount and a fresh check that no worktree holds
2065
- the branch, and by nothing else (not `git branch -d`: it measures against local
2066
- HEAD, and a claim has no upstream, so it refuses a pristine claim whenever local
2067
- `main` is behind `origin/main`). Any one of
2068
- them failing → it touches nothing and names the blocker. **That refusal is the
2069
- finding, never an obstacle**: a claim carrying commits or a pushed branch is not
2070
- auto-released, ever — run `worktree-audit.sh` (it audits every worktree; find
2071
- this claim's row in the output) and decide by hand.
2072
-
2073
- Run it over every claimed ticket with no PR when the run ends or the maintainer
2074
- drains, and on the spot for a claim abandoned mid-run (a bail before
2075
- implementing, a collision found after the claim). Update the released tickets'
2076
- ledger rows in the same step. See references/reaping.md.
1670
+ **The scratch root is computed, never invented and never recorded.** No harness
1671
+ supplies a scratch directory to a member, so a member left to name its own
1672
+ invents one — and the liveness read in **The tick's rows**, which looks at the files under the
1673
+ directory a member's own prompt assigned it, then watches a directory nobody
1674
+ else can find. You run `~/.fleet/bin/fleet-run scratch-root.mjs`: it prints one
1675
+ absolute path, `~/.fleet/scratch/<repo-key>/`, the same from the main checkout
1676
+ and from every linked worktree, and the same again when a compacted or
1677
+ re-invoked controller runs it, so nothing writes it down. It exits non-zero,
1678
+ with its reason, when the root would sit inside a git work tree that does not
1679
+ ignore it; that is an environment to fix, not a cue to pick another root.
1680
+ `<scratch>` in this file is that path, resolved before you send anything: every
1681
+ brief that carries one carries the absolute path you passed it — an
1682
+ implementer's `<scratch>/impl-<N>/`, the recipe deriver's
1683
+ `<scratch>/derive-recipe/`, a fix-applier's, a finisher's and a merge bot's
1684
+ `<scratch>/pr<N>/…`, and a review runner's `scratch` argument, which is the
1685
+ root itself. A member whose prompt names no path asks you by name and never
1686
+ makes one up.
2077
1687
 
2078
1688
  ## Worktree and claim model
2079
1689
 
@@ -2082,8 +1692,8 @@ Run none of this by hand: act through `claim-ticket.sh`, `release-ticket.sh`, `r
2082
1692
  **Claim, release and reap are unchanged on omp.** `claim-ticket.sh`,
2083
1693
  `release-ticket.sh`, `reap.sh` and `inflight.sh` are git-native and operate on
2084
1694
  the single shared ref store every worktree and the main checkout read and
2085
- write directly (**Phase 1**, **Reap after each merge pass**, **Release the claims
2086
- that never became PRs** above); nothing in omp's `task` model touches that
1695
+ write directly (**Phase 1**, the tick's reap, and `release-ticket.sh` under
1696
+ **Failure handling**); nothing in omp's `task` model touches that
2087
1697
  store, so none of the four scripts carries a dialect branch.
2088
1698
 
2089
1699
  **A fleet member is never dispatched with omp's `isolated: true`.** See references/isolation.md.
@@ -2157,7 +1767,7 @@ See references/pull.md.
2157
1767
 
2158
1768
  **Do not re-derive this by hand — `fleet-tick.mjs` computes it.** One invocation
2159
1769
  prints per-role `actual/target` and an explicit ACTION with the backlog gate
2160
- above and every row of the table below already applied; every wake in Phase 3
1770
+ above and every row of the table below already applied; every wake
2161
1771
  ends in it. The deficit is then *computed, not remembered*, which is
2162
1772
  the whole point: a table you must remember to consult is one you will not
2163
1773
  consult under a merge-side event storm, and that is how implementers reached
@@ -2244,7 +1854,7 @@ has to travel in the member's report rather than being something you already hol
2244
1854
 
2245
1855
  The member-facts scrape, the drain of the pending ticket-features rows and the
2246
1856
  commit of the metrics files are steps of `closeout.mjs finish`, which **End of
2247
- run** (Phase 3) drives. See references/close-out.md.
1857
+ run** (**What a wake owes beyond its call**) drives. See references/close-out.md.
2248
1858
 
2249
1859
  **Why not decide inside one run.** A run holds 2-3 implementer PRs, and ticket
2250
1860
  difficulty swamps the tier effect — a gojq parity harness and a two-statement
@@ -2508,7 +2118,7 @@ behind-pr:#M | behind-issue:#M` is an Exclusion (phase 1) — a ticket row like
2508
2118
  any other, never a section of its own.
2509
2119
 
2510
2120
  A PR's review is not a member, so it carries its own pair of row tokens,
2511
- written with `review` onto the PR's row (**Reviewers**): `review=wf:<runId>` |
2121
+ written with `review` onto the PR's row (**The reviewer**): `review=wf:<runId>` |
2512
2122
  `review=member:<name>` at launch, `<name>` being
2513
2123
  `review-pr-<n>` for the PR's first runner and the next letter its rows do not
2514
2124
  carry yet (`review-pr-<n>-b`, `-c`, …) for every later one,
@@ -2517,7 +2127,7 @@ when the result lands, `<run>` the review's `run-XXXXXXXX` run root under
2517
2127
  `<scratch>/pr<n>/`, which holds its `review.json`. A row with `review=` and no `reviewed=` is a review in
2518
2128
  flight, and the tick counts it as a live review unit unless gh reports its PR
2519
2129
  MERGED or CLOSED. `ci=<run-id>:<attempt>:<conclusion>`
2520
- and `held-behind:#<lower>` are row tokens the same way (Phase 3), and so is
2130
+ and `held-behind:#<lower>` are row tokens the same way, and so is
2521
2131
  `conflict-hold:#<pr>` — written by the merge bot itself (`run-merge-bot.md`
2522
2132
  step 1), or by you off the tick's `CONFLICT` line, naming the row's own PR. It
2523
2133
  is not an Exclusion: that gates a ticket's claim, this a PR's merge. `label-off=<finisher-pr-M[-x]>`
@@ -2591,7 +2201,7 @@ Match the failure to its row in the table below and run that response; where the
2591
2201
  | Failure | Response |
2592
2202
  |---|---|
2593
2203
  | SHA not on expected branch | Flag, do not enqueue, report |
2594
- | Implementer bails before implementing | Act by the report's `bailCause` as the **Relabel causes** table says, then refill (phase 3) |
2204
+ | Implementer bails before implementing | Act by the report's `bailCause` as the **Relabel causes** table says, then refill (**What a wake owes beyond its call**) |
2595
2205
  | Implementer blocked or ambiguous *mid-implementation* | Free the slot, `ledger.mjs settle impl-<N> blocked`, leave `in-progress`, report — the ticket is not In flight and waits for a human; the row above is the pre-code bail, not this one |
2596
2206
  | `review-pr` workflow throws or returns no tree | Retry once under the next letter (`review-pr-<pr#>-b`); still failing, report and leave the PR at review-due. Never enqueue the PR as reviewed — the workflow is not a member, so no other row here covers it |
2597
2207
  | Reviewer, fix-applier or red-check fixer cannot reach green | Report, leave the PR unlabeled, free the slot — a fix-applier or red-check fixer settles `failed` |
@@ -2605,6 +2215,7 @@ Match the failure to its row in the table below and run that response; where the
2605
2215
  | Member idle with work outstanding | Read the PR first, *then* ping. Idle ≠ done |
2606
2216
  | 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 |
2607
2217
  | Member **unaccounted** (a live token the tick flags `CHECK-LIVENESS`, or any live token nothing has reported on) | Registry census first: `read history://` once. A row other than `running` → `read agent://<member>` and the artifacts (PR head, worktree, labels), settle the real outcome with the existing vocabulary, and resume a `parked` member with `write agent://<member>` when its work is worth continuing. A `running` row → the mtime discriminator below, two polls: moved means BLOCKED and alive; frozen at both polls **and** no artifact movement → `write proc://<member>/kill`, the **Member confirmed dead or never dispatched** wake, fresh name. Three independent alive-signals — the first poll, the second poll, the artifacts — must all be silent before a kill; the tick settles and kills nothing |
2218
+ | 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 |
2608
2219
 
2609
2220
  ### Recovery
2610
2221
 
@@ -2625,6 +2236,17 @@ Audit every worktree before dispatching replacements with
2625
2236
  ahead-count and uncommitted files, the committed-vs-uncommitted distinction that
2626
2237
  decides whether a replacement redoes or destroys work.
2627
2238
 
2239
+ **A throw, an empty return, or a notification whose status is not `completed`
2240
+ is a failure event, not a clean review.** **Retry once
2241
+ under the next runner letter, then stop:** the `review-pr-<pr#>` runner
2242
+ already retried once inside its cell and reports `failed` with both errors, so
2243
+ the retry is a second runner under the next letter the PR's rows do not carry
2244
+ yet — `review-pr-<pr#>-b` after a first runner — recorded as
2245
+ `review=member:<that name>`. A second failure is reported, and the PR stays at
2246
+ review-due. A
2247
+ runner member that is killed rather than failed follows **Failure
2248
+ handling** (fresh name, inherited state stated).
2249
+
2628
2250
  ## Guards
2629
2251
 
2630
2252
  Each guard here is a check to run before the step it guards: run it, and where it fails, stop and report rather than go on.
@@ -2741,6 +2363,13 @@ failures arrive as *wrong findings*, not errors:
2741
2363
  tree's filenames, so a sibling's throwaway mutation reads exactly like a live
2742
2364
  worktree's error.
2743
2365
  See references/isolation.md.
2366
+ - **Every implementer body carries the claim discipline, because tickets that restate facts ship new wrong claims** — inherited from the ticket, and minted in prose nobody asked for. See references/correction-tickets.md.
2367
+ - **The settling command for "which files read X" must never be a literal-path
2368
+ grep.** Replace the file with `MUTATED` in a throwaway copy, run the suite, and read off
2369
+ which suites red: that answers which ones **depend on its contents**, which is
2370
+ the question a coverage claim actually needs, and it returns a different set
2371
+ from any spelling of the grep. See references/correction-tickets.md.
2372
+ - **A settling command over literal text uses `grep -F`, and its zero counts only after the same command finds a line known to match.** See references/correction-tickets.md.
2744
2373
 
2745
2374
  Suspect a neighbour before a member's own diff — for unexplained failures, and
2746
2375
  equally for any **finding** that came from reading source.