@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.
@@ -1,8 +1,9 @@
1
1
  # CI reading and staleness
2
2
 
3
3
  Why green not verdict, why absence is its own verdict, why conclusion not
4
- stable, why behind-count only honest signal. Assertions live in SKILL.md
5
- Phase 3 event loop + Merge bot sections; evidence here.
4
+ stable, why behind-count only honest signal, why the tick runs on every wake
5
+ and why the beat is shaped as it is. Assertions live in SKILL.md's wake table,
6
+ **Nothing to do: arm the beat** and **Own the CI waits**; evidence here.
6
7
 
7
8
  ## Own the CI waits — members cannot hold across a run
8
9
 
@@ -13,6 +14,9 @@ latest runs, keyed `<run-id>:<attempt>:<conclusion>` so each terminal state
13
14
  fires once, emits behind-count with it: `success` on branch 8 behind not
14
15
  actionable, that distinction most of traffic.
15
16
 
17
+ Members are turn-based and cannot hold across a ten-minute
18
+ run — they rebase, push, stop.
19
+
16
20
  ## Run-binding: take the whole row from one read
17
21
 
18
22
  Take run id, head, conclusion from **one** `gh run list --json` row. Watcher
@@ -100,3 +104,33 @@ agent — measured twice, see **A member runs its test suite in the foreground**
100
104
  roughly two hours — and `run-merge-bot.md`'s CI gate states the general rule:
101
105
  "whatever wakes you is external and may never come". So hold the wait yourself,
102
106
  exactly the way that gate does:
107
+
108
+ ## Tick on every wake
109
+
110
+ **Why every wake.** A merge cascade is a firehose of merges, CI greens and
111
+ rebases that holds your attention on the merge side while the implementer side
112
+ drains to 0 and stays there — 0 implementers emit no completion event, so a
113
+ refill that waited for one would never fire. A tick on every wake you are
114
+ already handling is what makes that drain visible. **Every wake is still an
115
+ edge, so none covers a fully drained queue**: no members and no open PRs is no
116
+ event at all, and the tick goes unrun. That state is the heartbeat's.
117
+
118
+ ## The beat's ceiling is a real bound
119
+
120
+ The interval doubles while the fleet asks for nothing and stops at `--ceiling`,
121
+ so a quiet night costs ~26 wakes instead of ~96. **The ceiling is a real bound,
122
+ not a formality**: supply grows from OUTSIDE the fleet — a ticket you triage,
123
+ one a reviewer filed `needs-triage` included — and that arrival emits no
124
+ event either, so the ceiling is your worst-case latency for noticing it.
125
+ Do not raise it past 30 minutes.
126
+
127
+ ## When you stop on purpose, say why
128
+
129
+ The beat leaves a dated mark on every hold, so a run
130
+ that dies abruptly (a crashed harness, a closed terminal, an OOM kill) is
131
+ detected by its mark going stale. A run that stops KNOWINGLY can do better
132
+ than being detected, and nothing else can write the reason afterwards:
133
+
134
+ ```bash
135
+ ~/.fleet/bin/fleet-run fleet-heartbeat.mjs --stop "budget exhausted"
136
+ ```
@@ -1,6 +1,6 @@
1
1
  # The CI watcher: why its block is shaped the way it is
2
2
 
3
- Evidence for the rules SKILL.md states beside Phase 3's watcher block, keyed by the rule each justifies.
3
+ Evidence for the rules SKILL.md states beside its watcher block, under **Own the CI waits**, keyed by the rule each justifies.
4
4
 
5
5
  ## The pass is budgeted at 4 core REST units per listed PR
6
6
 
@@ -97,3 +97,42 @@ fixed interval stalls the fleet longer than the outage itself would have. A
97
97
  retry-and-wait loop is what turns a 60s outage into a stalled member. Report it,
98
98
  keep the local work moving (git, tests and mutation runs are all unaffected),
99
99
  re-probe.
100
+
101
+ ## A probe that cannot read is a transition event, not silence
102
+
103
+ The natural
104
+ watcher shape — `st=$(ci-state.mjs --pr "$pr" 2>/dev/null) || continue` — fails
105
+ closed correctly and then throws the failure away, so a rate-limited probe emits
106
+ NOTHING and "no event" becomes indistinguishable from "not green yet". Every
107
+ open PR stalls unlabelled while the run reads as merely quiet; observed live
108
+ twice.
109
+
110
+ ## It has to latch
111
+
112
+ An unlatched line at a 120s poll gets the Monitor
113
+ auto-stopped for volume during even a two-minute outage, reintroducing the same
114
+ blindness by another route.
115
+
116
+ ## Guard every probe, not just the ones with a verdict field
117
+
118
+ The block makes
119
+ several API calls, and `gh pr list` is the one that looks like
120
+ plumbing rather than a probe. A non-quota failure there (secondary/abuse limit,
121
+ 5xx, network blip, expired token) does not lower `.resources.core.remaining`, so
122
+ the budget gate clears, and an unguarded `for pr in $(gh pr list …)` then
123
+ iterates zero times over the empty substitution: no error, no latch, no DEGRADED
124
+ line — total silence per tick, which is exactly what this section exists to
125
+ remove.
126
+
127
+ ## A readable list can still be a short one
128
+
129
+ `gh pr list` stops at its
130
+ `--limit` — 30 when the flag is absent — with exit 0 and no warning, so a list
131
+ that fills its limit reads exactly like one that happens to be that long, and
132
+ every open PR past it goes unwatched without a line.
133
+
134
+ ## Judge `ci-state` on its payload, never its exit code
135
+
136
+ `not-green` is an
137
+ ordinary, frequent state that exits non-zero, so a watcher treating any non-zero
138
+ exit as an outage misfires constantly on PRs that are merely in progress.
@@ -61,3 +61,9 @@ benign-looking answer. Measured once: `grep -c 'settled by:\s+x'` answers
61
61
  an absence counts only after the same command, with the same flags, finds a
62
62
  line known to match: a command that cannot find a match settles nothing, and
63
63
  under `Verified:` its zero reads as a checked absence.
64
+
65
+ ## The settling command for "which files read X" is never a literal-path grep
66
+
67
+ The path is assembled in more than one spelling here, one of them behind
68
+ a local `read` helper, so a grep for the path string answers with the files that
69
+ merely *name* it — comments included — and misses the files that build it.
@@ -1,6 +1,6 @@
1
1
  # The finisher: why its gate and duties are shaped the way they are
2
2
 
3
- Evidence for the finisher rules SKILL.md states in **Reviewers**, keyed by the rule each justifies.
3
+ Evidence for the finisher rules the finisher's agent body, `agents/fleet-finisher.agent.md`, and SKILL.md's **The finisher gate** state, keyed by the rule each justifies.
4
4
 
5
5
  ## An unanswered question from the member is an outbox item
6
6
 
@@ -107,3 +107,15 @@ to the fleet's worktree home, so a tree under `<scratch>` is a `kept` entry
107
107
  it reports and never collects. No claim built the tree you added, so it has
108
108
  `./agent-test` only if the repository tracks one — run that or `<testCmd>`
109
109
  in it, per `references/isolation.md`, **A reused worktree may lack the runner**.
110
+
111
+ ## Take the label off before you approve any push
112
+
113
+ The label is a
114
+ verdict on the tree the finisher read, and a GitHub label does not follow the
115
+ branch — on one PR it survived a push and came to sit on a commit nobody had
116
+ audited. The token is how the tick tells your
117
+ removal from a finisher that never labelled — a tick run between a removal and
118
+ its token re-dispatches a finisher into your take-off window. The label goes
119
+ before the approval because removing the artifact the merge bot gates on is
120
+ the reliable stop, where messaging the bot races it, and a label has been
121
+ observed holding until after an abort message arrived.
@@ -1,10 +1,10 @@
1
1
  # Member lifecycle: naming, fresh context, recovery
2
2
 
3
- Why member name load-bearing, why every member single-use, how a member's job outcome (`completed`/`failed`/`cancelled`) and peer liveness (`running`/`idle`/`parked`) decide its recovery. Assertions these justify live in SKILL.md's "Rules that fail silently", Phase 3, Reviewers, Failure handling sections; evidence here.
3
+ Why member name load-bearing, why every member single-use, how a member's job outcome (`completed`/`failed`/`cancelled`) and peer liveness (`running`/`idle`/`parked`) decide its recovery. Assertions these justify live in SKILL.md's "Rules that fail silently", wake table, Reading a review result, Failure handling sections; evidence here.
4
4
 
5
5
  ## Every member is named
6
6
 
7
- The name is the ledger token and the hub address (`hub send <name>`, `agent://<name>`); omit it and the ledger cannot match the member. Names follow unit of work: `impl-<issue#>`, `fix-pr-<pr#>` (default path's applier), `review-pr-<pr#>` (the PR's first reviewer; every later one on the PR — failed-runner retry, past-pin re-review — takes the next letter the PR's row does not carry yet, `review-pr-<pr#>-b`, `-c`, …; a reviewer's launch and result are recorded on the PR's row with `ledger.mjs review <pr#>`), `finisher-pr-<pr#>` (Phase 3's finisher), `merge-bot-<n>` (one merge pass; `ledger.mjs dispatch merge-bot` numbers it from the ledger's `## Dispatched` list — a per-run counter, never a PR).
7
+ The name is the ledger token and the hub address (`hub send <name>`, `agent://<name>`); omit it and the ledger cannot match the member. Names follow unit of work: `impl-<issue#>`, `fix-pr-<pr#>` (default path's applier), `review-pr-<pr#>` (the PR's first reviewer; every later one on the PR — failed-runner retry, past-pin re-review — takes the next letter the PR's row does not carry yet, `review-pr-<pr#>-b`, `-c`, …; a reviewer's launch and result are recorded on the PR's row with `ledger.mjs review <pr#>`), `finisher-pr-<pr#>` (the finisher gate's finisher), `merge-bot-<n>` (one merge pass; `ledger.mjs dispatch merge-bot` numbers it from the ledger's `## Dispatched` list — a per-run counter, never a PR).
8
8
 
9
9
  ## The coordination contract
10
10
 
@@ -1,6 +1,6 @@
1
1
  # Landing a change on main
2
2
 
3
- Evidence for the rule SKILL.md states in **Fix the tooling mid-run**, keyed by the rule it justifies.
3
+ Evidence for the rules SKILL.md states in **Fix the tooling mid-run** and in the `ready-to-merge` wake, keyed by the rule each justifies.
4
4
 
5
5
  ## A mid-run tooling fix lands through a PR
6
6
 
@@ -12,3 +12,8 @@ end of a 7-PR run. A mid-run tooling fix goes through a PR like every other
12
12
  change, and that PR faces every required check any other PR faces — the set
13
13
  `.github/rulesets/main.json` declares — so it carries a release label too.
14
14
  Plan it as a PR the run's own merge queue lands, never as an edit-and-continue.
15
+
16
+ ## You own the merge watcher, not the bot
17
+
18
+ A dying member takes a watcher down with it
19
+ and the queue stops silently.
@@ -72,7 +72,7 @@ walks the run from "could not look" to "certified clean" in one step, so
72
72
  `--record` itself refuses over a baseline it cannot read, over one a newer
73
73
  plugin wrote, and on any git read that fails. The exceptions are an
74
74
  older-version baseline and a baseline whose commit is missing from the
75
- repository; Phase 3's tick lines say how each is cleared.
75
+ repository; the tick's rows say how each is cleared.
76
76
  `main-checkout.mjs --check` prints the same answer
77
77
  without a tick.
78
78
  On `MAIN-CHECKOUT-DIRTY` the maintainer may instead ignore a stray path that is untracked and new since the baseline: an entry in `.git/info/exclude` or in the global excludes file hides it from the check, and the hold clears on the next tick with no re-record. Ignoring never clears a tracked or staged file's change, which `git status` reports whatever the excludes say, nor a pattern that also matches a path the baseline holds, which then reads as changed. Ignoring is the maintainer's decision, never the controller's: the controller never writes an exclude or a `.gitignore` entry. Keep the pattern narrow, because an ignored path is invisible to this check, so a later member write that matches it is never seen.
@@ -273,15 +273,15 @@ Before the rotation, `phase0.mjs` puts the held-aside receipt (`phase0.previous.
273
273
  `~/.fleet/bin/fleet-run reclaim-scratch.mjs --root <scratch>
274
274
  --after-rotate --apply`, `<scratch>` the path `scratch-root.mjs` prints,
275
275
  so a run that merges nothing still reclaims the previous run's leftovers
276
- (**Reclaim scratch after each merge pass** says what it removes).
276
+ (`reclaim-scratch.mjs`'s header says what it removes).
277
277
  `--after-rotate` goes on this call always and on no other: rotate leaves
278
278
  no ledger behind when it carried nothing, and the flag reads that absence
279
279
  as "no live token" — rotate's own verdict — where without it the script
280
280
  refuses. A ledger rotate did write is read as usual. Record nothing on the
281
281
  ledger and act on nothing in its receipt.
282
282
 
283
- Without `--after-rotate` — the per-pass sweep **Reclaim scratch after each
284
- merge pass** runs — no ledger refuses the call (exit 2, nothing removed):
283
+ Without `--after-rotate` — the per-pass sweep `tick --apply` runs after each
284
+ merge pass — no ledger refuses the call (exit 2, nothing removed):
285
285
  mid-run there is always one, so its absence is a fault to look at, never
286
286
  permission. A `gh` that cannot answer keeps every PR- and ticket-keyed entry.
287
287
  It prints one JSON receipt — `removed`, `kept` and `failed`, each entry with
@@ -342,8 +342,8 @@ invalid — a command that no longer runs, or an Install step that changed
342
342
  the tree — is the one mid-run trigger: dispatch `derive-recipe` again
343
343
  exactly as above, then retry what refused. The Install step's refusals
344
344
  fire after the claim has labelled the ticket and created its worktree, so
345
- release that claim with `release-ticket.sh` first (**Release the claims
346
- that never became PRs**). A red suite never triggers it: a failing test is
345
+ release that claim with `release-ticket.sh` first (SKILL.md's Failure handling
346
+ row for a claim abandoned mid-run). A red suite never triggers it: a failing test is
347
347
  a finding, not a stale Recipe. **Stall:** `RECIPE NOT PROVEN` from any
348
348
  derivation, or a second invalid-cache refusal after this run's one
349
349
  re-derivation, halts the run — no Pull, no claim, no review dispatch — and
@@ -232,7 +232,7 @@ terminating in exactly one dispatch or an empty shortlist:
232
232
  `tier=<CELL>` exactly when `DRAW` is not `-`; then phase 2 at once: claim
233
233
  what you are about to dispatch; dispatch what you have just claimed. Router
234
234
  exit 2 → no row and no dispatch: release the claim (`release-ticket.sh`, SKILL.md's
235
- **Release the claims that never became PRs**) and emit an event, a probe that could not look — a table the router
235
+ Failure handling row for a claim abandoned mid-run) and emit an event, a probe that could not look — a table the router
236
236
  refuses refuses every Pull.
237
237
 
238
238
  **The Pull table** — every verdict a Pull can reach, and the one thing each
@@ -259,7 +259,7 @@ dispatch.
259
259
  ## Relabels and exclusions
260
260
 
261
261
  **A Pull never re-admits a ticket already relabelled.** The shortlist file can
262
- be older than a relabel — this run's own Pulls, or a demotion in phase 3 — so
262
+ be older than a relabel — this run's own Pulls, or a bail's relabel under **Relabel causes** — so
263
263
  read the ticket's labels in step 3's full read (`--json
264
264
  title,body,comments,labels`) and drop it when `ready-for-agent` is gone. A Pull
265
265
  that trusted a list built before the relabel landed would hand a ticket the
@@ -324,8 +324,8 @@ its re-derive-once rule.
324
324
 
325
325
  **Materialize the isolation envelope as a file, not a briefing.** The claim
326
326
  writes the runner, `agent-test`, into the fresh worktree it creates: an exec of
327
- the Recipe's Test entrypoint, through the suite-slot wrapper (see SKILL.md's **The
328
- suite slot bound**), that exports one port triple derived from `<N>`
327
+ the Recipe's Test entrypoint, through the suite-slot wrapper (see `suite-slot.mjs`'s
328
+ header), that exports one port triple derived from `<N>`
329
329
  (so collisions stay impossible), kept out of `git status` through the
330
330
  repository's exclude file. A repository that tracks its own `agent-test` keeps
331
331
  it byte-identical, with whatever isolation that repository built, and takes the
@@ -1,6 +1,6 @@
1
1
  # Reaping merged branches and worktrees
2
2
 
3
- Why reap runs after each merge pass, why `commit-commands:clean_gone` disqualified, why hand-rolled reap shaped this way (implemented by `reap.sh`), and why a claim that never became a PR needs a different script (`release-ticket.sh`). Assertions these justify live in SKILL.md's "Reap after each merge pass" and "Release the claims that never became PRs" sections; evidence here.
3
+ Why reap runs after each merge pass, why `commit-commands:clean_gone` disqualified, why hand-rolled reap shaped this way (implemented by `reap.sh`), and why a claim that never became a PR needs a different script (`release-ticket.sh`). Assertions these justify live in SKILL.md's **Merge-bot pass reports done** wake and its Failure handling row for a claim abandoned mid-run; evidence here.
4
4
 
5
5
  ## Reap after each merge pass, not once at the end
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Reviewers: the evidence behind the review-side rules
2
2
 
3
- Evidence for rules SKILL.md states in Phase 2 and **Reviewers**, keyed by the rule each justifies.
3
+ Evidence for rules SKILL.md states in Phase 2 and **Reading a review result**, keyed by the rule each justifies.
4
4
 
5
5
  ## A sizing verdict is a measurement only if it post-dates the run that produced it
6
6
 
@@ -96,3 +96,38 @@ with `--detach`, and there `git branch --show-current` prints nothing and exits
96
96
  --branch "$(git branch --show-current)"` is handed an empty branch and exits 2
97
97
  before pushing anything. A brief rendered without it would leave the push no
98
98
  destination at all, which is why the renderer requires it.
99
+
100
+ ## A review runs off your turn and relays nothing
101
+
102
+ **`selectDimensions` sizes the fan-out to the diff and the verify budget
103
+ follows severity.** It cuts one immutable
104
+ snapshot, verifies every critical/important finding adversarially,
105
+ re-dispatches each crashed specialist and each crashed refuter pair once before
106
+ it assembles the result, and has `agent()` return **into the script**, so no
107
+ report can go undelivered and you relay nothing — the delivery failure that
108
+ cost one fleet five reports on one PR and four on another. None of it runs on
109
+ your turn: a review takes 20-40 minutes, and a controller that held its turn
110
+ for each one, one at a time, was what left implementer slots empty.
111
+
112
+ ## `testEnvironment` says what the shared test run is evidence about
113
+
114
+ The snapshot is `git archive`d and then
115
+ `git init`ed with one commit, and the block that cuts it compares that commit's
116
+ tree hash against the reviewed commit's: verified, a suite run in the
117
+ snapshot collects and runs what a checkout does, and a red is a fact about the
118
+ tree. UNVERIFIED, it is not — tests that ask git what ships decline or fail for
119
+ the environment, and this field carries which line of the cut failed.
120
+
121
+ ## `dimensionsUnrun` names a gap; an empty dimension does not
122
+
123
+ The rule this replaces — treat any dimension with nothing in
124
+ `survived`/`refuted`/`unverified` as unrun — was the workaround for having no
125
+ such field, and it over-refuses in the direction that costs work: a dimension
126
+ that ran clean has nothing in those three either.
127
+
128
+ ## A failed review is a failure event, not a clean review
129
+
130
+ The review throws on missing `args.pr`/`args.worktree` and on a snapshot agent
131
+ that returned no tree, and a failure surfaces mid-loop, where "react, never
132
+ block" makes it easy to log and carry on — leaving a PR that *reads* as
133
+ reviewed and is not.
@@ -6,9 +6,9 @@ Monitor and Bash commands run under **zsh**, and the shell in SKILL.md is
6
6
  prescriptive — copied verbatim into a watcher or into the merge bot's agent
7
7
  body. So every trap here is a zsh-vs-bash difference that ships as a
8
8
  green-looking gate: correct in the shell you tested in, silently wrong in the
9
- one it runs in. Phase 3's monitor paragraph points here instead of restating
10
- any of it. The merge bot's agent body is the one place this block is copied
11
- whole (see **Merge bot**); the other deliberate copy is named at the end.
9
+ one it runs in. **Own the CI waits**'s monitor paragraph points here instead of
10
+ restating any of it. The merge bot's agent body is the one place this block is
11
+ copied whole (see **The merge bot**); the other deliberate copy is named at the end.
12
12
 
13
13
  **zsh word-splits an unquoted command substitution's result; it does NOT split an unquoted parameter expansion.**
14
14
 
@@ -26,12 +26,12 @@ $prs
26
26
  EOF
27
27
  ```
28
28
 
29
- The inline `$(printf '%s\n' "$prs")` form splits correctly too and is what Phase 3's block uses, but it splits on *every* whitespace character, so it is safe only for values that cannot contain one — PR numbers cannot, branch names and paths can.
29
+ The inline `$(printf '%s\n' "$prs")` form splits correctly too and is what the watcher block under **Own the CI waits** uses, but it splits on *every* whitespace character, so it is safe only for values that cannot contain one — PR numbers cannot, branch names and paths can.
30
30
 
31
31
  **`</dev/null` every command inside a `while read` loop.**
32
32
 
33
33
  **A probe that could not look emits an event; it never `continue`s.** Stated in
34
- full at *A probe that cannot read is a transition event, not silence* in Phase 3,
34
+ full at *A probe that cannot read is a transition event, not silence* under **Own the CI waits**,
35
35
  and it binds every watcher in SKILL.md rather than that one block: the third
36
36
  value beside pass and fail is what turns a broken loop into a loud failure
37
37
  instead of a quiet one. A guard that drops an unreadable probe — `[ -n "$json" ]
@@ -21,14 +21,14 @@ The full text of each row `fleet-tick.mjs` prints and what it asks of the contro
21
21
  filter kept out of `supply`, read off the shortlist's `dropped`. Never
22
22
  actionable. Log them as specs awaiting to-tickets (phase 0), not as no work.
23
23
  - `DISPATCH fix-pr PR#<M> …` — a fix-applier per PR, for a review's survived or unverified findings,
24
- a dispositions mismatch no retry has answered, or a `conflict-hold:#<M>` — the merge bot's, or one you recorded off a `CONFLICT` line (**Reviewers**). Printed
24
+ a dispositions mismatch no retry has answered, or a `conflict-hold:#<M>` — the merge bot's, or one you recorded off a `CONFLICT` line (**The fix-applier**). Printed
25
25
  ahead of reviews on purpose: finishing what is started beats starting more.
26
26
  - `DISPATCH review PR#<M> …` — a review per PR, oldest first, off your turn
27
- (**Reviewers**).
28
- - `DISPATCH merge-bot` — the next `merge-bot-<n>` (**Merge bot**).
27
+ (**The reviewer**).
28
+ - `DISPATCH merge-bot` — the next `merge-bot-<n>` (**The merge bot**).
29
29
  - `DISPATCH finisher PR#<M> …` — a finisher settled `labelled`, yet the open
30
30
  list shows PR M without `ready-to-merge`, and no `label-off=` (**Run ledger**)
31
- accounts for the removal. Apply the finisher gate (**Reviewers**: the
31
+ accounts for the removal. Apply the finisher gate (**The finisher gate**: the
32
32
  `check` job green on the current head), then dispatch the next-suffix
33
33
  finisher at that head, one letter past the highest the PR has used: render
34
34
  its brief with `dispatch-prompt.mjs render finisher --kind repair --member