toolroll 0.9.75 → 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 (129) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +36 -8
  5. package/dist/approval-authority.js +6 -2
  6. package/dist/approval-rules-ui.d.ts +11 -0
  7. package/dist/approval-rules-ui.js +13 -1
  8. package/dist/assignment.js +3 -0
  9. package/dist/batch-checks.js +3 -1
  10. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +31 -31
  13. package/dist/builder.d.ts +14 -0
  14. package/dist/builder.js +141 -42
  15. package/dist/chat-decide.js +4 -0
  16. package/dist/check-failure.d.ts +92 -0
  17. package/dist/check-failure.js +267 -0
  18. package/dist/check-progress.d.ts +28 -1
  19. package/dist/check-progress.js +105 -6
  20. package/dist/check-report.d.ts +52 -6
  21. package/dist/check-report.js +193 -8
  22. package/dist/checkout-cleanup.js +2 -2
  23. package/dist/cli.js +8 -5
  24. package/dist/coding-handoff.d.ts +1 -0
  25. package/dist/coding-handoff.js +20 -11
  26. package/dist/coding-workspace.js +7 -5
  27. package/dist/commit-extract.d.ts +6 -0
  28. package/dist/commit-extract.js +63 -8
  29. package/dist/console-chrome.d.ts +41 -0
  30. package/dist/console-chrome.js +166 -0
  31. package/dist/contracts/console-api.d.ts +1 -0
  32. package/dist/contracts/console-api.js +1 -0
  33. package/dist/contracts/release-check.d.ts +12 -0
  34. package/dist/contracts/release-check.js +17 -0
  35. package/dist/contracts/verification-receipt.d.ts +20 -2
  36. package/dist/contracts/verification-receipt.js +15 -3
  37. package/dist/contracts/verification-view.d.ts +8 -0
  38. package/dist/controller-supervisor.d.ts +8 -0
  39. package/dist/controller-supervisor.js +33 -1
  40. package/dist/demo.js +3 -2
  41. package/dist/deploy-gate.d.ts +3 -0
  42. package/dist/deploy-gate.js +8 -2
  43. package/dist/desktop-update.d.ts +1 -1
  44. package/dist/discover.js +6 -1
  45. package/dist/dispose.d.ts +3 -0
  46. package/dist/dispose.js +6 -3
  47. package/dist/doctor.d.ts +7 -0
  48. package/dist/doctor.js +10 -2
  49. package/dist/envelope.js +2 -1
  50. package/dist/evidence.js +4 -1
  51. package/dist/exec.d.ts +7 -0
  52. package/dist/exec.js +7 -1
  53. package/dist/first-run.js +3 -1
  54. package/dist/git-floor.d.ts +16 -0
  55. package/dist/git-floor.js +36 -0
  56. package/dist/git-incident.d.ts +49 -0
  57. package/dist/git-incident.js +69 -0
  58. package/dist/git-views.d.ts +20 -0
  59. package/dist/git-views.js +120 -0
  60. package/dist/grant.js +7 -1
  61. package/dist/guides.js +4 -3
  62. package/dist/operate.d.ts +13 -0
  63. package/dist/operate.js +779 -319
  64. package/dist/planner.js +5 -1
  65. package/dist/prepared-evidence.d.ts +2 -1
  66. package/dist/prepared-evidence.js +8 -3
  67. package/dist/project-knowledge.js +6 -2
  68. package/dist/project-learning.js +23 -3
  69. package/dist/proof.js +6 -0
  70. package/dist/pull-request-flow.js +4 -0
  71. package/dist/read-executor-worker.js +8 -4
  72. package/dist/read-executor.d.ts +18 -1
  73. package/dist/read-executor.js +8 -0
  74. package/dist/release-adapters.d.ts +71 -2
  75. package/dist/release-adapters.js +471 -12
  76. package/dist/release-authority.d.ts +46 -0
  77. package/dist/release-authority.js +68 -0
  78. package/dist/release-auto.d.ts +184 -0
  79. package/dist/release-auto.js +612 -0
  80. package/dist/release-card.d.ts +15 -3
  81. package/dist/release-card.js +25 -9
  82. package/dist/release-ci-hold.d.ts +41 -0
  83. package/dist/release-ci-hold.js +53 -0
  84. package/dist/release-cli.js +77 -14
  85. package/dist/release-coverage.js +5 -2
  86. package/dist/release-recheck.d.ts +1 -1
  87. package/dist/release-recheck.js +6 -1
  88. package/dist/release.d.ts +207 -6
  89. package/dist/release.js +495 -20
  90. package/dist/repository-context.js +4 -6
  91. package/dist/result-follow-ups.js +3 -1
  92. package/dist/review-context.js +8 -6
  93. package/dist/runner.js +2 -1
  94. package/dist/sandbox-runtime.d.ts +8 -0
  95. package/dist/sandbox-runtime.js +14 -2
  96. package/dist/scope.d.ts +31 -2
  97. package/dist/scope.js +51 -4
  98. package/dist/scout.js +5 -1
  99. package/dist/secret-mint.d.ts +1 -0
  100. package/dist/secret-mint.js +14 -0
  101. package/dist/serve.js +101 -51
  102. package/dist/server/chat.js +5 -5
  103. package/dist/server/render-tasks.d.ts +12 -0
  104. package/dist/server/render-tasks.js +25 -14
  105. package/dist/server/request-context.d.ts +6 -0
  106. package/dist/server/route-table.js +3 -0
  107. package/dist/server/runtime.d.ts +2 -0
  108. package/dist/server/settings.js +29 -2
  109. package/dist/server/task-views.js +15 -2
  110. package/dist/setup-guide.d.ts +2 -2
  111. package/dist/storage.d.ts +5 -0
  112. package/dist/storage.js +28 -1
  113. package/dist/store.d.ts +11 -1
  114. package/dist/store.js +44 -5
  115. package/dist/task-base.d.ts +4 -0
  116. package/dist/task-base.js +22 -2
  117. package/dist/teardown-git.d.ts +90 -13
  118. package/dist/teardown-git.js +1027 -129
  119. package/dist/test-infra.d.ts +26 -0
  120. package/dist/test-infra.js +321 -0
  121. package/dist/up-processes.d.ts +137 -0
  122. package/dist/up-processes.js +165 -0
  123. package/dist/up-role-child.d.ts +1 -0
  124. package/dist/up-role-child.js +77 -0
  125. package/dist/verification-evidence.d.ts +1 -0
  126. package/dist/verification-evidence.js +13 -1
  127. package/dist/worktree.d.ts +4 -0
  128. package/dist/worktree.js +13 -7
  129. package/package.json +2 -1
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Why a release check failed, in one of four classes (docs/plans/eval-harness.md, phase 1 C), and what each class
3
+ * allows. This replaces the flat "two flakes" allowance (AUTO_FLAKE_CAP) for release checks and for GitHub CI.
4
+ *
5
+ * deterministic typecheck, build, a failing test, a scripted journey, an upgrade, a required test gone:
6
+ * nothing tolerated, never rerun to pass; the branch is fixed and the new commit released.
7
+ * infrastructure independently confirmed by the plane itself: the check never started, this worker lost the
8
+ * build, Toolroll's own shutdown stopped it — or, on GitHub, a job that failed before any of the
9
+ * workflow's own steps ran. At most one rerun per incident and two per candidate, under the same
10
+ * approval; a pass after them counts.
11
+ * unknown exit 1 with no failing test named, an unexplained timeout or signal, no result at all: an error
12
+ * that blocks. A diagnostic rerun may run, but its pass doesn't erase the failure: a person decides.
13
+ * agent-behaviour a real-model journey failed (or passed only on its retry): counted, never rerun to pass.
14
+ *
15
+ * Only plane-side facts can make a failure "infrastructure": the sealed receipt's own result (which the builder writes
16
+ * from the process it ran), and GitHub's own job and step records read by the plane's gh. Nothing the candidate writes —
17
+ * its output, a label it prints, its report — can. The report is candidate-written: it is read only to choose among
18
+ * the classes that block (deterministic, agent-behaviour, unknown), never to allow a rerun. gate-failure.ts's
19
+ * classifyGateFailure reads the log and is never evidence that a release failure is harmless.
20
+ */
21
+ import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
22
+ import { readCheckReport } from "./contracts/release-check.js";
23
+ import { regateTask } from "./dispose.js";
24
+ import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT } from "./release.js";
25
+ import { verificationEvidence } from "./verification-evidence.js";
26
+ /** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
27
+ export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
28
+ /** The ledger action that records each rerun the release asked for, and why. */
29
+ export const RELEASE_RECHECK_ACTION = "release check rerun";
30
+ /** The failures that mean the check never started or never finished for a reason the plane itself saw. */
31
+ const PLANE_INFRA = {
32
+ "spawn-failed": "the check never started: the machine couldn't start its command",
33
+ "retry-spawn-failed": "the check's retry never started: the machine couldn't start its command",
34
+ "custody-lost": "this worker lost the build before the check could finish",
35
+ "stopped-by-toolroll": "Toolroll's own shutdown stopped the check before it finished",
36
+ };
37
+ /**
38
+ * One failed check, classified. `facts` come from the plane's sealed receipt; `report` (the check's own report, sealed
39
+ * when valid) only ever picks among the classes that block. Null when the check passed.
40
+ */
41
+ export function classifyCheckFailure(facts, report) {
42
+ const result = facts.result;
43
+ const passedCoverage = facts.coverage === undefined || facts.coverage === "full" || facts.coverage === "composed";
44
+ if (result.configured && result.ran === true && result.exitCode === 0 && passedCoverage)
45
+ return null;
46
+ if (!result.configured)
47
+ return { class: "unknown", why: "no check is configured" };
48
+ if (result.ran !== true) {
49
+ const failure = result.failure ?? "";
50
+ const infra = PLANE_INFRA[failure];
51
+ if (infra !== undefined)
52
+ return { class: "infrastructure", why: infra };
53
+ if (failure === "timed-out" || failure === "retry-timed-out")
54
+ return { class: "unknown", why: "the check timed out, and nothing says why" };
55
+ if (failure === "baseline-unverifiable")
56
+ return { class: "unknown", why: "the last release Toolroll recorded couldn't be verified, so what the check owed is unknown and it wasn't run" };
57
+ if (failure === "no-result")
58
+ return { class: "unknown", why: "the check gave no complete result (no valid report, or a part, file or journey with no outcome)" };
59
+ if (failure === "coverage-incomplete")
60
+ return { class: "deterministic", why: facts.coverage === "identity-removed" ? "a required test was removed, renamed or newly skipped" : "the check didn't cover everything it owed" };
61
+ return { class: "unknown", why: `the check couldn't run to its end (${failure || "no reason recorded"})` };
62
+ }
63
+ // It ran and exited nonzero (or its coverage fell short): what failed, from its report — only among blocking classes.
64
+ if (report === null)
65
+ return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} and gave no report naming what failed` };
66
+ const deterministic = [], agent = [], unexplained = [];
67
+ for (const part of report.parts) {
68
+ if (!part.selected || !part.ran || part.code === null)
69
+ continue;
70
+ if (part.timedOut || part.signal != null) {
71
+ unexplained.push(`${part.part} ${part.timedOut ? "timed out" : `was ended by ${part.signal}`}`);
72
+ continue;
73
+ }
74
+ if ((part.part === "typecheck" || part.part === "build") && part.code !== 0)
75
+ deterministic.push(`the ${part.part} failed`);
76
+ }
77
+ for (const file of report.units?.files ?? [])
78
+ if (file.state === "failed")
79
+ deterministic.push(`${file.file} failed`);
80
+ if ((report.units?.runErrors ?? 0) > 0 && !(report.units?.files ?? []).some(file => file.state === "failed"))
81
+ unexplained.push(`${report.units.runErrors} unit errors no test owned`);
82
+ for (const run of report.journeys)
83
+ for (const group of run.groups) {
84
+ if (group.state !== "failed")
85
+ continue;
86
+ (run.kind === "real" ? agent : deterministic).push(`${run.script}${run.kind === "real" ? " real-model" : ""} journey group ${group.group} failed`);
87
+ }
88
+ for (const version of report.upgrades?.versions ?? [])
89
+ if (version.state === "failed")
90
+ deterministic.push(`the upgrade from ${version.version} failed`);
91
+ const list = (items) => `${items.slice(0, 4).join("; ")}${items.length > 4 ? `; and ${items.length - 4} more` : ""}`;
92
+ if (deterministic.length > 0)
93
+ return { class: "deterministic", why: list(deterministic) };
94
+ if (agent.length > 0)
95
+ return { class: "agent-behaviour", why: list(agent) };
96
+ if (unexplained.length > 0)
97
+ return { class: "unknown", why: list(unexplained) };
98
+ return { class: "unknown", why: `the check exited ${result.exitCode ?? "nonzero"} with no failing test identified` };
99
+ }
100
+ /** What passed only on a retry inside a passing check (a real-model journey group, a unit file): counted and reported,
101
+ * never silently part of a pass. Null when there is no valid report to read it from: unknown, never "no retries". */
102
+ export function retriedInside(report) {
103
+ if (report === null)
104
+ return null;
105
+ return [
106
+ ...report.journeys.flatMap(run => run.groups.filter(group => group.state === "passed" && group.retried).map(group => `${run.script}${run.kind === "real" ? " real-model" : ""} journey group ${group.group}`)),
107
+ ...(report.units?.files ?? []).filter(file => file.flaky).map(file => `${file.file}`),
108
+ ];
109
+ }
110
+ /** A run's check, classified from its sealed receipt and sealed report; null when it passed, or can't be read (then
111
+ * `unreadable` says why — an unreadable receipt is itself unknown). */
112
+ export function classifyRunCheck(store, root, runId) {
113
+ const gate = verificationEvidence(store, root, runId);
114
+ if (!gate.ok)
115
+ return { class: "unknown", why: `the check's receipt can't be read: ${gate.problem}` };
116
+ if (gate.bytes === null)
117
+ return { class: "unknown", why: "no machine check is recorded" };
118
+ let receipt;
119
+ try {
120
+ receipt = JSON.parse(gate.bytes.toString());
121
+ }
122
+ catch {
123
+ return { class: "unknown", why: "the check's receipt can't be read" };
124
+ }
125
+ if (receipt.result === undefined || typeof receipt.result !== "object")
126
+ return { class: "unknown", why: "the check's receipt names no result" };
127
+ const sealed = readSealedJson(store, root, runId, CHECK_REPORT_CAPTURE);
128
+ const parsed = sealed === null ? null : readCheckReport(sealed.value);
129
+ return classifyCheckFailure({ result: receipt.result, coverage: receipt.coverage }, parsed?.ok === true ? parsed.value : null);
130
+ }
131
+ /**
132
+ * What passed only on a retry inside a run's check (eval-harness review 1, finding 3): the summary sealed into its
133
+ * protected receipt, which a damaged or lost report can't take away; for an older receipt without one, read from the
134
+ * sealed report. Null — unknown — when neither can be read, or when a readable report disagrees with the receipt.
135
+ */
136
+ export function retriedOfRun(store, root, runId) {
137
+ const sealed = readSealedJson(store, root, runId, CHECK_REPORT_CAPTURE);
138
+ const parsed = sealed === null ? null : readCheckReport(sealed.value);
139
+ const fromReport = retriedInside(parsed?.ok === true ? parsed.value : null);
140
+ const gate = verificationEvidence(store, root, runId);
141
+ if (!gate.ok || gate.bytes === null)
142
+ return null;
143
+ let body;
144
+ try {
145
+ body = JSON.parse(gate.bytes.toString());
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ // The whole check rerun after the approved recovery setup (finding 6): its first attempt failed, and a missing
151
+ // executable is never confirmed infrastructure, so it counts as a retry like any other — reported, and an automatic
152
+ // approval refuses it.
153
+ const replayed = body.result?.setupReplayed === true ? [setupReplayWords(body.result.firstAttempt)] : [];
154
+ const receipt = body.check?.retried;
155
+ if (receipt === undefined)
156
+ return fromReport === null ? null : [...replayed, ...fromReport];
157
+ if (receipt === null || !Array.isArray(receipt) || receipt.some(one => typeof one !== "string"))
158
+ return null;
159
+ if (fromReport !== null && JSON.stringify([...fromReport].sort()) !== JSON.stringify([...receipt].sort()))
160
+ return null;
161
+ return [...replayed, ...receipt];
162
+ }
163
+ /** How a check rerun after the approved recovery setup reads among the retries. */
164
+ export const SETUP_REPLAY_RETRY = "the whole check, rerun after the approved setup";
165
+ /**
166
+ * The rerun's words with its first attempt's own class (eval-harness review 2, finding 5): a launch failure names the
167
+ * missing program; a receipt that doesn't classify it (an older one) says so, never that it was a launch failure.
168
+ */
169
+ export function setupReplayWords(first) {
170
+ const one = first !== null && typeof first === "object" ? first : null;
171
+ if (one !== null && one.class === "launch" && typeof one.program === "string" && Number.isSafeInteger(one.exitCode)) {
172
+ return `${SETUP_REPLAY_RETRY} (its first attempt never started: ${one.program.slice(0, 200)} wasn't found, exit ${String(one.exitCode)})`;
173
+ }
174
+ return `${SETUP_REPLAY_RETRY} (its first attempt's failure isn't classified)`;
175
+ }
176
+ /** The task's finished builder runs under `digest`, oldest first, each with whether its check passed. */
177
+ function builderRuns(store, taskRef, digest) {
178
+ return store.handle.prepare(`SELECT r.id, r.outcome, r.scope_digest, r.finished_at, c.status FROM run r LEFT JOIN run_check c ON c.run = r.id
179
+ WHERE r.task_ref = ? AND r.role = 'builder' AND r.finished_at IS NOT NULL AND r.scope_digest = ? ORDER BY r.id`).all(taskRef, digest)
180
+ .map(row => ({ id: Number(row["id"]), outcome: row["outcome"] === null ? null : String(row["outcome"]), scopeDigest: String(row["scope_digest"]), finishedAt: String(row["finished_at"]),
181
+ passed: (row["outcome"] === "built" || row["outcome"] === "no-change") && row["status"] === "passed" }));
182
+ }
183
+ /** The reruns the release asked for on this task (each source run once): run id → its class. */
184
+ function reruns(store, taskId) {
185
+ const rows = store.handle.prepare("SELECT run_id, outcome FROM action_ledger WHERE task_id = ? AND action = ? AND run_id IS NOT NULL ORDER BY id").all(taskId, RELEASE_RECHECK_ACTION);
186
+ return new Map(rows.map(row => [Number(row["run_id"]), String(row["outcome"])]));
187
+ }
188
+ /**
189
+ * Whether a passing check of a release task stands, given every earlier attempt on the same candidate and signed scope:
190
+ * each earlier failure must be confirmed infrastructure the release itself reran (at most two), and nothing in the
191
+ * passing run may have passed only on a retry. A deterministic, unknown or agent failure before it isn't erased by a
192
+ * later pass: a person decides. `retried` is reported even when it stands (a person's approval reads it; an automatic
193
+ * one refuses on it).
194
+ */
195
+ export function releaseCheckStanding(store, root, input) {
196
+ const ref = store.lookupRef(input.taskId);
197
+ const scope = store.getScope(input.taskId);
198
+ const run = store.getRun(input.run);
199
+ if (ref === null || scope === null || run === null || run.taskRef !== ref.id)
200
+ return { ok: false, why: `run ${input.run} isn't a build of ${input.taskId}`, retried: [] };
201
+ const retried = retriedOfRun(store, root, input.run);
202
+ const digest = run.scopeDigest ?? null;
203
+ if (digest === null)
204
+ return { ok: false, why: `run ${input.run} recorded no scope`, retried };
205
+ const asked = reruns(store, input.taskId);
206
+ const earlier = [];
207
+ for (const one of builderRuns(store, ref.id, digest)) {
208
+ if (one.id >= input.run || one.passed)
209
+ continue;
210
+ const classified = one.outcome === "built" || one.outcome === "no-change" ? classifyRunCheck(store, root, one.id) : { class: "unknown", why: `its build ended ${one.outcome ?? "without an outcome"}` };
211
+ if (classified === null)
212
+ continue;
213
+ if (classified.class !== "infrastructure")
214
+ return { ok: false, why: `an earlier check of this commit (run ${one.id}) failed: ${classified.why} (${classWords(classified.class)}). A later pass doesn't erase it, so a person decides`, retried };
215
+ if (asked.get(one.id) !== "infrastructure")
216
+ return { ok: false, why: `an earlier check of this commit (run ${one.id}) failed (${classified.why}) and wasn't rerun by the release, so a person decides`, retried };
217
+ earlier.push({ run: one.id, why: classified.why });
218
+ }
219
+ if (earlier.length > INFRA_RERUNS_PER_CANDIDATE)
220
+ return { ok: false, why: `infrastructure failed ${earlier.length} times on this commit (at most ${INFRA_RERUNS_PER_CANDIDATE} are rerun), so a person decides`, retried };
221
+ return { ok: true, earlier, retried };
222
+ }
223
+ export const classWords = (kind) => ({
224
+ deterministic: "a failure in the code or its tests: fix the branch",
225
+ infrastructure: "confirmed infrastructure",
226
+ unknown: "unexplained: a person decides",
227
+ "agent-behaviour": "a real-model journey: counted, never rerun to pass",
228
+ }[kind]);
229
+ /**
230
+ * After a failed release check: the plane classifies the task's newest attempt from its own records and, only for
231
+ * confirmed infrastructure within its limits (one rerun per incident, two per candidate), runs the check again on the
232
+ * same candidate under the same approval (dispose.ts regateTask keeps candidate, scope and approval). Every attempt
233
+ * stays in the evidence; each rerun is in the ledger with its class. Anything else stops, saying which class and why.
234
+ */
235
+ export function recheckReleaseTask(store, root, taskId, now) {
236
+ const ref = store.lookupRef(taskId);
237
+ const scope = store.getScope(taskId);
238
+ if (ref === null || scope === null || (scope.releaseTerms ?? null) === null || (scope.candidate ?? null) === null)
239
+ return { action: "stop", class: null, why: `${taskId} isn't a release task`, source: null };
240
+ // One transaction: the rerun and its ledger row land together, or neither does.
241
+ return store.transact(() => recheckInside(store, root, taskId, ref.repo, String(scope.candidate), now));
242
+ }
243
+ function recheckInside(store, root, taskId, repo, candidate, now) {
244
+ const ref = store.lookupRef(taskId);
245
+ const last = store.lastBuilderRun(ref.id);
246
+ if (last === null)
247
+ return { action: "stop", class: null, why: `${taskId} has no finished attempt`, source: null };
248
+ const asked = reruns(store, taskId);
249
+ // Already rerun for this incident: the new attempt is queued or running.
250
+ if (asked.has(last.id))
251
+ return { action: "pending", source: last.id };
252
+ const classified = last.outcome === "built" || last.outcome === "no-change" ? classifyRunCheck(store, root, last.id) : { class: "unknown", why: `its build ended ${last.outcome ?? "without an outcome"}` };
253
+ if (classified === null)
254
+ return { action: "stop", class: null, why: `run ${last.id}'s check passed; there is nothing to rerun`, source: last.id };
255
+ if (classified.class !== "infrastructure")
256
+ return { action: "stop", class: classified.class, why: classified.why, source: last.id };
257
+ const digest = last.scopeDigest ?? null;
258
+ const before = [...asked.entries()].filter(([run, kind]) => kind === "infrastructure" && store.getRun(run)?.scopeDigest === digest).length;
259
+ if (before >= INFRA_RERUNS_PER_CANDIDATE)
260
+ return { action: "stop", class: "infrastructure", why: `${classified.why}, and the check was already rerun ${before} times on this commit for infrastructure, so a person decides`, source: last.id };
261
+ const rerun = regateTask(store, taskId, now, { kind: "release", name: "toolroll release" });
262
+ if (!rerun.ok)
263
+ return { action: "stop", class: "infrastructure", why: `${classified.why}, but it can't run again: ${rerun.message}`, source: last.id };
264
+ store.recordAction({ at: now.toISOString(), actor: "toolroll", repo, taskId, runId: last.id, action: RELEASE_RECHECK_ACTION, outcome: "infrastructure", source: "work",
265
+ detail: `${classified.why}; rerun ${before + 1} of at most ${INFRA_RERUNS_PER_CANDIDATE} on ${candidate.slice(0, 7)}, under the same approval` });
266
+ return { action: "rerun", class: "infrastructure", why: classified.why, source: last.id, run: last.id };
267
+ }
@@ -37,7 +37,9 @@ export declare class CheckProgressTracker {
37
37
  private readonly events;
38
38
  private last;
39
39
  private recognized;
40
- private parallel;
40
+ /** Each part's parallel journey run (scripts/e2e-parallel.mjs), by its release-check tag ("" for untagged output):
41
+ * the scripted flows and app parts run at once, so one shared run would mix their groups. */
42
+ private readonly parallels;
41
43
  constructor(update: (snapshot: CheckProgressSnapshot) => void);
42
44
  feed(chunk: string, stream?: "stdout" | "stderr"): void;
43
45
  finish(): CheckProgressSnapshot | null;
@@ -49,3 +51,28 @@ export declare class CheckProgressTracker {
49
51
  /** The suite is the sum of its groups; its total is known once every group has reported one. */
50
52
  private summarize;
51
53
  }
54
+ /** How often a running check's display snapshot may be written: the page and the phone read it, nobody needs more. */
55
+ export declare const CHECK_PROGRESS_WRITE_MS = 1000;
56
+ export type ProgressWriter = {
57
+ /** A display snapshot: written now if the last write is old enough, else kept (the newest wins) for one later write.
58
+ * Never throws: a display write that fails is kept (a final one until it is saved) for the next write or flush. */
59
+ push: (snapshot: CheckProgressSnapshot) => void;
60
+ /** Write a kept snapshot now (the final one again, when its write failed); nothing waits after. Never throws: true
61
+ * when nothing is left unsaved. */
62
+ flush: () => boolean;
63
+ };
64
+ /**
65
+ * Coalesced display writes for a running check (isolated-checks.md, stage 1): a test runner can print thousands of
66
+ * lines a second, and every changed snapshot was its own write transaction. Display snapshots are now written at most
67
+ * once per interval, the latest one winning; the final snapshot is written at once, exactly, and nothing older is ever
68
+ * written after it. A final snapshot whose write fails is kept until a write of it succeeds (the next flush, or a later
69
+ * final push replacing it): Codex review round 1, finding 6. A failing display store never throws out of the writer,
70
+ * so it can't keep the check's outcome from being sealed. The check's evidence (its log, exit and report) never passes
71
+ * through here.
72
+ */
73
+ export declare function coalescedProgressWriter(save: (snapshot: CheckProgressSnapshot) => void, options?: {
74
+ intervalMs?: number;
75
+ now?: () => number;
76
+ setTimer?: (run: () => void, ms: number) => unknown;
77
+ clearTimer?: (timer: unknown) => void;
78
+ }): ProgressWriter;
@@ -92,7 +92,9 @@ export class CheckProgressTracker {
92
92
  events = new Set();
93
93
  last = "";
94
94
  recognized = false;
95
- parallel = null;
95
+ /** Each part's parallel journey run (scripts/e2e-parallel.mjs), by its release-check tag ("" for untagged output):
96
+ * the scripted flows and app parts run at once, so one shared run would mix their groups. */
97
+ parallels = new Map();
96
98
  constructor(update) {
97
99
  this.update = update;
98
100
  }
@@ -130,14 +132,26 @@ export class CheckProgressTracker {
130
132
  emit(final = false, force = false) {
131
133
  const snapshot = this.snapshot(final);
132
134
  const identity = JSON.stringify(snapshot);
133
- if (force || identity !== this.last) {
135
+ // Live progress is advisory: a snapshot the store would refuse (output read in a way that counted past a total) is
136
+ // never handed on, so reading output can't break the check that prints it (release check 3264).
137
+ if ((force || identity !== this.last) && parseCheckProgressSnapshot(identity) !== null) {
134
138
  this.last = identity;
135
139
  this.update(snapshot);
136
140
  }
137
141
  return snapshot;
138
142
  }
139
143
  read(raw) {
140
- const line = cleanLine(raw);
144
+ const cleaned = cleanLine(raw);
145
+ if (cleaned === "")
146
+ return;
147
+ // scripts/release-check.mjs streams each part's lines as they come, tagged `<part> | ` (partTag). The unit part and
148
+ // the scripted journeys are read as their own output; the other parts (the build, the real-model journeys, the
149
+ // upgrade path) count toward no suite here.
150
+ const tagged = /^(typecheck|build|unit|flows|app|flows-real|app-real|upgrade) \| ?(.*)$/.exec(cleaned);
151
+ if (tagged !== null && !["unit", "flows", "app"].includes(tagged[1]))
152
+ return;
153
+ const key = tagged === null ? "" : tagged[1];
154
+ const line = tagged === null ? cleaned : tagged[2].trim();
141
155
  if (line === "")
142
156
  return;
143
157
  const testFiles = /\bTest Files\b(.+)$/.exec(line);
@@ -153,6 +167,9 @@ export class CheckProgressTracker {
153
167
  }
154
168
  return;
155
169
  }
170
+ // The unit part speaks only through its Vitest summary; a test's own print is never a journey's.
171
+ if (key === "unit")
172
+ return;
156
173
  // scripts/e2e-parallel.mjs: "Running 7 groups, at most 4 at once (12.0 GB available, about 400 MB each): console,
157
174
  // task, …" (before: "Running 7 groups at once: …"), then every group's lines as "[group] [app] …", then
158
175
  // "✅ group 3.1 min" per group and "N of M groups passed …".
@@ -161,10 +178,10 @@ export class CheckProgressTracker {
161
178
  const total = safeCount(starting[1]);
162
179
  const names = starting[2].split(",").map(one => one.trim()).filter(one => one !== "");
163
180
  if (total !== null && names.length === total)
164
- this.parallel = { suite: null, total, groups: new Map(names.map(one => [one, emptyGroup()])) };
181
+ this.parallels.set(key, { suite: null, total, groups: new Map(names.map(one => [one, emptyGroup()])) });
165
182
  return;
166
183
  }
167
- const parallel = this.parallel;
184
+ const parallel = this.parallels.get(key) ?? null;
168
185
  if (parallel !== null) {
169
186
  const exited = /^(✅|❌)\s+(\S+)\s+[0-9.]+ min$/.exec(line);
170
187
  const group = exited === null ? undefined : parallel.groups.get(exited[2]);
@@ -177,7 +194,7 @@ export class CheckProgressTracker {
177
194
  if (summary !== null && parallel.suite !== null && safeCount(summary[2]) === parallel.total) {
178
195
  const passed = safeCount(summary[1]) ?? 0;
179
196
  this.summarize(parallel, passed === parallel.total);
180
- this.parallel = null;
197
+ this.parallels.delete(key);
181
198
  return;
182
199
  }
183
200
  }
@@ -289,3 +306,85 @@ export class CheckProgressTracker {
289
306
  }
290
307
  }
291
308
  const emptyGroup = () => ({ passed: 0, failed: 0, skipped: 0, total: null, result: null });
309
+ /** How often a running check's display snapshot may be written: the page and the phone read it, nobody needs more. */
310
+ export const CHECK_PROGRESS_WRITE_MS = 1_000;
311
+ /**
312
+ * Coalesced display writes for a running check (isolated-checks.md, stage 1): a test runner can print thousands of
313
+ * lines a second, and every changed snapshot was its own write transaction. Display snapshots are now written at most
314
+ * once per interval, the latest one winning; the final snapshot is written at once, exactly, and nothing older is ever
315
+ * written after it. A final snapshot whose write fails is kept until a write of it succeeds (the next flush, or a later
316
+ * final push replacing it): Codex review round 1, finding 6. A failing display store never throws out of the writer,
317
+ * so it can't keep the check's outcome from being sealed. The check's evidence (its log, exit and report) never passes
318
+ * through here.
319
+ */
320
+ export function coalescedProgressWriter(save, options = {}) {
321
+ const intervalMs = options.intervalMs ?? CHECK_PROGRESS_WRITE_MS;
322
+ const now = options.now ?? (() => Date.now());
323
+ const setTimer = options.setTimer ?? ((run, ms) => { const timer = setTimeout(run, ms); timer.unref?.(); return timer; });
324
+ const clearTimer = options.clearTimer ?? (timer => clearTimeout(timer));
325
+ let lastWrite = Number.NEGATIVE_INFINITY;
326
+ let pending = null;
327
+ let timer = null;
328
+ // Once a final snapshot arrives nothing older is written; `unsavedFinal` holds it until a write of it succeeds.
329
+ let finished = false;
330
+ let unsavedFinal = null;
331
+ const cancel = () => { if (timer !== null)
332
+ clearTimer(timer); timer = null; };
333
+ /** One display write; false when the store refused it (display only: never thrown). */
334
+ const write = (snapshot) => {
335
+ lastWrite = now();
336
+ try {
337
+ save(snapshot);
338
+ return true;
339
+ }
340
+ catch {
341
+ return false;
342
+ }
343
+ };
344
+ const writeFinal = () => {
345
+ if (unsavedFinal === null)
346
+ return true;
347
+ if (write(unsavedFinal))
348
+ unsavedFinal = null;
349
+ return unsavedFinal === null;
350
+ };
351
+ const later = () => {
352
+ timer = null;
353
+ const snapshot = pending;
354
+ pending = null;
355
+ // A display write that fails here only leaves the page one snapshot behind; the next one (or the final) follows.
356
+ if (snapshot !== null && !finished)
357
+ write(snapshot);
358
+ };
359
+ return {
360
+ push(snapshot) {
361
+ if (snapshot.final) {
362
+ finished = true;
363
+ cancel();
364
+ pending = null;
365
+ unsavedFinal = snapshot;
366
+ writeFinal();
367
+ return;
368
+ }
369
+ if (finished)
370
+ return;
371
+ const wait = lastWrite + intervalMs - now();
372
+ if (wait <= 0 && timer === null) {
373
+ pending = null;
374
+ write(snapshot);
375
+ return;
376
+ }
377
+ pending = snapshot;
378
+ if (timer === null)
379
+ timer = setTimer(later, Math.max(0, wait));
380
+ },
381
+ flush() {
382
+ cancel();
383
+ const snapshot = pending;
384
+ pending = null;
385
+ if (finished)
386
+ return writeFinal();
387
+ return snapshot === null || write(snapshot);
388
+ },
389
+ };
390
+ }
@@ -1,5 +1,5 @@
1
1
  import { type VerifiedEntry } from "./commit-extract.js";
2
- import { CHECK_PARTS, type CheckContext, type CheckInventory, type CheckReport, type ObligationSnapshot, type TargetedPlan } from "./contracts/release-check.js";
2
+ import { type CheckContext, type CheckPart, type CheckInventory, type CheckReport, type ObligationSnapshot, type SealedInventory, type TargetedPlan } from "./contracts/release-check.js";
3
3
  import { type Store, type VerifyCommand } from "./store.js";
4
4
  /** The sealed artifacts' capture labels: what a reader looks for, by exact name. */
5
5
  export declare const CHECK_REPORT_CAPTURE = "release check report v1";
@@ -46,11 +46,33 @@ export declare function treeFactsOf(gitDir: string, sha: string): {
46
46
  /** The execution context a check runs in: what must match for results to stand beside each other. The machine's load
47
47
  * budget is left out: it changes how fast a check runs, never what passes (scripts/release-check.mjs checkBudget). */
48
48
  export declare function checkContextOf(facts: TreeFacts, command: VerifyCommand, access: string): CheckContext;
49
- /** The parts a full check selects, as far as the plane knows them before it runs. */
50
- export declare const FULL_PARTS: readonly (typeof CHECK_PARTS[number])[];
49
+ /** Every part a check can have. */
50
+ export declare const FULL_PARTS: readonly CheckPart[];
51
+ /** Changes no test reads: the plane's copy of scripts/release-check.mjs NOTHING (check-report.test.ts holds them equal). */
52
+ export declare const NOTHING_PATHS: readonly RegExp[];
53
+ /** Model-facing code, whose change runs the real-model journeys: the plane's copy of scripts/release-check.mjs MODEL. */
54
+ export declare const MODEL_PATHS: readonly RegExp[];
55
+ /** Whether a changed path can affect an upgrade: the plane's copy of scripts/release-check.mjs needsUpgrade, without its
56
+ * version-only exception (the plane counts a version bump as a change, so it owes more, never less). */
57
+ export declare function upgradeRelevant(file: string): boolean;
58
+ /** The paths that differ between two verified trees: added, removed, or with another mode or blob. */
59
+ export declare function changedPaths(facts: TreeFacts, base: TreeFacts): string[];
60
+ /**
61
+ * The parts a Full check owes, decided by the plane before it runs (eval-harness review 1, finding 2), never by the
62
+ * check's own report: the typecheck and the build always; the unit suite when anything a test reads changed since
63
+ * `base` (the plane's own record of the last release, both trees verified here) and the tree has unit files; the
64
+ * scripted journeys likewise when the tree has the journey scripts; the real-model journeys when model-facing code
65
+ * changed; the upgrade path when a change can affect an upgrade and the tree has it. With no base every one the tree
66
+ * can run is owed. The check is told these on its handshake and runs at least them.
67
+ */
68
+ export declare function fullObligationsOf(facts: TreeFacts, base: TreeFacts | null): CheckPart[];
51
69
  /** What a check must account for, sealed before it runs: every unit file of the tree, whatever runs (a targeted
52
- * check accounts for the rest through its base), and the parts it selects. */
53
- export declare function inventoryOf(facts: TreeFacts, plan: TargetedPlan | null): CheckInventory;
70
+ * check accounts for the rest through its base), and the parts it owes (a full check's from fullObligationsOf), and
71
+ * whether CI may stand in for its unit suite. */
72
+ export declare function inventoryOf(facts: TreeFacts, plan: TargetedPlan | null, full?: {
73
+ owed: readonly CheckPart[];
74
+ release: boolean;
75
+ }): CheckInventory;
54
76
  /** A check about to run: its handshake (written on stdin, then closed) and the facts sealed with it. */
55
77
  export type PreparedAttempt = {
56
78
  runId: number;
@@ -64,10 +86,15 @@ export type PreparedAttempt = {
64
86
  /** The identities the check was asked to report one by one (required ones). */
65
87
  watch: string[];
66
88
  };
89
+ /** A Full check's preparation refused (eval-harness review 2, finding 4): the plane recorded a release whose commit it
90
+ * can't verify now, so the check's obligations can't be decided. The check isn't run (builder.ts: `baseline-unverifiable`). */
91
+ export declare class CheckBaselineUnverifiable extends Error {
92
+ }
67
93
  /**
68
94
  * Seal a check's attempt row before its command is spawned: intended mode, nonce hash, inventory and context (and for a
69
95
  * targeted check its plan and obligation snapshot, sealed as artifacts first). Null when the plane can't read the
70
- * commit, which leaves the check running exactly as it always has, with no handshake.
96
+ * commit, which leaves the check running exactly as it always has, with no handshake. Throws CheckBaselineUnverifiable,
97
+ * having sealed nothing, when a recorded last release can't be verified.
71
98
  */
72
99
  export declare function prepareCheckAttempt(store: Store, root: string, input: {
73
100
  runId: number;
@@ -79,6 +106,9 @@ export declare function prepareCheckAttempt(store: Store, root: string, input: {
79
106
  plan?: TargetedPlan | null;
80
107
  snapshot?: ObligationSnapshot | null;
81
108
  watch?: readonly string[];
109
+ /** A release's check (signed release terms or a release authority): CI never stands in for its unit suite. Sealed in
110
+ * the inventory (whose digest the attempt row holds); only an explicit false lets CI stand in. */
111
+ release?: boolean;
82
112
  }, now: Date): PreparedAttempt | null;
83
113
  /** Seal one JSON payload as a structured-output artifact under its capture label. */
84
114
  export declare function sealJson(store: Store, root: string, runId: number, name: string, value: unknown, capture: string, now: Date): number;
@@ -107,3 +137,19 @@ export declare function readSealedJson(store: Store, root: string, runId: number
107
137
  value: unknown;
108
138
  digest: string;
109
139
  } | null;
140
+ /**
141
+ * Why a Full release check has no result it can count — or null when it qualifies as one. The plane's own facts are the
142
+ * exit it saw (`exitCode`, null when the check didn't exit on its own: timed out or stopped) and the inventory it sealed
143
+ * before the check ran; the report must agree with both. A failed check (a nonzero exit) needs only a valid report: what
144
+ * it failed on is its result. A passing exit needs every obligation the plane sealed (the inventory's parts, never the
145
+ * report's own "selected") to have an outcome, and every part the report ran too: every part once, each run to its
146
+ * end; the unit suite finished with a result for every sealed unit file (CI stands in only when the sealed plan allowed
147
+ * it); every journey run finished, its groups listed before they ran, each passed group with a journey result; and the
148
+ * upgrade path finished, its versions listed, every one run. An explicitly empty inventory (no groups of that kind) is
149
+ * an outcome; a missing one is not. Missing, malformed, truncated or skipped-required results never read as a pass.
150
+ */
151
+ export declare function fullCheckProblem(input: {
152
+ report: CheckReport | null;
153
+ inventory: SealedInventory | null;
154
+ exitCode: number | null;
155
+ }): string | null;