@feigi/fleet-ctl 3.82.19 → 3.82.21

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.
@@ -18,9 +18,9 @@ Your report is the one file you write outside it: `<scratch>/reports/merge-bot-<
18
18
  the path your brief names, holding every PR you merged in `merged`. Your final
19
19
  message is the one line your brief gives, `merge-bot-<n>: done <path>`.
20
20
 
21
- The shell traps below are copied from the controller's skill file,
22
- `run-team/SKILL.md`. In them **Own the CI waits** is that file's section on the
23
- controller's CI watcher, which you do not run, and "this file" is that skill file; each trap applies to every shell
21
+ The shell traps below are copied from the controller's skill, `run-team`. In
22
+ them `references/ci-watcher.md` is that skill's file on the controller's CI
23
+ watcher, which you do not run; each trap applies to every shell
24
24
  command and loop you run.
25
25
 
26
26
  **zsh word-splits an unquoted command substitution's result; it does NOT split
@@ -54,7 +54,7 @@ EOF
54
54
  ```
55
55
 
56
56
  The inline `$(printf '%s\n' "$prs")` form splits correctly too and is what the
57
- watcher block under **Own the CI waits** uses, but it splits on *every* whitespace character, so it is safe only
57
+ watcher block in `references/ci-watcher.md` uses, but it splits on *every* whitespace character, so it is safe only
58
58
  for values that cannot contain one — PR numbers cannot, branch names and paths
59
59
  can. Measured on a two-line list whose first value is `two words`: three
60
60
  iterations through `$(printf …)`, two through `while IFS= read -r`. On an empty
@@ -70,8 +70,8 @@ word-split bug, so a correct heredoc fix missing this half looks exactly like no
70
70
  fix at all, and reads as one.
71
71
 
72
72
  **A probe that could not look emits an event; it never `continue`s.** Stated in
73
- full at *A probe that cannot read is a transition event, not silence* under **Own the CI waits**,
74
- and it binds every watcher in this file rather than that one block: the third
73
+ full at *A probe that cannot read is a transition event, not silence* in `references/ci-watcher.md`,
74
+ and it binds every watcher rather than that one block: the third
75
75
  value beside pass and fail is what turns a broken loop into a loud failure
76
76
  instead of a quiet one. A guard that drops an unreadable probe — `[ -n "$json" ]
77
77
  || continue` — makes *could not look* indistinguishable from *nothing to report*.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feigi/fleet-ctl",
3
- "version": "3.82.19",
3
+ "version": "3.82.21",
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": {
@@ -20,7 +20,12 @@
20
20
  // lack a member row (nothing from that run was checked). `--live`
21
21
  // (a run in progress; CI never passes it) adds a WARNING, never a
22
22
  // failure, per PR whose run-ledger row carries `reviewed=` but which has
23
- // no row here. `--ledger <path>` names another ledger than the run's own.
23
+ // no row here, except a PR that closes no issue: `append` keys a row on
24
+ // exactly one ticket and refuses it, so it is named in a `note` line and
25
+ // never warned about. A read of that PR that does not answer keeps the
26
+ // warning, after a line naming why the read failed. Both reads of a PR's
27
+ // closing references ignore an ambient GH_REPO and give `gh` 60s.
28
+ // `--ledger <path>` names another ledger than the run's own.
24
29
  //
25
30
  // `tier` resolution, in order (never tie-broken by `run_date`: the two files'
26
31
  // `run_date`s mean different things — this file's header measures 92 of 267
@@ -445,20 +450,37 @@ function readLedger(ledgerFile, requireFile) {
445
450
  }
446
451
  }
447
452
 
448
- function closingTicket(pr) {
453
+ // A PR's closingIssuesReferences array, or an Error naming why it could not be
454
+ // read. GH_REPO is blanked so an ambient one naming another repository never
455
+ // decides which PR is read; the timeout keeps a hung `gh` from stalling
456
+ // `append` or a close-out's `check --live`.
457
+ const GH_TIMEOUT_MS = 60_000;
458
+ function closingRefs(pr) {
449
459
  let raw;
450
460
  try {
451
- raw = execFileSync("gh", ["pr", "view", pr, "--json", "closingIssuesReferences"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
461
+ raw = execFileSync("gh", ["pr", "view", String(pr), "--json", "closingIssuesReferences"], {
462
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, GH_REPO: "" }, timeout: GH_TIMEOUT_MS,
463
+ });
452
464
  } catch (e) {
453
- die(`gh pr view ${pr} failed: ${e.stderr?.trim() || e.message}`);
465
+ throw new Error(`gh pr view ${pr} failed: ${e.stderr?.trim() || e.message}`);
454
466
  }
455
467
  let refs;
456
468
  try {
457
469
  refs = JSON.parse(raw).closingIssuesReferences;
458
470
  } catch (e) {
459
- die(`gh pr view ${pr} returned unreadable JSON: ${e.message}`);
471
+ throw new Error(`gh pr view ${pr} returned unreadable JSON: ${e.message}`);
472
+ }
473
+ if (!Array.isArray(refs)) throw new Error(`gh pr view ${pr} returned no closingIssuesReferences array`);
474
+ return refs;
475
+ }
476
+
477
+ function closingTicket(pr) {
478
+ let refs;
479
+ try {
480
+ refs = closingRefs(pr);
481
+ } catch (e) {
482
+ die(e.message);
460
483
  }
461
- if (!Array.isArray(refs)) die(`gh pr view ${pr} returned no closingIssuesReferences array`);
462
484
  const numbers = [...new Set(refs.map((r) => r?.number))];
463
485
  if (numbers.length !== 1 || !Number.isInteger(numbers[0])) {
464
486
  die(`PR #${pr} closes ${numbers.length === 0 ? "no issue" : `${numbers.length} issues (${numbers.map((n) => `#${n}`).join(", ")})`} per GitHub's closingIssuesReferences — a tier-outcomes row keys on exactly one ticket`);
@@ -534,6 +556,20 @@ function append(pr, paths) {
534
556
  console.log(line);
535
557
  }
536
558
 
559
+ // True only when `gh` answered and the PR's closingIssuesReferences is an empty
560
+ // array. Any other outcome (gh failing, unreadable JSON, a non-array) is false
561
+ // after a line naming why: a PR whose closing references cannot be read keeps
562
+ // its warning. That line never reads `WARNING PR #`, which closeout.mjs turns
563
+ // into a NEED.
564
+ function closesNoIssue(pr) {
565
+ try {
566
+ return closingRefs(pr).length === 0;
567
+ } catch (e) {
568
+ console.error(`${NAME}: could not read the closing references of PR #${pr}: ${e.message}`);
569
+ return false;
570
+ }
571
+ }
572
+
537
573
  function check(paths) {
538
574
  for (const flag of Object.keys(APPEND_FLAGS)) {
539
575
  if (arg(flag) !== null) die(`--${flag} is an append flag`);
@@ -546,6 +582,10 @@ function check(paths) {
546
582
  if (has("live")) {
547
583
  const ledger = readLedger(paths.ledger, true);
548
584
  for (const pr of unrecordedReviewedPrs(ledger, rows)) {
585
+ if (closesNoIssue(pr)) {
586
+ console.error(`${NAME}: note PR #${pr} carries reviewed= on the ledger and closes no issue, so it takes no row in ${paths.file}`);
587
+ continue;
588
+ }
549
589
  console.error(`${NAME}: WARNING PR #${pr} carries reviewed= on the ledger but has no row in ${paths.file} — run \`tier-outcomes.mjs append ${pr}\` if it is an implementer PR`);
550
590
  }
551
591
  }