toolroll 0.9.76 → 0.9.78
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 +273 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +46 -10
- package/dist/assignment.js +3 -0
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +18 -3
- package/dist/builder.js +152 -47
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +295 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +67 -6
- package/dist/check-report.js +254 -9
- package/dist/checkout-cleanup.js +2 -2
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-service.js +4 -1
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.js +5 -0
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +9 -5
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +19 -4
- package/dist/exec.d.ts +9 -0
- package/dist/exec.js +22 -2
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/grant.js +5 -1
- package/dist/guides.js +22 -11
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +969 -328
- package/dist/planner.js +2 -2
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +2 -2
- package/dist/proof.js +6 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +80 -2
- package/dist/release-adapters.js +441 -16
- package/dist/release-authority.js +6 -2
- package/dist/release-auto.d.ts +38 -7
- package/dist/release-auto.js +131 -15
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +95 -17
- package/dist/release-coverage.js +5 -2
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +8 -2
- package/dist/release.d.ts +144 -14
- package/dist/release.js +421 -81
- package/dist/review-context.js +8 -6
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +73 -9
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +26 -3
- package/dist/scout.js +2 -2
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +112 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +8 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/task-views.js +7 -2
- package/dist/server/tasks.js +4 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage-sweep.js +1 -1
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +135 -2
- package/dist/store.js +275 -13
- package/dist/surface.js +3 -1
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +52 -3
- package/dist/teardown-git.js +145 -33
- package/dist/test-infra.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +81 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/workspace-revision.js +3 -1
- package/package.json +2 -1
package/dist/release-cli.js
CHANGED
|
@@ -14,7 +14,8 @@ const sqlite = () => createRequire(import.meta.url)("node:sqlite");
|
|
|
14
14
|
import { checkedEnvelopeJson } from "./contracts/cli.js";
|
|
15
15
|
import { RELEASE_LIMITS, runRelease } from "./release.js";
|
|
16
16
|
import { processes, releaseAdapters } from "./release-adapters.js";
|
|
17
|
-
import { releaseGate } from "./release-gate.js";
|
|
17
|
+
import { journals, releaseGate } from "./release-gate.js";
|
|
18
|
+
import { rescueDeployment } from "./deploy-rescue.js";
|
|
18
19
|
import { databasePath, openStore, revokeRelease } from "./store.js";
|
|
19
20
|
import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
|
|
20
21
|
import { authenticateApprover, machineActing, personProof, proveCredential } from "./scope.js";
|
|
@@ -39,6 +40,13 @@ toolroll release --release-gate <id> [--yes] — lift the update pause a deploym
|
|
|
39
40
|
database, service definition and running service are all still the previous build's. --yes lifts that one pause.
|
|
40
41
|
Any missing or contradicting evidence refuses and leaves the pause in place.
|
|
41
42
|
|
|
43
|
+
toolroll release --recover-deploy [--stage <dir>] [--yes] — put back a deployment that stopped half way
|
|
44
|
+
|
|
45
|
+
Finds the one unfinished deployment (or the stage named) and shows what its recovery will do; --yes does it. When
|
|
46
|
+
the old service isn't proved stopped, it boots the service out and stops what it left running, but only processes
|
|
47
|
+
proved, by exact runtime path and arguments, to be that deployment's own; then it starts the previous service
|
|
48
|
+
again, waits for it, and lifts the update pause. Anything it can't prove refuses. Safe to run again.
|
|
49
|
+
|
|
42
50
|
toolroll release carry <release> --to <commit> [--request <id>] — carry a person's release approval to a later commit
|
|
43
51
|
|
|
44
52
|
Only tests, docs and the changelog may change after the approval, within the limits, with an identical production
|
|
@@ -74,33 +82,45 @@ Options
|
|
|
74
82
|
--limit <step>=<min> change one step's time limit (${Object.entries(RELEASE_LIMITS).map(([step, minutes]) => `${step} ${minutes}`).join(", ")})
|
|
75
83
|
--tap <owner/name> the Homebrew tap (default ap9000/homebrew-toolroll)
|
|
76
84
|
--release-gate <id> check (and with --yes, lift) one update pause left before a deployment's swap
|
|
77
|
-
--yes with --release-gate: lift the pause once it is proved safe
|
|
85
|
+
--yes with --release-gate: lift the pause once it is proved safe; with --recover-deploy: recover
|
|
86
|
+
--recover-deploy recover the unfinished deployment (see above)
|
|
87
|
+
--stage <dir> with --recover-deploy: the deployment's staging folder, when more than one is unfinished
|
|
78
88
|
--json answer with one machine envelope
|
|
79
89
|
|
|
80
90
|
A runner's lease is returned with \`toolroll worker release <lease>\`.`;
|
|
81
91
|
/** A runner lease id (claim.ts mints randomUUID()). */
|
|
82
92
|
export const LEASE_SHAPED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
83
93
|
export const LEASE_DEPRECATION = "`toolroll release <lease>` is now `toolroll worker release <lease>`; the old form goes in the next minor version.";
|
|
84
|
-
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule"]);
|
|
85
|
-
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start"]);
|
|
94
|
+
const VALUE_FLAGS = new Set(["--repo", "--limit", "--tap", "--release-gate", "--to", "--request", "--as", "--token", "--schedule", "--stage"]);
|
|
95
|
+
const BARE_FLAGS = new Set(["--new", "--json", "--help", "--yes", "--no-carry", "--start", "--recover-deploy"]);
|
|
86
96
|
export async function runReleaseCommand(args, write, deps = {}) {
|
|
87
|
-
|
|
97
|
+
// The flag, not a secret's value that happens to read "--json".
|
|
98
|
+
// A secret is never exactly one of these names (the loop below refuses that as a missing value).
|
|
99
|
+
const bare = new Set(args.filter(one => BARE_FLAGS.has(one)));
|
|
100
|
+
const json = bare.has("--json");
|
|
88
101
|
const refuse = (reason, message, code) => {
|
|
89
102
|
write(json ? checkedEnvelopeJson({ ok: false, command: "release", reason, message }) : message);
|
|
90
103
|
return code;
|
|
91
104
|
};
|
|
92
105
|
const positional = [];
|
|
93
106
|
const limits = {};
|
|
107
|
+
let stage;
|
|
94
108
|
let repo, tap, gate, to, request, as, token, scheduleFlag;
|
|
95
109
|
for (let i = 0; i < args.length; i += 1) {
|
|
96
|
-
const
|
|
97
|
-
if (BARE_FLAGS.has(
|
|
110
|
+
const raw = args[i];
|
|
111
|
+
if (BARE_FLAGS.has(raw))
|
|
98
112
|
continue;
|
|
113
|
+
// `--flag=value` for every value flag; a secret (--token) is also the next argument even when it starts with "--".
|
|
114
|
+
const equals = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
115
|
+
const arg = equals === -1 ? raw : raw.slice(0, equals);
|
|
99
116
|
if (VALUE_FLAGS.has(arg)) {
|
|
100
|
-
const value = args[i + 1];
|
|
101
|
-
|
|
117
|
+
const value = equals === -1 ? args[i + 1] : raw.slice(equals + 1);
|
|
118
|
+
// A secret may start with "--"; it is the value unless it is exactly one of this command's flags (never swallowed).
|
|
119
|
+
const namesFlag = value !== undefined && (BARE_FLAGS.has(value) || VALUE_FLAGS.has(value.split("=")[0]));
|
|
120
|
+
if (value === undefined || (equals === -1 && value.startsWith("--") && (arg !== "--token" || namesFlag)))
|
|
102
121
|
return refuse("usage", `${arg} takes a value.`, 2);
|
|
103
|
-
|
|
122
|
+
if (equals === -1)
|
|
123
|
+
i += 1;
|
|
104
124
|
if (arg === "--repo")
|
|
105
125
|
repo = value;
|
|
106
126
|
else if (arg === "--to")
|
|
@@ -117,6 +137,8 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
117
137
|
tap = value;
|
|
118
138
|
else if (arg === "--release-gate")
|
|
119
139
|
gate = value;
|
|
140
|
+
else if (arg === "--stage")
|
|
141
|
+
stage = value;
|
|
120
142
|
else {
|
|
121
143
|
const match = /^([a-z]+)=([0-9]+)$/.exec(value);
|
|
122
144
|
if (match === null || !(match[1] in RELEASE_LIMITS) || Number(match[2]) < 1)
|
|
@@ -125,23 +147,30 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
125
147
|
}
|
|
126
148
|
continue;
|
|
127
149
|
}
|
|
128
|
-
if (
|
|
150
|
+
if (raw.startsWith("--"))
|
|
129
151
|
return refuse("usage", `Unknown option ${arg}.\n${RELEASE_HELP}`, 2);
|
|
130
152
|
positional.push(arg);
|
|
131
153
|
}
|
|
132
|
-
if (
|
|
154
|
+
if (bare.has("--help")) {
|
|
133
155
|
write(RELEASE_HELP);
|
|
134
156
|
return 0;
|
|
135
157
|
}
|
|
136
158
|
if (gate !== undefined) {
|
|
137
159
|
if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new"].includes(arg)))
|
|
138
160
|
return refuse("usage", "--release-gate <id> takes no branch or release options.", 2);
|
|
139
|
-
return runGateRelease(gate,
|
|
161
|
+
return runGateRelease(gate, bare.has("--yes"), write, json, deps.gateWorld ?? installedGateWorld());
|
|
140
162
|
}
|
|
141
|
-
if (
|
|
142
|
-
|
|
163
|
+
if (bare.has("--recover-deploy")) {
|
|
164
|
+
if (positional.length > 0 || args.some(arg => ["--repo", "--limit", "--tap", "--new", "--release-gate"].includes(arg.split("=")[0])))
|
|
165
|
+
return refuse("usage", "--recover-deploy takes only --stage <dir>, --yes and --json.", 2);
|
|
166
|
+
return runRescue(stage, bare.has("--yes"), write, json, deps.rescueWorld ?? installedRescueWorld(deps.cwd ?? process.cwd()));
|
|
167
|
+
}
|
|
168
|
+
if (stage !== undefined)
|
|
169
|
+
return refuse("usage", "--stage goes with --recover-deploy.", 2);
|
|
170
|
+
if (bare.has("--yes"))
|
|
171
|
+
return refuse("usage", "--yes goes with --release-gate <id> or --recover-deploy; a release needs no confirmation beyond the owner's approval.", 2);
|
|
143
172
|
if (positional[0] === "nightly" && positional.length === 1)
|
|
144
|
-
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start:
|
|
173
|
+
return runNightlyCommand({ repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo), as, token, schedule: scheduleFlag, start: bare.has("--start") }, write, json, deps);
|
|
145
174
|
if (["carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"].includes(positional[0] ?? "") && positional.length === 2) {
|
|
146
175
|
return runAuthorityCommand(positional[0], positional[1], { to, request, as, token, repo: repo === undefined ? undefined : resolve(deps.cwd ?? process.cwd(), repo) }, write, json, deps);
|
|
147
176
|
}
|
|
@@ -169,7 +198,7 @@ export async function runReleaseCommand(args, write, deps = {}) {
|
|
|
169
198
|
const adapters = deps.adapters === undefined
|
|
170
199
|
? releaseAdapters({ checkout, say, ...(tap === undefined ? {} : { tap }), ...(limits["deploy"] === undefined ? {} : { deployMinutes: limits["deploy"] }) })
|
|
171
200
|
: { ...deps.adapters, say };
|
|
172
|
-
const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh:
|
|
201
|
+
const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh: bare.has("--new"), noCarry: bare.has("--no-carry") }, adapters);
|
|
173
202
|
return report(outcome, write, json, lines);
|
|
174
203
|
}
|
|
175
204
|
function report(outcome, write, json, lines) {
|
|
@@ -208,6 +237,55 @@ function installedGateWorld() {
|
|
|
208
237
|
.filter(path => existsSync(path)).map(path => ({ path, text: readFileSync(path, "utf8") })),
|
|
209
238
|
};
|
|
210
239
|
}
|
|
240
|
+
/** The installation's deployments, its plane's records and the release's own recovery road, for --recover-deploy. */
|
|
241
|
+
function installedRescueWorld(cwd) {
|
|
242
|
+
const database = databasePath(process.env, homedir());
|
|
243
|
+
const stateDir = dirname(database);
|
|
244
|
+
const adapters = releaseAdapters({ checkout: cwd, say: line => process.stderr.write(`${line}\n`) });
|
|
245
|
+
return {
|
|
246
|
+
database, stateDir,
|
|
247
|
+
journals: () => journals(stateDir),
|
|
248
|
+
runWorktree: run => {
|
|
249
|
+
if (!existsSync(database))
|
|
250
|
+
return null;
|
|
251
|
+
const db = new (sqlite().DatabaseSync)(database, { readOnly: true });
|
|
252
|
+
try {
|
|
253
|
+
const row = db.prepare("SELECT worktree FROM run WHERE id = ?").get(run);
|
|
254
|
+
return typeof row?.["worktree"] === "string" && row["worktree"] !== "" ? row["worktree"] : null;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
db.close();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
recover: async (input) => {
|
|
264
|
+
const answer = await adapters.toolroll.recoverDeploy(input);
|
|
265
|
+
return answer.ok ? { ok: true } : { ok: false, message: answer.message };
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function runRescue(stage, yes, write, json, world) {
|
|
270
|
+
let outcome;
|
|
271
|
+
try {
|
|
272
|
+
outcome = await rescueDeployment(stage, yes, world);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
outcome = { ok: false, reason: "failed", message: `${error.message} Run \`toolroll release --recover-deploy --yes\` again once the cause is fixed.` };
|
|
276
|
+
}
|
|
277
|
+
if (!outcome.ok) {
|
|
278
|
+
write(json ? checkedEnvelopeJson({ ok: false, command: "release", reason: outcome.reason, message: outcome.message }) : `✗ ${outcome.message}`);
|
|
279
|
+
return outcome.reason === "usage" ? 2 : 3;
|
|
280
|
+
}
|
|
281
|
+
if (json) {
|
|
282
|
+
write(checkedEnvelopeJson({ ok: true, command: "release", recovery: { acted: outcome.acted, stage: outcome.stage, phase: outcome.phase, said: outcome.words } }));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
for (const line of outcome.words)
|
|
286
|
+
write(outcome.acted ? `✓ ${line}` : line);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
211
289
|
function runGateRelease(id, yes, write, json, world) {
|
|
212
290
|
let outcome;
|
|
213
291
|
try {
|
package/dist/release-coverage.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* existing records and Git; nothing is written back, so no proof, receipt,
|
|
5
5
|
* scope digest or schema changes. Fails closed: a receipt that can't be read,
|
|
6
6
|
* a missing commit or any Git error leaves the result uncovered. */
|
|
7
|
-
import {
|
|
7
|
+
import { viewedGitSync } from "./teardown-git.js";
|
|
8
8
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
9
9
|
import { runCheckLevel } from "./check-levels.js";
|
|
10
10
|
/** How many of the newest passing release checks one read considers. */
|
|
@@ -25,7 +25,10 @@ export const gitIsAncestor = (repo, ancestor, descendant) => {
|
|
|
25
25
|
return known;
|
|
26
26
|
let answer;
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
// Every git version through a frozen read view, bounded (eval-harness review 1, finding 7).
|
|
29
|
+
const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000 }).status;
|
|
30
|
+
if (status !== 0)
|
|
31
|
+
throw Object.assign(new Error("git merge-base"), { status });
|
|
29
32
|
answer = true;
|
|
30
33
|
}
|
|
31
34
|
catch (error) {
|
package/dist/release-gate.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export type GateProof = {
|
|
|
61
61
|
};
|
|
62
62
|
/** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
|
|
63
63
|
export declare function gateOwner(db: Database): string | null;
|
|
64
|
+
/** Every deployment journal under staged-upgrades/: the stage it is in and what it holds, or why it can't be read. */
|
|
65
|
+
export declare function journals(stateDir: string): {
|
|
66
|
+
stage: string;
|
|
67
|
+
journal: Record<string, unknown> | null;
|
|
68
|
+
}[];
|
|
64
69
|
/** Whether the pause owned by `id` was left by a deployment that never began its swap, and the evidence that says so. */
|
|
65
70
|
export declare function proveBeforeSwap(id: string, world: GateReleaseWorld): GateProof;
|
|
66
71
|
export type GateReleaseOutcome = {
|
package/dist/release-gate.js
CHANGED
|
@@ -36,7 +36,7 @@ export function gateOwner(db) {
|
|
|
36
36
|
return ids.size === 1 ? [...ids][0] : null;
|
|
37
37
|
}
|
|
38
38
|
/** Every deployment journal under staged-upgrades/: the stage it is in and what it holds, or why it can't be read. */
|
|
39
|
-
function journals(stateDir) {
|
|
39
|
+
export function journals(stateDir) {
|
|
40
40
|
const root = join(stateDir, "staged-upgrades");
|
|
41
41
|
let stages = [];
|
|
42
42
|
try {
|
|
@@ -72,7 +72,7 @@ export declare function planTargetedRecheck(store: Store, root: string, input: {
|
|
|
72
72
|
now: Date;
|
|
73
73
|
}): RecheckDecision;
|
|
74
74
|
/** Coverage of a check: full, composed from A, partial (never passes), or identity-removed (asks again). */
|
|
75
|
-
export type Coverage = "full" | "composed" | "partial" | "identity-removed";
|
|
75
|
+
export type Coverage = "full" | "composed" | "partial" | "identity-removed" | "no-result";
|
|
76
76
|
/**
|
|
77
77
|
* Whether a targeted run B, with A's sealed evidence, its plan and snapshot, covers every item of B's inventory
|
|
78
78
|
* (coverage conservation), and every required identity passed — re-derived from sealed values only.
|
package/dist/release-recheck.js
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* Coverage trusts test code (Alex, 2026-10-25): these results protect against accidental damage, not a hostile test.
|
|
21
21
|
* Authority never reads them; the release's own records do.
|
|
22
22
|
*/
|
|
23
|
-
import { CHECK_CONTEXT_CAPTURE, CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, OBLIGATION_SNAPSHOT_CAPTURE, TARGETED_PLAN_CAPTURE, checkContextOf, isUnitFile, payloadDigest, projectGitDir, readSealedJson, treeFactsOf } from "./check-report.js";
|
|
23
|
+
import { CHECK_CONTEXT_CAPTURE, CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, OBLIGATION_SNAPSHOT_CAPTURE, TARGETED_PLAN_CAPTURE, checkContextOf, fullCheckProblem, isUnitFile, payloadDigest, projectGitDir, readSealedJson, treeFactsOf } from "./check-report.js";
|
|
24
24
|
import { readVerifiedCommit, verifiedTreeDiff } from "./commit-extract.js";
|
|
25
25
|
import { checkContextSchema, fileIdentity, groupIdentity, journeyIdentity, obligationSnapshotSchema, readCheckReport, sealedInventorySchema, targetedPlanSchema, unitIdentity, versionIdentity, } from "./contracts/release-check.js";
|
|
26
26
|
import { parseContract } from "./contracts/contract.js";
|
|
27
27
|
import { releaseAuthority, releaseAuthorityOfTask, releaseCarries } from "./release-authority.js";
|
|
28
|
-
import { liveReleaseOf } from "./scope.js";
|
|
28
|
+
import { liveReleaseOf, releaseCheckScope } from "./scope.js";
|
|
29
29
|
import { checkAttemptOf } from "./store.js";
|
|
30
30
|
/** The sanity pass every targeted check runs beside its plan: a few fast tests of the core rules, when the tree has them. */
|
|
31
31
|
export const SANITY_UNITS = Object.freeze(["src/scope.test.ts", "src/contract.test.ts", "src/check-report.test.ts", "src/update-safe-migrations.test.ts"]);
|
|
@@ -483,6 +483,12 @@ export function settleCoverage(store, root, input, gateOf) {
|
|
|
483
483
|
if (removedProtected(b.report, b.inventory, protectedIdentities(store, root, b.attempt.authority, input.runId)))
|
|
484
484
|
return removed();
|
|
485
485
|
}
|
|
486
|
+
// A release's Full check (its task carries signed release terms, before or after its authority opens — an automatic
|
|
487
|
+
// approval's check runs before) counts only with a complete, valid result (check-report.ts): anything less is no result.
|
|
488
|
+
// A release check purpose (pipeline-speed.md, P0) counts too: a termsless release is held to the same rules.
|
|
489
|
+
const release = b.attempt.authority !== null || releaseCheckScope(store.getScope(input.taskId));
|
|
490
|
+
if (release && fullCheckProblem({ report: b.report, inventory: b.inventory, exitCode: input.exitCode }) !== null)
|
|
491
|
+
return { coverage: "no-result", composed: null };
|
|
486
492
|
return { coverage: "full", composed: null };
|
|
487
493
|
}
|
|
488
494
|
if (b.plan === null || b.snapshot === null || b.attempt.baseRun === null || b.attempt.authority === null)
|
package/dist/release.d.ts
CHANGED
|
@@ -34,9 +34,34 @@
|
|
|
34
34
|
*/
|
|
35
35
|
/** The pull-request checks a release waits for, by exact job name (.github/workflows/ci.yml). Every one must have
|
|
36
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"];
|
|
38
|
-
/** The
|
|
39
|
-
|
|
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
|
|
39
|
+
* 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
|
+
/**
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PROTECTION_REPORT_ONLY_CHECKS: readonly string[];
|
|
47
|
+
/** The checks main's protection must require at minimum: every required check that isn't report-only. */
|
|
48
|
+
export declare const PROTECTION_MINIMUM_CHECKS: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Main's protection as the plane's gh reads it (release-adapters.ts readMainProtection): `effective` only when one layer
|
|
51
|
+
* (classic protection or one active ruleset) requires up-to-date branches, binds every PROTECTION_MINIMUM_CHECKS check to GitHub Actions,
|
|
52
|
+
* and nobody merging as this gh can bypass it. `missing` names each absent property; `notYetRequired` the report-only
|
|
53
|
+
* checks the effective layer doesn't name.
|
|
54
|
+
*/
|
|
55
|
+
export type MainProtection = {
|
|
56
|
+
effective: boolean;
|
|
57
|
+
missing: string[];
|
|
58
|
+
notYetRequired: string[];
|
|
59
|
+
};
|
|
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"];
|
|
64
|
+
export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
|
|
40
65
|
export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
|
|
41
66
|
export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
42
67
|
type WaitingStep = "gate" | "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
@@ -55,10 +80,20 @@ export type CheckRun = {
|
|
|
55
80
|
head?: string | null;
|
|
56
81
|
/** The workflow (its id, from GitHub's workflow-run API) that this check's suite is a run of, when known. */
|
|
57
82
|
workflow?: number | null;
|
|
83
|
+
/** That workflow's file in the repository (GitHub's workflow-run `path`), when known. */
|
|
84
|
+
workflowPath?: string | null;
|
|
85
|
+
/** GitHub's own job record (its steps, read by the plane's gh) shows this unsuccessful attempt ended before any of the
|
|
86
|
+
* workflow's own steps ran: the runner failed, not the tests. Absent or false: nothing shows that. */
|
|
87
|
+
beforeTests?: boolean | null;
|
|
58
88
|
};
|
|
59
|
-
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Failure classes replace a flat flake allowance (docs/plans/eval-harness.md; check-failure.ts says the classes): only a
|
|
91
|
+
* failure the plane itself confirmed as infrastructure is ever run again under the same approval — at most once per
|
|
92
|
+
* incident and twice per candidate — for a release check and for a GitHub CI job alike. A failing test, a typecheck, an
|
|
93
|
+
* attack test, a migration: none tolerated. An unexplained failure blocks, and a person decides.
|
|
94
|
+
*/
|
|
95
|
+
export declare const INFRA_RERUNS_PER_INCIDENT = 1;
|
|
96
|
+
export declare const INFRA_RERUNS_PER_CANDIDATE = 2;
|
|
62
97
|
/** One failed attempt that a later attempt of the same job passed. */
|
|
63
98
|
export type CheckFailure = {
|
|
64
99
|
id: number;
|
|
@@ -138,6 +173,8 @@ export type DeployStop = {
|
|
|
138
173
|
phase: string;
|
|
139
174
|
};
|
|
140
175
|
};
|
|
176
|
+
/** The tap's formula for a version: current, a proposal open, missing — or `newer`: the tap's default branch already
|
|
177
|
+
* names a later version, which an older release must never overwrite. */
|
|
141
178
|
export type HomebrewState = {
|
|
142
179
|
kind: "current";
|
|
143
180
|
} | {
|
|
@@ -146,7 +183,14 @@ export type HomebrewState = {
|
|
|
146
183
|
head: string;
|
|
147
184
|
} | {
|
|
148
185
|
kind: "missing";
|
|
186
|
+
} | {
|
|
187
|
+
kind: "newer";
|
|
188
|
+
version: string;
|
|
149
189
|
};
|
|
190
|
+
/** Homebrew after publication (pipeline-speed.md, P1): its own durable state, finished by `toolroll release <branch>`.
|
|
191
|
+
* `pending`: a change proposed or its checks still running; `failed`: its checks failed (a later run reads them again);
|
|
192
|
+
* `current`: merged, or already on the tap; `superseded`: the tap already names a newer version, so nothing was changed. */
|
|
193
|
+
export type HomebrewProgress = "pending" | "failed" | "current" | "superseded";
|
|
150
194
|
/** One commit that landed after the approval, with its files and line counts (release_carry). */
|
|
151
195
|
export type LandedCommit = {
|
|
152
196
|
sha: string;
|
|
@@ -168,6 +212,18 @@ export type AuthorityView = {
|
|
|
168
212
|
landed: LandedCommit[];
|
|
169
213
|
};
|
|
170
214
|
export type ReleaseAction = "dispatch" | "deploy" | "merge" | "complete";
|
|
215
|
+
/** The plane's answer after a failed release check: rerun (confirmed infrastructure), already rerunning, or stop. */
|
|
216
|
+
export type FailureClassName = "deterministic" | "infrastructure" | "unknown" | "agent-behaviour";
|
|
217
|
+
export type RecheckView = {
|
|
218
|
+
action: "rerun";
|
|
219
|
+
why: string;
|
|
220
|
+
} | {
|
|
221
|
+
action: "pending";
|
|
222
|
+
} | {
|
|
223
|
+
action: "stop";
|
|
224
|
+
class: FailureClassName | null;
|
|
225
|
+
why: string;
|
|
226
|
+
};
|
|
171
227
|
export type OperationView = {
|
|
172
228
|
id: string;
|
|
173
229
|
kind: "deploy" | "merge";
|
|
@@ -231,6 +287,20 @@ export type ReleaseAuthorityAdapter = {
|
|
|
231
287
|
* the authority could not be stopped (a stale epoch, a merge already reserved): the coordinator fails closed. */
|
|
232
288
|
needsApproval(authority: number, reason: string, epoch: number): Promise<boolean>;
|
|
233
289
|
operations(authority: number): Promise<OperationView[]>;
|
|
290
|
+
/** After a failed release check (check-failure.ts): the plane classifies it from its own records and, only for
|
|
291
|
+
* confirmed infrastructure within its limits, runs the check again on the same candidate under the same approval.
|
|
292
|
+
* Absent on an older plane: a failed check stops as before. */
|
|
293
|
+
recheck?(taskId: string): Promise<RecheckView>;
|
|
294
|
+
/** Whether a passing check stands given every earlier attempt on the same commit (an earlier failure that wasn't
|
|
295
|
+
* confirmed infrastructure isn't erased by a pass), and what passed only on a retry. */
|
|
296
|
+
standing?(taskId: string, run: number): Promise<{
|
|
297
|
+
ok: true;
|
|
298
|
+
retried: string[] | null;
|
|
299
|
+
} | {
|
|
300
|
+
ok: false;
|
|
301
|
+
why: string;
|
|
302
|
+
retried: string[] | null;
|
|
303
|
+
}>;
|
|
234
304
|
/** Releases that approve themselves when green (docs/plans/auto-approve-green.md): the plane's own seals, each proven
|
|
235
305
|
* again inside the plane. Absent: a plane that can't, and every release asks a person as before. */
|
|
236
306
|
auto?: AutoReleaseAdapter;
|
|
@@ -279,6 +349,17 @@ export type AutoReleaseAdapter = {
|
|
|
279
349
|
}>;
|
|
280
350
|
/** The automatic road ended without a release: the task asks a person, and the owner is told why. */
|
|
281
351
|
giveUp(taskId: string, why: string): Promise<void>;
|
|
352
|
+
/** The task is filed while CI still runs on `candidate` (recorded in the plane before the task is filed): its approval
|
|
353
|
+
* card says so until the plane reads CI passing. It holds nothing (Alex, 2026-10-29: a person may approve during CI). */
|
|
354
|
+
ciRunning?(taskId: string, candidate: string): Promise<void>;
|
|
355
|
+
/** The plane reads CI on `candidate` itself and, when every required check passed, ends the "CI still running" note
|
|
356
|
+
* (and lifts a hold an older coordinator placed); `ask` tells the owner of a held release it needs them now. */
|
|
357
|
+
ciPassed?(taskId: string, candidate: string, ask: boolean): Promise<{
|
|
358
|
+
ok: true;
|
|
359
|
+
} | {
|
|
360
|
+
ok: false;
|
|
361
|
+
why: string;
|
|
362
|
+
}>;
|
|
282
363
|
/** The release check failed while checking automatically: the owner is told; the commit may be checked again (a
|
|
283
364
|
* regate of the same commit and digest, within the plane's cap on failed runs). */
|
|
284
365
|
checkFailed(taskId: string, why: string): Promise<void>;
|
|
@@ -306,10 +387,6 @@ export type ReleaseAdapters = {
|
|
|
306
387
|
deleteBranch(branch: string, head: string): Promise<void>;
|
|
307
388
|
tree(sha: string): Promise<string>;
|
|
308
389
|
fileAt(sha: string, path: string): Promise<string | null>;
|
|
309
|
-
/** origin/main as the last sync fetched it. */
|
|
310
|
-
mainHead(): Promise<string>;
|
|
311
|
-
/** Whether `ancestor` is in `sha`'s history. */
|
|
312
|
-
contains(sha: string, ancestor: string): Promise<boolean>;
|
|
313
390
|
};
|
|
314
391
|
toolroll: {
|
|
315
392
|
/** Which runtime each launchd service and each CLI name (`toolroll`, `standing-orders`) runs from. */
|
|
@@ -377,11 +454,21 @@ export type ReleaseAdapters = {
|
|
|
377
454
|
github: {
|
|
378
455
|
authenticated(): Promise<boolean>;
|
|
379
456
|
pullRequest(repo: string, branch: string): Promise<PullRequest | null>;
|
|
380
|
-
|
|
457
|
+
/** `tag`: a release tag's push binds its workflow file too (only that exact tag's push). */
|
|
458
|
+
checks(repo: string, sha: string, options?: {
|
|
459
|
+
tag?: string;
|
|
460
|
+
}): Promise<CheckRun[]>;
|
|
381
461
|
/** Whether GitHub's check history of this commit can be read whole (fewer check suites than the check-runs
|
|
382
462
|
* endpoint's ceiling). Absent or false: an automatic release fails closed; a person's is unaffected. */
|
|
383
463
|
complete?(repo: string, sha: string): Promise<boolean>;
|
|
384
464
|
merge(repo: string, pr: number, head: string, message?: string): Promise<string>;
|
|
465
|
+
/** main's head as GitHub's API answers now (never a local ref a build could move). */
|
|
466
|
+
mainHead(repo: string): Promise<string>;
|
|
467
|
+
/** Whether `sha` already holds `ancestor`, by GitHub's own comparison. */
|
|
468
|
+
contains(repo: string, sha: string, ancestor: string): Promise<boolean>;
|
|
469
|
+
/** Whether GitHub refuses to merge a branch that is behind main ("Require branches to be up to date before
|
|
470
|
+
* merging"); null when it can't be read. Read only. */
|
|
471
|
+
upToDateRequired(repo: string): Promise<MainProtection | null>;
|
|
385
472
|
tag(repo: string, tag: string): Promise<string | null>;
|
|
386
473
|
createTag(repo: string, tag: string, sha: string): Promise<void>;
|
|
387
474
|
release(repo: string, tag: string): Promise<boolean>;
|
|
@@ -400,6 +487,14 @@ export type ReleaseAdapters = {
|
|
|
400
487
|
pr: number;
|
|
401
488
|
head: string;
|
|
402
489
|
}>;
|
|
490
|
+
/** Right before the merge: the change is the tap's own open pull request at exactly `head`, and its formula is
|
|
491
|
+
* exactly the tap's formula with npm's published tarball for `version` and that tarball's own sha256. */
|
|
492
|
+
verified(pr: number, head: string, version: string): Promise<{
|
|
493
|
+
ok: true;
|
|
494
|
+
} | {
|
|
495
|
+
ok: false;
|
|
496
|
+
why: string;
|
|
497
|
+
}>;
|
|
403
498
|
merge(pr: number, head: string): Promise<void>;
|
|
404
499
|
};
|
|
405
500
|
};
|
|
@@ -466,6 +561,9 @@ export type ReleaseJournal = {
|
|
|
466
561
|
pr: number | null;
|
|
467
562
|
head: string | null;
|
|
468
563
|
merged: boolean;
|
|
564
|
+
/** Absent in a journal from before Homebrew was finished apart: read from `merged`. */
|
|
565
|
+
state?: HomebrewProgress;
|
|
566
|
+
why?: string | null;
|
|
469
567
|
} | null;
|
|
470
568
|
/** The release's authority record (release-once.md), once a person approved terms: the id and generation. */
|
|
471
569
|
authority?: {
|
|
@@ -490,6 +588,13 @@ export type ReleaseJournal = {
|
|
|
490
588
|
why?: string;
|
|
491
589
|
ci?: number[];
|
|
492
590
|
} | null;
|
|
591
|
+
/** The gate filed the task while the pull request's checks still ran (pipeline-speed.md, D2 and "Decision: a person's
|
|
592
|
+
* release asks during CI"): the release check runs beside CI. A person may approve meanwhile; the plane's automatic
|
|
593
|
+
* approval waits for every check to pass, and nothing deploys, merges, tags or publishes before they do. */
|
|
594
|
+
ciAhead?: boolean;
|
|
595
|
+
/** Set with ciAhead and never cleared for this commit: a person's release reads CI again, fresh, right before the
|
|
596
|
+
* deploy, whatever was seen while asking (a green CI may have a newer attempt failing since). */
|
|
597
|
+
ciBeforeDeploy?: boolean;
|
|
493
598
|
/** When each step was about to change something outside this machine: what a resume must look for before acting. */
|
|
494
599
|
intents: Partial<Record<ReleaseStep, string>>;
|
|
495
600
|
stopped: {
|
|
@@ -556,6 +661,13 @@ export declare function checksVerdict(runs: readonly CheckRun[], required: reado
|
|
|
556
661
|
state: "failed";
|
|
557
662
|
failed: string[];
|
|
558
663
|
};
|
|
664
|
+
/**
|
|
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.
|
|
669
|
+
*/
|
|
670
|
+
export declare function tagChecksVerdict(runs: readonly CheckRun[]): ReturnType<typeof checksVerdict>;
|
|
559
671
|
/** Run the release from wherever its journal stopped. */
|
|
560
672
|
export declare function runRelease(options: ReleaseOptions, adapters: ReleaseAdapters): Promise<ReleaseOutcome>;
|
|
561
673
|
/** A probe's failure that is only the network to GitHub being slow, never a decision (a Stop) or another error. */
|
|
@@ -565,11 +677,14 @@ export declare function transientNetworkError(error: unknown): boolean;
|
|
|
565
677
|
* each reported attempt must be filed under `head` with a check-run id; a required check counts only from GitHub
|
|
566
678
|
* Actions (an app that names a check alike can't stand in for it); attempts are grouped by the job they belong to — the
|
|
567
679
|
* app, its check suite (a workflow run, whose re-runs stay in it) and the name — and every group is judged by its
|
|
568
|
-
* latest attempt; every required check needs a group. With `
|
|
569
|
-
*
|
|
680
|
+
* latest attempt; every required check needs a group. With `auto` (an automatic approval), every other failed attempt on
|
|
681
|
+
* the commit — an earlier attempt, or a job a newer run renamed or dropped — must be one GitHub's own job record shows ending before any workflow step ran (`beforeTests`: confirmed
|
|
682
|
+
* infrastructure), and at most INFRA_RERUNS_PER_CANDIDATE of them on the commit; any other earlier failure — a test,
|
|
683
|
+
* a typecheck, or one nothing explains — fails the verdict, so a person decides. The failures it tolerated are returned.
|
|
570
684
|
*/
|
|
571
685
|
export declare function prChecksVerdict(runs: readonly CheckRun[], head: string, required: readonly string[], options?: {
|
|
572
|
-
|
|
686
|
+
auto?: true;
|
|
687
|
+
workflow?: string;
|
|
573
688
|
}): {
|
|
574
689
|
state: "passed";
|
|
575
690
|
failures: CheckFailure[];
|
|
@@ -580,4 +695,19 @@ export declare function prChecksVerdict(runs: readonly CheckRun[], head: string,
|
|
|
580
695
|
state: "failed";
|
|
581
696
|
failed: string[];
|
|
582
697
|
};
|
|
698
|
+
/**
|
|
699
|
+
* What a failed release check means and what happens next, by its class (check-failure.ts), in words. Only confirmed
|
|
700
|
+
* infrastructure reruns on its own; nothing else is rerun to pass.
|
|
701
|
+
*/
|
|
702
|
+
export declare function checkFailedWords(input: {
|
|
703
|
+
sha: string;
|
|
704
|
+
run: number;
|
|
705
|
+
class: FailureClassName | null;
|
|
706
|
+
why: string;
|
|
707
|
+
branch: string;
|
|
708
|
+
checkout: string;
|
|
709
|
+
taskId: string;
|
|
710
|
+
}): string;
|
|
711
|
+
/** Whether GitHub's workflow-run `path` names exactly `file` (it may carry the ref it ran from, after an `@`). */
|
|
712
|
+
export declare function workflowIs(path: string | null | undefined, file: string): boolean;
|
|
583
713
|
export {};
|