@feigi/fleet-ctl 3.82.14 → 3.82.16

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.
@@ -15,7 +15,7 @@ argument-hint: [pr-number]
15
15
  3. **Run `checkCmd` and `testCmd` before you commit**, from the worktree, each copied verbatim — not a runner you picked. `checkCmd` is the Recipe's Check step and `testCmd` is the command your dispatcher handed you; **standalone, with no dispatcher to hand you them, they are the repo's own Check step and Test entrypoint**, read off the repo's Recipe cache (`derive-testcmd.sh <repo> check` prints the first, `derive-testcmd.sh <repo> test` the second). Read them off the repo, never invent one: "not a runner you picked" is what this step forbids, and on the standalone path nothing else defines the referent. With no usable Recipe cache (`derive-testcmd.sh` refuses), run the Recipe derivation step first — dispatch a `task` member, agent `fleet-recipe-deriver`, its prompt naming the main checkout's absolute path as `<repo>` and a scratch directory of its own; it proves all three commands in a throwaway worktree and writes the cache only on proof, and its `RECIPE NOT PROVEN` is a stop, reported with its reason. Never fall back to a runner of your own choosing. `tests 0` is a FAILED run, not a pass: a glob matching nothing still exits 0. **Zero is not the only no-work count.** A run reporting **0 passes with no failures** skipped everything it collected, and a count **materially below the whole suite** means you ran a partial tree — this suite reported `tests 937` when this line was written, so compare what you get against a full run rather than against zero. The number drifts; that is the safe direction, because a stale one produces a loud false red while its absence produces a silent false green. Red, zero-test, or either no-work count → fix it or move the finding to defer; never commit over it. Then commit and push — `~/.fleet/bin/fleet-run push-sync.mjs --branch <branch>`, which pushes the explicit refspec `HEAD:refs/heads/<branch>` and then moves the worktree holding `<branch>` to the pushed SHA, or names why it did not and exits 3 with the push landed: this step also runs from a `--detach` worktree (`run-team/SKILL.md`'s worktree-recreation form), where a bare `git push` exits 128 (`fatal: You are not currently on a branch`), any push whose destination is `HEAD` rather than a full or branch refname (`-u` or not, forced or not) exits 1 (`error: The destination you provided is not a full refname`), and a refspec push alone leaves the claim worktree's `refs/heads/<branch>` at its old tip — and report the test result alongside the SHA. **Then reconcile the PR body with what you actually did.** It merges into the permanent record and nothing downstream re-derives it: your commit messages are yours to fix and usually get fixed, the body is neither. Removing a change, or reversing one, leaves the body asserting work the diff no longer contains. Measured twice in one run — one body kept asserting a calibration its own fix had just deleted as false, another kept claiming a hunk the fix had removed as "this diff owns it" — both caught by a finisher's reading rather than by any rule, and both would otherwise have merged intact. `gh pr edit <pr> --body-file <path>`, and re-read the body against the final diff rather than against your memory of it. **Never `--no-verify` on a commit you push, or on the push itself.** The fleet runs in repos it knows nothing about: it must not assume a pre-commit hook exists, and must never bypass one that does on what reaches the branch — a hook that fails is a finding to report, not an obstacle. Nothing re-reviews what you commit here, so this run is the only gate between your edit and a merge.
16
16
  **Under the fleet, run the two through `recipe-check.mjs`, not one by one:** from the worktree, after `git add` and before the commit, `~/.fleet/bin/fleet-run recipe-check.mjs --member <your name> --testCmd "<testCmd>" --reports <scratch>/reports` — `<your name>` the `fix-pr-<pr>[-x]` you were dispatched as, and `<testCmd>` the whole command you were handed, one quoted argument. It runs the Recipe's Check step, which is `checkCmd`, then `<testCmd>` exactly as handed, against the staged tree, and records both results beside your report. The record is keyed to the tree, so run it again after any content change or rebase — a message-only amend keeps it valid — and the controller refuses an `applied` report whose head's tree has no green record. A non-zero exit is red; exit 75 is no test result — no slot was free and nothing ran — so run it again. **Standalone, with no report to settle against,** run `checkCmd` and `testCmd` yourself, each copied verbatim.
17
17
  4. **Under the fleet, do not hold this wait.** The controller owns a persistent CI Monitor and dispatches a finisher once your report has arrived and CI is green — so push, then report to the controller a **final verdict** (every specialist you dispatched + its outcome + your apply/defer adjudication + how many refuters you dispatched against out-of-scope suggestions + the pushed SHA) and stop. A specialist that has not reported to you is not a verdict line — `hub send` it by name for its report, then ask the controller, before ruling; see **Specialists**. The controller keys "review complete" on that verdict, **never on you merely going idle** — a slow specialist can still be running when your turn ends, and finishing off idle-alone ships a premature label (observed: a PR labelled while its contract-test specialist was still running and about to surface a vacuous pin). A turn-based reviewer re-reading `gh pr checks` each idle cycle rebuilds its whole context for nothing the Monitor lacks. **Standalone (no controller): watch** the run until the diff-validating jobs are green (bind via `ci-state.mjs` below); fix a **genuine** failure and repeat from 3. **Staleness is not a failure:** a `rebase-check` red or heavy jobs `skipped` **solely from a non-zero behind-count** never clears by waiting and must not be rebased away here — see **`skipped` is not `passed`** and **Do NOT rebase to label** below for what it looks like and why. Stop watching and go to 6. A turn-based reviewer that "waits for green" on a behind PR stalls forever and ends its turn without labelling. **`ci-state.mjs` returns `verdict: "no-ci"`** → there is no run to watch, so don't: go straight to 6 and let that step's `--declare-no-ci` branch decide.
18
- 5. File each deferred finding: `gh issue create --label needs-triage` with the finding, `file:line`, why deferred, `Deferred from PR #<pr> review`, and **the finding's `dimension` in the issue body** — the review returns it on every finding, and a backlog that does not carry it cannot be attributed to the specialist that generated it. **The body also carries the evidence triage needs: the refuter's verdict — the `verdictPath` reason, or the review's votes — and the candidate remedy.** **Every issue you create is labelled `needs-triage`; only triage sets `ready-for-agent`** — and the label is never omitted; an unlabelled issue is invisible to both `candidates.mjs --require-label` and the cockpit's pool query, reachable only by a human. **A confirmed defect does not settle its remedy**, so confirming one earns it no other label: a confirmed defect with the remedy still open files `needs-triage`, and so does a finding you are unsure is broken at all. **A finding a refuter CHECKED and did not survive is neither** — it is not a confirmed defect, and you are not unsure whether anything is broken: you measured that nothing is. Refutations go as **one issue per review, titled `PR #<pr> review: the suggestion band, checked`, labelled `wontfix`, and CLOSED** — a run that files them open instead has to be corrected afterwards, as one was on 2026-08-27. Left open they read as queued agent work and rot into filing artifacts; a measured triage pass found a quarter of its `needs-triage` backlog was refuted records, bulk-closable by title shape. Closed is still a durable home — `check` reads the tracker at `--state all`, so a future run re-deriving the finding lands on the evidence rather than re-litigating it. Split the band only where an entry is **not** a refutation: a residual the refuter surfaced, or an out-of-scope finding its refuter let survive or never decided, is real deferred work — and real deferred work earns its own open issue only past a second bar. **Worth a claim: a finding whose own claim is that correct code could be shaped better — style, naming, layout, redundancy, a micro-simplification — never gets its own open issue.** **Neither does a gap in a prose guard — a test holding prose to its wording — unless something acts on the guarded text: an agent follows it as an instruction (a skill, a command, an agent definition, a script's header or usage text, `AGENTS.md`), or a program other than its own guard reads it.** Whether anything does is your judgement, as `claimKind` is, and a gap below this bar is recorded `claimKind: shape`. A gap in a guard's own code — a slow pattern, a dead branch, an unchecked spawn result — takes the stakes of the prose it guards, and a helper several guards share takes the highest of theirs. A missed wording already in the tree is no guard gap — that prose is wrong — and neither is an input already in the tree that trips a guard's code; each files on its own merits. Once the prose is corrected, the observed wording may join the guard's literal ban, never a reader built to catch wordings nobody has written. A below-bar finding's only open question is "is this worth doing?", which answers itself under the repriced triage bar: measured 2026-08-15→31, 133 issues were triage-closed `wontfix` in sixteen days, 121 of them review deferrals, mostly this shape. Append it to the same closed record issue under a `Below the claim bar` heading — `file:line`, dimension, the claim, and a re-open trigger per entry. Closed is a durable home for these too: `check` reads `--state all`, so a run re-deriving the finding lands on the record instead of re-filing — and a later review returning the same finding as `survived`, or a maintainer hitting it, is the promotion signal: file it open then, citing the record. **An in-scope rediscovery step 2 applied is that same promotion signal, and it files nothing** — so the record carries no mark unless you write one: run `check` over each finding step 2 applied as well, and where its tracker rows put one on a `Below the claim bar` entry, comment on that record issue, opening `Promoted — applied, not filed`, and name the entry, that its refuter let it through, and the commit carrying the fix. That comment is the promotion mark this path leaves, and it is **not** a cross-reference — measured on this tracker, a comment mints `cross-referenced` on the issue its own text *names*, never on the issue it sits under (a comment citing a second issue landed on that issue's timeline and nowhere else), so the fixed opening is what carries the signal instead. **The bar reads the finding's claim, never its review band** — a finding that alleges wrong behavior, whatever band it sat in, `unverified`-with-crashed-refuters included, is above this bar and files open, `needs-triage`, which is why a band cannot carry this decision. Do **not** reach for phase 0's **decided?** test here. That is a *consumption* gate, and whether a filed issue is decided enough to reach it is triage's call, never the filer's. **The cost of this rule is watched:** open `needs-triage` issues citing `Deferred from PR` above 40 on 3 consecutive run dates send the filing rule back to the maintainer — batch triage, a fleet triage role, or a different bar. **Dedupe through `~/.fleet/bin/fleet-run ledger.mjs check "<subject>"`, never a bare `gh issue list --search`** — the duplicate class that guard exists to catch was filed from *here*: the ledger row for the finding rediscovered five times in one measured run carries a reviewer's own source tag, `(review-pr-108)`. `check` reads this run's filed list *and* the tracker at `--state all`, so a closed duplicate still counts. Exit **1** is already filed in this run; exit **3** is a clean ledger with matching tracker rows that score against the subject — read the ones it prints and comment on the existing follow-up rather than filing a second issue. **Exit 0 is not automatically "safe to file"**: it covers `clean`, `soft-hit` and `unverified` alike, and an unreachable `gh` degrades to a ledger-only answer that says `TRACKER NOT CHECKED`, so read the stdout `verdict` to tell a tracker that was searched and clean from one nobody read — under `unverified`, search it yourself before filing — or, for the applied-promotion comment above, before treating a tracker nobody could read as a reason to skip it. **`soft-hit` is the answer that costs most to misread**: it means related rows were found and none of them was established as a duplicate — a near-miss above the floor, or tracker rows the scorer rates 0.00 — so read the rows it printed and decide, and never treat that exit 0 as permission. Exit **2** is a usage error, meaning nothing was checked at all: fix the invocation and re-run, never read it as a duplicate. Full exit-code and `verdict` semantics live in `$(~/.fleet/bin/fleet-run --root)/skills/run-team/references/ledger.md`, which `SKILL.md`'s **Run ledger** section points at — read them there rather than restating them here. **The guard dedupes, it never drops:** a duplicate becomes a comment on the issue that already covers it, never a dropped finding. **Record every issue you create, at once:** immediately after each `gh issue create`, run `~/.fleet/bin/fleet-run ledger.mjs filed <N> "<subject>"` — `<N>` the number `gh issue create` printed, and **the same subject string you passed to `check`**, so the next `check` for that finding meets it in the filed list instead of scoring a rewording. Without it the guard's ledger half never holds a row, and its exit **1** can never fire. If `filed` fails, retry it once; if it fails again, leave the issue alone — it exists and is correct — and report it as `unrecorded: #N <subject>`, so the controller runs `filed` for it. Report `filed: #N <subject>` for every issue you created — under the fleet, in your report to the controller. **The one window the guard cannot see:** `check` → `gh issue create` → `filed` is three commands, and the ledger's lock covers each alone, so two filers can both read `clean` in the seconds before either records. That gap is accepted, never a reason to skip `filed`. Post the numbers as one PR comment.
18
+ 5. File each deferred finding: `gh issue create --label needs-triage` with the finding, `file:line`, why deferred, `Deferred from PR #<pr> review`, and **the finding's `dimension` in the issue body** — the review returns it on every finding, and a backlog that does not carry it cannot be attributed to the specialist that generated it. **The body also carries the evidence triage needs: the refuter's verdict — the `verdictPath` reason, or the review's votes — and the candidate remedy.** **Every issue you create is labelled `needs-triage`; only triage sets `ready-for-agent`** — and the label is never omitted; an unlabelled issue is invisible to both `candidates.mjs --require-label` and the cockpit's pool query, reachable only by a human. **A confirmed defect does not settle its remedy**, so confirming one earns it no other label: a confirmed defect with the remedy still open files `needs-triage`, and so does a finding you are unsure is broken at all. **A finding a refuter CHECKED and did not survive is neither** — it is not a confirmed defect, and you are not unsure whether anything is broken: you measured that nothing is. Refutations go as **one issue per review, titled `PR #<pr> review: the suggestion band, checked`, labelled `wontfix`, and CLOSED** — a run that files them open instead has to be corrected afterwards, as one was on 2026-08-27. Left open they read as queued agent work and rot into filing artifacts; a measured triage pass found a quarter of its `needs-triage` backlog was refuted records, bulk-closable by title shape. Closed is still a durable home — `check` reads the tracker at `--state all`, so a future run re-deriving the finding lands on the evidence rather than re-litigating it. Split the band only where an entry is **not** a refutation: a residual the refuter surfaced, or an out-of-scope finding its refuter let survive or never decided, is real deferred work — and real deferred work earns its own open issue only past a second bar. **Worth a claim: a finding whose own claim is that correct code could be shaped better — style, naming, layout, redundancy, a micro-simplification — never gets its own open issue.** **Neither does a gap in a prose guard — a test holding prose to its wording — unless something acts on the guarded text: an agent follows it as an instruction (a skill, a command, an agent definition, a script's header or usage text, `AGENTS.md`), or a program other than its own guard reads it.** Whether anything does is your judgement, as `claimKind` is, and a gap below this bar is recorded `claimKind: shape`. A gap in a guard's own code — a slow pattern, a dead branch, an unchecked spawn result — takes the stakes of the prose it guards, and a helper several guards share takes the highest of theirs. A missed wording already in the tree is no guard gap — that prose is wrong — and neither is an input already in the tree that trips a guard's code; each files on its own merits. Once the prose is corrected, the observed wording may join the guard's literal ban, never a reader built to catch wordings nobody has written. A below-bar finding's only open question is "is this worth doing?", which answers itself under the repriced triage bar: measured 2026-08-15→31, 133 issues were triage-closed `wontfix` in sixteen days, 121 of them review deferrals, mostly this shape. Append it to the same closed record issue under a `Below the claim bar` heading — `file:line`, dimension, the claim, and a re-open trigger per entry. Closed is a durable home for these too: `check` reads `--state all`, so a run re-deriving the finding lands on the record instead of re-filing — and a later review returning the same finding as `survived`, or a maintainer hitting it, is the promotion signal: file it open then, citing the record. **An in-scope rediscovery step 2 applied is that same promotion signal, and it files nothing** — so the record carries no mark unless you write one: run `check` over each finding step 2 applied as well, and where its tracker rows put one on a `Below the claim bar` entry, comment on that record issue, opening `Promoted — applied, not filed`, and name the entry, that its refuter let it through, and the commit carrying the fix. That comment is the promotion mark this path leaves, and it is **not** a cross-reference — measured on this tracker, a comment mints `cross-referenced` on the issue its own text *names*, never on the issue it sits under (a comment citing a second issue landed on that issue's timeline and nowhere else), so the fixed opening is what carries the signal instead. **The bar reads the finding's claim, never its review band** — a finding that alleges wrong behavior, whatever band it sat in, `unverified`-with-crashed-refuters included, is above this bar and files open, `needs-triage`, which is why a band cannot carry this decision. Do **not** reach for phase 0's **decided?** test here. That is a *consumption* gate, and whether a filed issue is decided enough to reach it is triage's call, never the filer's. **The cost of this rule is watched:** open `needs-triage` issues citing `Deferred from PR` above 40 on 3 consecutive run dates send the filing rule back to the maintainer — batch triage, a fleet triage role, or a different bar. **Dedupe through `~/.fleet/bin/fleet-run ledger.mjs check "<subject>"`, never a bare `gh issue list --search`** — the duplicate class that guard exists to catch was filed from *here*: the ledger row for the finding rediscovered five times in one measured run carries a reviewer's own source tag, `(review-pr-108)`. `check` reads this run's filed list *and* the tracker at `--state all`, so a closed duplicate still counts. Exit **1** is already filed in this run; exit **3** is a clean ledger with matching tracker rows that score against the subject — read the ones it prints and comment on the existing follow-up rather than filing a second issue. **Exit 0 is not automatically "safe to file"**: it covers `clean`, `soft-hit` and `unverified` alike, and an unreachable `gh` degrades to a ledger-only answer that says `TRACKER NOT CHECKED`, so read the stdout `verdict` to tell a tracker that was searched and clean from one nobody read — under `unverified`, search it yourself before filing — or, for the applied-promotion comment above, before treating a tracker nobody could read as a reason to skip it. **`soft-hit` is the answer that costs most to misread**: it means related rows were found and none of them was established as a duplicate — a near-miss above the floor, or tracker rows the scorer rates 0.00 — so read the rows it printed and decide, and never treat that exit 0 as permission. Exit **2** is a usage error, meaning nothing was checked at all: fix the invocation and re-run, never read it as a duplicate. Full exit-code and `verdict` semantics live in `$(~/.fleet/bin/fleet-run --root)/skills/run-team/references/ledger.md`, which `SKILL.md`'s wake section points at — read them there rather than restating them here. **The guard dedupes, it never drops:** a duplicate becomes a comment on the issue that already covers it, never a dropped finding. **Record every issue you create, at once:** immediately after each `gh issue create`, run `~/.fleet/bin/fleet-run ledger.mjs filed <N> "<subject>"` — `<N>` the number `gh issue create` printed, and **the same subject string you passed to `check`**, so the next `check` for that finding meets it in the filed list instead of scoring a rewording. Without it the guard's ledger half never holds a row, and its exit **1** can never fire. If `filed` fails, retry it once; if it fails again, leave the issue alone — it exists and is correct — and report it as `unrecorded: #N <subject>`, so the controller runs `filed` for it. Report `filed: #N <subject>` for every issue you created — under the fleet, in your report to the controller. **The one window the guard cannot see:** `check` → `gh issue create` → `filed` is three commands, and the ledger's lock covers each alone, so two filers can both read `clean` in the seconds before either records. That gap is accepted, never a reason to skip `filed`. Post the numbers as one PR comment.
19
19
  **A duplicate that should hold an open deferral is a comment, never a relabel.** Where the guard lands a finding whose home is an open issue on an existing one that is open, cite the refuters' verdict — or the review's votes — in the comment you leave there, and change no label. Never relabel `ready-for-agent` down to `needs-triage`: a `ready-for-agent` issue already holds a `needs-triage` finding, because triage already promoted it, and a later run's crashed refuters are no reason to send it back. A closed one cannot hold it: file a new open issue citing it, as the promotion rule above does for the closed record.
20
20
  **Name where each deferral went, in the disposition record.** Every entry you defer carries `issue`: the number of the issue you filed it to, commented it onto, or appended it to — the closed `PR #<pr> review: the suggestion band, checked` record included. A `suggestion`'s entry — an `unverified` finding with `refutersDispatched` of zero — also carries `verdictPath`, applied or deferred, in scope and, above the claim bar, out of scope: the absolute path of a file holding its refuter's verdict as JSON, `{"refuted": <boolean>, "reason": "<string>"}`, under your own `<scratch>/pr<N>/fix-XXXXXXXX/<finding>/` run root. An out-of-scope one whose refuter crashed before any verdict reached you still names the path its verdict would have taken there; out of scope, that file missing or holding no decision files the finding open `needs-triage`, while in scope it is a mismatch. `dispositions-check.mjs` reads each named issue back through `gh issue view --json labels,state,title` and each verdict file off disk; a mismatch names the row of its filing table the entry broke, so move or relabel that filing to the home the row names and run the check again. With `gh` unreachable, or an issue it cannot read, that filing goes unchecked: with nothing else broken the verdict is `unchecked` — never `ok`, and it exits 1 — so run the check again once `gh` answers.
21
21
  6. Diff-check green (run-bound per `ci-state.mjs`; under staleness see **`skipped` is not `passed`** for what the label then rests on) **and** deferrals filed **and** the PR carrying exactly one release label (`patch`/`minor`/`major`, counting only those three; **required on every PR** — `release-label.yml` auto-adds `patch` when none is present and hard-fails on more than one, so this never needs to check whether the repo defines the gate at all, and `gh label list` settles nothing here any more) **and** `dispositions-check.mjs --member fix-pr-<pr> --scratch <scratch>` run over your own record exiting 0 (standalone that is yours to run, from the worktree, after step 2 writes `<scratch>/dispositions-fix-pr-<pr>.json`; `--review <path>` names the review file, which you write yourself from the findings step 1 collected, in the shape **The review result file** describes — `pr`, `head`, `snapshot`, `counts` and the `survived`, `refuted` and `unverified` buckets; standalone you pass `--no-ledger` with it, so it writes no token and the exit status is the verdict — a `.fleet/ledger.md` the repository already holds is a previous run's, and without the flag it exits 2 for a ledger with no row for you; under the fleet `ledger.mjs dispatch` has already refused a finisher without it) → `gh pr comment <pr> --body "ready-to-merge-head: <prHead>"`, `prHead` being the full SHA your green `ci-state.mjs` reading reported, which the merge bot reads as the head the label was applied to, then `gh pr edit <pr> --add-label ready-to-merge`. Do not merge. A non-zero dispositions exit halts you before the label, and you report the violations it printed. Zero or more than one release label halts you before the label, naming which you found — a timed-out `gh pr create` opens the PR with its `--label` unapplied and no exit status to react to, and nothing after this step re-derives the release label. **Under the fleet a finisher does this** — a fresh small agent the controller dispatches (after your final verdict, never on your bare idle). Gate it on the diff-validating **`check`** job, **not** on `ci-state --quiet` exit 0, which a behind PR — the normal case once any sibling merges — never reaches: a `rebase-check` red or heavy jobs `skipped` off the behind-count hold it short of full green, so an exit-0 gate strands every behind PR unlabelled. Label when `check` is present and green **and no heavy job (the diff-validating suites — *not* the `rebase-check` currency gate) is in `failure`** — a `skipped` heavy job is behind-count staleness and fine — **solely** off that count, which is a condition to establish rather than infer, see the five-condition note in `$(~/.fleet/bin/fleet-run --root)/skills/run-team/SKILL.md`'s fix-applier prompt block. A `failure` is the diff's own fault and blocks the label, else a genuinely broken PR gets labelled off a green `check` and burns a merge-bot rebase+CI cycle before the failure resurfaces. Read per-job state for this: run `ci-state.mjs` **without** `--quiet`, or read its `jobs` — the `--quiet` payload drops the per-job `jobs`/`missing` and the `dropped` list, keeping `reasons` (which still distinguishes `is failure` from `is skipped`). You reach step 6 yourself only when green is already in hand at push time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.82.14",
3
+ "version": "3.82.16",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -59,8 +59,8 @@ phase, or in any later one, asks the maintainer which tickets to take.
59
59
  one stopped; before it, a re-run runs every step again.
60
60
 
61
61
  The `tier-roles` step runs `~/.fleet/bin/fleet-run tier-roles.mjs --check
62
- --in-flight <B>`, `<B>` being the `--in-flight` given, else the default
63
- **Defaults** (**Queue depth**) states. What it checks is in the header of `scripts/tier-roles.mjs`, under
62
+ --in-flight <B>`, `<B>` being the `--in-flight` given, else the default.
63
+ **Default: In-flight bound 5, fixed for the run.** See references/pull.md. What it checks is in the header of `scripts/tier-roles.mjs`, under
64
64
  the tier routes. A line it prints on a pass about omp's `task.maxConcurrency`
65
65
  is a notice in the step's output and never holds: read it, report it, and
66
66
  go on — the fleet never writes that setting.
@@ -151,8 +151,7 @@ phase, or in any later one, asks the maintainer which tickets to take.
151
151
  `REFRESHED shortlist: <n> entries; <k> lifted`. A head whose Pull ended
152
152
  this run is not counted, unless on a lifted exclusion. Phase 0 runs it by hand
153
153
  once because a dead run's shortlist is stale by definition.
154
- None of those triggers runs while `HOLD (in-flight <n>/<B>)` stands
155
- (**Queue depth**).
154
+ None of those triggers runs while `HOLD (in-flight <n>/<B>)` stands.
156
155
 
157
156
  **No multi-select, no maintainer tick.** Selection and ordering are batched —
158
157
  one scan, oldest first; admission never is. The invariant this replaces — one
@@ -185,7 +184,7 @@ it prints. There are no hand-run
185
184
  refill or dispatch edges left. Record first, because the tick reads the run off
186
185
  `.fleet/ledger.md` and nothing else of yours: a wake you have not written down is
187
186
  one it cannot see, and it names the same dispatch again. Every record goes
188
- through `ledger.mjs` (**Run ledger**), which `fleet-event.mjs` drives — never a
187
+ through `ledger.mjs` (references/ledger.md), which `fleet-event.mjs` drives — never a
189
188
  hand edit. What each kind writes is in the header comment of `fleet-event.mjs`.
190
189
  See references/ci-and-staleness.md.
191
190
 
@@ -283,9 +282,22 @@ owes beyond its call:
283
282
  context exhaustion caused (references/close-out.md).
284
283
  - **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
284
 
285
+ Write each state change through `ledger.mjs` before you act on it, never by hand: a wake is done when no row, filing or ruling lives only in your context.
286
+
287
+ Compaction parks every in-flight member and nothing reports it, so on resume take the registry census first (**Member unaccounted**, in **Failure handling**) before you trust a live token.
288
+
289
+ Token additions go through `ledger.mjs token`. `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row. Re-typing the row with `row` replaces the whole line, so it drops the tokens you do not restate. Write member tokens and `review=` tokens only through `ledger.mjs`, never by hand.
290
+
291
+ Never rotate the ledger mid-run: it is phase 0's step alone.
292
+
293
+ Record each ruling with `ledger.mjs ruled <pr> <decision>`, one line with its reason. Gate on the `ledger.ok` field of `check`, not on the stderr warning. The writer policy, the gap the lock does not close, and `check`'s exits: See references/ledger.md.
294
+
295
+ **Write the ledger line before dispatching, not after.** Record the member with `ledger.mjs dispatch`, or `review` for a review, before its `task` call.
296
+
286
297
  ### Dispatching the members a wake names
287
298
 
288
299
  - **The reviewer.** **Dispatch the review, never wait on it, and act when its result lands. That is the default path.** `DISPATCH review PR#<M>`: a `task` member named `review-pr-<pr#>`, agent `fleet-review-runner`, its prompt the five args `pr`, `branch`, `worktree`, `testCmd` and `scratch`. Record `review=member:review-pr-<pr#>` on the PR's row before the call. `scratch` is the scratch root itself, never a `pr<N>` partition (`references/reviewers.md`). Write the `review=` token with `ledger.mjs review <pr> <token>`; `ledger.mjs` refuses a bad grammar. The tick names every PR owed one, oldest first, and each gets one review, once per PR. The tick counts in-flight reviews off the ledger and nowhere else, so a review you launched and never recorded is one it names again. The runner writes the result file and reports its path and `reviewed=` token.
300
+ A row with `review=` and no `reviewed=` is a review in flight, and the tick counts it as a live review unit unless gh reports its PR MERGED or CLOSED.
289
301
  - **The fix-applier.** `DISPATCH fix-pr PR#<M>` on a `reviewed=` count that survived or unverified findings, with no fix-applier live: name `fix-pr-<pr#>` (the next suffix if that name is on record), never the PR's implementer; render with `dispatch-prompt.mjs render fix-applier --kind review --member fix-pr-<pr#> …`, then record `ledger.mjs dispatch <pr#> fix-pr-<pr#>` before the call. Its brief carries the PR number, the worktree abs path, the PR's branch, the same `testCmd` you passed the review, and the **path** `<scratch>/pr<pr>/<run>/review.json`, `<run>` off the PR's latest `reviewed=` — never the findings. Every `<scratch>` in that brief is the absolute path `scratch-root.mjs` printed. Its rulings are the fix-applier body's (`agents/fleet-fix-applier.agent.md`); you hold none. It holds every finding and you hold none, so there is nothing for you to rank, relay or pre-rule — and a report from a refuter it spawns that surfaces to you is its to retrieve, never yours to scan or pass on. It answers the hold and nothing else: survived or unverified findings of a returned review the row also carries stay unanswered through its settle, so the tick prints `DISPATCH fix-pr PR#<M>` again once it lands, and that one is a review fix-applier, dispatched as above. On a PR carrying `ready-to-merge`, the push moves the head after the label, so the next merge bot refuses it `head-moved-after-label-#<M>`: a fresh finisher, per **Failure handling**.
290
302
  - **The conflict hold.** `DISPATCH fix-pr PR#<M>` on a conflict hold: the row carries an unresolved `conflict-hold:#<M>`, the merge bot's or one you recorded off the tick's `CONFLICT` line. It is the same slot with a different job, and no review file to hand over. Name it `fix-pr-<M>`, or the next suffix (`-b`, `-c` …) when that name is on record; render with `dispatch-prompt.mjs render fix-applier --kind conflict --member <name> …`; dispatch with the `agent` `ledger.mjs dispatch` printed, in the PR's existing worktree. Read the definition off that output (`fleet-fix-applier` whatever the row's `tier=`), never recall it. Settle `applied:<head>`, or `no-op` if no conflict was left, which lifts the merge hold; a dispatched-but-live one does not, and `failed`/`killed` leave the PR fix-due for a replacement.
291
303
  - **Then dispatch a red-check fixer** — on a `check`-job `failure` bound to the PR's current head that is not **solely** staleness: a `rebase-check` red, or heavy jobs `skipped`, that comes only off the behind-count gets no fixer (the five-condition note in `agents/fleet-fix-applier.agent.md` establishes that). Name `fix-pr-<M>`, or the next free suffix; render with `dispatch-prompt.mjs render fix-applier --kind red-check --member <name> …`, record with `ledger.mjs dispatch <pr#> <name>` before the call, and dispatch with the printed `agent` in the PR's existing worktree. While a review or a review fix-applier on the PR is live or still owes its report — a dispositions mismatch awaiting its retry and a conflict hold awaiting its fix-applier included — the red-check fixer does not fire: their push starts a new run, and a fixer racing them would work on a SHA that is still moving. The common case, the implementer's own first CI firing turning red before any review exists, still dispatches the review against that head as normal. The tick prints no `DISPATCH` for it, so you reconcile it yourself, and it always goes after the review, never before. It is told outright not to watch, poll or wait for CI: your persistent Monitor owns the next run on the pushed head, and the finisher gate — review returned AND fix-applier report sent — applies to this member the same way. Settle it `applied:<head>` once a fix is pushed. Settle it `no-op` only when the red turns out not to be this member's to fix — a flake that passes on rerun, or staleness — with nothing pushed; `no-op` counts as landed, so it never stands for "could not fix". Settle it `failed` when it cannot reach green, which the **Failure handling** row "Reviewer, fix-applier or red-check fixer cannot reach green" covers. Its three dispatch edges are under **The finisher gate**; their rationale is in `references/member-lifecycle.md` (The red-check fixer's three edges).
@@ -373,6 +385,29 @@ treat as a live scratch write until you have checked it.
373
385
 
374
386
  **A refutation resting on an injection nothing proved landed is not a refutation — it is a cell that never ran.** See references/reviewers.md.
375
387
 
388
+ **Guard: accumulate per PR, never conclude inside one run.** **Append one row to
389
+ `docs/metrics/tier-outcomes.tsv` when you rule each PR's review, with
390
+ `~/.fleet/bin/fleet-run tier-outcomes.mjs append <pr> --closed-own-ticket yes|no --minted-false-claim yes|no --note "<text>"`,
391
+ never by hand** (that file's header carries the column meanings). The script
392
+ fills `run_date`, `pr`, `ticket` and `tier` itself — `tier` from what actually
393
+ ran, off the implementer's `tier-ok=` ledger verdict or else its one
394
+ member-outcomes row (a `tier-unverifiable=` member has no ledger verdict, so
395
+ its one member-outcomes row decides), and BLANK with a WARNING naming why
396
+ when neither settles
397
+ it; never type a tier in over that. That append is the whole duty. See references/pull.md.
398
+
399
+ The row's last four fields are the ticket's difficulty, and they are what lets a
400
+ tier comparison condition on the thing that swamps it. `sizing` is the
401
+ **member's** own `light`/`heavy` verdict from its dispatched `sizing-a-ticket`
402
+ run, which `append` reads itself off the member's report file
403
+ (`<scratch>/reports/impl-<ticket>[-x].json`, its `sizing.verdict`, the highest
404
+ retry letter's when there are several), so you pass no `--sizing`; the member
405
+ also puts it in the PR body's `Sizing:` line, with the signal it turned on;
406
+ `profile`, `loc` and `files` all come from `diff-stats.mjs` over the merged
407
+ diff.
408
+
409
+ **A verdict is a measurement only if it post-dates the run that produced it.** See references/reviewers.md.
410
+
376
411
  ### Nothing to do: arm the beat
377
412
 
378
413
  - **Nothing to do right now** → **do not end your turn.** Arm the heartbeat
@@ -450,6 +485,18 @@ Act on each line the tick prints as its row reads here, then stop: the next wake
450
485
  ~/.fleet/bin/fleet-run fleet-tick.mjs --apply [--in-flight <n>]
451
486
  ```
452
487
 
488
+ Read the depth off the tick and never derive it by hand: act on the row it prints, and when it prints `SUGGEST /triage, hold idle`, hold the implementer row idle and suggest `/triage` in the report. See references/tick-rows.md.
489
+
490
+ **The review backlog never gates, and the merge-queue depth is still no
491
+ signal.** The only gate on a Pull is the In-flight count. A deep backlog is a
492
+ `DISPATCH review` on the same tick, never a reason to idle an implementer.
493
+ A starved implementer queue never stalls the review or merge side.
494
+ For the merge queue, see references/pull.md.
495
+
496
+ **Reconcile, do not wait for an event.** With the shortlist empty the tick's rows
497
+ govern — refresh, and never Pull a ticket the shortlist does not hold.
498
+ See references/pull.md.
499
+
453
500
  It reads the run itself, and nobody states a count. The In-flight count —
454
501
  every open PR that closes an issue off `gh`, plus every claimed ticket whose
455
502
  newest implementer is live or owed a replacement and has no such PR yet, off
@@ -469,12 +516,11 @@ row with `ledger.mjs row` and tick again. The In-flight bound defaults to 5
469
516
  and is fixed for the run: pass `--in-flight` on every tick when `$ARGUMENTS`
470
517
  set it, and never otherwise.
471
518
 
472
- It prints `actual/target` and a named ACTION per role, with the whole **Queue
473
- depth** guard table applied in code. Act on each line as it reads:
519
+ It prints `actual/target` and a named ACTION per role. Act on each line as it reads:
474
520
 
475
521
  - `PULL #<N> …` — one Pull per named ticket, in the order printed: `pull.mjs next --head <N>`, or the `pull.mjs answer` the row names (phase 1), then the dispatch under **Dispatching the Pull's member**.
476
522
  - `REFRESHED shortlist: <n> entries; <k> lifted` — the tick refreshed the shortlist itself, and its `PULL` row already reads the result; `REFRESH FAILED` is yours, since `shortlist.mjs` could not run.
477
- - `SUGGEST /triage, hold idle` — nothing is admissible after the refresh: suggest `/triage` in the report, never run it.
523
+ - `SUGGEST /triage, hold idle` — nothing is admissible after the refresh: suggest `/triage` in the report, never run it. The suggestion is a report, not a blocking prompt.
478
524
  - `specs-dropped=#<N> …` — never actionable: log those tickets as specs awaiting to-tickets, not as no work.
479
525
  - `DISPATCH fix-pr PR#<M> …` — a fix-applier for that PR (review findings, a dispositions mismatch no retry has answered, or a conflict hold): render its brief with `dispatch-prompt.mjs render fix-applier --kind <kind>`, `ledger.mjs dispatch <M> fix-pr-<M>` (the next suffix when that name is already on record), then the `task` call.
480
526
  - `DISPATCH review PR#<M> …` — a review per PR, oldest first, off your turn: record `review=member:review-pr-<M>` with `ledger.mjs review <M> …`, then dispatch `review-pr-<M>`.
@@ -824,7 +870,7 @@ merge. `review-and-fix.md` states them; the body says they apply.
824
870
 
825
871
  **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
826
872
  (`ledger.mjs token <PR number> label-off=<latest finisher attempt>`, which keeps
827
- every token the row already has — **Run ledger**), then
873
+ every token the row already has, see references/ledger.md), then
828
874
  remove `ready-to-merge`, then approve. Then
829
875
  dispatch a **fresh** finisher against the new head; the first audit does not
830
876
  transfer, since it verified a different tree. The merge bot refuses that head on
@@ -1006,6 +1052,16 @@ report recovery, never the exit code alone.
1006
1052
 
1007
1053
  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.
1008
1054
 
1055
+ ### A member's claims
1056
+
1057
+ **Cross-check what members report about their environment.**
1058
+
1059
+ **Distrust negative claims hardest.** Make members state their search scope. Negative claim
1060
+ vs specific finding with paths → paths win. See references/reviewers.md.
1061
+
1062
+ Suspect a neighbour before a member's own diff — for unexplained failures, and
1063
+ equally for any **finding** that came from reading source.
1064
+
1009
1065
  ### Relabel causes
1010
1066
 
1011
1067
  - **Implementer bails before implementing** → `fleet-event.mjs impl-report impl-<N>`
@@ -1231,224 +1287,20 @@ this section's dispatch recipe — the cwd recipe above, the isolated-workspace
1231
1287
  measurement, or the teardown scoping — moves
1232
1288
  where the controller itself runs from.
1233
1289
 
1234
- ## Queue depth
1235
-
1236
- Read the depth off the tick and never derive it by hand: act on the row it prints, and when it prints `SUGGEST /triage, hold idle`, hold the implementer row idle and suggest `/triage` in the report.
1237
-
1238
- - **shortlist** — `.fleet/shortlist.json`'s entries with no `impl-` row and no
1239
- live `excluded` row: admissible, not yet pulled. The tick's `unclaimed=`.
1240
- Not after a refresh at unclaimed < B (below). A head whose Pull ended this
1241
- run is not counted, unless on a lifted exclusion.
1242
- - **supply** — `scanned`, the open `ready-for-agent` tickets `shortlist.mjs`'s
1243
- candidate scan returned, before any filter. Only a Pull's full read judges
1244
- decided?, so a shortlist of undecided tickets is supply that every Pull
1245
- relabels away — and relabelling is what stops it being counted next scan.
1246
- - **review backlog** — PRs owed a review. `fleet-tick.mjs` counts every
1247
- open PR without `ready-to-merge` **that closes an issue** and carries no `review=`
1248
- token on the ledger — one settled `=failed` is owed a review again, and so is
1249
- one whose latest finisher halted `past-pin` past its latest `reviewed=` head
1250
- (**Resolving a finisher halt**) — and it
1251
- prints them as `review-due=`. Exact, not a wider read: the ledger records
1252
- which PRs are under review or reviewed, so nothing about it is guesswork. The
1253
- closing-issue test is what keeps it from widening: a PR that closes no issue
1254
- — one you author yourself, for instance — is left unlabelled for the
1255
- maintainer, so no member of the fleet will ever review it, and counting it
1256
- floors the backlog at a depth nothing in the run can drain — the reviewer
1257
- row would then report a review owed for the rest of the run.
1258
- GitHub's own linked-issue set decides that, not a keyword regex over the body.
1259
- **That exemption is for a PR THIS run authors, never one a PRIOR run left
1260
- open** — step 0 folds those into this run's review queue, so count one when
1261
- you reason about backlog BY HAND. **The `fleet-tick.mjs` number still
1262
- excludes it**: nothing in its filter distinguishes an inherited PR from this
1263
- run's own, since a PR closing no issue fails the closing-issue test either
1264
- way — folding one in cannot reach the count. So the by-hand judgement and
1265
- the script's output differ here, and such a PR's review is dispatched by
1266
- hand.
1267
- - **In flight** — a ticket between its Claim and its PR's merge: an implementer
1268
- live or owed a replacement on it, or an open PR closing it. A PR closing
1269
- nothing is never in flight, and a ticket whose newest implementer settled
1270
- `blocked` is not either. The tick's `n`; the implementer row's counts read it
1271
- as `implementers=<i> prs=<p>`, n being i + p.
1272
- - **In-flight bound** — the most tickets a run lets be In flight at once, fixed
1273
- for the run, set by `/run-team`'s one argument. Reaching it holds every Pull
1274
- until a merge brings the count below it; reviewers, finishers and the merge
1275
- bot never slow for it. The tick's `B`.
1276
-
1277
- **Default: In-flight bound 5, fixed for the run.** See references/pull.md.
1278
-
1279
- **The review backlog never gates, and the merge-queue depth is still no
1280
- signal.** The only gate on a Pull is the In-flight count. A deep backlog is a
1281
- `DISPATCH review` on the same tick, never a reason to idle an implementer.
1282
- For the merge queue, see references/pull.md.
1283
-
1284
- **Reconcile, do not wait for an event.** With the shortlist empty the table
1285
- below governs — refresh, and never Pull a ticket the shortlist does not hold.
1286
- See references/pull.md.
1287
-
1288
- **Do not re-derive this by hand — `fleet-tick.mjs` computes it.** One invocation
1289
- prints per-role `actual/target` and an explicit ACTION with the backlog gate
1290
- above and every row of the table below already applied; every wake
1291
- ends in it. The deficit is then *computed, not remembered*, which is
1292
- the whole point: a table you must remember to consult is one you will not
1293
- consult under a merge-side event storm, and that is how implementers reached
1294
- 0/target with ~57 `ready-for-agent` in supply while nobody
1295
- noticed (#3). What follows stays here as the explanation of what the script decides —
1296
- never as a second, hand-run copy of it. The table's shortlist counts no head
1297
- whose Pull ended this run, unless on a lifted exclusion. A refresh at unclaimed < B leaves `unclaimed=` at
1298
- that count, so `SUGGEST /triage` can print it above 0.
1299
-
1300
- | shortlist (unclaimed) | implementer row prints |
1301
- |---|---|
1302
- | ≥ 1 | `PULL #N …`, one head per free slot — a Pull each (phase 1) |
1303
- | below the In-flight bound, missing or empty | the tick refreshes first — `REFRESHED shortlist: n entries; k lifted` — and Pulls from what it found |
1304
- | 0 after that refresh | `SUGGEST /triage, hold idle` |
1305
-
1306
- ### Specs, the refresh bound and the holds
1307
-
1308
- Whatever the row prints, a `specs-dropped=#<N> (…)` at the end of its counts
1309
- names the tickets the to-spec filter kept out of the shortlist — specs
1310
- awaiting to-tickets, so an empty shortlist carrying one is not "no work".
1311
-
1312
- The refresh runs at unclaimed < B and, with every other refresh trigger, never
1313
- while the bound holds: a refresh then cannot feed a Pull, and the first tick
1314
- back under the bound refreshes and Pulls in the same tick. Unclaimed there
1315
- counts no head whose Pull ended this run, unless on a lifted exclusion.
1316
-
1317
- Four holds outrank every row: `HOLD (draining)` once `ledger.mjs drain` has
1318
- recorded the drain, `HOLD (tier mismatch impl-<N>)` until a replacement is
1319
- dispatched at the right tier, `HOLD (tier unchecked impl-<N>)` until
1320
- `tier-check.mjs` has written its verdict on the newest implementer —
1321
- `tier-ok=`, `tier-mismatch=` or `tier-unverifiable=` — and
1322
- `HOLD (in-flight <n>/<B>)` when n tickets are between Claim and merge and
1323
- n ≥ B — refreshing to enable a Pull you are holding buys nothing.
1324
-
1325
- `HOLD (in-flight <n>/<B>)` ranks last of the four: the other three print in
1326
- its place. Its detail is the implementer row's counts, `implementers=<i>
1327
- prs=<p> unclaimed=<k> supply=<s>`. It is quiet — a merge brings n below B and
1328
- the next tick Pulls — except for a ticket whose newest implementer settled
1329
- `killed` or `tier-mismatch`: it counts as In flight with no PR that could
1330
- merge, so the row asks for the replacement's dispatch and names it in the
1331
- detail. The reviewer row reads `<units>/<B>`, its live review units against
1332
- the same bound, and never holds on it.
1333
-
1334
- A main checkout that is not `clean` against its Phase 0 baseline outranks all
1335
- of them, and every dispatching row besides: `HOLD (main checkout dirty)`,
1336
- `HOLD (main checkout unknown)` or `HOLD (main checkout no baseline)` on the
1337
- implementer, reviewer and merge-bot rows alike, beside the `MAIN-CHECKOUT-*`
1338
- line that says why.
1339
-
1340
- `/triage` is user-invoked only — suggest, never run. The suggestion is a report,
1341
- not a blocking prompt. Counts come from cheap `gh issue list --search`, no bodies.
1342
- A starved implementer queue never stalls the review or merge side.
1343
-
1344
- ## Tier outcomes guard
1345
-
1346
- This guard is done when you have run `tier-outcomes.mjs append` once for each PR whose review you rule; never conclude a tier result inside one run.
1347
-
1348
- **Guard: accumulate per PR, never conclude inside one run.** The unit is the PR —
1349
- supply is one Pull per free slot, so there are no implementer batches. **Append one row to
1350
- `docs/metrics/tier-outcomes.tsv` when you rule each PR's review, with
1351
- `~/.fleet/bin/fleet-run tier-outcomes.mjs append <pr> --closed-own-ticket yes|no --minted-false-claim yes|no --note "<text>"`,
1352
- never by hand** (that file's header carries the column meanings). The script
1353
- fills `run_date`, `pr`, `ticket` and `tier` itself — `tier` from what actually
1354
- ran, off the implementer's `tier-ok=` ledger verdict or else its one
1355
- member-outcomes row (a `tier-unverifiable=` member has no ledger verdict, so
1356
- its one member-outcomes row decides), and BLANK with a WARNING naming why
1357
- when neither settles
1358
- it; never type a tier in over that. A PR that already has a row is left
1359
- untouched, so re-running it is safe. That append is the whole duty.
1360
-
1361
- The row's last four fields are the ticket's difficulty, and they are what lets a
1362
- tier comparison condition on the thing that swamps it. `sizing` is the
1363
- **member's** own `light`/`heavy` verdict from its dispatched `sizing-a-ticket`
1364
- run, which `append` reads itself off the member's report file
1365
- (`<scratch>/reports/impl-<ticket>[-x].json`, its `sizing.verdict`, the highest
1366
- retry letter's when there are several), so you pass no `--sizing`; the member
1367
- also puts it in the PR body's `Sizing:` line, with the signal it turned on;
1368
- `profile`, `loc` and `files` all come from `diff-stats.mjs` over the merged
1369
- diff. **Phase 0 does not size anything** — it shortlists, a Pull claims, and
1370
- the sizing run happens inside the member after both, which is why the verdict
1371
- has to travel in the member's report rather than being something you already hold.
1372
-
1373
- **A verdict is a measurement only if it post-dates the run that produced it.** See references/reviewers.md.
1374
-
1375
- The member-facts scrape, the drain of the pending ticket-features rows and the
1376
- commit of the metrics files are steps of `closeout.mjs finish`, which **End of
1377
- run** (**What a wake owes beyond its call**) drives. See references/close-out.md.
1378
-
1379
- **Why not decide inside one run.** A run holds 2-3 implementer PRs, and ticket
1380
- difficulty swamps the tier effect — a gojq parity harness and a two-statement
1381
- shell reorder are not comparable units. Finding-counts are not comparable either:
1382
- `selectDimensions` trims the fan-out by diff profile, so one PR fields 29 agents
1383
- over six dimensions and its sibling fields 8 over three. And the fleet is not
1384
- stationary — this prompt gets edited mid-run when a defect earns it. Hence a
1385
- binary outcome per PR, accumulating, rather than a per-run verdict.
1386
-
1387
- **Why the rows this guard fired on could not answer the tier question, and this
1388
- is the load-bearing part.** Until the 2026-08-16 revert, `class` and `tier` were
1389
- perfectly confounded: routine always ran `sonnet`, correction always ran top
1390
- tier. So the comparison the file invites — routine-at-sonnet against
1391
- correction-at-top-tier — measures **class, not tier**, because those classes fail
1392
- in different ways by construction: corrections ship false claims in prose,
1393
- routines ship incomplete or regressing code. A run that "mixes both" does not fix
1394
- it. **Only a same-class comparison across tiers is informative**, and the old
1395
- rule could not produce one, since no routine ticket ever ran at top tier.
1396
-
1397
- **That last sentence is no longer true, and this is the part to re-read.** Since
1398
- the revert every routine ticket runs at top tier, so the file now holds 55
1399
- `routine`/`opus` rows against 11 `routine`/`sonnet` (as of 2026-08-28, 103 rows
1400
- — recount before citing). The same-class comparison the paragraph above calls
1401
- the only informative one therefore EXISTS now. It is still not clean: the two
1402
- groups are split almost entirely by calendar date, so it measures prompt
1403
- evolution at least as much as tier. The counter-evidence paragraph earlier in
1404
- this section carries the split and the three rows that cut against it. Recount
1405
- both, and every other figure in this section, with:
1406
-
1407
- ```bash
1408
- grep -vc '^#' docs/metrics/tier-outcomes.tsv
1409
- awk -F'\t' '!/^#/ && $4=="routine" {n++; d[$1]=1; if($6=="no") no++} \
1410
- END{print n, length(d), no+0}' docs/metrics/tier-outcomes.tsv
1411
- awk -F'\t' '!/^#/ && $4=="routine" {t[$5]++; if($6=="no") f[$5]++} \
1412
- END{for(k in t) print k, t[k], f[k]+0}' docs/metrics/tier-outcomes.tsv
1413
- ```
1414
-
1415
- **These rows are the historical corpus, and nothing appended to them fixes the
1416
- confound.** What fixes it is the exploration draw in the dispatch rule above:
1417
- one implementer in every five — every one, during burn-in — runs at a drawn
1418
- cell beside the policy cell's, against the same prompts on the same day, so
1419
- the comparison stops depending on which tier history happened to leave in
1420
- which week. Read each cell's comparisons, not the whole-file split, once there
1421
- are enough of them.
1422
-
1423
- **So read what the guard actually established: routine tickets sometimes fail to
1424
- close their own ticket. Not that `sonnet` caused it.** The revert is the
1425
- pre-committed rule being honoured, not a measurement. Restoring a cheap tier is
1426
- still the maintainer's call and still a change to the dispatch rule — but the
1427
- deliberate control it used to require is no longer something anyone has to
1428
- authorize one ticket at a time: the exploration draw above produces one in
1429
- every five Pulls by construction. **Do not read a cell early.** A cell's
1430
- comparison is one session holding an admissible row at that cell — its tier
1431
- check passed and its resolved effort is the cell's level — and one at
1432
- `slow-high` whose resolved model and effort differ. Count them per cell, never
1433
- pooled across cells, and stop until
1434
- there are at least ten of them across five or more distinct `run_date`s; below
1435
- that, a comparison count is a number, not evidence, and the last guard fired
1436
- with n=1 on the control side. **Report the count the per-cell readout prints,
1437
- never one from any other query:** `~/.fleet/bin/fleet-run cell-readout.mjs`
1438
- prints `<cell> <comparisons> <run_dates> <resolved models>` for each cell past
1439
- that floor and a cell below it only as a count on stderr; a `mixed (…)` models
1440
- column means the cell's history spans more than one model behind its role.
1441
- The pre-cell pair query counted pairs that are no cell's comparisons, and the
1442
- query before that one counted every session whose implementers merely
1443
- differed, so it cleared this floor by an order of magnitude while the
1444
- controlled comparison did not exist yet.
1445
-
1446
1290
  ## Why the implementer body carries what it does
1447
1291
 
1448
1292
  Read this before you edit any `fleet-implementer-<cell>` file, and stop editing when every cell's body is byte-identical again.
1449
1293
 
1450
1294
  Why the agent body carries what it does — read this before editing any `fleet-implementer-<cell>` file, and keep every cell's body byte-identical. See references/pull.md.
1451
1295
 
1296
+ - **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.
1297
+ - **The settling command for "which files read X" must never be a literal-path
1298
+ grep.** Replace the file with `MUTATED` in a throwaway copy, run the suite, and read off
1299
+ which suites red: that answers which ones **depend on its contents**, which is
1300
+ the question a coverage claim actually needs, and it returns a different set
1301
+ from any spelling of the grep. See references/correction-tickets.md.
1302
+ - **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.
1303
+
1452
1304
  **Prose-first** and **wrongly-REFUSE** are not padding — they are the case
1453
1305
  which shipped with **enumerate-and-declare** already in its prompt. It missed a
1454
1306
  shape its own ticket named in passing, because it enumerated from the mechanism
@@ -1508,213 +1360,6 @@ hold the old text — re-brief only if it changes what they do *now*.
1508
1360
 
1509
1361
  Plan it as a PR the run's own merge queue lands, never as an edit-and-continue. See references/merge-bot.md.
1510
1362
 
1511
- ## Run ledger
1512
-
1513
- Write each state change through `ledger.mjs` before you act on it, never by hand: this section is done when no row, filing or ruling lives only in your context.
1514
-
1515
- One git-ignored `.fleet/ledger.md` per run, updated at **every** state change via
1516
- `~/.fleet/bin/fleet-run ledger.mjs` subcommands (`row`, `filed`, `ruled`,
1517
- `check`, `read`, `dispatch`, `settle`, `review`, `drain`, `rotate`). Your context is the least
1518
- durable thing in the run: it compacts, and a controller that loses the dispatch
1519
- map or the filed list redoes finished work. Two duplicate tickets shipped in one
1520
- run from exactly that.
1521
- Compaction parks every in-flight member and nothing reports it, so on resume
1522
- take the registry census first (**Member unaccounted**, in **Failure
1523
- handling**) before you trust a live token.
1524
-
1525
- **The file lives one run.** Phase 0 rotates it before anything reads it, so
1526
- no exclusion, filing, ruling or drain marker outlives the run that wrote it,
1527
- and no row does either — except, when the previous run was this session's
1528
- own, a row still carrying a live token, which rotate carries forward with
1529
- its live `## Dispatched` entries (phase 0). Never rotate mid-run: it is
1530
- phase 0's step alone.
1531
-
1532
- One line per ticket, rewritten in place (`ledger.mjs row <ticket> <text>`; `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row):
1533
-
1534
- ```
1535
- #<t1> impl-<t1>=PR#<p1> → PR#<p1> → MERGED 73b356de
1536
- #<t2> impl-<t2>=PR#<p2> → PR#<p2> · fix-pr-<p2> · ports=16<t2> · ruled:6-applies · held-behind:#<t3>
1537
- #<t3> impl-<t3> · tier=task-high
1538
- #<t4> excluded · behind-pr:#<p2>
1539
- #<t5> excluded · behind-issue:#<t3>
1540
- ```
1541
-
1542
- **A member's token is `<member>` while it is live and `<member>=<outcome>` once
1543
- it settles** — the grammar is `ledger-grammar.mjs`'s, and the tick derives every
1544
- live count from it, so nobody states one. Write neither by hand:
1545
-
1546
- - **`ledger.mjs dispatch <ticket|pr> <member>`** — before the dispatch call,
1547
- marks the member live on its row and appends it to `## Dispatched` with
1548
- `brief=<first 8 hex of its brief's sha256>`. `dispatch
1549
- merge-bot` names the next bot itself. It prints `{member, agent, ticket,
1550
- line, created, total, brief}`, `brief` the `{path, sha256}` of the rendered
1551
- brief the `task` call is handed; `agent` is the definition the `task` call names — an
1552
- implementer's comes off its row's `tier=` (the tier check under **Dispatching the Pull's member** states the
1553
- mapping), a finisher's and a merge bot's are their own definitions, and a
1554
- fix-applier's is `fleet-fix-applier` for every kind of work. It
1555
- refuses a member whose `briefs/<member>.json` beside the ledger is missing,
1556
- names another role or carries no sha256, a brief file beside it that is missing or does not hash to that sha256, a name this session already used — on the ledger or in its
1557
- `## Retired` — and names the replacement (`-b`, `-c` …), a
1558
- second live member on one ticket or PR, any implementer once the run is
1559
- draining, an implementer whose row's `tier=` names no single definition
1560
- or one with no file under `agents/`, and a finisher on a ledger the tick
1561
- itself refuses.
1562
- - **`ledger.mjs settle <member> <outcome>`** — rewrites the token to
1563
- `<member>=<outcome>` in both places. Outcomes: `impl-N` = `PR#M | bailed |
1564
- released | killed | tier-mismatch | blocked`; `fix-pr-M` = `applied:<head> | no-op |
1565
- failed | killed`; `finisher-pr-M` = `labelled | failed | killed |
1566
- halted:<cause>`, `<cause>` one of `live-editor | rebase | past-pin |
1567
- unreadable | missing | absent | other` (**Resolving a finisher halt**);
1568
- `merge-bot-n` = `done | killed`. A settled member stays settled. `settle
1569
- impl-N=PR#M` also folds a `#M` row a PR-bound dispatch or a `review` made
1570
- before the settle into `#N`'s row, so the PR's row is the one row naming
1571
- each PR from then on — but only when `#N`'s row named no PR before this
1572
- write (a second `PR#` on it would misattribute the fold, or bury `#N`'s own
1573
- fresher `review=` state under an older one) and no implementer claims `#M`'s
1574
- row as its own. `settle impl-N=PR#M` refuses a settle after which a PR maps
1575
- to more than one row when it did not before, judged after the fold — a PR
1576
- has one row, the PR's row; a split already on disk is carried.
1577
- - **`ledger.mjs settle <member> --from-report [--reports <dir>]`** — the
1578
- settle for a member that wrote a report: reads `<dir>/<member>.json`
1579
- (`<dir>` defaults to `<scratch>/reports`), derives the outcome from it and,
1580
- before writing, checks what that outcome claims — `PR#M`: `verify-sha.sh`
1581
- on the report's branch and head, and PR M OPEN and closing N; `bailed`: a
1582
- `bailCause`, which the receipt names; `applied:<head>`: origin's branch at
1583
- that head; `no-op`: origin's branch at the head the PR's latest
1584
- `reviewed=` names; `labelled`: `ready-to-merge` on the PR and in the
1585
- read-back; `halted:<cause>`: a cause and non-empty evidence; `done`: every
1586
- PR the bot merged reads MERGED. `PR#M` and `applied:<head>` also need a
1587
- green Check-step record (`SETTLE`). It then records each `unrecorded` filing in
1588
- **filed** and, for a fix-applier, each refutation the report defers in
1589
- **ruled**, once however often it runs, and prints `{member, outcome,
1590
- checks, filed, ruled}`. Exit 3: a check failed — the member's claim and the
1591
- world disagree — and nothing is written; you rule. Exit 2: the report is
1592
- absent or invalid, or a check could not look, and nothing is written; a
1593
- check that could not look is run again once `gh` and `git` answer. The bare
1594
- form above, for an outcome a report carries, warns `settled without
1595
- --from-report` on stderr.
1596
- - **`ledger.mjs review <pr> <token>…`** — the only writer of `review=` and
1597
- `reviewed=` (below): appends each to the PR's row — the ticket row once
1598
- `settle impl-<N>=PR#M` has run, else `#M`, created when neither exists —
1599
- and prints `{pr, row, line, created, next}`, `row` the key written and
1600
- `next` the runner name the PR's next launch takes, null with every letter
1601
- taken. A
1602
- `review=<launch>=failed` whose `review=<launch>` the row holds settles that
1603
- launch in place, alone or with the next launch in the same call. Any other
1604
- token is refused.
1605
- - **`ledger.mjs drain "<reason>"`** — at most one standing drain marker; supply
1606
- stops, the review and merge sides keep going. It lives in this run's
1607
- ledger, so the next run's rotation retires it with everything else.
1608
- - **`ledger.mjs undrain "<reason>"`** — clears the drain marker and refuses when
1609
- no marker is set. `closeout.mjs reopen` runs it.
1610
- - **`ledger.mjs rotate`** — phase 0's first ledger write, every run: moves
1611
- the file as-is to `ledger.<UTC YYYY-MM-DDTHHMMSSZ>.md` beside it under the
1612
- same `<file>.lock`, and the next write starts the empty skeleton — unless
1613
- the previous run was this session's own, when it first writes the rows and
1614
- `## Dispatched` entries still live into a new ledger, and every other
1615
- settled name, or name given an out-of-vocabulary outcome, into its
1616
- `## Retired` (references/phase-0.md, "Rotate the ledger", says which). Refused
1617
- while the recorded controller is alive and not this run's ancestor, ledger
1618
- or none — or, with no record and a ledger to move, while the heartbeat mark
1619
- is still beating, or a tick or ledger write is inside its window with no
1620
- beat — or when the archive name already exists, the process
1621
- table cannot be read, or `.fleet/heartbeat.json` exists but cannot be used
1622
- (unreadable, not JSON, not an object, or a `controller` it cannot judge);
1623
- with no ledger and no such refusal it moves nothing and exits 0. Then it
1624
- records this run's controller in `.fleet/heartbeat.json`. The guard is
1625
- stated in references/phase-0.md, "Rotate the ledger".
1626
-
1627
- `row` refuses a malformed member token anywhere in its text — one whose outcome
1628
- is outside the vocabulary above, e.g. `merge-bot-1=dispatched` — at exit 2,
1629
- naming the token and writing nothing, because every reader would take it as a
1630
- permanent settle. It refuses a write that adds a `review=`/`reviewed=` token
1631
- (one the row already holds is carried), and one after which a PR maps to more
1632
- than one row when it did not before — `row <M>` creating `#M` for a PR a ticket
1633
- row maps, or a rewrite adding a `PR#M` mention to another row — naming the
1634
- PR's row (`settle impl-N=PR#M` applies the same rule after its fold).
1635
-
1636
- `→ PR#M` stays as the human-readable arrow; the tick reads only the `=`
1637
- tokens. `tier=<cell>` marks an Exploration Pull's cell and `route=<REASON>` a router fallback (the dispatch's CELL line), and `excluded ·
1638
- behind-pr:#M | behind-issue:#M` is an Exclusion (phase 1) — a ticket row like
1639
- any other, never a section of its own.
1640
-
1641
- A PR's review is not a member, so it carries its own pair of row tokens,
1642
- written with `review` onto the PR's row (**The reviewer**): `review=wf:<runId>` |
1643
- `review=member:<name>` at launch, `<name>` being
1644
- `review-pr-<n>` for the PR's first runner and the next letter its rows do not
1645
- carry yet (`review-pr-<n>-b`, `-c`, …) for every later one,
1646
- settled dead as `…=failed`, then `reviewed=<head>:<survived>/<refuted>/<unverified>:<run>`
1647
- when the result lands, `<run>` the review's `run-XXXXXXXX` run root under
1648
- `<scratch>/pr<n>/`, which holds its `review.json`. A row with `review=` and no `reviewed=` is a review in
1649
- flight, and the tick counts it as a live review unit unless gh reports its PR
1650
- MERGED or CLOSED. `ci=<run-id>:<attempt>:<conclusion>`
1651
- and `held-behind:#<lower>` are row tokens the same way, and so is
1652
- `conflict-hold:#<pr>` — written by the merge bot itself (`run-merge-bot.md`
1653
- step 1), or by you off the tick's `CONFLICT` line, naming the row's own PR. It
1654
- is not an Exclusion: that gates a ticket's claim, this a PR's merge. `label-off=<finisher-pr-M[-x]>`
1655
- is yours, written before you take `ready-to-merge` off PR M on purpose (taking
1656
- it off before you approve a push, or clearing one left on a moved head),
1657
- naming that PR's latest finisher attempt: the tick reads a missing label as a
1658
- finisher's miss only after the last attempt a `label-off=` names, so without
1659
- it your own removal reads as one. `row` and `token` refuse a `label-off=` naming no
1660
- `finisher-pr` member that `## Dispatched` or some row already carries.
1661
-
1662
- `review` holds a PR's review record to these rules, read across every row that
1663
- maps to the PR — its settled `impl-<N>=PR#<M>` token, else its first `PR#`
1664
- mention, else its key — in ledger order. Every `reviewed=` answers the nearest
1665
- earlier launch that is neither answered nor `=failed`. A launch identity (a
1666
- workflow's runId, or a runner's name, `=failed` aside) appears once, and no launch takes a runner name the
1667
- ledger's `## Retired` holds — an earlier run of this session launched it —
1668
- naming the next free reviewer name instead. No launch appears
1669
- while an earlier one is open — unanswered and not `=failed`. Every
1670
- `review=`/`reviewed=` parses. `row` holds the rest: a launch open before the
1671
- write is still on one of the PR's rows after it, `=failed` allowed, and a
1672
- member name appears once on the line, whatever its outcome. A write that adds
1673
- a violation is refused, exit 2, nothing written; one already on the ledger and
1674
- carried forward is not. A second launch refused for an open one waits for
1675
- that one's `reviewed=`, or — once the Member-killed row confirms its runner
1676
- dead — settles it `=failed` in the same `review` call as the new launch.
1677
-
1678
- Plus two append-only lists:
1679
-
1680
- - **filed** (`ledger.mjs filed <issue> <subject>`, checked with `ledger.mjs check
1681
- <subject>` before every `gh issue create`) — so a finding already recorded
1682
- this run is not filed twice. That is not a guarantee against duplicates at
1683
- large; the tracker query below is what covers those. The FILER writes this
1684
- one, not you: `review-and-fix.md` step 5 and finisher duty 2 each run `filed`
1685
- immediately after their own `gh issue create`, with the subject they passed
1686
- to `check`, and report `filed: #N <subject>`. A filer whose `filed` failed
1687
- twice reports `unrecorded: #N <subject>` instead — an `unrecorded` entry of
1688
- its report file — and `settle <member> --from-report` runs `filed` for it.
1689
- - **ruled** (`ledger.mjs ruled <pr> <decision>`) — PR + decision + one-line reason,
1690
- so a replacement controller does not re-litigate a settled call.
1691
-
1692
- **Writer policy: the controller owns run state (`row`/`token`/`settle`/`dispatch`/`review`/`drain`/`ruled`); filers append filings with `filed`; all writes are serialized by `<file>.lock`.** See references/ledger.md.
1693
-
1694
- **The suite slots wedge the same ways, and clear the same way.**
1695
- `~/.fleet/suite-slots/<i>.lock` is built on the ledger's lock primitive, so an empty or
1696
- unparseable `<i>.lock` and a leftover `<i>.lock.reap` are permanent, a zombie
1697
- holder is transient, and a dead holder is taken over. A wait that expired with
1698
- exit 75 names each wedged file — remove it by hand once no suite run is
1699
- running, the way references/ledger.md removes `<file>.lock`.
1700
-
1701
- **The gap the lock does not close:** `check` → `gh issue create` → `filed` is three commands, and the lock covers each one alone, so two filers can both read `clean` in the seconds before either records. See references/ledger.md.
1702
-
1703
- `check` exits **0** clean, **1** already in this run's filed list, **2** usage error (no JSON on stdout), **3** the ledger is clean but open or closed tracker issues match — read those and decide. See references/ledger.md.
1704
-
1705
- The ledger half reports its own readability as its own field, `ledger.ok`, the
1706
- peer of `tracker.ok`: false when the file did not exist, and false too when the
1707
- path led to something that did not parse as a ledger — which is what tells a
1708
- ledger that was read and held nothing filed from one there was nothing to read. The absent case is the normal one for a run's first `check` — `.fleet/`
1709
- is git-ignored and created lazily by the first write — so it stays `clean` at
1710
- exit 0, and `ledger.ok` is the field that says why rather than the verdict
1711
- moving. The stderr warning is deliberately narrower than the field: it fires on
1712
- a path with no file, so a `--file` that landed on the wrong existing file is a
1713
- silent `ok:false` with nothing on stderr. Gate on the field, not on the warning.
1714
-
1715
- **Write the ledger line before dispatching, not after.** A member that dies
1716
- between spawn and write is invisible — and members die in batches.
1717
-
1718
1363
  ## Failure handling
1719
1364
 
1720
1365
  Match the failure to its row in the table below and run that response; where the row says report or flag, stop there.
@@ -1737,6 +1382,10 @@ Match the failure to its row in the table below and run that response; where the
1737
1382
  | 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 |
1738
1383
  | 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 |
1739
1384
  | 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 |
1385
+ | A second review launch refused for an open one | Wait for that one's `reviewed=`, or, once the **Member killed** row has confirmed its runner dead, settle it `=failed` in the same `ledger.mjs review` call as the new launch. |
1386
+ | `ledger.mjs settle <member> --from-report` exits 3 | A check failed: the member's claim and the world disagree, and nothing is written. You rule. |
1387
+ | `ledger.mjs settle <member> --from-report` exits 2 | The report is absent or invalid, or a check could not look; nothing is written. A check that could not look is run again once `gh` and `git` answer. |
1388
+ | A suite wait exits 75 naming a wedged `~/.fleet/suite-slots/<i>.lock` | Remove each named file by hand once no suite run is running, the way references/ledger.md removes `<file>.lock`, then run the suite again. |
1740
1389
 
1741
1390
  ### Recovery
1742
1391
 
@@ -1768,10 +1417,6 @@ review-due. A
1768
1417
  runner member that is killed rather than failed follows **Failure
1769
1418
  handling** (fresh name, inherited state stated).
1770
1419
 
1771
- ## Guards
1772
-
1773
- 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.
1774
-
1775
1420
  **Verify every reported SHA.** `~/.fleet/bin/fleet-run verify-sha.sh
1776
1421
  <branch> <sha>` before enqueueing — a member can commit in a nested worktree,
1777
1422
  leaving the SHA on a stray branch while its report reads normally. Exit codes as
@@ -1787,14 +1432,7 @@ ticket over a failure a re-run would have cleared.
1787
1432
 
1788
1433
  **Never `--delete-branch` — the merge bot deletes the head branch itself.**
1789
1434
  `gh pr merge <n> --merge` alone: the flag's local cleanup errors on a `main`
1790
- held by another worktree, or strands the feature worktree on `main`. After the
1791
- merge, `delete-merged-branch.sh <pr>` deletes the branch from `origin` and reads
1792
- the deletion back; `reap.sh` takes the `[gone]` local branch and its worktree.
1793
- No repo setting is relied on to do it. A branch another open PR still
1794
- uses as its head or base is kept instead — `branch-kept-#<that PR>` in the bot's
1795
- report, a deliberate keep and not a failure.
1796
- The branch-delete step also releases the claim label: it drops `in-progress` from
1797
- every issue the PR closes.
1435
+ held by another worktree, or strands the feature worktree on `main`.
1798
1436
 
1799
1437
  **Never force a rebase to start.** No `git clean`, `git checkout .`,
1800
1438
  `git reset --hard`, `git stash`. Uncommitted changes may exist nowhere else.
@@ -1804,14 +1442,6 @@ empty answer here and licenses the rebase over work this check exists to
1804
1442
  protect. Stashing empties it either way, so that check and
1805
1443
  `no-undo-audit.sh` both go quiet on work nothing else holds.
1806
1444
 
1807
- **Cross-check what members report about their environment.** Wrong often enough
1808
- to matter, and a confident wrong report from a reviewer flips a verdict.
1809
-
1810
- **Distrust negative claims hardest.** "Nothing else references this", "the sweep
1811
- is clean" — most likely false, least likely checked: a grep that found nothing
1812
- looks like a grep never run. Make members state their search scope. Negative claim
1813
- vs specific finding with paths → paths win.
1814
-
1815
1445
  **Members share one filesystem, one docker stack and one process table**, and the
1816
1446
  failures arrive as *wrong findings*, not errors:
1817
1447
 
@@ -1830,70 +1460,18 @@ failures arrive as *wrong findings*, not errors:
1830
1460
  and the controller re-checks any measurement taken in the window.
1831
1461
 
1832
1462
  **Do not guess the victim — the blast radius is the machine, not the Pull.**
1833
- In that incident the controller reasoned from dispatch scope to "almost
1834
- certainly `impl-<N>`" and told the maintainer so; the named member then proved
1835
- it was not the victim (it had never invoked those files standalone, and its
1836
- mutation baselines post-dated the window). Idle members of *previous* runs,
1837
- other omp sessions on the same machine, and the maintainer's own shell are
1838
- all `pkill -f` targets and none of them appear in your ledger. Warn every live
1463
+ Warn every live
1839
1464
  member, record the incident as unattributed, and re-check measurements by
1840
- timestamp rather than by who you think was running.
1465
+ timestamp rather than by who you think was running. See references/isolation.md.
1841
1466
 
1842
1467
  - **A warning acted on silently is indistinguishable from one that never
1843
- arrived.** The same incident: the controller's warning DID reach the member,
1844
- which re-ran, found nothing changed, and said nothing — correctly reading
1845
- "send once, never re-send" as covering it. The controller then had to spend a
1846
- round trip asking whether its own outbox was lossy. So the briefs say that
1847
- **confirmation state includes warnings received and acted on, even
1848
- when the outcome is no change** — one line, in the report the member already
1849
- owes. That is the cheap half of the *never read silence as assent* rule, paid
1850
- by the member rather than by the controller's guesswork.
1851
-
1852
- - **Specialist tree isolation is `review-and-fix.md`'s job — do not restate it.**
1853
- It owns the object-store rule, the two-tree split, snapshot provisioning, and
1854
- which suites a snapshot cannot validly run. Your only duty is to *not*
1855
- contradict it.
1856
- - **Filesystem isolation is not stack isolation.** A private copy of the
1857
- tree and a test command solve different problems; the compose project
1858
- name comes from the environment, not the working directory, so three
1859
- agents on three snapshots still collide on one postgres. "I'm on my own
1860
- copy" is exactly the intuition that skips the command — say both, every
1861
- time. Which command depends on the audience: a member in a worktree
1862
- uses `./agent-test`; a specialist on a snapshot does not, and takes the
1863
- one `review-and-fix.md` hands out. Which runner the worktree holds
1864
- decides why: the runner a claim writes is untracked, so a `git archive`
1865
- snapshot never carries it; one the repository tracks does ride along,
1866
- but no claim exported ports into it — its isolation is whatever that
1867
- repository built, not one triple per snapshot. See
1868
- references/isolation.md.
1468
+ arrived.** See references/isolation.md.
1469
+ - **Specialist tree isolation is `review-and-fix.md`'s job — do not restate it.** See references/isolation.md.
1470
+ - **Filesystem isolation is not stack isolation.** See references/isolation.md.
1869
1471
  - **Scratchpad paths need two levels — the member's own partition, then one
1870
- directory per child it dispatches — and nothing outside them.** An
1871
- implementer partitions at `<scratch>/impl-<N>/`; the review side's partition
1872
- is `<scratch>/pr<N>/…`, which the review core creates itself, so a review
1873
- runner is handed the scratch root, never that partition; any member that
1874
- dispatches a child writes an absolute directory under its own partition into
1875
- that child's prompt, one per child and never shared, and a child never
1876
- derives its own. Finding ids restart at 1 every review, so two fix-appliers
1877
- on different PRs both reach for `unv1`; one
1878
- agent overwrote a sibling's `package.json`, and a probe built a git repo at
1879
- the *checkout root*. Read from the object store at a pinned ref, write only
1880
- under your own path. See references/isolation.md.
1881
- - **IDE/harness diagnostics attribute by bare filename, with no path.** **Never
1882
- relay a diagnostic without reproducing it in that member's specific worktree**
1883
- (re-run the check that raised it from there): probe copies carry the real
1884
- tree's filenames, so a sibling's throwaway mutation reads exactly like a live
1885
- worktree's error.
1886
- See references/isolation.md.
1887
- - **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.
1888
- - **The settling command for "which files read X" must never be a literal-path
1889
- grep.** Replace the file with `MUTATED` in a throwaway copy, run the suite, and read off
1890
- which suites red: that answers which ones **depend on its contents**, which is
1891
- the question a coverage claim actually needs, and it returns a different set
1892
- from any spelling of the grep. See references/correction-tickets.md.
1893
- - **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.
1894
-
1895
- Suspect a neighbour before a member's own diff — for unexplained failures, and
1896
- equally for any **finding** that came from reading source.
1472
+ directory per child it dispatches — and nothing outside them.** See references/isolation.md.
1473
+ - **Never relay a diagnostic without reproducing it in that member's specific worktree**
1474
+ (re-run the check that raised it from there). See references/isolation.md.
1897
1475
 
1898
1476
  ## Red flags
1899
1477
 
@@ -1942,3 +1520,15 @@ One running table, updated as events land:
1942
1520
  ```
1943
1521
 
1944
1522
  Plus a queue-depth line: shortlist, supply, whether triage was suggested.
1523
+
1524
+ **Do not read a cell early.** A cell's
1525
+ comparison is one session holding an admissible row at that cell — its tier
1526
+ check passed and its resolved effort is the cell's level — and one at
1527
+ `slow-high` whose resolved model and effort differ. Count them per cell, never
1528
+ pooled across cells, and stop until
1529
+ there are at least ten of them across five or more distinct `run_date`s; below
1530
+ that, a comparison count is a number, not evidence. **Report the count the per-cell readout prints,
1531
+ never one from any other query:** `~/.fleet/bin/fleet-run cell-readout.mjs`
1532
+ prints `<cell> <comparisons> <run_dates> <resolved models>` for each cell past
1533
+ that floor and a cell below it only as a count on stderr; a `mixed (…)` models
1534
+ column means the cell's history spans more than one model behind its role. See references/pull.md.
@@ -1,6 +1,6 @@
1
1
  # Isolation: filesystem vs stack
2
2
 
3
- Why isolation envelope file not briefing, why private filesystem not private stack. Assertions these justify live in references/pull.md Claiming section and SKILL.md Guards section; evidence here.
3
+ Why isolation envelope file not briefing, why private filesystem not private stack. Assertions these justify live in references/pull.md Claiming section and SKILL.md Failure handling section; evidence here.
4
4
 
5
5
  ## Materialize the isolation envelope as a file, not a briefing
6
6
 
@@ -262,3 +262,29 @@ get is not yours to choose — members are briefed on `./agent-test` with the
262
262
  arguments the Test entrypoint takes, and the runner hands them on exactly as
263
263
  its caller's shell expanded them. Clearing your own leftover process needs its
264
264
  PID, not a pattern.
265
+
266
+ ## Do not guess the victim — the blast radius is the machine, not the Pull
267
+
268
+ In the `pkill -f` incident "A pattern kill reaches siblings" measures, the controller reasoned from dispatch scope to "almost
269
+ certainly `impl-<N>`" and told the maintainer so; the named member then proved
270
+ it was not the victim (it had never invoked those files standalone, and its
271
+ mutation baselines post-dated the window). Idle members of *previous* runs,
272
+ other omp sessions on the same machine, and the maintainer's own shell are
273
+ all `pkill -f` targets and none of them appear in your ledger.
274
+
275
+ ## A warning acted on silently is indistinguishable from one that never arrived
276
+
277
+ The same `pkill -f` incident: the controller's warning DID reach the member,
278
+ which re-ran, found nothing changed, and said nothing — correctly reading
279
+ "send once, never re-send" as covering it. The controller then had to spend a
280
+ round trip asking whether its own outbox was lossy. So the briefs say that
281
+ **confirmation state includes warnings received and acted on, even
282
+ when the outcome is no change** — one line, in the report the member already
283
+ owes. That is the cheap half of the *never read silence as assent* rule, paid
284
+ by the member rather than by the controller's guesswork.
285
+
286
+ ## Specialist tree isolation is `review-and-fix.md`'s job
287
+
288
+ It owns the object-store rule, the two-tree split, snapshot provisioning, and
289
+ which suites a snapshot cannot validly run. Your only duty is to *not*
290
+ contradict it.
@@ -1,6 +1,6 @@
1
1
  # The run ledger: the evidence behind its rules
2
2
 
3
- Evidence for the rules SKILL.md states in **Run ledger**, keyed by the rule each justifies.
3
+ Evidence for the rules SKILL.md's wake and dispatch sections state, keyed by the rule each justifies.
4
4
 
5
5
  ## Writer policy: every write is serialized by `<file>.lock`
6
6
 
@@ -55,3 +55,49 @@ them before filing. The stdout JSON names all of that in one field:
55
55
  `unverified`. Only `already-filed` and `tracker-hit` carry a non-zero code, so
56
56
  `verdict` is the only thing that tells a searched-and-clean tracker from one
57
57
  that was never read and from one whose rows you have to read yourself.
58
+
59
+ ## Why the ledger outlives your context
60
+
61
+ One git-ignored `.fleet/ledger.md` per run is updated at every state change through the `~/.fleet/bin/fleet-run ledger.mjs` subcommands (`row`, `filed`, `ruled`, `check`, `read`, `dispatch`, `settle`, `review`, `drain`, `rotate`). Your context is the least durable thing in the run: it compacts, and a controller that loses the dispatch map or the filed list redoes finished work. Two duplicate tickets shipped in one run from exactly that.
62
+
63
+ ## One line per ticket
64
+
65
+ One line per ticket, rewritten in place (`ledger.mjs row <ticket> <text>`; `ledger.mjs token <key> <token>…` adds a token and keeps every token already on the row):
66
+
67
+ ```
68
+ #<t1> impl-<t1>=PR#<p1> → PR#<p1> → MERGED 73b356de
69
+ #<t2> impl-<t2>=PR#<p2> → PR#<p2> · fix-pr-<p2> · ports=16<t2> · ruled:6-applies · held-behind:#<t3>
70
+ #<t3> impl-<t3> · tier=task-high
71
+ #<t4> excluded · behind-pr:#<p2>
72
+ #<t5> excluded · behind-issue:#<t3>
73
+ ```
74
+
75
+ ## Writing before dispatch
76
+
77
+ A member that dies between spawn and write is invisible — and members die in batches.
78
+
79
+ ## filed and ruled
80
+
81
+ `filed` is checked with `ledger.mjs check <subject>` before every `gh issue create`, so a finding already recorded this run is not filed twice. That is not a guarantee against duplicates at large; the tracker query, described under `check`'s exits and its `verdict` above, is what covers those.
82
+
83
+ `ruled` records PR + decision + one-line reason, so a replacement controller does not re-litigate a settled call.
84
+
85
+ ## Label-off and conflict holds
86
+
87
+ `label-off=<finisher-pr-M[-x]>` is written before you take `ready-to-merge` off PR M on purpose (taking it off before you approve a push, or clearing one left on a moved head), naming that PR's latest finisher attempt: the tick reads a missing label as a finisher's miss only after the last attempt a `label-off=` names, so without it your own removal reads as one.
88
+
89
+ `ci=<run-id>:<attempt>:<conclusion>` and `held-behind:#<lower>` are row tokens the same way, and so is `conflict-hold:#<pr>` — written by the merge bot itself (`run-merge-bot.md` step 1), or by you off the tick's `CONFLICT` line, naming the row's own PR. It is not an Exclusion: that gates a ticket's claim, this a PR's merge.
90
+
91
+ ## The review pair on a PR's row
92
+
93
+ A PR's review is not a member, so it carries its own pair of row tokens, written with `review` onto the PR's row (**The reviewer**): `review=wf:<runId>` | `review=member:<name>` at launch, `<name>` being `review-pr-<n>` for the PR's first runner and the next letter its rows do not carry yet (`review-pr-<n>-b`, `-c`, …) for every later one, settled dead as `…=failed`, then `reviewed=<head>:<survived>/<refuted>/<unverified>:<run>` when the result lands, `<run>` the review's `run-XXXXXXXX` run root under `<scratch>/pr<n>/`, which holds its `review.json`.
94
+
95
+ ## The ledger's readability field: `ledger.ok`
96
+
97
+ The ledger half reports its own readability as its own field, `ledger.ok`, the peer of `tracker.ok`: false when the file did not exist, and false too when the path led to something that did not parse as a ledger — which is what tells a ledger that was read and held nothing filed from one there was nothing to read. The absent case is the normal one for a run's first `check` — `.fleet/` is git-ignored and created lazily by the first write — so it stays `clean` at exit 0, and `ledger.ok` is the field that says why rather than the verdict moving. The stderr warning is deliberately narrower than the field: it fires on a path with no file, so a `--file` that landed on the wrong existing file is a silent `ok:false` with nothing on stderr.
98
+
99
+ ## Suite slots
100
+
101
+ **The suite slots wedge the same ways, and clear the same way.**
102
+
103
+ `~/.fleet/suite-slots/<i>.lock` is built on the ledger's lock primitive, so an empty or unparseable `<i>.lock` and a leftover `<i>.lock.reap` are permanent, a zombie holder is transient, and a dead holder is taken over. A wait that expired with exit 75 names each wedged file — remove it by hand once no suite run is running, the way the Writer policy section above removes `<file>.lock`.
@@ -426,6 +426,71 @@ is not enough either: the two rows must actually have RUN a different model or
426
426
  effort, because a deliberate dispatch whose two sides resolve to the same model
427
427
  (a `modelRoles` override, measured on omp 2026-09-12) controls nothing.
428
428
 
429
+ ## Accumulate per PR, never conclude a tier result inside one run
430
+
431
+ The unit is the PR —
432
+ supply is one Pull per free slot, so there are no implementer batches. The
433
+ guard this section speaks of is SKILL.md's **Guard: accumulate per PR, never
434
+ conclude inside one run**, under **Reading a review result**.
435
+
436
+ **Why not decide inside one run.** A run holds 2-3 implementer PRs, and ticket
437
+ difficulty swamps the tier effect — a gojq parity harness and a two-statement
438
+ shell reorder are not comparable units. Finding-counts are not comparable either:
439
+ `selectDimensions` trims the fan-out by diff profile, so one PR fields 29 agents
440
+ over six dimensions and its sibling fields 8 over three. And the fleet is not
441
+ stationary — this prompt gets edited mid-run when a defect earns it. Hence a
442
+ binary outcome per PR, accumulating, rather than a per-run verdict.
443
+
444
+ **Why the rows this guard fired on could not answer the tier question, and this
445
+ is the load-bearing part.** Until the 2026-08-16 revert, `class` and `tier` were
446
+ perfectly confounded: routine always ran `sonnet`, correction always ran top
447
+ tier. So the comparison the file invites — routine-at-sonnet against
448
+ correction-at-top-tier — measures **class, not tier**, because those classes fail
449
+ in different ways by construction: corrections ship false claims in prose,
450
+ routines ship incomplete or regressing code. A run that "mixes both" does not fix
451
+ it. **Only a same-class comparison across tiers is informative**, and the old
452
+ rule could not produce one, since no routine ticket ever ran at top tier.
453
+
454
+ **That last sentence is no longer true, and this is the part to re-read.** Since
455
+ the revert every routine ticket runs at top tier, so the file now holds 55
456
+ `routine`/`opus` rows against 11 `routine`/`sonnet` (as of 2026-08-28, 103 rows
457
+ — recount before citing). The same-class comparison the paragraph above calls
458
+ the only informative one therefore EXISTS now. It is still not clean: the two
459
+ groups are split almost entirely by calendar date, so it measures prompt
460
+ evolution at least as much as tier. Recount
461
+ both, and every other figure in this section, with:
462
+
463
+ ```bash
464
+ grep -vc '^#' docs/metrics/tier-outcomes.tsv
465
+ awk -F'\t' '!/^#/ && $4=="routine" {n++; d[$1]=1; if($6=="no") no++} \
466
+ END{print n, length(d), no+0}' docs/metrics/tier-outcomes.tsv
467
+ awk -F'\t' '!/^#/ && $4=="routine" {t[$5]++; if($6=="no") f[$5]++} \
468
+ END{for(k in t) print k, t[k], f[k]+0}' docs/metrics/tier-outcomes.tsv
469
+ ```
470
+
471
+ **These rows are the historical corpus, and nothing appended to them fixes the
472
+ confound.** What fixes it is the exploration draw in the dispatch rule above:
473
+ one implementer in every five — every one, during burn-in — runs at a drawn
474
+ cell beside the policy cell's, against the same prompts on the same day, so
475
+ the comparison stops depending on which tier history happened to leave in
476
+ which week. Read each cell's comparisons, not the whole-file split, once there
477
+ are enough of them.
478
+
479
+ **So read what the guard actually established: routine tickets sometimes fail to
480
+ close their own ticket. Not that `sonnet` caused it.** The revert is the
481
+ pre-committed rule being honoured, not a measurement. Restoring a cheap tier is
482
+ still the maintainer's call and still a change to the dispatch rule — but the
483
+ deliberate control it used to require is no longer something anyone has to
484
+ authorize one ticket at a time: the exploration draw above produces one in
485
+ every five Pulls by construction.
486
+
487
+ The last guard fired with n=1 on the control side. The pre-cell pair query
488
+ counted pairs that are no cell's comparisons, and the query before that one
489
+ counted every session whose implementers merely differed, so it cleared the
490
+ cell floor SKILL.md's **Report** states — at least ten comparisons across five
491
+ or more distinct `run_date`s — by an order of magnitude while the controlled
492
+ comparison did not exist yet.
493
+
429
494
  ## Why the implementer body carries enumerate-and-declare
430
495
 
431
496
  Why the agent body carries what it does — read this before editing any
@@ -27,6 +27,10 @@ in-between width cannot be told apart from a shifted one. The `note` field is
27
27
  free text and sits before those four, so the script refuses a tab or newline in
28
28
  it: one tab would shift all four for that row alone.
29
29
 
30
+ **Phase 0 does not size anything** — it shortlists, a Pull claims, and
31
+ the sizing run happens inside the member after both, which is why the verdict
32
+ has to travel in the member's report rather than being something you already hold.
33
+
30
34
  ## Know the trim before you rely on it
31
35
 
32
36
  **Know the trim before you rely on it — it is wider than `docsOnly` suggests.**
@@ -135,3 +139,12 @@ reviewed and is not.
135
139
  ## The review's scratch is the scratch root, not its partition
136
140
 
137
141
  **`scratch` is the scratch root itself, the absolute path `scratch-root.mjs` prints, never the review side's `<scratch>/pr<N>` partition:** the review creates `pr<N>/` under it, so a `pr<N>` passed in nests every run root at `pr<N>/pr<N>/`, and the review refuses a `scratch` whose last component is `pr` plus digits before dispatching anything.
142
+
143
+ ## Cross-check what members report, and distrust negative claims hardest
144
+
145
+ Wrong often enough
146
+ to matter, and a confident wrong report from a reviewer flips a verdict.
147
+
148
+ "Nothing else references this", "the sweep
149
+ is clean" — most likely false, least likely checked: a grep that found nothing
150
+ looks like a grep never run.
@@ -14,7 +14,7 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
14
14
  lifted), the implementer row's `unclaimed=` prints that same
15
15
  `<u>`, not the refreshed file's count.
16
16
  - `SUGGEST /triage, hold idle` — nothing is admissible after the refresh.
17
- Suggest, never run (**Queue depth**). A head whose Pull already ended in
17
+ Suggest, never run. A head whose Pull already ended in
18
18
  this run is not counted toward the refresh, so a shortlist of only such
19
19
  heads triggers the refresh before this row prints. It is not printed while the
20
20
  refreshed shortlist holds a head whose Pull ended only on an exclusion whose
@@ -30,7 +30,7 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
30
30
  (**The reviewer**).
31
31
  - `DISPATCH merge-bot` — the next `merge-bot-<n>` (**The merge bot**). The `<scratch>` its brief takes is the absolute `scratch-root.mjs` output, and the bot writes only under `<scratch>/pr<N>/merge-bot-<n>/`.
32
32
  - `DISPATCH finisher PR#<M> …` — a finisher settled `labelled`, yet the open
33
- list shows PR M without `ready-to-merge`, and no `label-off=` (**Run ledger**)
33
+ list shows PR M without `ready-to-merge`, and no `label-off=` (references/ledger.md)
34
34
  accounts for the removal. Apply the finisher gate (**The finisher gate**: the
35
35
  `check` job green on the current head), then dispatch the next-suffix
36
36
  finisher at that head, one letter past the highest the PR has used: render
@@ -245,3 +245,11 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
245
245
  could-not-look (exit 2), never a mismatch. The settle runs neither command,
246
246
  and a repository whose Recipe cache is absent or refused has no gate; the
247
247
  record is the member's own word, at the trust level of its report.
248
+
249
+ ## Read the depth off the tick, never derive it by hand
250
+
251
+ **Do not re-derive this by hand — `fleet-tick.mjs` computes it.** The deficit is then *computed, not remembered*, which is
252
+ the whole point: a table you must remember to consult is one you will not
253
+ consult under a merge-side event storm, and that is how implementers reached
254
+ 0/target with ~57 `ready-for-agent` in supply while nobody
255
+ noticed.