toolroll 0.9.79 → 0.9.81

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.81 — 2026-10-30
4
+
5
+ - **The release proof works after the merge.** GitHub empties a CI run's
6
+ pull-request link once the pull request merges and its branch is deleted,
7
+ so 0.9.80's tag could never prove its checks and wasn't published. A run
8
+ with no link now stands only when its branch has had exactly one pull
9
+ request, ever, and that pull request merged into main. 0.9.80's changes
10
+ ship in this release.
11
+
12
+ ## 0.9.80 — 2026-10-30
13
+
14
+ - **Each test runs once, where it adds something** (docs/plans/test-matrix.md).
15
+ Pull requests run the unit suite on Node 24 (Linux and macOS) and every
16
+ area lane (Windows, Linux fence and cgroup, git 2.41 and 2.47); a pull
17
+ request that only changes Markdown under `docs/`, the changelog or the
18
+ README skips the area lanes. Node 22 runs on Linux and macOS once per
19
+ release, on the `release-*` branch's pull request, in the new
20
+ Release candidate workflow. Release tags no longer rerun the macOS suites:
21
+ a release proof reads GitHub's record that the tagged tree passed every
22
+ required check on its pull request before npm publication. A push to main
23
+ typechecks and builds, and runs the full CI only when it can't prove the
24
+ merge commit was tested. The nightly runs everything. A release now needs
25
+ every required check executed, never skipped, and refuses to merge while
26
+ main's branch protection doesn't require all of them. Hosted unit shards
27
+ require their platform's native fence attacks to run.
28
+
3
29
  ## 0.9.79 — 2026-10-29
4
30
 
5
31
  - **`toolroll release` rides out a short network failure instead of
package/dist/guides.js CHANGED
@@ -473,20 +473,23 @@ with a time limit.
473
473
  saved first; if either name doesn't answer as the new build, both go
474
474
  back. It goes on only when the console, worker and both names run the
475
475
  gated commit. A rerun relinks without deploying again.
476
- 8. **ci**: waits for every pull-request check on the gated commit:
477
- Ubuntu on Node 22 and 24, Linux and Windows containment. Pending,
478
- missing, skipped, cancelled or failed never merges.
476
+ 8. **ci**: waits for every pull-request check on the gated commit: Node 24
477
+ on Ubuntu and macOS, the area lanes (Linux cgroup and fence, Windows,
478
+ git 2.41 and 2.47), and Node 22 on Ubuntu and macOS, which runs on a
479
+ release-* branch's pull request.
480
+ Pending, missing, skipped, cancelled or failed never merges, and main's
481
+ protection must require every one of them.
479
482
  9. **merge**: squash-merges with the head pinned to the gated commit,
480
483
  once main has nothing the branch lacks, then deletes the remote branch
481
484
  only if its head still matches. A resumed merge also finishes this cleanup.
482
485
  10. **tree**: main's merge commit must have exactly the gated tree, or
483
486
  nothing is tagged.
484
- 11. **tag**: pushes \`v<version>\` on that merge commit. In publish.yml,
485
- macOS Node 22 and 24 must both pass before npm and GitHub publication
486
- (the candidate's macOS checks already passed in ci.yml before the merge).
487
- macos.yml keeps the nightly matrix.
488
- 12. **publish**: waits for npm, the GitHub release and the tag's publish
489
- job (which needs the macOS legs); not main's own CI on the merge commit.
487
+ 11. **tag**: pushes \`v<version>\` on that merge commit. The tag runs no
488
+ tests again: publish.yml's release proof reads GitHub's record that the
489
+ tagged tree is the merged pull request's head tree and passed every
490
+ required check there, and npm and GitHub publication need it.
491
+ 12. **publish**: waits for npm, the GitHub release and the tag's release
492
+ proof and publish jobs; not main's own push run on the merge commit.
490
493
  13. **homebrew**: the release is published once npm and the GitHub release
491
494
  are verified. It proposes the tap's formula change (checksum from npm's
492
495
  tarball) and looks at its checks once: \`Published; Homebrew pending\`
@@ -1290,7 +1290,7 @@ export async function readMainProtection(run, gh, repo, branch = "main") {
1290
1290
  else {
1291
1291
  const lacking = PROTECTION_MINIMUM_CHECKS.filter(name => !checks.includes(name));
1292
1292
  if (lacking.length > 0)
1293
- own.push(`${where}: required checks lack ${lacking.join(", ")}`);
1293
+ own.push(`${where}: required checks lack ${lacking.join(", ")} (add ${lacking.length === 1 ? "it" : "them"} to main's required status checks: a release's proof trusts what main required)`);
1294
1294
  const unbound = PROTECTION_MINIMUM_CHECKS.filter(name => checks.includes(name) && !bound.includes(name));
1295
1295
  if (unbound.length > 0)
1296
1296
  own.push(`${where}: required checks must require GitHub Actions (app 15368) as their source: ${unbound.join(", ")}`);
@@ -19,7 +19,7 @@ export type ObservedCheckRun = {
19
19
  suite?: number | null;
20
20
  head?: string | null;
21
21
  workflow?: number | null;
22
- /** The workflow's file (GitHub's workflow-run `path`): a required check counts only from ci.yml. */
22
+ /** The workflow's file (GitHub's workflow-run `path`): a required check counts only from its own (REQUIRED_PR_CHECKS_WORKFLOW). */
23
23
  workflowPath?: string | null;
24
24
  /** GitHub's job record shows this unsuccessful attempt ended before any workflow step ran (release-adapters.ts). */
25
25
  beforeTests?: boolean | null;
package/dist/release.d.ts CHANGED
@@ -32,16 +32,18 @@
32
32
  * Everything outside this module is an adapter (release-adapters.ts), so the state machine is tested without git,
33
33
  * GitHub, npm or a running Toolroll.
34
34
  */
35
- /** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
36
- * concluded `success` on the gated commit; src/ci-workflow.test.ts holds the workflow to this list. */
37
- export declare const REQUIRED_PR_CHECKS: readonly ["node 22 · ubuntu-latest", "node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment", "git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 22 · macos-latest", "node 24 · macos-latest"];
38
- /** The workflow file every REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
35
+ /** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
36
+ * concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
37
+ * this list, and publish.yml's release proof to the same list. */
38
+ export declare const REQUIRED_PR_CHECKS: readonly ["node 24 · ubuntu-latest", "linux native containment · delegated cgroup v2", "linux fence · sandbox-runtime", "Windows Job Object containment", "git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 24 · macos-latest", "node 22 · ubuntu-latest", "node 22 · macos-latest"];
39
+ /** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
39
40
  * added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
40
- export declare const REQUIRED_PR_CHECKS_WORKFLOW = ".github/workflows/ci.yml";
41
+ export declare const REQUIRED_PR_CHECKS_WORKFLOW: Readonly<Record<typeof REQUIRED_PR_CHECKS[number], string>>;
41
42
  /**
42
- * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1): the
43
- * git lanes join main's protection once they have run there, so a protection without them is reported, never refused.
44
- * A later release empties this list; from then on main's protection must name every REQUIRED_PR_CHECKS check.
43
+ * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
44
+ * Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
45
+ * required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
46
+ * (and so to publish) until it does.
45
47
  */
46
48
  export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
47
49
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
@@ -57,10 +59,11 @@ export type MainProtection = {
57
59
  missing: string[];
58
60
  notYetRequired: string[];
59
61
  };
60
- /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, pipeline-speed.md item 3):
61
- * the publish job and the macOS legs it needs, each named. Nothing else reported on the merge commit (main's own CI push
62
- * run) holds publication. */
63
- export declare const RELEASE_TAG_CHECKS: readonly ["publish", "tag macOS · node 22", "tag macOS · node 24"];
62
+ /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
63
+ * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
64
+ * the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
65
+ * holds publication. */
66
+ export declare const RELEASE_TAG_CHECKS: readonly ["release proof", "publish"];
64
67
  export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
65
68
  export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
66
69
  export type ReleaseStep = typeof RELEASE_STEPS[number];
@@ -662,10 +665,10 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
662
665
  failed: string[];
663
666
  };
664
667
  /**
665
- * Publication's verdict on the tag commit (pipeline-speed.md, item 3): only RELEASE_TAG_CHECKS, from GitHub Actions
666
- * runs of publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's
667
- * own CI run — is not waited for: the candidate's checks already passed on its exact head before the merge, and the
668
- * merged tree was proved equal to it.
668
+ * Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
669
+ * publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
670
+ * push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
671
+ * tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
669
672
  */
670
673
  export declare function tagChecksVerdict(runs: readonly CheckRun[]): ReturnType<typeof checksVerdict>;
671
674
  /** Run the release from wherever its journal stopped. */
@@ -686,7 +689,7 @@ export declare function transientNetworkError(error: unknown): boolean;
686
689
  */
687
690
  export declare function prChecksVerdict(runs: readonly CheckRun[], head: string, required: readonly string[], options?: {
688
691
  auto?: true;
689
- workflow?: string;
692
+ workflow?: string | Readonly<Record<string, string>>;
690
693
  }): {
691
694
  state: "passed";
692
695
  failures: CheckFailure[];
package/dist/release.js CHANGED
@@ -38,37 +38,54 @@ import { dirname, join } from "node:path";
38
38
  import { personProof } from "./person-proof.js";
39
39
  import { RELEASE_CI_RUNNING_WORDS as CI_RUNNING_WORDS } from "./release-ci-hold.js";
40
40
  import { transientFailure } from "./release-net.js";
41
- /** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
42
- * concluded `success` on the gated commit; src/ci-workflow.test.ts holds the workflow to this list. */
41
+ /** The pull-request checks a release waits for, by exact job name (docs/plans/test-matrix.md). Every one must have
42
+ * concluded `success` — executed, never skipped — on the gated commit; src/ci-workflow.test.ts holds the workflows to
43
+ * this list, and publish.yml's release proof to the same list. */
43
44
  export const REQUIRED_PR_CHECKS = [
44
- "node 22 · ubuntu-latest",
45
+ // The unit suite on Node 24 (ci.yml "test-shard" and "test"), on every pull request.
45
46
  "node 24 · ubuntu-latest",
47
+ // The area lanes (ci.yml), on every pull request except a documentation-only one (scripts/ci-scope.mjs), where they
48
+ // are skipped; a release's pull request runs them.
46
49
  "linux native containment · delegated cgroup v2",
47
50
  "linux fence · sandbox-runtime",
48
51
  "Windows Job Object containment",
49
52
  // The git lanes (ci.yml job "git"): the declared floor (git-floor.ts) and Debian trixie's 2.47.
50
53
  "git 2.41 · debian trixie",
51
54
  "git 2.47 · debian trixie",
52
- // The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a): tested on the candidate
53
- // before it merges, no longer only on the tag after.
54
- "node 22 · macos-latest",
55
+ // The unit suite on macOS (ci.yml jobs "macos-shard" and "macos"; pipeline-speed.md, item 3a), Node 24.
55
56
  "node 24 · macos-latest",
57
+ // Node 22, the engines floor, on Linux and macOS (release-candidate.yml): on a release-* branch's pull request,
58
+ // skipped on every other pull request.
59
+ "node 22 · ubuntu-latest",
60
+ "node 22 · macos-latest",
56
61
  ];
57
- /** The workflow file every REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
62
+ /** The workflow file each REQUIRED_PR_CHECKS job belongs to: a job of that name from any other workflow (a lookalike
58
63
  * added beside it) never stands for it (Codex review round 1, inherited CI limitation). */
59
- export const REQUIRED_PR_CHECKS_WORKFLOW = ".github/workflows/ci.yml";
64
+ export const REQUIRED_PR_CHECKS_WORKFLOW = {
65
+ "node 24 · ubuntu-latest": ".github/workflows/ci.yml",
66
+ "linux native containment · delegated cgroup v2": ".github/workflows/ci.yml",
67
+ "linux fence · sandbox-runtime": ".github/workflows/ci.yml",
68
+ "Windows Job Object containment": ".github/workflows/ci.yml",
69
+ "git 2.41 · debian trixie": ".github/workflows/ci.yml",
70
+ "git 2.47 · debian trixie": ".github/workflows/ci.yml",
71
+ "node 24 · macos-latest": ".github/workflows/ci.yml",
72
+ "node 22 · ubuntu-latest": ".github/workflows/release-candidate.yml",
73
+ "node 22 · macos-latest": ".github/workflows/release-candidate.yml",
74
+ };
60
75
  /**
61
- * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1): the
62
- * git lanes join main's protection once they have run there, so a protection without them is reported, never refused.
63
- * A later release empties this list; from then on main's protection must name every REQUIRED_PR_CHECKS check.
76
+ * The pull-request checks main's GitHub protection is not yet required to name (eval-harness review 2, finding 1).
77
+ * Empty since the test matrix (docs/plans/test-matrix.md): publication's release proof trusts what main's protection
78
+ * required at the merge, so main's protection must name every REQUIRED_PR_CHECKS check, and a release refuses to merge
79
+ * (and so to publish) until it does.
64
80
  */
65
- export const PROTECTION_REPORT_ONLY_CHECKS = ["git 2.41 · debian trixie", "git 2.47 · debian trixie", "node 22 · macos-latest", "node 24 · macos-latest"];
81
+ export const PROTECTION_REPORT_ONLY_CHECKS = [];
66
82
  /** The checks main's protection must require at minimum: every required check that isn't report-only. */
67
83
  export const PROTECTION_MINIMUM_CHECKS = REQUIRED_PR_CHECKS.filter(name => !PROTECTION_REPORT_ONLY_CHECKS.includes(name));
68
- /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, pipeline-speed.md item 3):
69
- * the publish job and the macOS legs it needs, each named. Nothing else reported on the merge commit (main's own CI push
70
- * run) holds publication. */
71
- export const RELEASE_TAG_CHECKS = ["publish", "tag macOS · node 22", "tag macOS · node 24"];
84
+ /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
85
+ * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
86
+ * the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
87
+ * holds publication. */
88
+ export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
72
89
  export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
73
90
  export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
74
91
  /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
@@ -192,10 +209,10 @@ export function checksVerdict(runs, required) {
192
209
  return waiting.length > 0 ? { state: "pending", waiting } : { state: "passed" };
193
210
  }
194
211
  /**
195
- * Publication's verdict on the tag commit (pipeline-speed.md, item 3): only RELEASE_TAG_CHECKS, from GitHub Actions
196
- * runs of publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's
197
- * own CI run — is not waited for: the candidate's checks already passed on its exact head before the merge, and the
198
- * merged tree was proved equal to it.
212
+ * Publication's verdict on the tag commit (test-matrix.md): only RELEASE_TAG_CHECKS, from GitHub Actions runs of
213
+ * publish.yml bound to the tag's push, each by its newest attempt. Every other check on the merge commit — main's own
214
+ * push run — is not waited for: the candidate's checks already passed on its exact head before the merge, the merged
215
+ * tree was proved equal to it, and publish.yml's release proof checks both again from GitHub's record.
199
216
  */
200
217
  export function tagChecksVerdict(runs) {
201
218
  const names = RELEASE_TAG_CHECKS;
@@ -1422,9 +1439,12 @@ export function prChecksVerdict(runs, head, required, options = {}) {
1422
1439
  }
1423
1440
  // A required name counts only from the workflow that defines it: a job of the same name in another workflow (or
1424
1441
  // one whose workflow GitHub doesn't name) fails the verdict rather than stand in for it.
1425
- if (options.workflow !== undefined && required.includes(run.name) && !workflowIs(run.workflowPath, options.workflow)) {
1426
- failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${options.workflow})`);
1427
- continue;
1442
+ if (options.workflow !== undefined && required.includes(run.name)) {
1443
+ const want = typeof options.workflow === "string" ? options.workflow : options.workflow[run.name];
1444
+ if (want === undefined || !workflowIs(run.workflowPath, want)) {
1445
+ failed.push(`${run.name} (from ${run.workflowPath ?? "a workflow GitHub doesn't name"}, not ${want ?? "a workflow this release knows for it"})`);
1446
+ continue;
1447
+ }
1428
1448
  }
1429
1449
  if (run.app === "github-actions" && (typeof run.suite !== "number" || !Number.isSafeInteger(run.suite))) {
1430
1450
  failed.push(`${run.name} (no workflow run)`);
@@ -1468,6 +1488,13 @@ export function prChecksVerdict(runs, head, required, options = {}) {
1468
1488
  waiting.push(`${name} (${latest.status})`);
1469
1489
  continue;
1470
1490
  }
1491
+ // Skipped is never success (test-matrix.md): GitHub's protection reads a skipped job as passing a merge, a release
1492
+ // never does. A documentation-only pull request skips the area lanes, and any pull request but a release-* branch's
1493
+ // skips Node 22, so a release needs every lane executed on its exact head.
1494
+ if (latest.conclusion === "skipped" && required.includes(name)) {
1495
+ failed.push(`${name} (skipped: a release needs it run on this exact commit; release from a branch named release-*, whose pull request runs every lane)`);
1496
+ continue;
1497
+ }
1471
1498
  if (latest.conclusion !== "success") {
1472
1499
  failed.push(`${name} (${latest.conclusion ?? "no conclusion"})`);
1473
1500
  continue;
@@ -1525,5 +1552,12 @@ export function checkFailedWords(input) {
1525
1552
  }
1526
1553
  /** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
1527
1554
  export function workflowIs(path, file) {
1528
- return typeof path === "string" && (path === file || path.startsWith(`${file}@`));
1555
+ if (typeof path !== "string")
1556
+ return false;
1557
+ if (path === file)
1558
+ return true;
1559
+ // GitHub may append @ref, but ci.yml@evil.yml is also a valid *different* workflow filename. Multiple @ signs
1560
+ // likewise cannot distinguish that filename from a ref. Refuse ambiguous suffixes instead of granting its checks.
1561
+ const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
1562
+ return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
1529
1563
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.79",
3
+ "version": "0.9.81",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"