@feigi/fleet-ctl 3.18.10 → 3.18.12

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.
@@ -6,20 +6,20 @@
6
6
  //
7
7
  // Rows are rewritten in place, one per ticket. `filed` and `ruled` are
8
8
  // append-only, because their whole purpose is to outlive the reasoning that
9
- // produced them; `filed` appends at most one row per issue (#2087).
9
+ // produced them; `filed` appends at most one row per issue.
10
10
  //
11
- // `dispatch`, `settle` and `drain` (#1799) exist so a reader can derive every
11
+ // `dispatch`, `settle` and `drain` exist so a reader can derive every
12
12
  // liveness count from this file alone, rather than from row text a controller
13
13
  // typed by hand: `dispatch` writes a member's live token onto its row and
14
14
  // appends it to `## Dispatched`, `settle` rewrites that token to
15
15
  // `<member>=<outcome>` in both places (an implementer's settle to `PR#M` also
16
- // folds the PR's own `#M` row into its ticket's, #1876), and `drain` writes the
16
+ // folds the PR's own `#M` row into its ticket's), and `drain` writes the
17
17
  // one marker that stops supply. The token grammar is
18
18
  // ledger-grammar.mjs's. `## Dispatched` gains an entry per dispatch and never
19
19
  // loses or reorders one — settling annotates an entry in place — which is what
20
20
  // lets `merge-bot-<n>` be counted from it.
21
21
  //
22
- // Writer policy (#531, reversing #151's item #7): the controller owns run
22
+ // Writer policy: the controller owns run
23
23
  // state (`row`/`settle`/`dispatch`/`drain`/`ruled`/`rotate`); filers append
24
24
  // filings with `filed`; all writes are serialized by `<file>.lock`. Every
25
25
  // write subcommand except `rotate` — which moves the file whole and never
@@ -45,24 +45,24 @@ const NAME = "ledger";
45
45
  const AGENTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
46
46
 
47
47
  // die() shared with the other fleet scripts (writeSync-based, pipe-safe —
48
- // see arg.mjs for the #176/#328/#363 rationale). arg()/has() themselves are
48
+ // see arg.mjs for the rationale). arg()/has() themselves are
49
49
  // NOT shared here: this file splices its flags out of argv and refuses in its
50
- // own wording (#362), where arg() refuses under a message generated from the
50
+ // own wording, where arg() refuses under a message generated from the
51
51
  // flag name.
52
52
  //
53
- // Their RULES are shared, which is a different thing (#567). isFlagLike() and
53
+ // Their RULES are shared, which is a different thing. isFlagLike() and
54
54
  // hasEqualsForm() are arg.mjs's refusal rules as exported predicates; the
55
55
  // three guards below call them and supply their own die() text. This file
56
56
  // used to restate the expressions instead, and nothing failed when a
57
- // restatement drifted from the original — shared-refusal.test.mjs is what
58
- // fails now, and it reds even on a re-inlined copy that changes no behaviour
57
+ // restatement drifted from the original — a test over the shared source is
58
+ // what fails now, and it reds even on a re-inlined copy that changes no behaviour
59
59
  // at all, which is the only kind a behavioural test cannot see.
60
60
  const die = makeDie(NAME);
61
61
 
62
62
  // The cause of a failed child process, for `check`'s repository probe and its
63
63
  // tracker query — each captures the child's stderr instead of forwarding it, so
64
64
  // what the child printed reaches no terminal and this string is the only copy
65
- // of it (#638).
65
+ // of it.
66
66
  //
67
67
  // Trim BEFORE choosing, not after. A whitespace-only stderr is truthy, so it
68
68
  // wins a choice made on the raw values and then trims away to nothing, leaving
@@ -87,7 +87,7 @@ function cause(...candidates) {
87
87
 
88
88
  // The budget every `git` child in this file gets, in milliseconds.
89
89
  //
90
- // Both git probes ran UNBOUNDED before #1199, while the `gh` query between
90
+ // Both git probes ran UNBOUNDED before they were bounded, while the `gh` query between
91
91
  // them carried 20 s. That asymmetry was the defect: measured with a `git` that
92
92
  // answers and then never returns, `check` — the fleet's pre-filing duplicate
93
93
  // guard — was still running at 60 s having written nothing at all to stdout,
@@ -102,8 +102,8 @@ function cause(...candidates) {
102
102
  // p50 11-15 ms and max 24.7 ms, so this is ~400x the worst latency observed
103
103
  // under load. Generous on purpose, because a probe killed while HEALTHY does
104
104
  // not fail loudly here — defaultLedgerPath() degrades to a cwd-relative
105
- // ledger and only warns (a pre-existing fallback #155 leaves alone, not
106
- // something it introduces). It also stays under the `gh` bound beside it —
105
+ // ledger and only warns (a fallback that predates this budget, not
106
+ // something the budget introduces). It also stays under the `gh` bound beside it —
107
107
  // though "dominant" overstates that relationship: a normal invocation
108
108
  // without `--file` pays this same budget TWICE in sequence, once here and
109
109
  // once more in repoCheck() below, before ever reaching `gh` at all. At
@@ -135,14 +135,14 @@ const GIT_TIMEOUT_MS = gitBudget(10, process.env.LEDGER_GIT_TIMEOUT);
135
135
  // is precisely the duplicate-filing guard failing open. Resolve against the
136
136
  // git COMMON dir (shared by every worktree) rather than the cwd, via
137
137
  // git-env.mjs's workspaceDirFromGitCommonDir() — the resolution itself is
138
- // shared with fleet-state.mjs and board.mjs since #1658, and only the
138
+ // shared with fleet-state.mjs and board.mjs, and only the
139
139
  // filename and the warning below are this caller's own. No canonicalisation
140
140
  // is asked for: that is board.mjs's opt-in, and taking it here would change
141
141
  // the path this function RETURNS (and `check` prints) without changing which
142
142
  // file it names.
143
143
  function defaultLedgerPath() {
144
- // GIT_DIR/GIT_WORK_TREE scrubbed (#1599, gitEnv()): unlike the tracker-query
145
- // probe below in this same file, this call passed no env at all until now.
144
+ // GIT_DIR/GIT_WORK_TREE scrubbed (gitEnv()): unlike the tracker-query
145
+ // probe below in this same file, this call used to pass no env at all.
146
146
  // An ambient GIT_DIR answers `--git-common-dir` for a DIFFERENT repository,
147
147
  // so the ONE ledger every run's duplicate-filing guard reads gets resolved
148
148
  // underneath THAT repository instead of the caller's own — measured
@@ -161,7 +161,7 @@ function defaultLedgerPath() {
161
161
  // Could not resolve the shared git dir → fall back to a cwd-relative path.
162
162
  // That re-opens the worktree fail-open this resolution exists to close (a
163
163
  // member reads a cwd-local ledger, not the run's), so say so rather than
164
- // degrading the duplicate-filing guard in silence. Since #1199 this probe
164
+ // degrading the duplicate-filing guard in silence. This probe
165
165
  // is bounded too, and an ETIMEDOUT stall prints byte-identical stderr to
166
166
  // an instant "not a repository" failure without naming which — the
167
167
  // sibling probe in runCheck() already names it via cause(); match that
@@ -189,7 +189,7 @@ const argv = process.argv.slice(2);
189
189
  // process.argv, and why the scan is not anchored to a position.
190
190
  //
191
191
  // The same refusals arg.mjs gives the scripts that route through its arg() and
192
- // has(), in this reader's own wording (#362) — and since #567 that parity is
192
+ // has(), in this reader's own wording — and that parity is
193
193
  // the shared hasEqualsForm() predicate itself rather than a claim about two
194
194
  // expressions that were free to drift apart.
195
195
  // Flag names lived here AND in OWN_FLAGS below with no shared source — a
@@ -199,8 +199,8 @@ const argv = process.argv.slice(2);
199
199
  // silently accepted at exit 0). One name each, declared once, reused by
200
200
  // the indexOf() calls and OWN_FLAGS below. Not by the hasEqualsForm() calls
201
201
  // right below, which keep their own literal text on purpose —
202
- // shared-refusal.test.mjs pins that pair's source verbatim against arg.mjs's
203
- // rule (#567), so interpolating them here would fail that pin without
202
+ // a test pins that pair's source verbatim against arg.mjs's
203
+ // rule, so interpolating them here would fail that pin without
204
204
  // fixing anything it exists to catch.
205
205
  const FILE_FLAG_NAME = "file";
206
206
  const REQUIRE_FILE_FLAG_NAME = "require-file";
@@ -208,7 +208,7 @@ if (hasEqualsForm("file", argv)) die("--file needs a space-separated value, not
208
208
  if (hasEqualsForm("require-file", argv)) die("--require-file is a boolean flag, not --require-file=");
209
209
  const fileIdx = argv.indexOf(`--${FILE_FLAG_NAME}`);
210
210
  const file = fileIdx === -1 ? defaultLedgerPath() : argv[fileIdx + 1];
211
- // #362: `--file` took whatever token followed it, so `--file --require-file`
211
+ // `--file` took whatever token followed it, so `--file --require-file`
212
212
  // made the FLAG the path and the splice below then ate it — `--require-file`,
213
213
  // the flag whose entire job is to turn a missing ledger into a hard failure,
214
214
  // silently absent, and the duplicate-filing check answering "safe to file" at
@@ -231,8 +231,8 @@ const file = fileIdx === -1 ? defaultLedgerPath() : argv[fileIdx + 1];
231
231
  // The `file &&` term is load-bearing and must not fold into isFlagLike(),
232
232
  // which answers TRUE for an absent value: without it a truly trailing `--file`
233
233
  // would land on this clause's wording instead of the pre-existing "given with
234
- // no path" one below, which is the behaviour #362's own Measured block records
235
- // as already correct and which ledger.test.mjs pins.
234
+ // no path" one below, which is the existing behaviour for a truly trailing
235
+ // `--file` and which the ledger test pins.
236
236
  if (fileIdx !== -1 && file && isFlagLike(file)) die("--file needs a path");
237
237
  if (fileIdx !== -1) argv.splice(fileIdx, 2);
238
238
  if (!file) die("--file given with no path");
@@ -262,7 +262,7 @@ const headerRe = (name) => new RegExp(`(^|\\n)${name.replace(/[.*+?^${}()|[\]\\]
262
262
  // first, then newline, so a `\` in entry text can never be mistaken for the
263
263
  // start of an escape sequence introduced by this encoding. Without this, an
264
264
  // entry containing a real newline — or a line that happens to look like
265
- // `## Filed` or `- #999 ...` — gets misparsed on reload: real records
265
+ // `## Filed` or `- #<issue> ...` — gets misparsed on reload: real records
266
266
  // silently drop, or phantom ones get injected.
267
267
  function escapeText(s) {
268
268
  return s.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
@@ -271,7 +271,7 @@ function unescapeText(s) {
271
271
  return s.replace(/\\(\\|n)/g, (_, c) => (c === "n" ? "\n" : "\\"));
272
272
  }
273
273
 
274
- // #584: a `--`-prefixed token in `check`'s free-text tail used to fold
274
+ // A `--`-prefixed token in `check`'s free-text tail used to fold
275
275
  // straight into the duplicate-filing subject, so a misspelled flag searched
276
276
  // for a DIFFERENT subject and read "safe to file" where the correct spelling
277
277
  // answers ALREADY FILED at exit 1. A `--` token can legitimately BE that data
@@ -285,7 +285,7 @@ function unescapeText(s) {
285
285
  // carrying a `--`-prefixed element is refused by name. Structural, not a
286
286
  // distance threshold.
287
287
  //
288
- // `check`'s tail ALONE — narrower than #584's first pass, which called this
288
+ // `check`'s tail ALONE — narrower than the first pass, which called this
289
289
  // from `filed`, `row` and `ruled` too. The length rule is only sound where
290
290
  // the documented convention IS one quoted argument, and `check` is the only
291
291
  // subcommand where it is: both places that instruct a caller to run it spell
@@ -296,36 +296,35 @@ function unescapeText(s) {
296
296
  // usage strings for them. Applied there, the rule refused what the docs
297
297
  // prescribe: measured on the tree ahead of this narrowing, `filed 999 the
298
298
  // --basee flag is unread`, `row 42 the --basee flag is unread` and `ruled 77
299
- // the --basee flag is unread` each exited 2, where the tree before #584
300
- // (07dc927) answered all three at exit 0. #1161 ruled the guard back to
299
+ // the --basee flag is unread` each exited 2, where the tree before this guard
300
+ // existed answered all three at exit 0. The guard was ruled back to
301
301
  // `check` rather than respelling three conventions SKILL.md carries, two
302
302
  // other tickets holding that file open.
303
303
  //
304
- // Stated here and nowhere else (#1557). This paragraph is the one copy of why
304
+ // Stated here and nowhere else. This paragraph is the one copy of why
305
305
  // the rule is `check`'s alone; arg.mjs's refuseUnknown() comment and
306
- // ledger.test.mjs's two stray-tail blocks each used to carry their own, and
306
+ // the ledger test's two stray-tail blocks each used to carry their own, and
307
307
  // now point here instead. Anything that reads on the docs or the
308
308
  // measurement belongs in this paragraph, not beside a caller.
309
309
  //
310
- // Narrowing it does not keep #584's signature whole on those three: only the
310
+ // Narrowing it does not keep the original guard's signature whole on those three: only the
311
311
  // ID-slot vector survives — a stray flag one token to the LEFT is still
312
312
  // refused by refuseStrayInId() below. The tail-slot vector does not: a
313
313
  // stray `--` word inside filed/row/ruled's tail is now accepted as data (no
314
314
  // rule can tell it apart from the subject a caller typed), so `filed 999
315
315
  // --typo-flag some new subject` exits 0 and writes a row a later `check
316
316
  // "some new subject"` cannot exact-match — a near-miss at best, the same
317
- // wrong-subject shape #584 closed, reopened here on the tail. That is the
318
- // cost of taking the ticket's option 2 (narrow the guard rather than
319
- // requote the docs, #1161) instead of option 1; it is why it is the
317
+ // wrong-subject shape the guard closed, reopened here on the tail. That is the
318
+ // cost of narrowing the guard rather than requoting the docs; it is why it is the
320
319
  // DOCUMENTED convention, not the hazard, that decides where the length rule
321
320
  // may be read at all.
322
321
  //
323
- // The cost `check` keeps is real and is what #365's AC prices, so it is
322
+ // The cost `check` keeps is real, so it is
324
323
  // stated rather than denied: a legitimate subject carrying a `--` word
325
324
  // anywhere, given unquoted, was accepted before this guard and is refused by
326
325
  // it — `check the --basee flag is unread` answered at exit 0 before and exits
327
- // 2 now. Quoting the subject accepts it unchanged. The trade the ticket ruled
328
- // for is that this refusal is loud and recoverable where the wrong-subject
326
+ // 2 now. Quoting the subject accepts it unchanged. The trade taken
327
+ // is that this refusal is loud and recoverable where the wrong-subject
329
328
  // answer it replaces was silent. One residual of that trade is not this
330
329
  // file's to close: SKILL.md's ledger section spells `check <subject>`
331
330
  // unquoted inside the same entry that spells `filed <issue> <subject>`, where
@@ -334,9 +333,9 @@ function unescapeText(s) {
334
333
  // Narrowed on `check` too, not closed: a lone stray with no subject beside it
335
334
  // is a one-element tail, so it is still taken as the subject and answered at
336
335
  // exit 0. Harmless because it then searches for a string nothing matches, and
337
- // ledger.test.mjs's degenerate-subject case pins it so it stays deliberate.
336
+ // the ledger test's degenerate-subject case pins it so it stays deliberate.
338
337
  //
339
- // #1744: one dash short is the same slip, and the `--` test let it through —
338
+ // One dash short is the same slip, and the `--` test let it through —
340
339
  // `check -require-file widget guard missing` scored "-require-file widget
341
340
  // guard missing" and answered exit 0 where the correctly-spelled call answers
342
341
  // ALREADY FILED at exit 1. That test cannot simply lose a dash: single-dash
@@ -345,7 +344,7 @@ function unescapeText(s) {
345
344
  // negative number. Measured when this was written, 23 of the repo's 1068
346
345
  // issue titles carry one as a whitespace-split word, 20 of them letter-led,
347
346
  // so neither the prefix nor a `-<letter>` shape tells the slip apart. A NAME
348
- // does, the way arg.mjs's stray() tells a stray from a value (#463): a
347
+ // does, the way arg.mjs's stray() tells a stray from a value: a
349
348
  // single-dash word is refused only when one more dash, ahead of any `=value`,
350
349
  // makes it a flag this script reads — OWN_FLAGS, the two spliced out of argv
351
350
  // above. None of those titles carries one. It sits behind the same length
@@ -357,26 +356,26 @@ function unescapeText(s) {
357
356
  // `-f` is exactly the other-tool short flag above; both still fold into the
358
357
  // subject.
359
358
  const OWN_FLAGS = [`--${FILE_FLAG_NAME}`, `--${REQUIRE_FILE_FLAG_NAME}`].map((f) => f.toLowerCase());
360
- // The one-dash name clause below, before #1766, matched only a's FIRST
359
+ // The one-dash name clause below once matched only a's FIRST
361
360
  // whitespace-split word (ahead of any `=value`), not the whole element: a
362
361
  // quoted multi-word tail element (`check "-require-file widget" guard
363
362
  // missing`, one argv element) reconstructed to "--require-file widget"
364
363
  // under a whole-token match, which is not in OWN_FLAGS, so it fell through
365
- // to exit 0 — the same verdict flip #1744 exists to refuse, just via a
364
+ // to exit 0 — the same verdict flip the one-dash clause exists to refuse, just via a
366
365
  // quoted shape rather than a bare token. Splitting first mirrored the `--`
367
366
  // clause above it, which already matches on startsWith rather than
368
- // requiring the whole element. #1766 below widened the split to check
369
- // EVERY word of the element, not only the first — see that paragraph for
370
- // why the first-word version stopped being enough.
367
+ // requiring the whole element. The split now checks EVERY word of the
368
+ // element, not only the first — see the paragraph on the four missed shapes
369
+ // for why the first-word version stopped being enough.
371
370
  //
372
- // #1766: that first-word, exact-case split still missed four shapes of the
371
+ // That first-word, exact-case split still missed four shapes of the
373
372
  // same name, each confirmed by direct probe against the shipped script: a
374
373
  // case-variant spelling (`-REQUIRE-FILE`), whitespace ahead of the dash —
375
374
  // including U+00A0, which `\s` already matches, but the FIRST-word split
376
375
  // still lost the real word to the empty token that whitespace leaves at
377
376
  // index 0 — and the flag name landing anywhere but first in a quoted
378
377
  // multi-word element (`"widget -require-file"`, the mirror of the shape
379
- // just above, where #1744's fix only checked the front). None of the four
378
+ // just above, where the one-dash fix only checked the front). None of the four
380
379
  // changes the rule itself — still a NAME match against OWN_FLAGS, never a
381
380
  // prefix or shape test — only how a word is pulled out of the element:
382
381
  // every whitespace-split word, lower-cased, before the OWN_FLAGS lookup —
@@ -384,7 +383,7 @@ const OWN_FLAGS = [`--${FILE_FLAG_NAME}`, `--${REQUIRE_FILE_FLAG_NAME}`].map((f)
384
383
  // stays correct even if FILE_FLAG_NAME/REQUIRE_FILE_FLAG_NAME ever gain a
385
384
  // capital letter.
386
385
  //
387
- // #1850: the per-word test above only ever builds the ONE-dash-PREPENDED
386
+ // The per-word test above only ever builds the ONE-dash-PREPENDED
388
387
  // form of a word (`-${word...}`), so it matches a word already spelled with
389
388
  // one dash but never a word already spelled with this script's own TWO-dash
390
389
  // flag name — that spelling comes out of the split as `--require-file`,
@@ -393,15 +392,15 @@ const OWN_FLAGS = [`--${FILE_FLAG_NAME}`, `--${REQUIRE_FILE_FLAG_NAME}`].map((f)
393
392
  // two-dash name when it leads the WHOLE tail element; the same name landing
394
393
  // anywhere else in a quoted multi-word element — trailing, in the middle, or
395
394
  // alone behind leading whitespace — was invisible to both clauses and folded
396
- // back into the subject: the identical verdict flip #1744 and #1766 exist to
397
- // refuse, one dash count over. Reproducible on `main` before #1766 too;
398
- // #1766 only made it visible by contrast, once the one-dash mirror shape
399
- // started being refused. Fixed the same way #1744 chose for the one-dash
400
- // case — a NAME match, never a prefix or shape test — by also checking a
395
+ // back into the subject: the identical verdict flip the one-dash clauses exist to
396
+ // refuse, one dash count over. Reproducible before the widened split too;
397
+ // the split only made it visible by contrast, once the one-dash mirror shape
398
+ // started being refused. Fixed the same way the one-dash case is
399
+ // — a NAME match, never a prefix or shape test — by also checking a
401
400
  // word's own AS-SPELLED form against OWN_FLAGS, alongside the
402
401
  // one-dash-prepended form already there.
403
402
  //
404
- // #1851: a Unicode format character (general category Cf — U+200B zero-width
403
+ // A Unicode format character (general category Cf — U+200B zero-width
405
404
  // space, U+2060 word joiner, U+00AD soft hyphen, the bidi controls, …) is
406
405
  // invisible and sits outside `\s`, so the split above never cut on it and it
407
406
  // rode along inside the word: `\u200B-require-file` reads as `-require-file`
@@ -421,7 +420,7 @@ const OWN_FLAGS = [`--${FILE_FLAG_NAME}`, `--${REQUIRE_FILE_FLAG_NAME}`].map((f)
421
420
  // among others); stripping them costs a refusal only of such a mark glued to
422
421
  // an own-flag name, which no subject carries.
423
422
  //
424
- // #1904: invisible characters OUTSIDE Cf — U+3164 and the other Hangul
423
+ // Invisible characters OUTSIDE Cf — U+3164 and the other Hangul
425
424
  // fillers (letters), U+034F and the variation selectors (combining marks) —
426
425
  // are text rather than format controls, so the Cf strip above let them ride
427
426
  // inside the word exactly as a zero-width space rode before it. The strip is
@@ -430,8 +429,8 @@ const OWN_FLAGS = [`--${FILE_FLAG_NAME}`, `--${REQUIRE_FILE_FLAG_NAME}`].map((f)
430
429
  // rather than a hand-curated block list, which would miss the members no
431
430
  // ticket named (U+17B4, the Mongolian selectors U+180B–180F, …). A union,
432
431
  // not DICP alone: DICP leaves out the visible Cf marks just above, so
433
- // swapping it in would drop cases #1851 already refuses. Same site and the
434
- // same after-the-split order as #1851; U+FEFF stays the one code point in
432
+ // swapping it in would drop cases the Cf strip already refuses. Same site and the
433
+ // same after-the-split order as the Cf strip; U+FEFF stays the one code point in
435
434
  // both the strip and `\s`, no DICP member adding another. An emoji's own
436
435
  // presentation selector (U+FE0F) is stripped too, which costs nothing: only
437
436
  // a word that is an own-flag spelling once stripped is refused.
@@ -456,7 +455,7 @@ function refuseStrayInCheckTail(tail) {
456
455
 
457
456
  // The id slot ahead of `filed`/`row`/`ruled`'s tail is a hazard of its own,
458
457
  // and the only one of the three a rule can act on. refuseStrayInCheckTail()
459
- // above is not read on those subcommands at all (#1161), and would not
458
+ // above is not read on those subcommands at all, and would not
460
459
  // catch this shape if it were: a stray flag one token earlier lands in
461
460
  // `issue`/`ticket`/`pr`, and the tail left behind carries no `--` element to
462
461
  // find. Nor could that helper be read over the whole of `rest` instead —
@@ -464,12 +463,12 @@ function refuseStrayInCheckTail(tail) {
464
463
  // tail with a `--` element, pinned here as must-keep-working. A legitimate
465
464
  // id is bare digits, optionally `#`-prefixed, and never carries a leading
466
465
  // dash at all — single or double — so this slot refuses ANY leading dash,
467
- // not only a double one (#1678). The free-text tail cannot take that same
466
+ // not only a double one. The free-text tail cannot take that same
468
467
  // test: a subject can legitimately open with a double-dash-leading word —
469
468
  // exactly what `--flag-like subject text` above and check's own
470
469
  // dash-leading-subject pin exist to keep working. A single-dash-leading word
471
470
  // is ordinary subject text too, which is why check's tail refuses one only
472
- // by name (#1744, on refuseStrayInCheckTail() above). So only the id slot
471
+ // by name (see refuseStrayInCheckTail() above). So only the id slot
473
472
  // can take the bare prefix test at all, for the same no-free-text reason
474
473
  // given above.
475
474
  function refuseStrayInId(value, what) {
@@ -477,7 +476,7 @@ function refuseStrayInId(value, what) {
477
476
  }
478
477
 
479
478
  // Set by load(), the only function that reads the file, so `ledger.ok` can
480
- // report what the parse saw rather than what a later stat() guesses (#231).
479
+ // report what the parse saw rather than what a later stat() guesses.
481
480
  let ledgerParsed = false;
482
481
 
483
482
  function load() {
@@ -501,7 +500,8 @@ function load() {
501
500
  // Set here, past the read and the early return, so it can only be true of a
502
501
  // file this function actually opened and recognised as a ledger.
503
502
  ledgerParsed = headerRe(FILED).test(text);
504
- // A ledger written before #1799 has neither `## Dispatched` nor `## Drain`,
503
+ // A ledger written before `dispatch` and `drain` existed has neither
504
+ // `## Dispatched` nor `## Drain`,
505
505
  // which reads as nothing dispatched through `dispatch` and no drain — the
506
506
  // truth about that file, not a default standing in for it.
507
507
  const drainEntries = section(DRAIN);
@@ -536,7 +536,7 @@ function save(d) {
536
536
  // atomic on a POSIX filesystem — a crash mid-write leaves the temp file
537
537
  // corrupt but never truncates the durability file itself. The temp name
538
538
  // carries the pid: a shared `${file}.tmp` let one writer rename another's
539
- // temp file out from under it, and the loser died on ENOENT (#531).
539
+ // temp file out from under it, and the loser died on ENOENT.
540
540
  const tmp = `${file}.${process.pid}.tmp`;
541
541
  writeFileSync(tmp, out);
542
542
  renameSync(tmp, file);
@@ -546,7 +546,7 @@ function save(d) {
546
546
  console.error(` wrote ${file}`);
547
547
  }
548
548
 
549
- // ── The write lock (#531) ────────────────────────────────────────────────────
549
+ // ── The write lock ───────────────────────────────────────────────────────────
550
550
  //
551
551
  // Held from before load() through save() on every write subcommand, because
552
552
  // save() rewrites the whole document from what load() read: without it two
@@ -572,8 +572,8 @@ function save(d) {
572
572
  // the same dead pid, and RE-CHECKING that pid is still dead. Unlink-then-
573
573
  // create, or a bare rename, lets two waiters that both saw the dead pid each
574
574
  // remove the other's FRESH lock and write at once; the re-read is what makes
575
- // the takeover safe. The re-check narrows the one window the re-read leaves
576
- // (#2088), and the order the two run in is what narrows it: isDead(deadPid)
575
+ // the takeover safe. The re-check narrows the one window the re-read leaves,
576
+ // and the order the two run in is what narrows it: isDead(deadPid)
577
577
  // runs before the re-read, not after, so a live writer that takes the lock in
578
578
  // the gap between them is caught by the read that follows, not hidden behind
579
579
  // one taken before it arrived. Read first, and a recycled pid that came back
@@ -610,8 +610,8 @@ function save(d) {
610
610
  //
611
611
  // What the lock does NOT cover: `check` -> `gh issue create` -> `filed` is
612
612
  // three commands, and each takes the lock (or not) alone, so two filers can
613
- // both read `clean` before either records. Accepted (#531): the measured
614
- // same-run accidental duplicate, #298 -> #302, was filed minutes apart, and a
613
+ // both read `clean` before either records. Accepted: the measured
614
+ // same-run accidental duplicate was filed minutes apart, and a
615
615
  // claim/reserve subcommand closing that window was ruled out.
616
616
  const LOCK = `${file}.lock`;
617
617
  const REAP = `${LOCK}.reap`;
@@ -771,11 +771,8 @@ async function acquireLock() {
771
771
  // the other four subcommands ignored it entirely: `read --require-file` on an
772
772
  // absent file printed the empty payload at exit 0, byte-identical to a real
773
773
  // empty ledger on both streams, and `row --require-file` CREATED the very file
774
- // whose absence the flag exists to refuse (#816). One flag read in one place is
775
- // also what docs/specs/2026-07-23-fleet-plugin-design.md's `ledger.mjs` row
776
- // already documents
777
- // — "Exit 2 on any subcommand — ... `--require-file` with no ledger file" — so
778
- // the implementation is what had drifted, not the contract.
774
+ // whose absence the flag exists to refuse. One flag read in one place is
775
+ // what makes it hold on every subcommand, not only `check`.
779
776
  //
780
777
  // Ahead of load() rather than after it: load() answers "absent" and "present
781
778
  // but unparseable" with the same empty lists, which is the ambiguity this flag
@@ -787,8 +784,8 @@ async function acquireLock() {
787
784
  //
788
785
  // Also ahead of "unknown subcommand" validation: `--require-file` against a
789
786
  // missing file with a bogus subcommand reports the require-file refusal, not
790
- // a usage error. Deliberate/accepted, not reordered — the doc line above
791
- // bundles both under one undifferentiated exit 2, with no ordering between them.
787
+ // a usage error. Deliberate/accepted, not reordered — both refusals exit 2,
788
+ // so a caller gating on the exit code reads the same failure either way.
792
789
  if (requireFile && !existsSync(file)) die(`--require-file given but ledger file does not exist: ${file}`);
793
790
 
794
791
  const WRITE_COMMANDS = new Set(["row", "filed", "ruled", "dispatch", "settle", "drain", "rotate"]);
@@ -801,7 +798,7 @@ const data = cmd === "rotate" ? null : load();
801
798
  // The payload subcommands end by falling out of this chain, never by calling
802
799
  // process.exit(). On a pipe, process.stdout.write is async and process.exit()
803
800
  // discards whatever is still queued, so a payload past the buffer arrives cut
804
- // — at exit 0, which types a corrupt read as a successful one (#246).
801
+ // — at exit 0, which types a corrupt read as a successful one.
805
802
  // candidates.mjs states the same reason at its own exit line; this was the
806
803
  // second script on that shape. The consumer that made it visible is board.mjs,
807
804
  // which reads `read` through execFileSync — a pipe — and accepts its ledger
@@ -816,7 +813,7 @@ const data = cmd === "rotate" ? null : load();
816
813
  // statement of its branch and became one directly. Its already-filed exit sits
817
814
  // mid-branch, where falling through would run the near-miss ranking and the
818
815
  // tracker search that exit exists to skip — so `check`'s branch is a function
819
- // now (runCheck, below the chain), and a `return` is what skips them (#808).
816
+ // now (runCheck, below the chain), and a `return` is what skips them.
820
817
  //
821
818
  // What that leaves is a property of every payload this file emits rather than
822
819
  // of whichever branches a sweep happened to reach: none of them reaches
@@ -840,17 +837,17 @@ if (cmd === "read") {
840
837
  // Every reader parses member tokens anywhere in a row's text, so a token
841
838
  // with an outcome outside its family's vocabulary would land as a permanent
842
839
  // settle — `settle merge-bot-1 done` then refuses as "already settled as
843
- // dispatched" (#2139). Refused before anything is written, new row or
840
+ // dispatched". Refused before anything is written, new row or
844
841
  // rewrite alike; a well-formed token, live or settled, still goes through.
845
842
  const malformed = memberTokens(line).find((t) => t.error !== null);
846
843
  if (malformed) die(`row ${key}: malformed member token '${malformed.name}=${malformed.outcome}' — ${malformed.error}`);
847
- // #2331: `label-off=<attempt>` tells the tick a missing `ready-to-merge` is
844
+ // `label-off=<attempt>` tells the tick a missing `ready-to-merge` is
848
845
  // the controller's own removal, not a finisher's miss — so one naming no
849
846
  // finisher-pr member this run has would mask a real miss behind a record of
850
847
  // nothing. Known means in `## Dispatched` or among a row's member tokens as
851
848
  // they stand before this write, the two places dispatch's live-sibling check
852
849
  // reads: a token `row` wrote never enters `## Dispatched`, and this line's
853
- // own copy of a name vouches for nothing. Local only — never `gh` (#152).
850
+ // own copy of a name vouches for nothing. Local only — never `gh`.
854
851
  const known = new Set([...data.dispatched.map(parseToken), ...data.rows.flatMap(memberTokens)]
855
852
  .filter((t) => t && t.family === "finisher-pr").map((t) => t.name));
856
853
  const badOff = line.split(/\s+/).find((tok) => {
@@ -884,17 +881,17 @@ if (cmd === "read") {
884
881
  const id = issue.replace(/^#/, "");
885
882
  // isDigits() gates id the same way the sibling ticket/PR check gates key
886
883
  // (line ~1527): left unvalidated, a non-digit id becomes a `#<id> ` prefix
887
- // subjectOf()'s digits-only regex (#933) cannot strip, so a near-duplicate
884
+ // subjectOf()'s digits-only regex cannot strip, so a near-duplicate
888
885
  // `check` against that row silently degrades from the hard "already
889
886
  // filed" block to a soft near-miss suggestion instead.
890
887
  if (!isDigits(id)) die(`'${issue}' is not an issue number`);
891
888
  // Both callers retry once on a failure they observed, and an observed
892
889
  // failure is not proof the write failed — a timeout after the row landed
893
- // reads as one — so a repeat filing of an issue already on file is a no-op
894
- // (#2087). Identity is the issue number alone, matched NUMERICALLY — so a
890
+ // reads as one — so a repeat filing of an issue already on file is a no-op.
891
+ // Identity is the issue number alone, matched NUMERICALLY — so a
895
892
  // non-canonical spelling (`08`) still dedupes against `8` — as the token
896
893
  // between `#` and the first whitespace: never `check`'s fuzzy subject
897
- // matcher, and never a prefix test, which would dedupe #8 against #80.
894
+ // matcher, and never a prefix test, which would dedupe issue 8 against issue 80.
898
895
  // The existing row wins, subject and all — a retry reworded is still the
899
896
  // same filing.
900
897
  const existing = data.filed.find((row) => Number(row.match(/^#(\S+)/)?.[1]) === Number(id));
@@ -953,7 +950,7 @@ if (cmd === "read") {
953
950
  // it, whose results that answer would only discard. A function is what makes
954
951
  // leaving early expressible — `process.exitCode` and a `return`, reaching the
955
952
  // same exit by the same path every other subcommand takes, where an arm of the
956
- // chain had only `process.exit()` and the payload it abandoned (#808).
953
+ // chain had only `process.exit()` and the payload it abandoned.
957
954
  // Hoisted, so the dispatch chain above stays the file's spine.
958
955
  function runCheck() {
959
956
  // The first check of a run legitimately has no file yet, so absence alone
@@ -962,7 +959,7 @@ function runCheck() {
962
959
  // would notice. Warn loudly by default; --require-file makes absence a
963
960
  // hard failure for callers that know the file must already exist — and that
964
961
  // refusal is read ahead of the dispatch now, not here, because four other
965
- // subcommands needed the same one (#816). Reaching this line at all means
962
+ // subcommands needed the same one. Reaching this line at all means
966
963
  // the flag was absent.
967
964
  //
968
965
  // `ledger.ok` is the parse's answer, not this stat()'s: load() sets it only
@@ -973,21 +970,21 @@ function runCheck() {
973
970
  // contract is absence rather than shape — so the two are two observations
974
971
  // now, each true of its own question, where before they were one that was
975
972
  // true of neither. Narrower, not airtight: a ledger truncated AFTER the
976
- // header still parses, with the rows below it lost (#231).
973
+ // header still parses, with the rows below it lost.
977
974
  const ledger = { ok: ledgerParsed };
978
975
  if (!existsSync(file)) {
979
976
  console.error(
980
977
  `${NAME}: WARNING — ledger file not found: ${file}. Every check will read "safe to file" until it exists.`,
981
978
  );
982
979
  } else if (!ledger.ok) {
983
- // #817: the existence probe above is silent once `file` exists, so a
980
+ // The existence probe above is silent once `file` exists, so a
984
981
  // `--file` landing on a real-but-wrong path — a typo'd neighbour, a
985
982
  // corrupted "## Filed" header, a 0-byte file — got the SAME silence as a
986
983
  // clean read. `ledger.ok` (above) already tells the machine-readable half
987
984
  // apart; this is that same "opened it, it did not parse" fact stated on
988
985
  // stderr, in wording that does not borrow "file not found" — the file
989
986
  // demonstrably exists, so claiming otherwise would be a fresh version of
990
- // the defect #231 removed from the JSON half.
987
+ // the defect already removed from the JSON half.
991
988
  console.error(
992
989
  `${NAME}: WARNING — ${file} exists but does not look like a ledger (no "${FILED}" header found). Every check will read "safe to file" until it is fixed.`,
993
990
  );
@@ -997,7 +994,7 @@ function runCheck() {
997
994
  // Quoted here, unlike the three usage strings above, because
998
995
  // refuseStrayInCheckTail() is the reason: `check` is the subcommand whose tail
999
996
  // must arrive as ONE argument, and this is the only spelling of its call
1000
- // inside the script (#1161). The other three take their tail as the words a
997
+ // inside the script. The other three take their tail as the words a
1001
998
  // caller typed.
1002
999
  if (!subject) die("usage: ledger.mjs check \"<subject>\"");
1003
1000
  const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
@@ -1022,7 +1019,7 @@ function runCheck() {
1022
1019
  // first walk had already produced — equal sizes are exactly the case
1023
1020
  // where the two walks are the same walk.
1024
1021
  // Not `small.isSubsetOf(big)`: that's Node v22.0.0+, above this repo's
1025
- // declared >=20.11.0 floor (#1954; node-floor-sweep.test.mjs reds it).
1022
+ // declared >=20.11.0 floor (a test reds it).
1026
1023
  return (small.size === big.size || small.size >= 4) && [...small].every((t) => big.has(t));
1027
1024
  };
1028
1025
  // Strip the leading `#NNN ` issue number: it is metadata, not part of the
@@ -1049,7 +1046,7 @@ function runCheck() {
1049
1046
  // ordinary four-word check against one wide filed row arrived cut at the
1050
1047
  // pipe buffer. The code it arrived under was already this 1, so what the
1051
1048
  // exit lost was the payload alone, the `verdict` field parsing consumers
1052
- // read included (#808). The `return` is what keeps the ranking and the
1049
+ // read included. The `return` is what keeps the ranking and the
1053
1050
  // tracker query below unreached; nothing after it assigns exitCode on
1054
1051
  // this path, so 1 is what the process leaves with.
1055
1052
  process.exitCode = 1;
@@ -1060,7 +1057,7 @@ function runCheck() {
1060
1057
  // "was this near-verbatim wording already filed" — and answers it well. It
1061
1058
  // says nothing about the same finding described in DIFFERENT words, which is
1062
1059
  // how a second discoverer actually words it: one measured run rediscovered
1063
- // #114 five times, each in its own phrasing, and the check caught none of
1060
+ // one issue five times, each in its own phrasing, and the check caught none of
1064
1061
  // them. Rank the filed list by token overlap and hand the top rows back with
1065
1062
  // a score, instead of collapsing all of that into `found: false`.
1066
1063
  //
@@ -1068,7 +1065,7 @@ function runCheck() {
1068
1065
  // and the singular fold only sharpen a ranking, but folding them into
1069
1066
  // isMatch() would widen what counts as ALREADY FILED — the one behaviour
1070
1067
  // here that callers gate on and that must not move.
1071
- // Widened for #153: modals, negations and temporals ("should", "never",
1068
+ // Widened to cover modals, negations and temporals ("should", "never",
1072
1069
  // "still"...) are noise words that are never the subject's distinctive
1073
1070
  // term, yet the old, shorter list let them survive to outrank one — "the
1074
1071
  // guard should never fail open on a fork" picked "should guard never" over
@@ -1109,7 +1106,7 @@ function runCheck() {
1109
1106
  };
1110
1107
  const round2 = (n) => Math.round(n * 100) / 100;
1111
1108
  const scored = scoreTokens(subject);
1112
- // No floor was tuned here: the measured #114 rewording shares exactly one
1109
+ // No floor was tuned here: the measured rewording shares exactly one
1113
1110
  // content word, and a threshold picked to look tidy would drop the very case
1114
1111
  // this exists for. The top-3 cap, not the floor, is what keeps the output
1115
1112
  // short. `rankedNear`'s filter still imposes one, because it tests the
@@ -1122,13 +1119,13 @@ function runCheck() {
1122
1119
  // output would need a wider precision, not just a different filter.
1123
1120
  const NEAR_SHOWN = 3;
1124
1121
  // The floor at which a near-miss stops being decoration and becomes the
1125
- // verdict's own answer — `soft-hit` rather than `clean` (#388). A tuning
1122
+ // verdict's own answer — `soft-hit` rather than `clean`. A tuning
1126
1123
  // value, deliberately named and deliberately here beside the display cap
1127
1124
  // rather than inlined in the verdict below.
1128
1125
  //
1129
- // Chosen from the rows #388 measured: the near-misses a reader went on to
1126
+ // Chosen from measured rows: the near-misses a reader went on to
1130
1127
  // confirm as the genuinely adjacent issue scored from 0.20 up to 0.45, so a
1131
- // floor at 0.20 admits them. #388 also records an adjacent row at 0.09, which
1128
+ // floor at 0.20 admits them. The same measurement includes an adjacent row at 0.09, which
1132
1129
  // this floor does not reach — the floor buys a short answer, never
1133
1130
  // completeness, and the rows themselves stay printed and stay in the payload
1134
1131
  // at every verdict for exactly that reason. Raise it and adjacent rows fall
@@ -1144,8 +1141,8 @@ function runCheck() {
1144
1141
  // scores tie across it routinely — measured, five rows at 1.00 with the
1145
1142
  // caller shown three — so the cut is arbitrary among equals. A bare
1146
1143
  // three-row list is then indistinguishable from a complete one, which is
1147
- // the "no silent caps" rule `candidates.mjs`'s `refuseIfCapped` legislates one script over
1148
- // (#154). It enforces that rule by refusing outright; refusing is wrong
1144
+ // the "no silent caps" rule `candidates.mjs`'s `refuseIfCapped` legislates one script over.
1145
+ // It enforces that rule by refusing outright; refusing is wrong
1149
1146
  // here — these rows are advisory context for a decision, not the work queue,
1150
1147
  // and a single tracker hit already forces exit 3. Report the COUNT withheld
1151
1148
  // and the best score among them, never the rows: printing the rows would
@@ -1157,22 +1154,22 @@ function runCheck() {
1157
1154
  // The ledger can only see what THIS run recorded. An issue that already
1158
1155
  // exists on the tracker but never reached this `filed` list — filed by an
1159
1156
  // earlier run, by the maintainer, by hand — is structurally invisible to
1160
- // every match above, and that is the measured failure: #114 was rediscovered
1157
+ // every match above, and that is the measured failure: one issue was rediscovered
1161
1158
  // five times in one run and `check` reported it safe to file every time. Ask
1162
1159
  // the tracker.
1163
1160
  //
1164
1161
  // gh ANDs the search terms, so every extra term can only narrow the result —
1165
1162
  // keep the query SHORT: three of the subject's most distinctive words.
1166
1163
  // Measured against the live tracker, "candidates opposite states" returns
1167
- // #114. Whether a fourth term helps or hurts turns on whether it happens to
1168
- // occur in the target issue's text — gh searches bodies, not just titles —
1169
- // which the subject cannot know: "code" leaves #114 in, "open" drops it. So
1164
+ // that issue. Whether a fourth term helps or hurts turns on whether it
1165
+ // happens to occur in the target issue's text — gh searches bodies, not just titles —
1166
+ // which the subject cannot know: "code" leaves it in, "open" drops it. So
1170
1167
  // three is a recall-preserving floor, not a measured optimum. Longest-first is a crude stand-in for distinctiveness (no
1171
1168
  // corpus to weigh terms against) and the >= 3 filter erases short but
1172
1169
  // distinctive identifiers like `CI` or `gh`; upgrade to a real frequency
1173
1170
  // weighting if the query starts missing.
1174
1171
  //
1175
- // #153 widened STOP (above) to stop modals/negations/temporals from
1172
+ // STOP was widened (above) to stop modals/negations/temporals from
1176
1173
  // outranking real content words, and stops there — this is a pin, not a
1177
1174
  // retune. Two things it deliberately leaves broken: longest-first still
1178
1175
  // prefers a long ordinary word over a short distinctive one ("postgres"
@@ -1198,7 +1195,7 @@ function runCheck() {
1198
1195
  // How many tracker rows the caller is shown. gh is asked for one MORE than
1199
1196
  // this (below): its list arrives with no total, so a full page and a
1200
1197
  // truncated one are byte-identical, and the extra row's presence is the only
1201
- // truncation signal available — #154. Cheap: one row, one query, and it is
1198
+ // truncation signal available. Cheap: one row, one query, and it is
1202
1199
  // ranked alongside the rest rather than discarded.
1203
1200
  const TRACKER_SHOWN = 5;
1204
1201
  let tracker;
@@ -1207,7 +1204,7 @@ function runCheck() {
1207
1204
  // every subject as a tracker hit. Not searching is the honest answer.
1208
1205
  // `hits` is omitted, not `[]` — the tracker was never read, so an empty
1209
1206
  // list here would be a claim of cleanliness this branch never earned
1210
- // (issue #152: a consumer testing `.length` must not read this as clean).
1207
+ // (a consumer testing `.length` must not read this as clean).
1211
1208
  tracker = { ok: false, query: null, error: "subject has no distinctive terms to search for" };
1212
1209
  } else {
1213
1210
  // gh resolves "the repository" from the child process's cwd, which
@@ -1216,7 +1213,7 @@ function runCheck() {
1216
1213
  // --git-common-dir above; in the documented flow (no --file, run from
1217
1214
  // the repo) the two agree, but an explicit --file naming a ledger
1218
1215
  // outside the caller's repo diverges silently: the query searches the
1219
- // wrong tracker and reports a confident, empty result (#155). Bind gh's
1216
+ // wrong tracker and reports a confident, empty result. Bind gh's
1220
1217
  // cwd to the ledger's OWN repository instead of leaving it implicit. No
1221
1218
  // --repo flag needed — gh's remote-based resolution does the rest once it
1222
1219
  // is pointed at the right directory, and a worktree ledger (shared .git,
@@ -1224,18 +1221,17 @@ function runCheck() {
1224
1221
  //
1225
1222
  // cwd is not the whole story: inherited git vars outrank it, so an
1226
1223
  // ambient GIT_DIR/GIT_WORK_TREE retargets BOTH the probe below and gh's
1227
- // own remote resolution at the other repository, and the #155
1224
+ // own remote resolution at the other repository, and the same
1228
1225
  // confident-empty result comes straight back with `ok: true` on it.
1229
1226
  // Plausible here: a git hook, `rebase --exec`, `bisect run`. Scrub them
1230
- // off both children — the fleet's own fixtures already do exactly this
1231
- // (inflight.test.mjs).
1227
+ // off both children — the fleet's own fixtures already do exactly this.
1232
1228
  //
1233
1229
  // GH_REPO is the same hazard one layer up, and worse: gh reads it BEFORE
1234
1230
  // it ever consults git, so no amount of git-var hygiene covers it and the
1235
1231
  // bound cwd is simply ignored. Measured against a live tracker with the
1236
1232
  // cwd binding in place: `GH_REPO=<some other real repo>` searched that
1237
- // repo and returned `{"ok":true,"hits":[],"verdict":"clean"}` — #155
1238
- // verbatim, in the documented flow, no --file divergence needed. Not
1233
+ // repo and returned `{"ok":true,"hits":[],"verdict":"clean"}` — the
1234
+ // same wrong-tracker answer, in the documented flow, no --file divergence needed. Not
1239
1235
  // hypothetical here either: this repo's own
1240
1236
  // .github/workflows/release-label.yml exports GH_REPO to every step that
1241
1237
  // shells out to gh.
@@ -1260,14 +1256,14 @@ function runCheck() {
1260
1256
  // More than one cause lands here: a ledger path genuinely outside any
1261
1257
  // repository, but also git missing entirely (spawn ENOENT, so `status`
1262
1258
  // is null and `null !== 0`), a dubious-ownership refusal, an unreadable
1263
- // `.git` gitfile, and since #1199 a probe that overran GIT_TIMEOUT_MS —
1259
+ // `.git` gitfile, and a probe that overran GIT_TIMEOUT_MS —
1264
1260
  // spawnSync reports that the same way ENOENT arrives, `status` null with
1265
1261
  // the reason in `error.message`, so it needs no arm of its own and
1266
1262
  // cause() names it (`spawnSync git ETIMEDOUT`) without this branch
1267
1263
  // having to. Do not name one of them — carry git's own reason,
1268
1264
  // because this call leaves stdio at the default pipe, so git's stderr
1269
1265
  // reaches no terminal and this string is the only place the cause is
1270
- // ever seen (the same call the gh catch below makes, #176). The CAUSE is
1266
+ // ever seen (the same call the gh catch below makes). The CAUSE is
1271
1267
  // capped for the same reason: it ships on stdout inside `tracker.error`
1272
1268
  // — see cause(), which owns both that cap and the choice between the
1273
1269
  // fields a failed git can put a reason in. Empty here is a tolerable
@@ -1278,7 +1274,7 @@ function runCheck() {
1278
1274
  // That cap covers the cause and NOTHING else. `tracker.error` as a whole
1279
1275
  // is deliberately unbounded here, because the directory interpolates
1280
1276
  // raw: the field measured 1100 characters from a 557-character
1281
- // directory, a cause already cut to 500, and 43 of fixed text (#940).
1277
+ // directory, a cause already cut to 500, and 43 of fixed text.
1282
1278
  // Capping the directory would buy the payload no ceiling — `subject`
1283
1279
  // and `tracker.query` ride in the same JSON uncapped: as JSON fields
1284
1280
  // from a 3000-character subject word they measure 3012 and 3010
@@ -1324,14 +1320,14 @@ function runCheck() {
1324
1320
  // field still describes itself truthfully. Validating `title` here would
1325
1321
  // degrade that row to `unverified` over the one field the hit line does
1326
1322
  // not need, which is the opposite of what a guard against undescribable
1327
- // hits is for (#232).
1323
+ // hits is for.
1328
1324
  //
1329
1325
  // Absence is the whole of that claim, deliberately: a `title` that is
1330
1326
  // PRESENT and not a string is not handled here or anywhere below —
1331
1327
  // `h.title || ""` keeps a truthy non-string, and scoreTokens then calls
1332
1328
  // `.toLowerCase()` on it, so the read degrades to `unverified` reporting
1333
- // a TypeError where a tracker reason belongs. That is #643's, which
1334
- // rules on field TYPE where this guard rules on field PRESENCE.
1329
+ // a TypeError where a tracker reason belongs. That is a question of field TYPE; this
1330
+ // guard rules on field PRESENCE.
1335
1331
  if (!Array.isArray(parsed) || parsed.some((h) => !h || typeof h.number !== "number"
1336
1332
  || typeof h.state !== "string" || typeof h.url !== "string")) {
1337
1333
  throw new Error("gh returned JSON that is not an issue list");
@@ -1360,7 +1356,7 @@ function runCheck() {
1360
1356
  // --git-common-dir resolution above already closed once. Degrade to the
1361
1357
  // ledger-only answer and say so.
1362
1358
  // Capped, and deliberately still carrying the stderr — the opposite call
1363
- // from the fleet's other gh catches (#176). Those omit it because
1359
+ // from the fleet's other gh catches. Those omit it because
1364
1360
  // execFileSync forwarded the child's bytes to our stderr already, so
1365
1361
  // interpolating emits them twice; this call sets `stdio`, which turns
1366
1362
  // that forwarding OFF, so this string is the only place the cause is
@@ -1372,8 +1368,8 @@ function runCheck() {
1372
1368
  //
1373
1369
  // stderr first, then the message: on a non-zero exit Node builds the
1374
1370
  // message out of the same bytes prefixed by the command, so it is the
1375
- // LONGER copy of the cause, not a smaller fallback (#176, measured
1376
- // again here). The message earns its place wherever stderr holds no
1371
+ // LONGER copy of the cause, not a smaller fallback (measured). The
1372
+ // message earns its place wherever stderr holds no
1377
1373
  // cause — and stderr holds none in three shapes, only one of which is
1378
1374
  // the field being absent: no `stderr` property at all on the output
1379
1375
  // this file refused to parse or refused the shape of, the property
@@ -1391,7 +1387,7 @@ function runCheck() {
1391
1387
  // in `e.stderr`, which on a timeout holds whatever gh printed before
1392
1388
  // the kill: measured end to end, a gh that warned about cached
1393
1389
  // credentials and then hung reported that warning as the cause of a
1394
- // 20-second stall and named the timeout nowhere (#638). The flip moves
1390
+ // 20-second stall and named the timeout nowhere. The flip moves
1395
1391
  // exactly one failure, the timeout that printed something — the other
1396
1392
  // aborts leave `e.stderr` undefined, so the message already won there.
1397
1393
  // A Node that did set `e.code` on a plain non-zero exit would degrade
@@ -1440,7 +1436,7 @@ function runCheck() {
1440
1436
  // HIT` is imperative, blocking language, and a row the scorer rates 0.00
1441
1437
  // rendered in it was indistinguishable from a genuine duplicate — measured,
1442
1438
  // and recovered from only by readers who went and searched the tracker by
1443
- // hand (#388). Under this heading stderr and the verdict now say the same
1439
+ // hand. Under this heading stderr and the verdict now say the same
1444
1440
  // thing: a set with a scoring row is a hit, a set without one is rows to
1445
1441
  // read.
1446
1442
  const heading = bestHit > 0 ? "TRACKER HIT" : "TRACKER ROW";
@@ -1449,7 +1445,7 @@ function runCheck() {
1449
1445
  }
1450
1446
  // `more than N`, never `N`: with the probe row back, the exact count is
1451
1447
  // precisely what is not known, and printing `5` for it is the silent cap
1452
- // restated as a number (#154).
1448
+ // restated as a number.
1453
1449
  console.error(bestHit > 0
1454
1450
  ? `${NAME}: not in this run's filed list, but ${tracker.truncated ? `more than ${TRACKER_SHOWN}` : tracker.hits.length} tracker issue(s) match '${query}' — review before filing`
1455
1451
  : `${NAME}: not in this run's filed list; the tracker rows matching '${query}' all score 0.00 against this subject — gh matched something the title-based score cannot see, so read them, but they are not a finding of duplication`);
@@ -1459,16 +1455,16 @@ function runCheck() {
1459
1455
  } else {
1460
1456
  // Not "found no related issues" — that asserts the tracker has nothing,
1461
1457
  // when all that is actually established is that a ${terms.length}-term
1462
- // heuristic query came back empty (#153). A query built from a few
1458
+ // heuristic query came back empty. A query built from a few
1463
1459
  // longest-surviving words can miss the very issue it should have found
1464
1460
  // (see the STOP comment above); "no matches for this query" says what was
1465
1461
  // established and leaves the rest unclaimed.
1466
1462
  console.error(`${NAME}: not previously filed; tracker search '${query}' (${terms.length} term${terms.length === 1 ? "" : "s"}) returned no matches — not a certification the tracker has nothing on this`);
1467
1463
  }
1468
1464
  // Named explicitly so a consumer does not have to reconstruct it from
1469
- // `tracker.ok` plus `tracker.hits` — issue #152.
1465
+ // `tracker.ok` plus `tracker.hits`.
1470
1466
  //
1471
- // The scores decide, not the presence of rows (#388). Both halves of the
1467
+ // The scores decide, not the presence of rows. Both halves of the
1472
1468
  // answer were measured reporting the opposite of what their own numbers said:
1473
1469
  // `tracker-hit` over rows this file rates 0.00, and `clean` printed directly
1474
1470
  // above near-miss rows that named the right issue. Nothing new is computed
@@ -1500,13 +1496,13 @@ function runCheck() {
1500
1496
  }
1501
1497
  // `nearTotal` alongside `near`, and `tracker.truncated` alongside `hits`:
1502
1498
  // both lists are capped and neither cap was previously visible from the
1503
- // payload a consumer parses (#154). They differ in what is knowable —
1499
+ // payload a consumer parses. They differ in what is knowable —
1504
1500
  // the ledger is fully in hand, so the near-miss total is exact, while gh
1505
1501
  // reports no total, so the tracker can only say that more exist.
1506
1502
  // `ledger.ok` beside `tracker.ok`, and deliberately NOT inside `verdict`:
1507
1503
  // the two halves each report their own readability, which is what makes an
1508
1504
  // unread ledger distinguishable from one read and found empty — those
1509
- // payloads were otherwise identical in every field (#231). Folding it into
1505
+ // payloads were otherwise identical in every field. Folding it into
1510
1506
  // the verdict instead would answer `unverified` for the run's FIRST check
1511
1507
  // on any fresh clone, where `.fleet/` does not exist until save() creates
1512
1508
  // it — see the comment on the repository probe above.
@@ -1517,16 +1513,16 @@ function runCheck() {
1517
1513
  // suggestion, not a finding of duplication. Exit 0 covers "clean",
1518
1514
  // "unverified" and "soft-hit" alike, which `verdict` names explicitly and the
1519
1515
  // exit code deliberately still does not: minting a code for unverified would
1520
- // break `check "$s" && gh issue create` on every offline run (ruled against
1521
- // in #152), and a soft hit is the same kind of answer — advisory rows, no
1522
- // established duplicate — so it inherits that ruling rather than reopening
1523
- // it. The exit code is a pure function of `verdict`; only `tracker-hit`
1524
- // blocks, so a verdict added later leaves 3 alone unless it says so here.
1516
+ // break `check "$s" && gh issue create` on every offline run, and a soft hit
1517
+ // is the same kind of answer — advisory rows, no established duplicate — so
1518
+ // it inherits that decision rather than reopening it. The exit code is a
1519
+ // pure function of `verdict`; only `tracker-hit` blocks, so a verdict added
1520
+ // later leaves 3 alone unless it says so here.
1525
1521
  //
1526
- // A hit set scoring 0.00 no longer forces 3 (#388). gh can match an issue
1522
+ // A hit set scoring 0.00 no longer forces 3. gh can match an issue
1527
1523
  // body the title-based score cannot see, which is why those rows are still
1528
1524
  // printed and still shipped in the payload — but the instruction the callers
1529
- // carry makes exit 3 binding, and #388 measured what that costs: every
1525
+ // carry makes exit 3 binding, and measurement shows what that costs: every
1530
1526
  // recorded case of a hard stop over rows this file rates zero was survived
1531
1527
  // only by a reader who overrode it and searched the tracker by hand, and
1532
1528
  // obeying it would have dropped a real deferral.
@@ -1589,7 +1585,7 @@ function runRotate() {
1589
1585
  console.log(JSON.stringify({ rotated: true, archive }));
1590
1586
  }
1591
1587
 
1592
- // ── dispatch / settle (#1799) ────────────────────────────────────────────────
1588
+ // ── dispatch / settle ────────────────────────────────────────────────────────
1593
1589
  //
1594
1590
  // Hoisted beside runCheck() for the same reason: each has several refusals
1595
1591
  // ahead of its one write, and the chain above stays the file's spine. Every
@@ -1603,7 +1599,7 @@ function rowKey(r) {
1603
1599
 
1604
1600
  // A row's PR is its first `PR#<n>` mention: the `→ PR#346` arrow or the
1605
1601
  // implementer's settled `impl-324=PR#346` token, which name the same PR.
1606
- // compute-board.mjs parseRow() reads the settled token (#1820), so the row a
1602
+ // compute-board.mjs parseRow() reads the settled token, so the row a
1607
1603
  // PR-bound member lands on is the card the cockpit shows that PR on.
1608
1604
  function rowPr(r) {
1609
1605
  const m = /\bPR\s*#(\d+)\b/.exec(r);
@@ -1750,8 +1746,8 @@ function runDispatch() {
1750
1746
  }
1751
1747
 
1752
1748
  // The definition the `task` call names, printed so the controller reads it
1753
- // off this output rather than recalling it from prose a compaction drops
1754
- // (#2208). An implementer's comes off its row's `tier=`, the same function
1749
+ // off this output rather than recalling it from prose a compaction drops.
1750
+ // An implementer's comes off its row's `tier=`, the same function
1755
1751
  // tier-check.mjs judges it by — so a row that names no single definition,
1756
1752
  // or names one with no file under `agents/` (a well-formed `tier=` whose
1757
1753
  // definition has not shipped: a `task` call naming it cannot resolve), is
@@ -1759,9 +1755,9 @@ function runDispatch() {
1759
1755
  // after the call.
1760
1756
  //
1761
1757
  // A fix-applier's turns on whether its PR sits on an unresolved conflict
1762
- // hold (#2299), and that is the tick's own verdict, not a second reading
1758
+ // hold, and that is the tick's own verdict, not a second reading
1763
1759
  // of row `i`: a PR's tokens can be split across its ticket row and its own
1764
- // `#<pr>` row (#2283), and a settle can live in `## Dispatched` alone, so
1760
+ // `#<pr>` row, and a settle can live in `## Dispatched` alone, so
1765
1761
  // one row's text can read held where the tick reads cleared, and the
1766
1762
  // reverse. A ledger the tick refuses is refused here too, before the member
1767
1763
  // is live, rather than named a definition off a reading the tick rejects.
@@ -1792,7 +1788,7 @@ function runDispatch() {
1792
1788
  }
1793
1789
  // Every error caught here carries its own cause and remedy — a row's `tier=`
1794
1790
  // (fix it with `ledger.mjs row`) for an implementer, a family
1795
- // ledger-grammar.mjs has no definition case for otherwise (#2330) — so this
1791
+ // ledger-grammar.mjs has no definition case for otherwise — so this
1796
1792
  // refusal's tail names neither.
1797
1793
  let agent;
1798
1794
  try {
@@ -1860,7 +1856,7 @@ function runSettle() {
1860
1856
  return;
1861
1857
  }
1862
1858
 
1863
- // #1876: capture whether the row this settle is about to write to already
1859
+ // Capture whether the row this settle is about to write to already
1864
1860
  // named a PR, before the rewrite below touches it. The fold further down
1865
1861
  // must refuse to run once a row already has PR-specific content on it —
1866
1862
  // see the fold's own comment for why.
@@ -1878,12 +1874,12 @@ function runSettle() {
1878
1874
  i = memberRowIndex(parsed);
1879
1875
  if (i !== -1 && !carries(data.rows[i], name)) data.rows[i] = `${data.rows[i]} · ${token}`;
1880
1876
  }
1881
- // #1876: the tick owes an open PR a review while its implementer is still
1877
+ // The tick owes an open PR a review while its implementer is still
1882
1878
  // live, so a PR-bound member can be dispatched before this settle names the
1883
1879
  // PR here — and memberRowIndex() then finds no row naming it, so
1884
1880
  // runDispatch() keys a row of its own to the PR. Once this row names the PR
1885
1881
  // too, two rows would: the cockpit draws two cards and the tick merges the
1886
- // two rows' tokens in row order (#2283), not in the order they were written.
1882
+ // two rows' tokens in row order, not in the order they were written.
1887
1883
  // Fold that row's tokens onto this one, in order, so later PR-bound writes
1888
1884
  // find the one row through rowPr(). Only
1889
1885
  // an implementer settles to `PR#M` (ledger-grammar.mjs), and only when the