toolroll 0.9.76 → 0.9.77

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +28 -8
  5. package/dist/assignment.js +3 -0
  6. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  7. package/dist/browser/workspace.css +2 -2
  8. package/dist/browser/workspace.js +31 -31
  9. package/dist/builder.d.ts +14 -0
  10. package/dist/builder.js +140 -41
  11. package/dist/chat-decide.js +4 -0
  12. package/dist/check-failure.d.ts +92 -0
  13. package/dist/check-failure.js +267 -0
  14. package/dist/check-progress.d.ts +28 -1
  15. package/dist/check-progress.js +105 -6
  16. package/dist/check-report.d.ts +52 -6
  17. package/dist/check-report.js +193 -8
  18. package/dist/checkout-cleanup.js +2 -2
  19. package/dist/cli.js +8 -5
  20. package/dist/coding-handoff.d.ts +1 -0
  21. package/dist/coding-handoff.js +20 -11
  22. package/dist/commit-extract.d.ts +6 -0
  23. package/dist/commit-extract.js +63 -8
  24. package/dist/console-chrome.d.ts +41 -0
  25. package/dist/console-chrome.js +166 -0
  26. package/dist/contracts/release-check.d.ts +12 -0
  27. package/dist/contracts/release-check.js +17 -0
  28. package/dist/contracts/verification-receipt.d.ts +20 -2
  29. package/dist/contracts/verification-receipt.js +15 -3
  30. package/dist/contracts/verification-view.d.ts +8 -0
  31. package/dist/controller-supervisor.d.ts +8 -0
  32. package/dist/controller-supervisor.js +33 -1
  33. package/dist/demo.js +3 -2
  34. package/dist/deploy-gate.js +5 -0
  35. package/dist/dispose.d.ts +3 -0
  36. package/dist/dispose.js +6 -3
  37. package/dist/doctor.d.ts +7 -0
  38. package/dist/doctor.js +10 -2
  39. package/dist/envelope.js +2 -1
  40. package/dist/evidence.js +4 -1
  41. package/dist/exec.d.ts +7 -0
  42. package/dist/exec.js +7 -1
  43. package/dist/git-floor.d.ts +16 -0
  44. package/dist/git-floor.js +36 -0
  45. package/dist/grant.js +5 -1
  46. package/dist/guides.js +4 -3
  47. package/dist/operate.d.ts +13 -0
  48. package/dist/operate.js +773 -319
  49. package/dist/prepared-evidence.d.ts +2 -1
  50. package/dist/prepared-evidence.js +8 -3
  51. package/dist/project-knowledge.js +6 -2
  52. package/dist/project-learning.js +2 -2
  53. package/dist/proof.js +6 -0
  54. package/dist/read-executor-worker.js +8 -4
  55. package/dist/read-executor.d.ts +18 -1
  56. package/dist/read-executor.js +8 -0
  57. package/dist/release-adapters.d.ts +48 -1
  58. package/dist/release-adapters.js +343 -10
  59. package/dist/release-authority.js +6 -2
  60. package/dist/release-auto.d.ts +38 -7
  61. package/dist/release-auto.js +131 -15
  62. package/dist/release-card.d.ts +15 -3
  63. package/dist/release-card.js +25 -9
  64. package/dist/release-ci-hold.d.ts +41 -0
  65. package/dist/release-ci-hold.js +53 -0
  66. package/dist/release-cli.js +21 -12
  67. package/dist/release-coverage.js +5 -2
  68. package/dist/release-recheck.d.ts +1 -1
  69. package/dist/release-recheck.js +6 -1
  70. package/dist/release.d.ts +108 -11
  71. package/dist/release.js +294 -39
  72. package/dist/review-context.js +8 -6
  73. package/dist/runner.js +2 -1
  74. package/dist/sandbox-runtime.d.ts +8 -0
  75. package/dist/sandbox-runtime.js +14 -2
  76. package/dist/scope.js +7 -1
  77. package/dist/secret-mint.d.ts +1 -0
  78. package/dist/secret-mint.js +14 -0
  79. package/dist/serve.js +101 -51
  80. package/dist/server/chat.js +5 -5
  81. package/dist/server/render-tasks.d.ts +2 -0
  82. package/dist/server/render-tasks.js +2 -1
  83. package/dist/server/request-context.d.ts +6 -0
  84. package/dist/server/runtime.d.ts +2 -0
  85. package/dist/server/task-views.js +7 -2
  86. package/dist/setup-guide.d.ts +2 -2
  87. package/dist/storage.d.ts +5 -0
  88. package/dist/storage.js +28 -1
  89. package/dist/store.d.ts +10 -0
  90. package/dist/store.js +25 -1
  91. package/dist/task-base.d.ts +4 -0
  92. package/dist/task-base.js +22 -2
  93. package/dist/teardown-git.d.ts +52 -3
  94. package/dist/teardown-git.js +143 -31
  95. package/dist/test-infra.js +1 -1
  96. package/dist/up-processes.d.ts +137 -0
  97. package/dist/up-processes.js +165 -0
  98. package/dist/up-role-child.d.ts +1 -0
  99. package/dist/up-role-child.js +77 -0
  100. package/dist/verification-evidence.d.ts +1 -0
  101. package/dist/verification-evidence.js +13 -1
  102. package/package.json +2 -1
package/dist/builder.d.ts CHANGED
@@ -34,6 +34,20 @@ export declare function approvedCommandShell(command: string, platform?: NodeJS.
34
34
  * must not turn an ordinary product failure into environment recovery.
35
35
  */
36
36
  export declare function verificationExecutableMissing(result: ExecResult, platform?: NodeJS.Platform): boolean;
37
+ /**
38
+ * The approved command's own program — its first word — when the plane, looking it up itself before the check starts,
39
+ * finds no such executable; null when it exists or can't be told (eval-harness review 2, finding 5): a first word with
40
+ * any quoting, expansion, assignment or redirection, a shell builtin, or Windows (cmd.exe's lookup isn't modelled).
41
+ * A path is looked up from the checkout; a bare name on `path`'s absolute folders.
42
+ */
43
+ export declare function approvedProgramMissing(command: string, cwd: string, path: string | undefined, platform?: NodeJS.Platform): string | null;
44
+ /**
45
+ * Whether a check's first attempt failed at its launch, so the approved recovery setup may run and the check run again
46
+ * (eval-harness review 1, finding 6; review 2, finding 5). Only a result the plane made without spawning qualifies.
47
+ * Any shell output, including a perfect "not found" line after a missing-program lookup, remains a check failure.
48
+ * A later passing check still counts as a retry (check-failure.ts), which an automatic release approval refuses.
49
+ */
50
+ export declare function launchFailedBeforeCheck(result: ExecResult, nonce: string | null, missing: string | null, platform?: NodeJS.Platform): boolean;
37
51
  export type BuildRequest = {
38
52
  taskId: string;
39
53
  taskRef: number;
package/dist/builder.js CHANGED
@@ -8,14 +8,15 @@ import { queueBatchCheck } from "./batch-checks.js";
8
8
  import { batchesFullCheck } from "./batch-policy.js";
9
9
  import { skillsContext } from "./project-skills.js";
10
10
  import { failedVerificationEvidence, sealVerificationReceipt, verificationEvidence, reuseObservationVerification } from "./verification-evidence.js";
11
- import { CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, prepareCheckAttempt, sealCheckOutcome } from "./check-report.js";
11
+ import { CHECK_INVENTORY_CAPTURE, CHECK_REPORT_CAPTURE, CheckBaselineUnverifiable, prepareCheckAttempt, reportFromOutput, sealCheckOutcome } from "./check-report.js";
12
+ import { retriedInside } from "./check-failure.js";
12
13
  import { planTargetedRecheck, settleCoverage } from "./release-recheck.js";
13
14
  import { effectiveCheckFacts } from "./verification-evidence.js";
14
15
  import { learningContext } from "./project-learning.js";
15
16
  import { knowledgeContext } from "./project-knowledge.js";
16
17
  import { flowGoalCuts } from "./flow-engine.js";
17
18
  import { readCodingHandoff, verifyCodingHandoffBase } from "./coding-handoff.js";
18
- import { PREPARED_EVIDENCE_FILE, PREPARED_EVIDENCE_GIT, preparedScreenshotMatches, readPreparedEvidence, writePreparedEvidence } from "./prepared-evidence.js";
19
+ import { PREPARED_EVIDENCE_FILE, preparedEvidenceGit, preparedScreenshotMatches, readPreparedEvidence, writePreparedEvidence } from "./prepared-evidence.js";
19
20
  /**
20
21
  * The first thing here that runs an agent.
21
22
  *
@@ -46,9 +47,9 @@ import { PREPARED_EVIDENCE_FILE, PREPARED_EVIDENCE_GIT, preparedScreenshotMatche
46
47
  * watchdog and a high runaway-turn breaker still stop a pathological loop;
47
48
  * repair remains narrowly time-bounded because its job is narrowly scoped.
48
49
  */
49
- import { existsSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
50
+ import { accessSync, constants as fsConstants, existsSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
50
51
  import { homedir } from "node:os";
51
- import { join, resolve } from "node:path";
52
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
52
53
  import { run, runClaudeStreamJsonl, runOwnerTag, streamsInput } from "./exec.js";
53
54
  import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
54
55
  import { witnessedRunner } from "./process-custody.js";
@@ -74,7 +75,7 @@ import { closeSteerMailbox, openSteerMailbox, STEER_HANDLED } from "./steer-mail
74
75
  import { LiveInput } from "./live-input.js";
75
76
  import { LIVE_TICK_MS, tickLiveSteer } from "./steer-live.js";
76
77
  import { withRunActivity } from "./task-activity.js";
77
- import { CheckProgressTracker } from "./check-progress.js";
78
+ import { CheckProgressTracker, coalescedProgressWriter } from "./check-progress.js";
78
79
  import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, isImagePath, WORKTREE_EVIDENCE_DIR, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, steerFolderName, statusFileName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
79
80
  import { attemptKeyOf, closeRunQuestions, noteOpenTool, recordAgentTurn } from "./agent-board.js";
80
81
  import { openStatusFile, pulseAgentBoard, removeStatusFile, statusBrief } from "./agent-pulse.js";
@@ -125,6 +126,57 @@ export function verificationExecutableMissing(result, platform = process.platfor
125
126
  return false;
126
127
  return /^'[^'\r\n]+' is not recognized as an internal or external command,\s+operable program or batch file\.\s*$/i.test(result.stderr.trim());
127
128
  }
129
+ /** Shell words that are never a program on PATH: a command starting with one has no program the plane can look up. */
130
+ const SHELL_WORDS = new Set(["!", ".", ":", "[", "alias", "bg", "break", "case", "cd", "command", "continue", "eval", "exec", "exit", "export", "false", "fg", "for",
131
+ "function", "getopts", "hash", "if", "jobs", "kill", "local", "pwd", "read", "readonly", "return", "set", "shift", "source", "test", "times", "trap", "true", "type",
132
+ "ulimit", "umask", "unalias", "unset", "until", "wait", "while", "{", "time", "echo", "printf"]);
133
+ /**
134
+ * The approved command's own program — its first word — when the plane, looking it up itself before the check starts,
135
+ * finds no such executable; null when it exists or can't be told (eval-harness review 2, finding 5): a first word with
136
+ * any quoting, expansion, assignment or redirection, a shell builtin, or Windows (cmd.exe's lookup isn't modelled).
137
+ * A path is looked up from the checkout; a bare name on `path`'s absolute folders.
138
+ */
139
+ export function approvedProgramMissing(command, cwd, path, platform = process.platform) {
140
+ if (platform === "win32")
141
+ return null;
142
+ // Only a single plain invocation: shell expansion or another command could execute before a missing first word.
143
+ if (!/^[ \t]*[A-Za-z0-9._+/@-]+(?:[ \t]+[A-Za-z0-9._+/@,:=%-]+)*[ \t]*$/.test(command))
144
+ return null;
145
+ const first = /^\s*([A-Za-z0-9._+/@-]+)(?:\s|$)/.exec(command)?.[1];
146
+ if (first === undefined || SHELL_WORDS.has(first))
147
+ return null;
148
+ const runnable = (file) => { try {
149
+ accessSync(file, fsConstants.X_OK);
150
+ return statSync(file).isFile();
151
+ }
152
+ catch {
153
+ return false;
154
+ } };
155
+ if (first.includes("/"))
156
+ return runnable(resolve(cwd, first)) ? null : first;
157
+ if (path === undefined || path.split(delimiter).some(dir => !isAbsolute(dir)))
158
+ return null;
159
+ for (const dir of (path ?? "").split(delimiter))
160
+ if (isAbsolute(dir) && runnable(join(dir, first)))
161
+ return null;
162
+ return first;
163
+ }
164
+ // Only the plane can issue this evidence, and only when it never spawns the command. A lookup followed by a shell
165
+ // launch is racy: the build can install a program between them and forge the shell's entire diagnostic.
166
+ const unstartedChecks = new WeakMap();
167
+ /**
168
+ * Whether a check's first attempt failed at its launch, so the approved recovery setup may run and the check run again
169
+ * (eval-harness review 1, finding 6; review 2, finding 5). Only a result the plane made without spawning qualifies.
170
+ * Any shell output, including a perfect "not found" line after a missing-program lookup, remains a check failure.
171
+ * A later passing check still counts as a retry (check-failure.ts), which an automatic release approval refuses.
172
+ */
173
+ export function launchFailedBeforeCheck(result, nonce, missing, platform = process.platform) {
174
+ if (missing === null || unstartedChecks.get(result) !== missing || platform === "win32" || result.code !== 127 || !verificationExecutableMissing(result, platform))
175
+ return false;
176
+ if (result.stdout.trim() !== "" || (nonce !== null && reportFromOutput(result.stdout, nonce).ok))
177
+ return false;
178
+ return true;
179
+ }
128
180
  /** Long enough for real work; short enough that a stuck build ends the same night. */
129
181
  export const DEFAULT_BUILD_TIMEOUT_MS = 30 * 60_000;
130
182
  export const DEFAULT_MAX_TURNS = 40;
@@ -647,7 +699,7 @@ export async function build(store, request) {
647
699
  if (refusal !== null)
648
700
  return { ok: false, reason: "no-op", message: refusal };
649
701
  // Check the exact candidate before setup or the full gate spends anything.
650
- const listed = await git(GIT, [...PREPARED_EVIDENCE_GIT, "ls-tree", "-z", preparedCandidate, "--", PREPARED_EVIDENCE_FILE], { cwd: worktree, maxBuffer: 2048 });
702
+ const listed = await git(GIT, [...preparedEvidenceGit(), "ls-tree", "-z", preparedCandidate, "--", PREPARED_EVIDENCE_FILE], { cwd: worktree, maxBuffer: 2048 });
651
703
  if (listed.code !== 0)
652
704
  return { ok: false, reason: "no-op", message: "The committed screenshot inventory could not be inspected. Restore the saved candidate before review." };
653
705
  const required = scope?.acceptance.some(criterion => criterion.evidence.includes("screenshot")) ?? false;
@@ -1936,11 +1988,16 @@ export async function settleProviderOutcome(captured, result) {
1936
1988
  store.setRunPhase(request.runId, "verifying-proof");
1937
1989
  await settleProof(captured, diffEvidence.statId, head);
1938
1990
  }
1939
- catch {
1991
+ catch (error) {
1940
1992
  // Adjudication itself must never fail the attempt — if even the
1941
1993
  // catch-all inside settleProof somehow throws, the build still
1942
1994
  // stands; the run simply has no verdict, which every surface
1943
- // treats the same as "no proof was written".
1995
+ // treats the same as "no proof was written". It is said, though:
1996
+ // release check 3264 ended with no log and no word why.
1997
+ try {
1998
+ store.addRunNote(request.runId, "Toolroll", `The check's result could not be saved: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), clock());
1999
+ }
2000
+ catch { /* the missing verdict still says enough */ }
1944
2001
  }
1945
2002
  }
1946
2003
  }
@@ -2226,9 +2283,12 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2226
2283
  const safe = hits.length > 0 ? redactSecretLines(value, hits) : value;
2227
2284
  return boundStreamHeadTail(safe, cap);
2228
2285
  };
2229
- const attemptOutcome = (label, result) => `${label}: (exit ${result.code}${result.notFound ? " · could not start" : ""}${result.timedOut ? " · timed out" : ""})`;
2286
+ // How the command ended, each fact apart: a signal or the plane's own hard stop is never read as a plain exit
2287
+ // (release check 3261 was recorded as "exit 1" when a signal had ended it).
2288
+ const endedBy = (result, sep) => `${result.signal ? `${sep}ended by ${result.signal}` : ""}${result.stoppedByPlane === true ? `${sep}stopped by Toolroll's own shutdown` : ""}`;
2289
+ const attemptOutcome = (label, result) => `${label}: (exit ${result.code}${result.notFound ? " · could not start" : ""}${result.timedOut ? " · timed out" : ""}${endedBy(result, " · ")})`;
2230
2290
  const attemptLog = (label, command, result) => {
2231
- const prefix = `=== ${label} ===\n$ ${command}\n(exit ${result.code}${result.notFound ? ", could not start" : ""}${result.timedOut ? ", timed out" : ""})\n\n--- stdout ---\n`;
2291
+ const prefix = `=== ${label} ===\n$ ${command}\n(exit ${result.code}${result.notFound ? ", could not start" : ""}${result.timedOut ? ", timed out" : ""}${endedBy(result, ", ")})\n\n--- stdout ---\n`;
2232
2292
  const between = "\n\n--- stderr ---\n";
2233
2293
  checkLogSourceBodyBytes += Buffer.byteLength(prefix) + Buffer.byteLength(result.stdout) + Buffer.byteLength(between) + Buffer.byteLength(result.stderr);
2234
2294
  return `${prefix}${boundedAttemptStream(result.stdout)}${between}${boundedAttemptStream(result.stderr)}`;
@@ -2303,6 +2363,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2303
2363
  else {
2304
2364
  const verifyRunner = request.verify ?? run;
2305
2365
  const verifyShell = approvedCommandShell(configured.command);
2366
+ let baselineUnverifiable = null;
2306
2367
  // A Full check of a prepared candidate (a release gate, the nightly check): its attempt is sealed before the command
2307
2368
  // runs — intended mode, nonce, inventory, context — and the handshake goes on its stdin (check-report.ts).
2308
2369
  // A release task's check is targeted when the release's last full check allows it (release-recheck.ts): its plan and
@@ -2310,36 +2371,54 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2310
2371
  if (repo !== null && chosenCheck?.level === "full" && (store.getScope(request.taskId)?.candidate ?? null) !== null) {
2311
2372
  const access = checkLaunchOf(store, runId, captured.effective.profile).access;
2312
2373
  const decision = planTargetedRecheck(store, root, { runId, taskId: request.taskId, repo, head: sealedHead, command: configured, access, now: now() });
2313
- checkAttempt = prepareCheckAttempt(store, root, { runId, repo, head: sealedHead, command: configured, access, authority: decision.authority,
2314
- ...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch }, now());
2374
+ // A release's check (signed terms or an open authority): CI never stands in for its unit suite.
2375
+ const release = decision.authority !== null || (store.getScope(request.taskId)?.releaseTerms ?? null) !== null;
2376
+ try {
2377
+ checkAttempt = prepareCheckAttempt(store, root, { runId, repo, head: sealedHead, command: configured, access, authority: decision.authority,
2378
+ ...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch, release }, now());
2379
+ }
2380
+ catch (error) {
2381
+ // A recorded last release that can't be verified (eval-harness review 2, finding 4): nothing runs.
2382
+ if (!(error instanceof CheckBaselineUnverifiable))
2383
+ throw error;
2384
+ checkAttempt = null;
2385
+ baselineUnverifiable = error.message;
2386
+ recordCheckNote(error.message);
2387
+ }
2315
2388
  if (checkAttempt !== null && decision.mode === "targeted")
2316
2389
  store.addRunNote(runId, "Toolroll", `Re-running only what failed at run #${decision.plan.base}: ${decision.plan.units.length} unit files, ${decision.plan.groups.reduce((n, one) => n + one.groups.length, 0)} journey groups, ${decision.plan.versions.length} upgrade versions${decision.plan.parts.length > 0 ? `, and in full: ${decision.plan.parts.join(", ")}` : ""}.`, now());
2317
2390
  }
2318
2391
  else
2319
2392
  checkAttempt = null;
2320
- const checkProgress = new CheckProgressTracker(snapshot => {
2321
- store.saveCheckProgress(runId, snapshot, now());
2322
- });
2323
- const runVerification = async (label) => {
2393
+ // Live progress is advisory (release check 3264 lost its log to one refused snapshot): display snapshots are
2394
+ // coalesced (at most one write a second), the final one is kept until it is written, and a failing save never
2395
+ // throws out of the writer, so the check's log, report and receipt are always sealed.
2396
+ const progressWrites = coalescedProgressWriter(snapshot => { store.saveCheckProgress(runId, snapshot, now()); });
2397
+ const checkProgress = new CheckProgressTracker(snapshot => progressWrites.push(snapshot));
2398
+ const runVerification = async (label, missing = null) => {
2324
2399
  const streamed = { stdout: false, stderr: false };
2325
2400
  // The check runs under the stop watch (v52), owned by this run.
2326
- const result = await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, commandRunner(store, runId, captured.effective.profile, verifyRunner)), verifyShell.file, verifyShell.args, {
2327
- cwd: worktree,
2328
- timeoutMs: configured.timeoutMs,
2329
- processGroup: true,
2330
- owner: runOwnerTag(store, runId),
2331
- beforeSpawn: () => !stopRequestedFor(store, runId, request.shouldStop),
2332
- onSpawn: pid => {
2333
- request.onProviderSpawn?.(pid);
2334
- if (stopRequestedFor(store, runId, request.shouldStop))
2335
- throw new Error("the attempt was stopped before spawn custody completed");
2336
- },
2337
- envAllowlist: SETUP_ENV_ALLOWLIST,
2338
- omitEnv: SETUP_ENV_DENYLIST,
2339
- ...(checkAttempt === null ? {} : { stdin: checkAttempt.stdin }),
2340
- onStdout: chunk => { streamed.stdout = true; checkProgress.feed(chunk, "stdout"); },
2341
- onStderr: chunk => { streamed.stderr = true; checkProgress.feed(chunk, "stderr"); },
2342
- }));
2401
+ const result = missing !== null
2402
+ ? { code: 127, stdout: "", stderr: `Toolroll did not start the check: ${missing} wasn't found.`, timedOut: false, notFound: false }
2403
+ : await underStopWatch(store, runId, () => runWithIsolatedDatabase(witnessedRunner(store, runId, now, commandRunner(store, runId, captured.effective.profile, verifyRunner)), verifyShell.file, verifyShell.args, {
2404
+ cwd: worktree,
2405
+ timeoutMs: configured.timeoutMs,
2406
+ processGroup: true,
2407
+ owner: runOwnerTag(store, runId),
2408
+ beforeSpawn: () => !stopRequestedFor(store, runId, request.shouldStop),
2409
+ onSpawn: pid => {
2410
+ request.onProviderSpawn?.(pid);
2411
+ if (stopRequestedFor(store, runId, request.shouldStop))
2412
+ throw new Error("the attempt was stopped before spawn custody completed");
2413
+ },
2414
+ envAllowlist: SETUP_ENV_ALLOWLIST,
2415
+ omitEnv: SETUP_ENV_DENYLIST,
2416
+ ...(checkAttempt === null ? {} : { stdin: checkAttempt.stdin }),
2417
+ onStdout: chunk => { streamed.stdout = true; checkProgress.feed(chunk, "stdout"); },
2418
+ onStderr: chunk => { streamed.stderr = true; checkProgress.feed(chunk, "stderr"); },
2419
+ }));
2420
+ if (missing !== null)
2421
+ unstartedChecks.set(result, missing);
2343
2422
  if (!streamed.stdout)
2344
2423
  checkProgress.feed(result.stdout, "stdout");
2345
2424
  if (!streamed.stderr)
@@ -2388,15 +2467,21 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2388
2467
  return "own-install-failed";
2389
2468
  return await sealedTreeState(buildCheckoutGit(request.git ?? run, worktree)) === "clean" ? null : "setup-changed-files";
2390
2469
  };
2391
- const ownInstall = await ownInstallIfChanged();
2392
- const first = ownInstall === null ? await runVerification("Project check · attempt 1") : null;
2470
+ const ownInstall = baselineUnverifiable === null ? await ownInstallIfChanged() : null;
2471
+ // A missing program short-circuits the first launch; never execute build code and then infer that nothing ran.
2472
+ const missingProgram = configured.recoverySetupDigest === null ? null : approvedProgramMissing(configured.command, worktree, process.env["PATH"]);
2473
+ const first = baselineUnverifiable === null && ownInstall === null ? await runVerification("Project check · attempt 1", missingProgram) : null;
2393
2474
  if (first === null) {
2394
- verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: ownInstall ?? "own-install-failed" };
2475
+ verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: baselineUnverifiable !== null ? "baseline-unverifiable" : ownInstall ?? "own-install-failed" };
2395
2476
  }
2396
2477
  else if (first.notFound || first.timedOut) {
2397
2478
  verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: first.timedOut ? "timed-out" : "spawn-failed" };
2398
2479
  }
2399
- else if (!verificationExecutableMissing(first)) {
2480
+ else if (first.stoppedByPlane === true) {
2481
+ // This worker's own hard stop ended it (a shutdown): it never finished, and its exit says nothing about the code.
2482
+ verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "stopped-by-toolroll" };
2483
+ }
2484
+ else if (!launchFailedBeforeCheck(first, checkAttempt?.nonce ?? null, missingProgram)) {
2400
2485
  verifyCommand = { configured: true, ran: true, exitCode: first.code };
2401
2486
  }
2402
2487
  else {
@@ -2511,7 +2596,8 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2511
2596
  verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "dependency-still-missing" };
2512
2597
  }
2513
2598
  else {
2514
- verifyCommand = { configured: true, ran: true, exitCode: retried.code, setupReplayed: true };
2599
+ // The first attempt, classified on its own: it never started (its program was missing).
2600
+ verifyCommand = { configured: true, ran: true, exitCode: retried.code, setupReplayed: true, firstAttempt: { class: "launch", program: missingProgram, exitCode: first.code } };
2515
2601
  }
2516
2602
  }
2517
2603
  }
@@ -2519,11 +2605,19 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2519
2605
  }
2520
2606
  }
2521
2607
  }
2522
- checkProgress.finish();
2608
+ // The display's final snapshot (Codex review round 1, finding 6): the writer keeps it until a write succeeds and
2609
+ // never throws; a display failure of any kind never keeps the check's outcome below from being sealed.
2610
+ try {
2611
+ checkProgress.finish();
2612
+ }
2613
+ catch { /* display only */ }
2614
+ if (!progressWrites.flush())
2615
+ progressWrites.flush();
2523
2616
  // The check's own report (the last line carrying the nonce), sealed beside what was sealed before it ran.
2524
2617
  const last = verification.last;
2618
+ let sealedReport = null;
2525
2619
  if (checkAttempt !== null && last !== null && !last.notFound) {
2526
- sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
2620
+ sealedReport = sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
2527
2621
  }
2528
2622
  // What the check covered (receipt version 3): full, composed from the release's last full check, partial, or
2529
2623
  // identity-removed (which stops the release carrying).
@@ -2532,10 +2626,15 @@ async function settleProof(captured, statArtifactId, sealedHead) {
2532
2626
  const sha = (capture) => { const found = store.artifactsFor(runId).filter(a => a.kind === "structured-output" && a.capture === capture); return found.length === 1 ? found[0].sha256 : null; };
2533
2627
  // The one place coverage meets the outcome: a partial or identity-removed check is a failed attempt for every
2534
2628
  // reader from here on (the verdict, the run's check, the receipt, reuse, the deploy gates).
2535
- const effective = effectiveCheckFacts(verifyCommand, { coverage: settled.coverage, check: { attempt: checkAttempt.mode, report: sha(CHECK_REPORT_CAPTURE), inventory: sha(CHECK_INVENTORY_CAPTURE) }, composed: settled.composed });
2629
+ const effective = effectiveCheckFacts(verifyCommand, { coverage: settled.coverage, check: { attempt: checkAttempt.mode, report: sha(CHECK_REPORT_CAPTURE), inventory: sha(CHECK_INVENTORY_CAPTURE), retried: retriedInside(sealedReport) }, composed: settled.composed });
2536
2630
  verifyCommand = effective.result;
2537
2631
  releaseFacts = effective.facts;
2538
2632
  }
2633
+ else if (verifyCommand.configured && verifyCommand.ran && chosenCheck?.level === "full" && (store.getScope(request.taskId)?.releaseTerms ?? null) !== null) {
2634
+ // A release's Full check that couldn't be sealed before it ran (no nonce, no inventory) has no result it can count.
2635
+ verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "no-result" };
2636
+ recordCheckNote("The release check ran without a sealed attempt (its commit couldn't be read before it ran), so it has no result that can count.");
2637
+ }
2539
2638
  }
2540
2639
  if (configured !== null && checkLog.length > 0) {
2541
2640
  const summary = `=== Attempt summary ===\n${checkOutcomes.length === 0 ? "No command started." : checkOutcomes.map(one => `- ${one}`).join("\n")}`;
@@ -30,6 +30,7 @@ import { requestResultChanges } from "./result-actions.js";
30
30
  import { revisionSourceOf } from "./result-review.js";
31
31
  import { APPROVAL_PROOF, approvalOf, approvalProofDetail, deployGateScope, quarantinedAuthor } from "./scope.js";
32
32
  import { releaseCardOf } from "./release-card.js";
33
+ import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
33
34
  import { phoneText } from "./telegram-status.js";
34
35
  /** The words, said once. */
35
36
  export const DECIDE_LABELS = {
@@ -662,6 +663,9 @@ export function applyDecideTap(store, seat, input) {
662
663
  }
663
664
  case "approve": {
664
665
  if (row.digest.startsWith(RELEASE_DIGEST)) {
666
+ const ciHold = releaseCiHoldOf(store, row.taskId);
667
+ if (ciHold !== null)
668
+ return notDone(releaseCiHoldWords(ciHold));
665
669
  const release = releaseInChat(store, row.taskId, who.name, now, seat.channel, seat, input.root);
666
670
  if (!release.ok)
667
671
  return notDone(release.why);
@@ -0,0 +1,92 @@
1
+ import { type CheckReport } from "./contracts/release-check.js";
2
+ import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT, type FailureClassName } from "./release.js";
3
+ import type { Store } from "./store.js";
4
+ export type FailureClass = FailureClassName;
5
+ export type ClassifiedFailure = {
6
+ class: FailureClass;
7
+ why: string;
8
+ };
9
+ /** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
10
+ export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
11
+ /** The ledger action that records each rerun the release asked for, and why. */
12
+ export declare const RELEASE_RECHECK_ACTION = "release check rerun";
13
+ /** What a sealed verification receipt says about the check (receipt.result, and its coverage). */
14
+ export type CheckFacts = {
15
+ result: {
16
+ configured: boolean;
17
+ ran?: boolean;
18
+ exitCode?: number;
19
+ attemptFailed?: boolean;
20
+ failure?: string;
21
+ };
22
+ coverage?: string | undefined;
23
+ };
24
+ /**
25
+ * One failed check, classified. `facts` come from the plane's sealed receipt; `report` (the check's own report, sealed
26
+ * when valid) only ever picks among the classes that block. Null when the check passed.
27
+ */
28
+ export declare function classifyCheckFailure(facts: CheckFacts, report: CheckReport | null): ClassifiedFailure | null;
29
+ /** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
30
+ * never silently part of a pass. Null when there is no valid report to read it from: unknown, never "no retries". */
31
+ export declare function retriedInside(report: CheckReport | null): string[] | null;
32
+ /** A run's check, classified from its sealed receipt and sealed report; null when it passed, or can't be read (then
33
+ * `unreadable` says why — an unreadable receipt is itself unknown). */
34
+ export declare function classifyRunCheck(store: Store, root: string, runId: number): ClassifiedFailure | null;
35
+ /**
36
+ * What passed only on a retry inside a run's check (eval-harness review 1, finding 3): the summary sealed into its
37
+ * protected receipt, which a damaged or lost report can't take away; for an older receipt without one, read from the
38
+ * sealed report. Null — unknown — when neither can be read, or when a readable report disagrees with the receipt.
39
+ */
40
+ export declare function retriedOfRun(store: Store, root: string, runId: number): string[] | null;
41
+ /** How a check rerun after the approved recovery setup reads among the retries. */
42
+ export declare const SETUP_REPLAY_RETRY = "the whole check, rerun after the approved setup";
43
+ /**
44
+ * The rerun's words with its first attempt's own class (eval-harness review 2, finding 5): a launch failure names the
45
+ * missing program; a receipt that doesn't classify it (an older one) says so, never that it was a launch failure.
46
+ */
47
+ export declare function setupReplayWords(first: unknown): string;
48
+ /**
49
+ * Whether a passing check of a release task stands, given every earlier attempt on the same candidate and signed scope:
50
+ * each earlier failure must be confirmed infrastructure the release itself reran (at most two), and nothing in the
51
+ * passing run may have passed only on a retry. A deterministic, unknown or agent failure before it isn't erased by a
52
+ * later pass: a person decides. `retried` is reported even when it stands (a person's approval reads it; an automatic
53
+ * one refuses on it).
54
+ */
55
+ export declare function releaseCheckStanding(store: Store, root: string, input: {
56
+ taskId: string;
57
+ run: number;
58
+ }): {
59
+ ok: true;
60
+ earlier: {
61
+ run: number;
62
+ why: string;
63
+ }[];
64
+ retried: string[] | null;
65
+ } | {
66
+ ok: false;
67
+ why: string;
68
+ retried: string[] | null;
69
+ };
70
+ export declare const classWords: (kind: FailureClass) => string;
71
+ export type RecheckAnswer = {
72
+ action: "rerun";
73
+ class: "infrastructure";
74
+ why: string;
75
+ source: number;
76
+ run: number;
77
+ } | {
78
+ action: "pending";
79
+ source: number;
80
+ } | {
81
+ action: "stop";
82
+ class: FailureClass | null;
83
+ why: string;
84
+ source: number | null;
85
+ };
86
+ /**
87
+ * After a failed release check: the plane classifies the task's newest attempt from its own records and, only for
88
+ * confirmed infrastructure within its limits (one rerun per incident, two per candidate), runs the check again on the
89
+ * same candidate under the same approval (dispose.ts regateTask keeps candidate, scope and approval). Every attempt
90
+ * stays in the evidence; each rerun is in the ledger with its class. Anything else stops, saying which class and why.
91
+ */
92
+ export declare function recheckReleaseTask(store: Store, root: string, taskId: string, now: Date): RecheckAnswer;