@feigi/fleet-ctl 3.88.12 → 3.89.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.88.12",
3
+ "version": "3.89.0",
4
4
  "description": "Agent fleet: run-team controller, merge bot, PR reviewer, and the ticket pipeline they share",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -846,7 +846,7 @@ if (cmd === "read") {
846
846
  } else if (cmd === "check") {
847
847
  runCheck();
848
848
  } else if (cmd === "dispatch") {
849
- runDispatch();
849
+ await runDispatch();
850
850
  } else if (cmd === "settle") {
851
851
  if (FROM_REPORT) await runSettleFromReport();
852
852
  else runSettle();
@@ -1894,6 +1894,36 @@ function dispositionsRefusal(pr, review) {
1894
1894
  return null;
1895
1895
  }
1896
1896
 
1897
+ // Why PR `pr`'s finisher waits on its ruling, or null when it does not: the
1898
+ // PR is an implementer's — a settled `impl-<N>=PR#<pr>` token on a row names
1899
+ // it, the token tier-outcomes.mjs `append` resolves a ticket from — and the
1900
+ // main checkout's tier-outcomes.tsv has no row whose `pr` is it. Any row
1901
+ // passes, whatever its date or verdicts, so a replacement finisher is never
1902
+ // held once the first passed. The ledger lives in the main checkout's
1903
+ // `.fleet/` (fleet-dir.mjs), so the file is found beside that directory —
1904
+ // no git, no gh. A missing file is no row (`append` creates it); one that
1905
+ // cannot be read or parsed refuses, naming it, and is never a pass.
1906
+ // tier-outcomes.mjs is imported here, not at the top: its import graph is
1907
+ // several modules no other subcommand loads.
1908
+ async function rulingRefusal(pr) {
1909
+ const { parseTierOutcomes, implementerClaims } = await import("./tier-outcomes.mjs");
1910
+ const tickets = [...new Set(implementerClaims(data, pr).map((c) => c.ticket))];
1911
+ if (tickets.length === 0) return null;
1912
+ const tsv = join(dirname(dirname(resolve(file))), "docs", "metrics", "tier-outcomes.tsv");
1913
+ if (existsSync(tsv)) {
1914
+ let rows;
1915
+ try {
1916
+ rows = parseTierOutcomes(readFileSync(tsv, "utf8"));
1917
+ } catch (e) {
1918
+ return `cannot read the PR's ruling off ${tsv}: ${e.code ?? e.message} — fix the file, then dispatch the finisher again`;
1919
+ }
1920
+ if (rows.some((r) => r.pr === String(pr))) return null;
1921
+ }
1922
+ return `PR #${pr}, the implementer PR of ${tickets.length === 1 ? "ticket" : "tickets"} ${tickets.map((n) => `#${n}`).join(", ")}, has no ruling — `
1923
+ + `${tsv} has no row for it; rule its review with \`~/.fleet/bin/fleet-run tier-outcomes.mjs append ${pr} --closed-own-ticket yes|no `
1924
+ + `--minted-false-claim yes|no --note "<text>"\`, then dispatch the finisher again`;
1925
+ }
1926
+
1897
1927
  // The open `ready-to-merge` PRs, ascending, as gh answers from the ledger's
1898
1928
  // own directory with runCheck()'s environment: the queue a merge bot is
1899
1929
  // dispatched against, recorded on its `## Dispatched` entry. Anything but a
@@ -1925,7 +1955,7 @@ function queueSnapshot() {
1925
1955
  return [...new Set(prs)].sort((a, b) => a - b);
1926
1956
  }
1927
1957
 
1928
- function runDispatch() {
1958
+ async function runDispatch() {
1929
1959
  const usage = "usage: ledger.mjs dispatch <ticket|pr> <member>, or dispatch merge-bot";
1930
1960
  if (rest.length === 0 || rest.length > 2) die(usage);
1931
1961
  const [key, name] = rest.length === 2 ? rest : [null, rest[0]];
@@ -2047,8 +2077,11 @@ function runDispatch() {
2047
2077
  // run the tick reads — and no fix-applier on the PR is still live. A PR
2048
2078
  // whose latest review counts no survived and no unverified finding had
2049
2079
  // nothing for a fix-applier to rule on, and one never reviewed has no
2050
- // review file: neither is gated on a verdict. Refused here, before anything
2051
- // is written, so the check cannot be skipped by forgetting to run it.
2080
+ // review file: neither is gated on a verdict. Then an implementer's PR
2081
+ // waits on its ruling, its tier-outcomes row, written while the PR is open
2082
+ // and its review evidence still in scratch; see rulingRefusal(). Refused
2083
+ // here, before anything is written, so no check can be skipped by
2084
+ // forgetting to run it.
2052
2085
  if (member.family === "finisher-pr") {
2053
2086
  const fold = tickFold("conflict hold and dispositions verdict");
2054
2087
  if (fold.conflictHeld.includes(member.number)) {
@@ -2056,7 +2089,7 @@ function runDispatch() {
2056
2089
  + `and let it settle applied:/no-op, then dispatch the finisher again — not dispatching ${member.name}`);
2057
2090
  }
2058
2091
  const review = fold.reviewed.find((r) => r.pr === member.number);
2059
- const refusal = dispositionsRefusal(member.number, review);
2092
+ const refusal = dispositionsRefusal(member.number, review) ?? await rulingRefusal(member.number);
2060
2093
  if (refusal !== null) die(`${member.name}: ${refusal} — not dispatching ${member.name}`);
2061
2094
  }
2062
2095
  // Every error caught here carries its own cause and remedy — a row's `tier=`
@@ -495,7 +495,8 @@ function closingNumbers(pr) {
495
495
 
496
496
  // Every settled `impl-<N>=PR#<pr>` token in the run ledger's rows, each with the
497
497
  // row that carries it and its ticket N. The dispatched list is not read.
498
- function implementerClaims(ledger, pr) {
498
+ // Exported for ledger.mjs's finisher dispatch, which gates the same PRs.
499
+ export function implementerClaims(ledger, pr) {
499
500
  const claims = [];
500
501
  for (const text of ledger?.rows ?? []) {
501
502
  const key = text.split(/\s/)[0];
@@ -195,9 +195,9 @@ owes beyond its call:
195
195
  once the run is draining with no open PR and no live implementer.
196
196
  3. `~/.fleet/bin/fleet-run closeout.mjs finish --reason "<reason>"`, which
197
197
  then records `fleet-heartbeat.mjs --stop` last, because every reader takes
198
- a recorded stop as the end of the run. A `NEED tier-outcomes append
199
- <pr>` is a PR the ledger marks reviewed with no verdict row: append it with
200
- the verdict you rule (**Guard: accumulate per PR**) and run `finish` again.
198
+ a recorded stop as the end of the run. A `NEED tier-outcomes append <pr>`
199
+ is a reviewed PR with no verdict row — for an implementer's, one that never
200
+ reached its finisher: append it with the verdict you rule (**Guard: accumulate per PR**) and run `finish` again.
201
201
  A PR closing none or several: a row only if an impl token names it.
202
202
  A step it fails is explained in references/close-out.md. Exit 0 is the run
203
203
  closed.
@@ -280,10 +280,10 @@ digest asks of you:
280
280
  **A refutation resting on an injection nothing proved landed is not a refutation — it is a cell that never ran.** See references/reviewers.md.
281
281
 
282
282
  **Guard: accumulate per PR, never conclude inside one run.** **Append one row to
283
- `docs/metrics/tier-outcomes.tsv` when you rule each PR's review, with
283
+ `docs/metrics/tier-outcomes.tsv` when you rule each PR's review, before its finisher, with
284
284
  `~/.fleet/bin/fleet-run tier-outcomes.mjs append <pr> --closed-own-ticket yes|no --minted-false-claim yes|no --note "<text>"`,
285
285
  never by hand** (that file's header carries the column meanings). The script
286
- fills `tier` itself; never type one in. That append is the whole duty. See references/pull.md.
286
+ fills `tier` itself; never type one in. `ledger.mjs dispatch` refuses the finisher until the row exists. That append is the whole duty. See references/pull.md.
287
287
 
288
288
  **A verdict is a measurement only if it post-dates the run that produced it.** See references/reviewers.md.
289
289
 
@@ -520,7 +520,7 @@ Pointers: the `/resume` rules are in `references/phase-0.md`; the tick HOLD tier
520
520
 
521
521
  ## The finisher gate
522
522
 
523
- 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.
523
+ 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, no reply is owed and the PR's ruling is recorded; hold otherwise, and on a repo with no CI and no declaration report it and add no label.
524
524
 
525
525
  ### The tests
526
526
 
@@ -542,8 +542,8 @@ Dispatch a finisher only when every test here passes at once — the review retu
542
542
  by name. An empty outbox catches neither — *vacuously* empty on the
543
543
  implementer's firing, *genuinely* empty on a mid-work push. **The outbox is
544
544
  the last test, not the only one, and it stands wherever a fix-applier
545
- exists**, so never dispatch off it while you do owe it a ruling: empty it
546
- first — including any ruling you have withdrawn or reversed — then dispatch
545
+ exists**, so never dispatch off it while you do owe it a reply: empty it
546
+ first — including any reply you have withdrawn or reversed — then dispatch
547
547
  (below); a final report is not proof it stopped. A PR that needs nothing fixed
548
548
  reaches a finisher through the **fix-applier reports `no-op`** bullet below,
549
549
  never by waiting for a second CI event; a red that fired before the review
@@ -560,7 +560,7 @@ Dispatch a finisher only when every test here passes at once — the review retu
560
560
  itself, so nothing further is owed and no further event is coming. Edit either bullet without the other and what strands is exactly the PR that needed no fixes.
561
561
  - **`ci-state.mjs --pr <N>` reads `verdict: "no-ci"`** → the CI edge never
562
562
  fires: dispatch the finisher off the reviewer's final verdict the moment it
563
- lands, never while you still owe it a ruling. Its gate is then the
563
+ lands, never while you still owe it a reply. Its gate is then the
564
564
  `--declare-no-ci` declaration, not a `check` job: with it, label off the
565
565
  reviewer's verified suite run; **without it, do not label** — report that this
566
566
  repo has no CI configured and no declaration, and stop. See `review-and-fix.md` step 6.
@@ -593,9 +593,9 @@ with `dispatch-prompt.mjs render finisher --kind fresh --member finisher-pr-<pr#
593
593
  (`--kind repair` once an earlier finisher reported `labelled` without the label landing), record it with
594
594
  `ledger.mjs dispatch <pr#> finisher-pr-<pr#>` before the call, then **dispatch
595
595
  it with the `agent` that command printed and omit `model` on the call**. **Never dispatch
596
- one while you still owe the fix-applier a ruling. The test is your OUTBOX, not the
596
+ one while you still owe the fix-applier a reply. The test is your OUTBOX, not the
597
597
  member's last message** — anything you have decided that it has not received,
598
- including a ruling you have since **withdrawn or reversed**, a withdrawal
598
+ including a reply you have since **withdrawn or reversed**, a withdrawal
599
599
  recorded only in the ledger included. **That is the test of THIS condition
600
600
  only**: the report condition above is the other. Relay everything — reversals
601
601
  too — then dispatch. See references/finisher.md.
@@ -642,7 +642,7 @@ merge, and a finisher on it would race the conflict fix-applier rebasing it.
642
642
  Dispatch that fix-applier, let it settle `applied:`/`no-op`, then dispatch the
643
643
  finisher again.
644
644
 
645
- **An unanswered question from the member is an outbox item, and it blocks dispatch with the same weight as a ruling you have already made.** See references/finisher.md.
645
+ **An unanswered question from the member is an outbox item, and it blocks dispatch with the same weight as a reply you have already decided on.** See references/finisher.md.
646
646
 
647
647
  **A final report is not proof the member stopped.** **The standing CI facts are
648
648
  in the fix-applier body, not in per-event messages.**
@@ -22,8 +22,10 @@
22
22
  metrics, commits the three metrics files and opens their PR, sets the
23
23
  phase-0 receipt aside as `.fleet/phase0.previous.json`, then records
24
24
  `fleet-heartbeat.mjs --stop` last, because every reader takes a recorded
25
- stop as the end of the run. A `NEED tier-outcomes append <pr>` is a PR
26
- the ledger marks reviewed with no verdict row: append it with the verdict
25
+ stop as the end of the run. A `NEED tier-outcomes append <pr>` is a
26
+ reviewed PR with no verdict row — for an implementer's PR, one that never
27
+ reached its finisher, since `ledger.mjs dispatch` refuses that finisher
28
+ until the row exists: append it with the verdict
27
29
  you rule (**Guard: accumulate per PR**) and run `finish` again. A reviewed
28
30
  PR that closes no issue, or several, with no settled `impl-<N>=PR#<pr>`
29
31
  token naming it takes no row: the check names it in a `note` line and
@@ -28,10 +28,10 @@ Its four duties are a checklist — audit the
28
28
  worktree, confirm every deferral has a tracker home and re-run the acceptance
29
29
  mutation, apply one release label, report — and the
30
30
  merge gate downstream still catches whatever it gets wrong. **Never dispatch
31
- one while you still owe the fix-applier a ruling**: the pinned SHA is only as good
31
+ one while you still owe the fix-applier a reply**: the pinned SHA is only as good
32
32
  as the guarantee nothing else is inbound. **The test is your OUTBOX, not the
33
33
  member's last message** — anything you have decided that it has not received,
34
- including a ruling you have since **withdrawn or reversed**. **That is the test
34
+ including a reply you have since **withdrawn or reversed**. **That is the test
35
35
  of THIS condition only**: an empty outbox says nothing is inbound from you, and
36
36
  nothing about whether the member considers itself finished, which is what the
37
37
  report condition above is for. A withdrawal you
@@ -71,9 +71,9 @@ and will act on it. Relay everything — reversals too — then dispatch.
71
71
  anything to, so the outbox is *vacuously* empty, and on a fix-applier's
72
72
  mid-work push nothing has been sent since the dispatch, so it is *genuinely*
73
73
  empty. **The outbox is the last test, not the only one, and it stands wherever
74
- a fix-applier exists** — that is exactly where a ruling you still owe it is
74
+ a fix-applier exists** — that is exactly where a reply you still owe it is
75
75
  outstanding, so never dispatch off it while you do. Empty your outbox to it
76
- first — including any ruling you have withdrawn or reversed — then dispatch
76
+ first — including any reply you have withdrawn or reversed — then dispatch
77
77
  (*The fix-applier pushes and exits; the finisher is dispatched fresh*); a final report is not proof it stopped. A PR that needs nothing fixed
78
78
  reaches a finisher through the **fix-applier reports `no-op`** bullet below,
79
79
  never by waiting for a second CI event this head will never produce. The
@@ -102,12 +102,21 @@ and will act on it. Relay everything — reversals too — then dispatch.
102
102
  waiting for it stalls the whole PR — the same silent-stall shape reported
103
103
  before this verdict existed. Dispatch the finisher off the reviewer's final
104
104
  verdict instead, the moment it lands — and, same as above, never while you still
105
- owe it a ruling — or hold anything else it has not received, a ruling you have
105
+ owe it a reply — or hold anything else it has not received, a reply you have
106
106
  withdrawn or reversed included. The finisher's gate is then the
107
107
  `--declare-no-ci` declaration, not a `check` job: with it, label off the
108
108
  reviewer's verified suite run; **without it, do not label** — report that this
109
109
  repo has no CI configured and no declaration, and stop. Absence never reads as
110
110
  pass. See `review-and-fix.md` step 6.
111
+ - **The PR's ruling is recorded** → an implementer's PR, one a settled
112
+ `impl-<N>=PR#<M>` token on the ledger names, gets its finisher only once the
113
+ main checkout's `docs/metrics/tier-outcomes.tsv` has a row for it, appended
114
+ when you rule its review (SKILL.md's **Guard: accumulate per PR**).
115
+ `ledger.mjs dispatch` refuses the finisher until that row exists, after its
116
+ conflict-hold and dispositions refusals, and prints the `tier-outcomes.mjs
117
+ append` command to run. Any row for the PR passes, so a replacement finisher
118
+ is never held once the first passed; a PR no such token names is not gated.
119
+ No flag skips it: a PR `append` refuses is fixed on the ledger, as at close-out.
111
120
 
112
121
  Gate on the `check` job, **not** on `ci-state --quiet` exit 0: a behind PR never
113
122
  reaches full green, so an exit-0 gate strands it unlabelled. The finisher reads
@@ -138,7 +147,7 @@ no verified suite run on record` and add no label.
138
147
  ## An unanswered question from the member is an outbox item
139
148
 
140
149
  **An unanswered question from the member is an outbox item, and it blocks
141
- dispatch with the same weight as a ruling you have already made.** It does not
150
+ dispatch with the same weight as a reply you have already decided on.** It does not
142
151
  feel like one — you have decided nothing yet, so the outbox reads empty — but a
143
152
  member waiting on you is a member that will push once you answer, and the SHA
144
153
  you are about to pin is exactly what that push moves. Measured on one run: