@feigi/fleet-ctl 3.91.1 → 3.91.2

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.91.1",
3
+ "version": "3.91.2",
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": {
@@ -1984,7 +1984,8 @@ function checkReaders(where) {
1984
1984
  // A `gh pr view --json <fields>` answer: an object whose every requested
1985
1985
  // field has the type that field has. Anything else was not what was asked
1986
1986
  // for, so it could not look.
1987
- const prFieldOk = { state: (v) => typeof v === "string", labels: Array.isArray, closingIssuesReferences: Array.isArray };
1987
+ const isString = (v) => typeof v === "string";
1988
+ const prFieldOk = { state: isString, labels: Array.isArray, closingIssuesReferences: Array.isArray, url: isString };
1988
1989
  const prView = (pr, fields) => {
1989
1990
  const what = `gh pr view ${pr} --json ${fields}`;
1990
1991
  const { r, v } = lookJson(what, ["pr", "view", String(pr), "--json", fields]);
@@ -1997,6 +1998,34 @@ function checkReaders(where) {
1997
1998
  }
1998
1999
  return v;
1999
2000
  };
2001
+ // PR `pr`'s prView() answer to `fields`, which name closingIssuesReferences
2002
+ // and url, as `view`, with the issue numbers, unique, of its closing
2003
+ // references that are in the PR's own repository: its url less
2004
+ // `/pull/<pr>`. A reference is in it when its url is that repository's
2005
+ // `/issues/<number>`, or, carrying no url, when its repository's
2006
+ // `<owner>/<name>` ends that repository's url. A reference with no integer
2007
+ // number, or with neither a url nor a repository to place it by, could not
2008
+ // look: it is never taken for the PR's own repository's.
2009
+ const prViewClosing = (pr, fields) => {
2010
+ const view = prView(pr, fields);
2011
+ const what = `gh pr view ${pr} --json ${fields}`;
2012
+ const suffix = `/pull/${pr}`;
2013
+ if (!view.url.endsWith(suffix)) die(`${where}: could not look — ${what} answered url ${view.url}, not PR #${pr}'s; nothing written`);
2014
+ const own = view.url.slice(0, -suffix.length);
2015
+ const str = (v) => typeof v === "string" && v !== "";
2016
+ const numbers = new Set();
2017
+ for (const ref of view.closingIssuesReferences) {
2018
+ const n = ref?.number;
2019
+ if (!Number.isInteger(n)) die(`${where}: could not look — ${what} answered a reference with no issue number; nothing written`);
2020
+ const repo = ref.repository;
2021
+ let isOwn;
2022
+ if (str(ref.url)) isOwn = ref.url === `${own}/issues/${n}`;
2023
+ else if (str(repo?.name) && str(repo?.owner?.login)) isOwn = own.endsWith(`/${repo.owner.login}/${repo.name}`);
2024
+ else die(`${where}: could not look — ${what} answered a reference to #${n} with neither a url nor a repository; nothing written`);
2025
+ if (isOwn) numbers.add(n);
2026
+ }
2027
+ return { view, numbers: [...numbers] };
2028
+ };
2000
2029
  // The title of issue `n`, as `gh issue view <n> --json title` answers it.
2001
2030
  const issueTitle = (n) => {
2002
2031
  const what = `gh issue view ${n} --json title`;
@@ -2006,28 +2035,27 @@ function checkReaders(where) {
2006
2035
  }
2007
2036
  return v.title;
2008
2037
  };
2009
- return { repoDir, look, verifyShaOf, prView, issueTitle };
2038
+ return { repoDir, look, verifyShaOf, prView, prViewClosing, issueTitle };
2010
2039
  }
2011
2040
 
2012
2041
  // The finisher gate's closing-reference checks for the PR `member` finishes,
2013
- // read off the PR's closingIssuesReferences and the titles of the issues it
2014
- // names; its second argument is rulingOf()'s answer, which let the PR
2015
- // through. Check 1, when a settled implementer's token names the PR: each
2016
- // ticket it names is closed by the body when the PR's ruling is
2017
- // closed_own_ticket=yes, and is not when the ruling is no. Check 2, for every
2018
- // PR: no reference may be the suggestion-band record of this PR's review — an
2019
- // issue whose title, trimmed, is recordTitle(), as dispositions-check.mjs
2020
- // finds the record. Returns one line per failure. A read the checks need
2021
- // dies, naming the read, and writes nothing.
2042
+ // read off the PR's closingIssuesReferences in its own repository — a
2043
+ // reference into another repository closes no issue here, whatever its
2044
+ // number — and the titles of the issues they name; its second argument is
2045
+ // rulingOf()'s answer, which let the PR through. Check 1, when a settled
2046
+ // implementer's token names the PR: each ticket it names is closed by the
2047
+ // body when the PR's ruling is closed_own_ticket=yes, and is not when the
2048
+ // ruling is no. Check 2, for every PR: no reference may be the
2049
+ // suggestion-band record of this PR's review — an issue whose title, trimmed,
2050
+ // is recordTitle(), as dispositions-check.mjs finds the record. Returns one
2051
+ // line per failure. A read the checks need dies, naming the read, and writes
2052
+ // nothing.
2022
2053
  async function closingRefusals(member, { tickets, ruling, tsv }) {
2023
2054
  const pr = member.number;
2024
2055
  const where = `dispatch ${member.name}`;
2025
2056
  const { recordTitle } = await import("./dispositions-check.mjs");
2026
- const { prView, issueTitle } = checkReaders(where);
2027
- const refs = [...new Set(prView(pr, "closingIssuesReferences").closingIssuesReferences.map((r) => r?.number))];
2028
- if (!refs.every(Number.isInteger)) {
2029
- die(`${where}: could not look — gh pr view ${pr} --json closingIssuesReferences answered a reference with no issue number; nothing written`);
2030
- }
2057
+ const { prViewClosing, issueTitle } = checkReaders(where);
2058
+ const refs = prViewClosing(pr, "closingIssuesReferences,url").numbers;
2031
2059
  const failures = [];
2032
2060
  if (tickets.length > 0) {
2033
2061
  const verdict = ruling.closed_own_ticket;
@@ -2491,7 +2519,8 @@ function recordFiled(id, subject) {
2491
2519
  // checked before anything is written:
2492
2520
  //
2493
2521
  // impl-N=PR#M verify-sha.sh <branch> <head> exits 0; PR M
2494
- // reads OPEN and its closingIssuesReferences name N;
2522
+ // reads OPEN and its closingIssuesReferences in its
2523
+ // own repository name N;
2495
2524
  // the Check-step record (below) holds for <head>
2496
2525
  // impl-N=bailed bailCause is not null; the receipt names it
2497
2526
  // fix-pr-M=applied:<head> origin's refs/heads/<branch> is <head>, fetched
@@ -2603,7 +2632,7 @@ async function runSettleFromReport() {
2603
2632
  if (report.member !== member) invalid(`$.member: ${JSON.stringify(report.member)} is not ${member}`);
2604
2633
 
2605
2634
  // The gh and git reads this check makes, shared with the finisher gate.
2606
- const { repoDir, look, verifyShaOf, prView } = checkReaders(where);
2635
+ const { repoDir, look, verifyShaOf, prView, prViewClosing } = checkReaders(where);
2607
2636
  // origin's head of `branch`, or null when origin has no such branch.
2608
2637
  const remoteHead = (branch) => {
2609
2638
  const ref = `refs/heads/${branch}`;
@@ -2653,9 +2682,9 @@ async function runSettleFromReport() {
2653
2682
  const sha = verifyShaOf(report.branch, report.head);
2654
2683
  check(`verify-sha.sh ${report.branch} ${report.head} exits 0`, sha.status === 0);
2655
2684
  recipeGate(report.head);
2656
- const pr = prView(report.pr, "state,closingIssuesReferences");
2685
+ const { view: pr, numbers } = prViewClosing(report.pr, "state,closingIssuesReferences,url");
2657
2686
  check(`PR #${report.pr} reads OPEN`, pr.state === "OPEN");
2658
- check(`PR #${report.pr} closes #${live.number}`, pr.closingIssuesReferences.some((c) => c?.number === live.number));
2687
+ check(`PR #${report.pr} closes #${live.number}`, numbers.includes(live.number));
2659
2688
  } else {
2660
2689
  outcome = "bailed";
2661
2690
  check(`bailCause: ${report.bailCause}`, report.bailCause !== null);
@@ -117,7 +117,7 @@ and will act on it. Relay everything — reversals too — then dispatch.
117
117
  append` command to run. Any row for the PR passes, so a replacement finisher
118
118
  is never held once the first passed; a PR no such token names is not gated.
119
119
  No flag skips it: a PR `append` refuses is fixed on the ledger, as at close-out.
120
- - **The PR's closing references match its ruling and exclude its own suggestion-band record** → `ledger.mjs dispatch` reads the PR's `closingIssuesReferences` from `gh pr view <M>` before it dispatches any finisher. When the ruled row says `closed_own_ticket=yes`, each ticket a settled `impl-<N>=PR#<M>` token names must be closed by the body (`Closes #<N>`); when it says `no`, none may be, and no reference may be the PR's own suggestion-band record. A blank verdict is refused until it is set. A refusal names the PR and the ticket. The controller repairs the body itself with `gh pr edit <M> --body-file <file>`, then dispatches the finisher again. A `gh` read that fails refuses the dispatch and writes nothing.
120
+ - **The PR's closing references match its ruling and exclude its own suggestion-band record** → `ledger.mjs dispatch` reads the PR's `closingIssuesReferences` from `gh pr view <M>` before it dispatches any finisher, counting only those in the PR's own repository: a reference into another repository closes none of this repository's tickets, whatever its number. When the ruled row says `closed_own_ticket=yes`, each ticket a settled `impl-<N>=PR#<M>` token names must be closed by the body (`Closes #<N>`); when it says `no`, none may be, and no reference may be the PR's own suggestion-band record. A blank verdict is refused until it is set. A refusal names the PR and the ticket. The controller repairs the body itself with `gh pr edit <M> --body-file <file>`, then dispatches the finisher again. A `gh` read that fails refuses the dispatch and writes nothing.
121
121
 
122
122
  Gate on the `check` job, **not** on `ci-state --quiet` exit 0: a behind PR never
123
123
  reaches full green, so an exit-0 gate strands it unlabelled. The finisher reads