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