@feigi/fleet-ctl 3.91.0 → 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.0",
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": {
@@ -412,10 +412,11 @@ export function mapCi(ciJson, pr) {
412
412
  // A receipt with no session to give is `{ error }`, which the panel shows:
413
413
  // the board names no session rather than guessing one. So is a recorded
414
414
  // session whose NAME is not an omp `<ISO>_<uuid>` session directory:
415
- // phase0.mjs accepts any existing directory as the session, so a `--session`
416
- // naming the encoded-cwd project dir instead of its own session child lands in
417
- // the receipt, and readOmpSpend would book every session under it — the
418
- // controller's own main-session transcript included — as this run's agents.
415
+ // phase0.mjs refuses a directory with no `<dir>.jsonl` beside it, but a
416
+ // receipt it wrote before that refusal, or one written by hand, can record the
417
+ // encoded-cwd project dir instead of its own session child, and readOmpSpend
418
+ // would book every session under it — the controller's own main-session
419
+ // transcript included — as this run's agents.
419
420
  // A recorded directory that does not exist yet is not an error — that is
420
421
  // gatherSpend()'s quiet `null`, below.
421
422
  export function receiptSpendDir(stateDir) {
@@ -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);
@@ -8,9 +8,11 @@
8
8
  // The session directory is `--session`, else `PI_ARTIFACTS_DIR`, which omp
9
9
  // exports to its Python eval kernel and the processes that kernel starts (the
10
10
  // bash tool's environment does not carry it); with neither, exit 2. It is
11
- // accepted when it is an existing directory, or when `<dir>.jsonl` exists
12
- // beside it — omp creates the directory when the session's first subagent
13
- // writes there.
11
+ // accepted when `<dir>.jsonl` exists beside it and the path is a directory or
12
+ // does not exist yet — omp creates the directory when the session's first
13
+ // subagent writes there. An existing directory with no `<dir>.jsonl` beside
14
+ // it names no session (the directory holding every session of a checkout is
15
+ // one): from either source it exits 2 naming the path, writing no receipt.
14
16
  //
15
17
  // Reached through the Resolver and run from the MAIN checkout (a linked
16
18
  // worktree refuses: the checks below judge the checkout the controller reads
@@ -528,15 +530,19 @@ function main() {
528
530
  }
529
531
  const source = given === null ? "PI_ARTIFACTS_DIR" : "--session";
530
532
  const session = resolve(given ?? fromEnv);
531
- // An existing directory, or `<session>.jsonl` beside a path that does not
532
- // exist yet: omp creates the directory when the session's first subagent
533
- // writes there.
533
+ // A session is named by its transcript, `<session>.jsonl`, beside the
534
+ // directory: an existing directory with none beside it — the one holding
535
+ // every session of a checkout, say — names no session. The directory itself
536
+ // may not exist yet: omp creates it when the session's first subagent writes
537
+ // there.
538
+ const transcript = existsSync(`${session}.jsonl`);
534
539
  try {
535
540
  if (!statSync(session).isDirectory()) die(`${source} ${session} is not a directory`);
541
+ if (!transcript) die(`${source} ${session} has no ${session}.jsonl beside it, so it names no session`);
536
542
  } catch (e) {
537
543
  if (e.code === undefined) throw e;
538
544
  if (e.code !== "ENOENT") die(`${source} ${session} is not readable (${e.code})`);
539
- if (!existsSync(`${session}.jsonl`)) die(`${source} ${session} does not exist, and no ${session}.jsonl beside it names a session`);
545
+ if (!transcript) die(`${source} ${session} does not exist, and no ${session}.jsonl beside it names a session`);
540
546
  }
541
547
  const positive = (name) => {
542
548
  const n = F.numArg(name);
@@ -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
@@ -7,7 +7,12 @@ environment carries `PI_ARTIFACTS_DIR`, this run's own session directory,
7
7
  which phase 0 records in its receipt; the bash tool's does not, so there
8
8
  phase 0 exits 2. `expanduser` is required: a literal `~/…` fails with
9
9
  `FileNotFoundError`. So is the `stderr` redirect: the cell never shows a
10
- child's stderr. `--session <dir>` overrides the variable.
10
+ child's stderr. `--session <dir>` overrides the variable. Either way the
11
+ directory must have its transcript `<dir>.jsonl` beside it; the directory
12
+ itself may not exist yet. An existing directory with no `<dir>.jsonl` beside
13
+ it — `~/.omp/agent/sessions/<encoded-cwd>`, which holds every session of the
14
+ checkout — names no session: phase 0 exits 2 naming the path and where it
15
+ came from, and writes no receipt.
11
16
 
12
17
  Exit 3 is a step that failed: its line carries the refusal, so fix the cause
13
18
  and run it again.