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.
- package/CHANGELOG.md +192 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +28 -8
- package/dist/assignment.js +3 -0
- 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/builder.d.ts +14 -0
- package/dist/builder.js +140 -41
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +267 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +52 -6
- package/dist/check-report.js +193 -8
- package/dist/checkout-cleanup.js +2 -2
- 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/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -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-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/dispose.d.ts +3 -0
- package/dist/dispose.js +6 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.js +4 -1
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +7 -1
- 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 +4 -3
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +773 -319
- 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 +48 -1
- package/dist/release-adapters.js +343 -10
- 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-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.js +21 -12
- package/dist/release-coverage.js +5 -2
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +6 -1
- package/dist/release.d.ts +108 -11
- package/dist/release.js +294 -39
- package/dist/review-context.js +8 -6
- package/dist/runner.js +2 -1
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.js +7 -1
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +101 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/render-tasks.d.ts +2 -0
- package/dist/server/render-tasks.js +2 -1
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/task-views.js +7 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +10 -0
- package/dist/store.js +25 -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 +143 -31
- package/dist/test-infra.js +1 -1
- 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 +77 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- 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,
|
|
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, [...
|
|
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
|
-
|
|
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
|
-
|
|
2314
|
-
|
|
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
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
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 =
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
request.
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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")}`;
|
package/dist/chat-decide.js
CHANGED
|
@@ -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;
|