tickmarkr 2.8.1 → 2.8.3

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.
@@ -121,8 +121,9 @@ function codexRetirement(upgrade) {
121
121
  // literal-string fallback. Every emitted root is therefore a TOML basic string that parses back to the exact
122
122
  // path (git's own trailing newline is stripped through a sentinel, never a path's). No common-root fallback;
123
123
  // packed-refs is not granted (packed-ref ops fail closed); stash, tags, notes and branches outside the leaf
124
- // are denied. Declared: sibling refs in the same leaf (other task branches, the run's integration branch
125
- // under the default prefix) stay writable until 2.8.1's integration identity.
124
+ // are denied. Declared (queue row 151): sibling refs in the same leaf (other task branches, the run's integration
125
+ // branch under the default prefix) stay writable; since 2.8.1 a write to the integration ref parks the merge and is
126
+ // never published.
126
127
  // doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
127
128
  export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=\${tkr_common%?/} && tkr_gitdir=\${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=\${tkr_head%/*};; (*) tkr_leaf=;; esac && [ "$(printf %s "$tkr_common$tkr_gitdir$tkr_leaf" | LC_ALL=C tr -d '\\0-\\37\\177"\\\\')" = "$tkr_common$tkr_gitdir$tkr_leaf" ] && { [ "$tkr_gitdir" = "$tkr_common" ] || printf '"%s",' "$tkr_gitdir"; } && printf '"%s/objects"' "$tkr_common" && { [ -z "$tkr_leaf" ] || printf ',"%s/%s","%s/logs/%s"' "$tkr_common" "$tkr_leaf" "$tkr_common" "$tkr_leaf"; })]"`;
128
129
  // OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
@@ -1,6 +1,6 @@
1
1
  import { type ClassifiedWorkerResult } from "./prompt.js";
2
2
  import { type WorkerAdapter } from "./types.js";
3
3
  export declare const QWEN_VERSION_IDENTITY: RegExp;
4
- export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
4
+ export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Configure tools.executionSandbox on Linux or a supported legacy sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
5
5
  export declare function parseQwenResult(raw: string, nonce: string): ClassifiedWorkerResult;
6
6
  export declare const qwen: WorkerAdapter;
@@ -3,12 +3,14 @@ import { parseWorkerResult } from "./prompt.js";
3
3
  import { channelsFromConfig, shq, } from "./types.js";
4
4
  export const QWEN_VERSION_IDENTITY = /^\d+\.\d+\.\d+/;
5
5
  const QWEN_SKIP_UPDATE = "QWEN_CODE_SKIP_UPDATE_CHECK_ONCE=true";
6
- // Verbatim stderr from tests/fixtures/qwen/safe-mode.stderr (qwen 0.21.15). These bytes are launch
7
- // harness, not evidence that a reviewer took a turn. Keep the declaration closed and row-shaped so
8
- // llm.ts can sweep partial terminal paints without a broad SAFE MODE/warning regex.
6
+ // Verbatim stderr rows, in render order: the SAFE MODE row both recorded versions print, then the yolo
7
+ // row of tests/fixtures/qwen/safe-mode.stderr (qwen 0.21.15) and of safe-mode-0.25.1.stderr (qwen 0.25.1).
8
+ // These bytes are launch harness, not evidence that a reviewer took a turn. Keep the declaration closed
9
+ // and row-shaped so llm.ts can sweep partial terminal paints without a broad SAFE MODE/warning regex.
9
10
  export const QWEN_HARNESS_BANNER_ROWS = [
10
11
  "⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.",
11
12
  "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
13
+ "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Configure tools.executionSandbox on Linux or a supported legacy sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
12
14
  ];
13
15
  function decodeQwenEvents(events) {
14
16
  const text = [];
@@ -1,4 +1,5 @@
1
1
  import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
2
+ import { filesGlob } from "../../graph/files-glob.js";
2
3
  import { execFileSync } from "node:child_process";
3
4
  import { userInfo } from "node:os";
4
5
  import { allAdapters } from "../../adapters/registry.js";
@@ -242,7 +243,7 @@ export async function approve(argv, cwd = process.cwd()) {
242
243
  if (amendedFiles.length === task.files.length)
243
244
  throw new Error("--files must extend the parked files[] revision");
244
245
  const amended = { ...graph, tasks: graph.tasks.map((t) => t.id === taskId ? { ...t, files: amendedFiles } : t) };
245
- const conflicts = separabilityErrors(amended.tasks);
246
+ const conflicts = scopeGrantConflicts(cwd, journal, runId, taskId, amended.tasks, events);
246
247
  if (conflicts.length)
247
248
  throw new Error(`refusing scope-request approval for ${taskId}: ${conflicts.join("\n")}`);
248
249
  // C4: a live grant may not take a task past the bounds that fail every compile. The exception
@@ -394,6 +395,110 @@ function integrationBase(events, byName) {
394
395
  const branch = String(events.find((e) => e.event === "run-start").data.branch);
395
396
  return "anchor" in anchor ? { branch, base: anchor.anchor } : { branch, base: "unknown", unreadable: anchor.unreadable };
396
397
  }
398
+ /**
399
+ * Queue 169 (D-1982/D-1983/D-1986): the in-run OBS-212 check a scope-request grant runs. Only the
400
+ * pairs this amendment changes are judged — the requester against each unordered sibling; a pair
401
+ * another amendment created was judged by that grant and is never re-judged here. A sibling is
402
+ * exempt only when this run already merged it into the requester's branch: its replayed status is
403
+ * done, its NEWEST journaled merge row names a full lowercase object id, and that commit is an
404
+ * ancestor of the requester's task branch (the run's integration branch plus --taskId, read exactly
405
+ * as the failed-task recheck reads it — a marked run's branch comes from its run-start record, never
406
+ * the current config). Every other overlapping pair keeps the compile-time refusal, an unreadable
407
+ * git answer included. The surviving errors are separabilityErrors' own rows, so the refusal text
408
+ * and its pair order are unchanged.
409
+ */
410
+ function scopeGrantConflicts(cwd, journal, runId, requesterId, tasks, events) {
411
+ const errors = separabilityErrors(tasks);
412
+ if (!errors.length)
413
+ return errors;
414
+ const pairs = separabilityPairs(tasks);
415
+ if (pairs.length !== errors.length)
416
+ return errors; // a misaligned local fold refuses everything
417
+ const statuses = journal.replayStatuses();
418
+ let taskRef;
419
+ const requesterRef = () => (taskRef ??= `${integrationBase(events, () => integrationBranch(loadConfig(cwd), runId)).branch}--${requesterId}`);
420
+ const kept = [];
421
+ for (let i = 0; i < errors.length; i++) {
422
+ const { a, b } = pairs[i];
423
+ const sibling = a === requesterId ? b : b === requesterId ? a : undefined;
424
+ if (sibling === undefined)
425
+ continue;
426
+ if (mergedSiblingExempt(cwd, events, statuses.get(sibling), sibling, requesterRef))
427
+ continue;
428
+ kept.push(errors[i]);
429
+ }
430
+ return kept;
431
+ }
432
+ /** A full lowercase git object id: 40 (SHA-1) or 64 (SHA-256) hex, nothing else. */
433
+ const FULL_OBJECT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
434
+ /**
435
+ * The one exemption: the sibling is done in THIS run's replay and its newest merge row's commit is a
436
+ * full object id that is an ancestor of the requester's task branch. Any unreadable git answer — a
437
+ * missing requester branch, a commit the repository lacks, a failed ancestry read — is no exemption.
438
+ */
439
+ function mergedSiblingExempt(cwd, events, status, siblingId, requesterRef) {
440
+ if (status !== "done")
441
+ return false;
442
+ const merge = [...events].reverse().find((e) => e.event === "merge" && e.taskId === siblingId);
443
+ const commit = typeof merge?.data.commit === "string" ? merge.data.commit : undefined;
444
+ if (commit === undefined || !FULL_OBJECT_ID.test(commit))
445
+ return false;
446
+ const ref = requesterRef();
447
+ try {
448
+ execFileSync("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
449
+ execFileSync("git", ["merge-base", "--is-ancestor", commit, ref], { cwd, stdio: "ignore" });
450
+ }
451
+ catch {
452
+ return false;
453
+ }
454
+ return true;
455
+ }
456
+ /** The conflicting unordered pairs of separabilityErrors, in its exact emission order. */
457
+ function separabilityPairs(tasks) {
458
+ const byId = new Map(tasks.map((t) => [t.id, t.deps ?? []]));
459
+ const pairs = [];
460
+ for (let i = 0; i < tasks.length; i++) {
461
+ for (let j = i + 1; j < tasks.length; j++) {
462
+ const a = tasks[i], b = tasks[j];
463
+ if (pairReachable(a.id, byId).has(b.id) || pairReachable(b.id, byId).has(a.id))
464
+ continue; // ordered
465
+ if (pairOverlap(a.files ?? [], b.files ?? []).length > 0)
466
+ pairs.push({ a: a.id, b: b.id });
467
+ }
468
+ }
469
+ return pairs;
470
+ }
471
+ /** Tasks reachable from `id` through deps (transitive) — separabilityErrors' own fold. */
472
+ function pairReachable(id, byId) {
473
+ const seen = new Set();
474
+ const stack = [...(byId.get(id) ?? [])];
475
+ while (stack.length) {
476
+ const next = stack.pop();
477
+ if (seen.has(next))
478
+ continue;
479
+ seen.add(next);
480
+ stack.push(...(byId.get(next) ?? []));
481
+ }
482
+ return seen;
483
+ }
484
+ /** separabilityErrors' conservative overlap: identical patterns, or a glob matching the other's literal path. */
485
+ function pairOverlap(a, b) {
486
+ const hits = new Set();
487
+ const isGlob = (p) => /[*?[\]{}]/.test(p);
488
+ for (const pa of a) {
489
+ for (const pb of b) {
490
+ if (pa === pb) {
491
+ hits.add(pa);
492
+ continue;
493
+ }
494
+ if (isGlob(pa) && !isGlob(pb) && filesGlob(pa)(pb))
495
+ hits.add(`${pb} ⊂ ${pa}`);
496
+ else if (isGlob(pb) && !isGlob(pa) && filesGlob(pb)(pa))
497
+ hits.add(`${pa} ⊂ ${pb}`);
498
+ }
499
+ }
500
+ return [...hits].sort();
501
+ }
397
502
  /** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
398
503
  function openDecision(journal, taskId) {
399
504
  const { events, sourceIndexes } = readJournalEvents(journal);
@@ -363,9 +363,9 @@ const VOCAB_RE = /\b(?:error|fail(?:ed|ure|ing)?)\b/i;
363
363
  // OBS-791: `[vitest-worker]: Timeout calling "<method>"` is the SAME birpc mechanism one layer up.
364
364
  // Vitest's bundled birpc has a fixed 60s DEFAULT_TIMEOUT with no override (fixed upstream only in
365
365
  // vitest 4.x), so a suite whose tests all pass can still die at teardown when the worker->host RPC
366
- // window closes. `.github/workflows/release.yml` has forgiven this exact fingerprint since 2026-08-11
367
- // while this classifier called it a regression — the product charged a worker for the failure the
368
- // release gate was written to forgive. The METHOD NAME IS DELIBERATELY NOT PINNED: the timeout is a
366
+ // window closes. Public CI's proof forgives this exact fingerprint (the RPC-only rule; since v2.8.2 decided
367
+ // from the run's own record, queue rows 148 and 149) and this classifier once called it a regression — the
368
+ // product charged a worker for the failure its own release proof forgives. The METHOD NAME IS DELIBERATELY NOT PINNED: the timeout is a
369
369
  // property of the RPC window, not of `onTaskUpdate`, and pinning one method would forgive a run and
370
370
  // charge its sibling for the same infrastructure event. This stays a CLOSED signature — the
371
371
  // `[vitest-worker]: ` prefix is vitest's own RPC layer and never user assertion text — and the
@@ -1,7 +1,7 @@
1
1
  import { type WorkerAdapter } from "../adapters/types.js";
2
2
  import type { Effort } from "../graph/schema.js";
3
3
  import { type ExecutorDriver, type Slot } from "../drivers/types.js";
4
- import type { SeatLaunchCause } from "../drivers/orca.js";
4
+ import { type SeatLaunchCause } from "../drivers/orca.js";
5
5
  export declare const GATE_PANE_SEP = " \u00B7 ";
6
6
  export declare const COMPLETION_FAKING_CHECKLIST = "## Completion-faking checklist\nHunt for these concrete completion-faking shortcuts before ruling on any criterion:\n- hardcoded-result: output or fixture hardcoded to satisfy the stated criterion instead of real logic\n- test-weakening: tests skipped, deleted, or assertions loosened until failing behavior looks green\n- vacuous-assertion: a test that cannot fail (asserts a constant, asserts its own setup, no assertion)\n- fixture-overfit: implementation narrowed to the exact test inputs rather than the described behavior\n- echo-not-implement: criterion text echoed in names, comments, or strings without the behavior itself\n- stub-left-behind: TODO, throw, or no-op stub where the real implementation should be\n- error-swallowing: catch or fallback that hides failures instead of handling them\n- self-mocking: the code under test mocked or faked so the test exercises the mock\n- check-bypass: lint, type, or CI checks disabled, relaxed, or excluded to get green\n- rename-as-work: code moved or renamed and presented as the requested change\n- scope-padding: unrelated edits padding the diff while the criterion's behavior is untouched\nWhen a criterion fails, the verdict MUST name which shortcut above it matches, or state that none does.";
7
7
  /** Fable F3: per-call nonce echoed in verdict JSON and gate exit markers. */
package/dist/gates/llm.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { constants } from "node:buffer";
3
4
  import { randomBytes } from "node:crypto";
4
5
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { tmpdir } from "node:os";
6
7
  import { join } from "node:path";
7
- import { matchesTrustDialog } from "../adapters/types.js";
8
+ import { matchesTrustDialog, shq } from "../adapters/types.js";
8
9
  import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
10
+ import { CHECKOUT_MARK, checkoutProofLine } from "../drivers/orca.js";
9
11
  import { bannerShell, paneDispatchCommand, PLAIN_BANNER } from "../brand.js";
10
12
  import { sh } from "../run/git.js";
11
13
  import { harvestCpuFlatWindowMs, normalizeStallSnapshot, WorkerTreeCpuAccountant, } from "../run/stall.js";
@@ -204,6 +206,64 @@ const START_MARKER = "TICKMARKR_START_";
204
206
  const START_ROW = /^TICKMARKR_START_[\w-]+$/;
205
207
  const IDENTITY_LINE = /^(?:review\s*·|tickmarkr(?::|$))/;
206
208
  const IDENTITY_OPENERS = ["review ·", "tickmarkr"];
209
+ // Row 135(a): the orca driver's launch preamble. create returns only once it read the complete checkout
210
+ // frame, so every gate read holds the frame row whole — exactly checkoutProofLine's form (declared length
211
+ // equal to half its lowercase hex, terminated) — or, as the last row, a strict prefix of that form.
212
+ // Above it, as the first non-blank row, the shell may echo the typed launch line, which names that
213
+ // frame's own checkout as `cd <shq checkout>`. Anything else shaped like them is the seat's. Frame rows
214
+ // are read untrimmed: a trailing space or tab can never complete a frame or extend its prefix.
215
+ function frameCheckout(row) {
216
+ const hex = /^TICKMARKR_CHECKOUT \d+:(.*);$/.exec(row)?.[1];
217
+ if (hex === undefined)
218
+ return undefined;
219
+ const checkout = Buffer.from(hex, "hex").toString("utf8");
220
+ return checkoutProofLine(checkout) === row ? checkout : undefined;
221
+ }
222
+ function partialFrameRow(row) {
223
+ if (row.length > 0 && `${CHECKOUT_MARK} `.startsWith(row))
224
+ return true;
225
+ const m = /^TICKMARKR_CHECKOUT (0|[1-9]\d*)(?::([0-9a-f]*))?$/.exec(row);
226
+ if (m === null || !printableFrameLength(m[1]))
227
+ return false;
228
+ return m[2] === undefined || frameHexCompletable(m[2], Number(m[1]));
229
+ }
230
+ // checkoutProofLine prints `TICKMARKR_CHECKOUT <n>:`, 2n hex digits and `;` as one string, so the length it
231
+ // declares is a safe integer whose row fits the runtime's longest string. Any other length, unsafe or
232
+ // non-finite included, is never its output, nor a prefix of one.
233
+ function printableFrameLength(digits) {
234
+ const n = Number(digits);
235
+ return Number.isSafeInteger(n) && CHECKOUT_MARK.length + digits.length + 2 * n + 3 <= constants.MAX_STRING_LENGTH;
236
+ }
237
+ // A hex prefix checkoutProofLine could still extend to `bytes` bytes: valid UTF-8 so far, with its open
238
+ // sequence closable inside the declared length. A trailing half byte is tried at each of its 16 values.
239
+ function frameHexCompletable(hex, bytes) {
240
+ if (hex.length > bytes * 2)
241
+ return false;
242
+ const whole = Buffer.from(hex.slice(0, hex.length - (hex.length % 2)), "hex");
243
+ const candidates = hex.length % 2 === 0 ? [whole]
244
+ : Array.from({ length: 16 }, (_, lo) => Buffer.concat([whole, Buffer.of(parseInt(hex.at(-1), 16) * 16 + lo)]));
245
+ return candidates.some((c) => {
246
+ const owed = utf8Owed(c);
247
+ return owed !== undefined && owed <= bytes - c.length;
248
+ });
249
+ }
250
+ // Continuation bytes the last sequence still owes, or undefined when no continuation makes it UTF-8.
251
+ function utf8Owed(bytes) {
252
+ try {
253
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
254
+ }
255
+ catch {
256
+ return undefined;
257
+ }
258
+ let lead = bytes.length;
259
+ while (lead > 0 && bytes.length - lead < 3 && (bytes[lead - 1] & 0xc0) === 0x80)
260
+ lead--;
261
+ if (lead === 0)
262
+ return 0;
263
+ const b = bytes[lead - 1];
264
+ const size = b >= 0xf0 ? 4 : b >= 0xe0 ? 3 : b >= 0xc0 ? 2 : 1;
265
+ return Math.max(0, size - (bytes.length - lead + 1));
266
+ }
207
267
  // Stages of the preamble walk. Each complete harness row is accepted only at or after its stage.
208
268
  const ECHO = 0, START = 1, BANNER = 2, IDENTITY = 3, SEAT = 4;
209
269
  // The char offset where the seat's own text begins, or -1 when the capture ends inside the preamble.
@@ -215,6 +275,7 @@ function seatStart(output, adapterBannerRows) {
215
275
  let bannerAt; // next banner row expected once the banner has begun
216
276
  let adapterBannerAt;
217
277
  let identitySeen = false;
278
+ let frameSeen = false;
218
279
  let offset = 0;
219
280
  for (let i = 0; i < rows.length; i++) {
220
281
  const row = rows[i].replace(/[ \t]+$/, "");
@@ -222,8 +283,15 @@ function seatStart(output, adapterBannerRows) {
222
283
  const last = i === rows.length - 1;
223
284
  // Complete harness rows: equality against the shape the preamble allows at this stage.
224
285
  let accepted = false;
286
+ const below = last ? undefined : frameCheckout(rows[i + 1]);
225
287
  if (!identitySeen && stage < SEAT && t.length === 0)
226
288
  accepted = true; // blank rows between preamble rows
289
+ else if (stage === ECHO && !frameSeen && frameCheckout(rows[i]) !== undefined) {
290
+ frameSeen = true;
291
+ accepted = true;
292
+ }
293
+ else if (below !== undefined && row.includes(`cd ${shq(below)}`) && rows.slice(0, i).every((r) => r.trim() === ""))
294
+ accepted = true; // orca launch echo
227
295
  else if (stage <= ECHO && completeEchoRow(row))
228
296
  accepted = true;
229
297
  else if (stage <= START && START_ROW.test(row)) {
@@ -239,13 +307,10 @@ function seatStart(output, adapterBannerRows) {
239
307
  bannerAt++;
240
308
  accepted = true;
241
309
  }
242
- else if (stage <= IDENTITY && adapterBannerAt === undefined && adapterBannerRows.includes(row)) {
310
+ else if (stage <= IDENTITY && adapterBannerRows.indexOf(row, adapterBannerAt ?? 0) >= 0) {
311
+ // A closed list in render order: each row at most once, in list order, skipping allowed.
243
312
  stage = BANNER;
244
- adapterBannerAt = adapterBannerRows.indexOf(row) + 1;
245
- accepted = true;
246
- }
247
- else if (stage <= IDENTITY && adapterBannerAt !== undefined && row === adapterBannerRows[adapterBannerAt]) {
248
- adapterBannerAt++;
313
+ adapterBannerAt = adapterBannerRows.indexOf(row, adapterBannerAt ?? 0) + 1;
249
314
  accepted = true;
250
315
  }
251
316
  else if (!identitySeen && stage <= IDENTITY && IDENTITY_LINE.test(t)) {
@@ -262,21 +327,20 @@ function seatStart(output, adapterBannerRows) {
262
327
  // The last row may be a partial paint: a prefix of the next harness row the preamble allows.
263
328
  if (stage <= ECHO && partialEchoRow(row))
264
329
  return -1;
330
+ if (stage === ECHO && !frameSeen && partialFrameRow(rows[i]))
331
+ return -1;
265
332
  if (stage <= START && (START_MARKER.startsWith(row) || /^TICKMARKR_START_[\w-]*$/.test(row)))
266
333
  return -1;
267
334
  if (stage <= BANNER && (bannerAt === undefined
268
335
  ? BANNER_ROWS.some((b) => b.startsWith(row))
269
336
  : BANNER_ROWS[bannerAt]?.startsWith(row) === true))
270
337
  return -1;
271
- if (stage <= IDENTITY && (adapterBannerAt === undefined
272
- ? adapterBannerRows.some((b) => b.startsWith(row))
273
- : adapterBannerRows[adapterBannerAt]?.startsWith(row) === true))
338
+ if (stage <= IDENTITY && adapterBannerRows.slice(adapterBannerAt ?? 0).some((b) => b.startsWith(row)))
274
339
  return -1;
275
340
  // The identity row is painted right after the banner, so its prefix is a partial paint only there;
276
341
  // with no banner in the capture, "review" or "tick" alone is the seat's own first row.
277
342
  if (!identitySeen && stage <= IDENTITY
278
- && (bannerAt === BANNER_ROWS.length
279
- || (adapterBannerRows.length > 0 && adapterBannerAt === adapterBannerRows.length))
343
+ && (bannerAt === BANNER_ROWS.length || adapterBannerAt !== undefined)
280
344
  && IDENTITY_OPENERS.some((o) => o.startsWith(t)))
281
345
  return -1;
282
346
  return offset;
@@ -7306,7 +7306,8 @@ export async function runDaemon(repoRoot, opts = {}) {
7306
7306
  throw error;
7307
7307
  });
7308
7308
  fatalPhase = "tip-verify";
7309
- // OBS-34: post-merge integration-tip verify — strict exit codes, no baseline forgiveness.
7309
+ // OBS-34: post-merge integration-tip verify — the battery forgives only what its baseline entry recorded red
7310
+ // (src/run/merge.ts, the same predicate as the gate battery).
7310
7311
  const lastMergedTask = [...journal.read()].reverse().find((e) => e.event === "merge" && e.taskId)?.taskId;
7311
7312
  // 2.8.1 T1 (ANL-023 §7.1): every close opens on the integration identity and closes on it below.
7312
7313
  const identity = { repo: repoRoot, branch, expected: published }; // 2.8.1 T2: E
@@ -74,7 +74,12 @@ export function wallBudget(events) {
74
74
  return;
75
75
  span("queue", opened, ts);
76
76
  // One open wait per owner, closed in journal order: the list stays sorted and non-overlapping.
77
- queued.set(key, [...(queued.get(key) ?? []), [opened, ts]]);
77
+ // Appended in place: a copy per close made a run with n waits cost O(n^2) (ANL-022 §3 T1 #5).
78
+ const list = queued.get(key);
79
+ if (list)
80
+ list.push([opened, ts]);
81
+ else
82
+ queued.set(key, [[opened, ts]]);
78
83
  waits.delete(key);
79
84
  };
80
85
  const service = (gate, task, a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -77,7 +77,6 @@
77
77
  "tsconfig.json",
78
78
  "vitest.config.ts",
79
79
  ".github/pull_request_template.md",
80
- "scripts/assert-test-file-count.sh",
81
80
  "scripts/emit-schema.ts",
82
81
  "scripts/probe-rig.mjs",
83
82
  "scripts/run-ci-vitest.sh",
@@ -691,12 +691,13 @@ subject of a release proof is never *"the run"* and never *"the fix leg"* — it
691
691
  the one commit that carries the run's work AND every leg merged into it. Name that commit's sha in the
692
692
  proof, in the same act as the grade, and the proof is bound to that sha alone. **When the subject moves — a
693
693
  later merge, a new integration tip, an amended or re-cut commit, a fresh export, a re-tag — the proof is
694
- VOID, and the new subject is graded from scratch**, count oracle included, since the oracle is derived from
695
- the merged tree and not from either parent. Two greens assembled into one verdict are two claims about two
696
- trees that never contained each other: the union's own conflict resolutions, its ordering and its
697
- collected-test total are exactly what neither green observed — which is why `RELEASING.md` step 4 matches
698
- the run's head SHA to the mirror's `HEAD` before a single job log is graded. This is the void-condition duty
699
- pointed at bytes: **a proof that names no commit cannot notice its subject leaving.**
694
+ VOID, and the new subject is graded from scratch**, its tracked test set included, since that set is
695
+ recomputed from the merged tree and not from either parent. Two greens assembled into one verdict are two
696
+ claims about two trees that never contained each other: the union's own conflict resolutions, its ordering
697
+ and its collected test set are exactly what neither green observed — which is why `RELEASING.md` step 4
698
+ hands the grader the mirror's `HEAD` and the grader refuses a run of any other head before a single record
699
+ is downloaded. This is the void-condition duty pointed at bytes: **a proof that names no commit cannot
700
+ notice its subject leaving.**
700
701
 
701
702
  **THE RECIPROCAL DUTY, and it is yours because you write both documents:** when you issue a ruling that
702
703
  re-scopes, narrows, splits or re-owns anything, **the ruling must name every sealed document its subject
@@ -1059,7 +1060,8 @@ they are left implicit:
1059
1060
  test exists — the acceptance oracle runs the sentence as vitest `-t` and a paraphrase matches zero tests.
1060
1061
  - Every fix-leg verify carries `--author <the seat that wrote it>`; without it the author is HUMAN, every
1061
1062
  vendor stays eligible, and a same-vendor approval is not the cross-vendor review the criterion names.
1062
- - A grader change is proved on a captured REAL job log, never on a synthetic one.
1063
+ - A grader change is proved on a recorded run — GitHub's structured run record and the proof groups its
1064
+ jobs uploaded — never on a synthetic job log: the grader reads no log text at all.
1063
1065
  - A public-CI red is classified in a throttled Linux container first (`docker run --cpus=2 node:22`),
1064
1066
  by measurement: starvation on the base tree is not a defect on the branch.
1065
1067
 
@@ -1603,19 +1605,31 @@ Create specialist seats using `orca terminal create --worktree path:<repo> --com
1603
1605
  - **RUN BOTH RELEASE PROOFS BEFORE STANDING DOWN.** The first is the exported-tree suite, which
1604
1606
  maintainers run from the private repo with its `verify:export` script — that tooling is deliberately
1605
1607
  not part of this package, so the command does not exist in a public checkout; it must install,
1606
- build, lint, and only then run the suite in the exported tree; then
1607
- `TICKMARKR_E2E=1 npx vitest run tests/e2e/orca-smoke.e2e.test.ts` must exercise the installed-Orca
1608
- smoke. A skipped smoke is not proof. Record both results before declaring the release complete.
1609
-
1610
- - **GRADE PUBLIC CI FROM JOB LOGS AT RUN-END ONLY (law 48).** A badge is not the grade, and
1611
- `gh run view --job --log` is empty while the run is in progress. At run-end execute
1612
- `scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from every job of the
1613
- declared inventory it prints first — serial `test` and `test-macos`, or sharded, per OS, the four
1614
- `suite <os> k/4` shards, `single-fork <os>` and `aggregate <os>`; a missing, duplicated or unknown
1615
- job reads UNREADABLE, never a skip. Its UNREADABLE state is a hard stop, not a green default.
1616
- Derive the expected glob pathspec count with `git ls-files ':(glob)tests/**/*.test.ts'`
1617
- (including the `.tsx` sibling where present), never a bare
1618
- `tests/**/*.test.ts`: bare `**` is not a git glob (law 49).
1608
+ build, lint, and only then run the suite in the exported tree, and only its zero exit writes the export
1609
+ proof that `export-public.sh --onto` reads before it writes the mirror commit. Then the installed-Orca
1610
+ smoke must exercise its live leg, decided from its own record and never from Vitest's exit status:
1611
+ `TICKMARKR_CI_RECORD=/tmp/tickmarkr-orca-smoke.record.json TICKMARKR_E2E=1 npx --no-install vitest run tests/e2e/orca-smoke.e2e.test.ts`,
1612
+ then
1613
+ `npx --no-install tsx scripts/ci/cli.ts verdict --require-test 'tests/e2e/orca-smoke.e2e.test.ts::e2e: orca driver smoke > live: the real-orca terminal leg — vitest-skipped without the gate or a reachable runtime' /tmp/tickmarkr-orca-smoke.record.json`
1614
+ must exit 0. The forced-stall proof (RELEASING.md step 6) is decided the same way: its record from
1615
+ `TICKMARKR_CI_RECORD=/tmp/tickmarkr-forced-stall.record.json TICKMARKR_TEST_FORCE_STALL=1 npx --no-install vitest run tests/e2e/forced-stall.e2e.test.ts -t 'the enabled forced-stall release proof makes the integration tip verifier recover a real RPC-stranded serial file as parallel exit1 then serial-only exit0 under distinct nonces, so a helper-only green or a full-suite retry fails'`,
1616
+ then
1617
+ `npx --no-install tsx scripts/ci/cli.ts verdict --require-test 'tests/e2e/forced-stall.e2e.test.ts::the enabled forced-stall release proof makes the integration tip verifier recover a real RPC-stranded serial file as parallel exit1 then serial-only exit0 under distinct nonces, so a helper-only green or a full-suite retry fails' /tmp/tickmarkr-forced-stall.record.json`
1618
+ must exit 0. A skipped smoke or stall leaf is not proof: Vitest exits 0 when the named leaf skips (the gate
1619
+ unset, Orca unreachable, a drifted `-t` title), and the verdict reads it RED. Record both results before
1620
+ declaring the release complete.
1621
+
1622
+ - **GRADE PUBLIC CI FROM ITS RUN RECORD AND PROOF GROUPS AT RUN-END ONLY (law 48).** A badge is not
1623
+ the grade, and an in-progress run is never graded. At run-end execute
1624
+ `scripts/grade-ci.sh <run-id> <mirror HEAD sha> <mirror clone> <tag>` and require GREEN from every job
1625
+ of the declared inventory it prints first — serial `test` and `test-macos`; a missing, duplicated or
1626
+ unknown job reads UNREADABLE, never a skip, and a sharded run reads UNREADABLE until its proof-group
1627
+ selection ships (ANL-022 §7.4). The grader is bound to the head: a run of any other commit reads
1628
+ UNREADABLE before anything is downloaded. It reads GitHub's structured step outcomes and conclusions and
1629
+ recomputes each job's uploaded proof group (`proof-<job>`: the Vitest records and the floors sidecar)
1630
+ against the test files that commit tracks in the mirror clone — there is no count to derive, and no job
1631
+ log text is read, so a log gh labels `UNKNOWN STEP` or a test that prints timeout text cannot move the
1632
+ grade. Its UNREADABLE state is a hard stop, not a green default.
1619
1633
 
1620
1634
  - **RE-READ THE REGISTRY BEFORE RULING (law 51).** A successful Release run and the first
1621
1635
  `npm view` are separate observations: registry propagation can briefly return the prior version.
@@ -1,45 +1,59 @@
1
1
  #!/bin/bash
2
- # grade-ci.sh <run-id> <expected-count> [tag] — grade EVERY job of a public CI run's closed inventory from
3
- # its JOB LOG. Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
4
- # Exit 0 GREEN, 1 RED, 2 UNREADABLE, 3 INFRA_KILLED: a job the GitHub runner shut down or cancelled with
5
- # no failed or timed-out test (the classifier's class, D-897) — never green, never a pass, only unproven.
2
+ # grade-ci.sh <run-id> <expected-sha> <repo-dir> [tag] — grade EVERY job of a public CI run of <expected-sha>
3
+ # from GitHub's structured run record and each job's uploaded proof group (v2.8.2 T3, ANL-022 §7.2b; queue rows
4
+ # 165 and 148). NO LOG TEXT IS READ: no job log, no step label, no `##[error]` annotation decides anything.
5
+ # Grade only at run-end. Exit 0 GREEN, 1 RED, 2 UNREADABLE, 3 INFRA_KILLED: a cancelled run, job or declared
6
+ # step with no failed declared step — never green, never a pass, only unproven.
6
7
  # UNREADABLE dominates a mixed result, then RED: a failure anywhere dominates a kill.
7
8
  #
9
+ # INPUT: <expected-sha> is 40 lowercase hex naming a commit in <repo-dir> — the mirror clone (RELEASING.md step
10
+ # 4) or the Release workflow's own checkout — else exit 2 before any gh call. No count argument: the manifest of
11
+ # tracked test files is recomputed from that commit.
12
+ # ONE READ: `gh run view <run-id> --json headSha,status,conclusion,jobs`. IDENTITY: UNREADABLE, before any
13
+ # download, unless the run's headSha is <expected-sha> and its status is completed.
14
+ #
8
15
  # Two CLOSED inventories (v2.7.8 T4; the workflow renders these names): serial {test, test-macos}; sharded,
9
16
  # for os in ubuntu-latest and macos-latest, `suite <os> k/4` for k 1..4, `single-fork <os>`, `aggregate <os>`.
10
17
  # A run holding any sharded name is graded against the sharded inventory, else the serial one. The first
11
18
  # line is `INVENTORY form=<serial|sharded> declared=<n> observed=<n>`; each missing, duplicated or unknown
12
19
  # name prints `INVENTORY_DEFECT kind=<missing|duplicate|unknown> job=<json-quoted name>` and makes the run
13
- # UNREADABLE: a job the inventory does not name is never skipped.
20
+ # UNREADABLE: a job the inventory does not name is never skipped. The sharded proof-group selection is ANL-022
21
+ # §7.4's, so until it ships a sharded run reads UNREADABLE after its INVENTORY line.
14
22
  #
15
- # A verdict line is read only from the log lines whose gh step column equals its declared step, exactly
16
- # once, else UNREADABLE — a line a test or any other step prints, before or after the real one, never decides:
17
- # COUNT_ORACLE <GREEN|RED> expected=<n|UNREADABLE> actual=<n|UNREADABLE> step `Assert collected test-file count`
18
- # COVERAGE_FLOORS verdict=… floors=… failing=… (T1's final floors line) step `Recheck coverage floors`
19
- # SET_ORACLE verdict=… expected=… actual=… defects=… (T3's set oracle) step `Assert collected test-file set`
20
- # ROLES — the shared classifier (classify-vitest-log.sh) stays the one log verdict:
21
- # serial the base rule (count oracle GREEN at the expected count, the classifier given that count
22
- # reading CLEAN or RPC_ONLY, passed+skipped equal to it) AND exactly one COVERAGE_FLOORS
23
- # verdict=GREEN, so a dropped floors step reads UNREADABLE.
24
- # shard, single-fork
25
- # GREEN only on conclusion success whose whole log the classifier, given no count, reads CLEAN
26
- # or RPC_ONLY: their wrapper already applied the shard contract in-job.
27
- # aggregate GREEN only on conclusion success with exactly one COVERAGE_FLOORS verdict=GREEN, exactly one
28
- # `SET_ORACLE verdict=GREEN expected=N actual=N defects=0`, and the lines of its step
29
- # `Merge coverage (one threshold check per OS)` classified CLEAN or RPC_ONLY.
30
- # Exit annotations: SERIAL keeps the one exception run-ci-vitest.sh grants (a failure explained ONLY by steps
31
- # that exited exactly 1 on a log of their own the classifier reads RPC_ONLY); any other failing annotation is
32
- # never GREEN. Shard, single-fork and aggregate admit none — their wrapper applied the exception in-job.
33
- # INFRA_KILLED is the base rule for every role.
23
+ # Per serial job, every part from the same read or the job's own artifact:
24
+ # steps each DECLARED step below present exactly once in .jobs[].steps[] (missing or duplicated ->
25
+ # UNREADABLE); GREEN needs every one `success`. A cancelled job or declared step is INFRA_KILLED
26
+ # unless a declared step concluded `failure` (then RED); the steps a kill skipped do not count.
27
+ # conclusions GREEN also needs the job's and the run's conclusion `success`, so a failing undeclared step (an
28
+ # action's post-step) reds the grade; a cancelled run is INFRA_KILLED, and any other run conclusion
29
+ # no job's grade accounts for is RED.
30
+ # proof `gh run download <run-id> -n proof-<job>` into a fresh directory (missing -> UNREADABLE), then
31
+ # the grader's own checkout's `tsx scripts/ci/cli.ts proof --head <expected-sha> --repo <repo-dir>
32
+ # <dir>` (that checkout is the git toplevel holding this script; without its cli.ts, exit 2):
33
+ # the group's original records read CLEAN or RPC_ONLY at <expected-sha>, the tracked set recomputed
34
+ # from the commit holds every file once and finished, and the floors sidecar reads GREEN.
35
+ # Output: the INVENTORY lines, `RUN head=… expected=… status=… conclusion=…`, per job `<job>: steps=[…]
36
+ # proof=[<PROOF line>]` then `<job>: <GRADE>`, and a final `VERDICT rc=<n> …`.
34
37
  set -u
35
38
 
36
- run=${1:?run id required}
37
- expected=${2:?expected tracked test-file count required}
38
- tag=${3:-$run}
39
+ [ "$#" -ge 3 ] && [ "$#" -le 4 ] \
40
+ || { echo "UNREADABLE: usage: grade-ci.sh <run-id> <expected-sha> <repo-dir> [tag]"; exit 2; }
41
+ run=$1
42
+ sha=$2
43
+ repo_dir=$3
44
+ tag=${4:-$run}
39
45
  here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
40
- repo=${TKR_GRADE_CI_REPO:-alzahrani-khalid/tickmarkr}
46
+ # the grader's own checkout — the same for the canonical copy and the installed .claude/skills one
47
+ checkout=$(git -C "$here" rev-parse --show-toplevel 2>/dev/null)
48
+ gh_repo=${TKR_GRADE_CI_REPO:-alzahrani-khalid/tickmarkr}
41
49
  out_dir=${TKR_GRADE_CI_DIR:-${TKR_STATE_DIR:-.tickmarkr}/overseer/diag}
42
- mkdir -p "$out_dir" || { echo "UNREADABLE: cannot create log directory $out_dir"; exit 2; }
50
+
51
+ [[ $sha =~ ^[0-9a-f]{40}$ ]] || { echo "UNREADABLE: the expected head is not 40 lowercase hex"; exit 2; }
52
+ git -C "$repo_dir" cat-file -e "$sha^{commit}" 2>/dev/null \
53
+ || { echo "UNREADABLE: $sha is not a commit in $repo_dir"; exit 2; }
54
+ [ -n "$checkout" ] && [ -f "$checkout/scripts/ci/cli.ts" ] \
55
+ || { echo "UNREADABLE: the grader's own checkout holds no scripts/ci/cli.ts to recompute the proof"; exit 2; }
56
+ mkdir -p "$out_dir" || { echo "UNREADABLE: cannot create the proof directory $out_dir"; exit 2; }
43
57
 
44
58
  serial_inventory=$'test\ntest-macos'
45
59
  sharded_inventory=$(for os in ubuntu-latest macos-latest; do
@@ -47,21 +61,24 @@ sharded_inventory=$(for os in ubuntu-latest macos-latest; do
47
61
  echo "single-fork $os"
48
62
  echo "aggregate $os"
49
63
  done)
50
- COUNT_STEP="Assert collected test-file count"
51
- FLOORS_STEP="Recheck coverage floors"
52
- SET_STEP="Assert collected test-file set"
53
- MERGE_STEP="Merge coverage (one threshold check per OS)"
54
- COUNT_FORMAT='COUNT_ORACLE (GREEN|RED) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE)'
55
- FLOORS_FORMAT='COVERAGE_FLOORS verdict=(GREEN|RED|UNREADABLE) floors=([0-9]+|UNREADABLE) failing=([0-9]+|UNREADABLE)'
56
- SET_FORMAT='SET_ORACLE verdict=(GREEN|RED|UNREADABLE) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE) defects=[0-9]+'
64
+ # the serial job's declared steps as GitHub names them (an unnamed `run:` step is `Run <command>`)
65
+ serial_steps='Run npm ci
66
+ Run npm run build
67
+ Run npm run lint
68
+ Run coverage proof (parallel suite project)
69
+ Recheck coverage floors
70
+ Run single-fork projects (independent of the step above)
71
+ Assert collected test-file set
72
+ Upload proof group'
73
+ cli=("$checkout/node_modules/.bin/tsx" "$checkout/scripts/ci/cli.ts")
57
74
 
58
75
  verdict=0
59
76
  mark_unreadable() { verdict=2; }
60
77
  mark_red() { [ "$verdict" -eq 2 ] || verdict=1; }
61
78
  mark_killed() { [ "$verdict" -ne 0 ] || verdict=3; }
62
- field() { printf '%s\n' "$classified" | sed -n "s/^VITEST_LOG.* $1=\([^ ]*\).*/\1/p"; }
79
+ finish() { echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE 3=INFRA_KILLED)"; exit "$verdict"; }
63
80
  count() { printf '%s' "$1" | grep -c ''; }
64
- # json <name> — one JSON string; awk -v decodes gh's @tsv escapes (\t \n \r \\) first
81
+ # json <name> — one JSON string; awk -v decodes the @tsv escapes (\t \n \r \\) first
65
82
  json() {
66
83
  LC_ALL=C awk -v s="$1" 'BEGIN {
67
84
  for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
@@ -71,28 +88,37 @@ json() {
71
88
  }'
72
89
  }
73
90
  defect() { echo "INVENTORY_DEFECT kind=$1 job=$(json "$2")"; mark_unreadable; }
74
- # step_log <log> <step> — the job-log lines gh's step column attributes to <step>, as gh printed them
75
- step_log() { awk -F'\t' -v s="$2" 'NF >= 3 && $2 == s' "$1"; }
76
- # verdict_line <log> <step> <family> <format> — the ONE <family> line <step> printed, in <format>;
77
- # else MISSING, MULTIPLE(n) or MALFORMED, each UNREADABLE
78
- verdict_line() {
79
- local lines
80
- lines=$(step_log "$1" "$2" | awk -v esc="$(printf '\033')" '{
81
- gsub(/\^\[\[[0-9;]*m/, ""); gsub(esc "\\[[0-9;]*m", "")
82
- sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z ?/, ""); sub(/[ \t\r]+$/, ""); print
83
- }' | grep -E "^$3( |\$)")
84
- case $(count "$lines") in
85
- 0) echo MISSING ;;
86
- 1) if printf '%s\n' "$lines" | grep -qxE "$4"; then printf '%s\n' "$lines"; else echo MALFORMED; fi ;;
87
- *) echo "MULTIPLE($(count "$lines"))" ;;
88
- esac
89
- }
90
- readable() { case $1 in MISSING|MULTIPLE*|MALFORMED) return 1 ;; esac; }
91
- floors_green() { printf '%s\n' "$1" | grep -qxE 'COVERAGE_FLOORS verdict=GREEN floors=[0-9]+ failing=0'; }
92
91
 
93
- jobs=$(gh run view "$run" --repo "$repo" --json jobs \
94
- --jq '.jobs[] | [.databaseId, .name, .status, (.conclusion // "")] | @tsv') \
95
- || { echo "UNREADABLE: job list"; exit 2; }
92
+ run_json=$(gh run view "$run" --repo "$gh_repo" --json headSha,status,conclusion,jobs) \
93
+ || { echo "UNREADABLE: gh run view $run"; mark_unreadable; finish; }
94
+ # the read as @tsv-escaped rows — RUN <head> <status> <conclusion>, JOB <index> <name> <status> <conclusion>,
95
+ # STEP <job index> <name> <conclusion>; a null or empty value prints `null`; any other shape fails closed
96
+ rows=$(printf '%s' "$run_json" | node -e '
97
+ const field = (v) => v === null || v === undefined || v === "" ? "null"
98
+ : String(v).replace(/\\/g, "\\\\").replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
99
+ const scalar = (v) => v === null || v === undefined || typeof v === "string" || typeof v === "number";
100
+ let text = "";
101
+ process.stdin.setEncoding("utf8").on("data", (d) => { text += d; }).on("end", () => {
102
+ try {
103
+ const run = JSON.parse(text);
104
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
105
+ if (!object(run) || !Array.isArray(run.jobs) || ![run.headSha, run.status, run.conclusion].every(scalar)) throw 0;
106
+ const out = [["RUN", run.headSha, run.status, run.conclusion]];
107
+ run.jobs.forEach((job, i) => {
108
+ if (!object(job) || ![job.name, job.status, job.conclusion].every(scalar)) throw 0;
109
+ if (job.steps !== undefined && job.steps !== null && !Array.isArray(job.steps)) throw 0;
110
+ out.push(["JOB", i, job.name, job.status, job.conclusion]);
111
+ for (const step of job.steps ?? []) {
112
+ if (!object(step) || ![step.name, step.conclusion].every(scalar)) throw 0;
113
+ out.push(["STEP", i, step.name, step.conclusion]);
114
+ }
115
+ });
116
+ process.stdout.write(out.map((row) => row.map(field).join("\t") + "\n").join(""));
117
+ } catch { process.exit(2); }
118
+ });') || { echo "UNREADABLE: gh run view $run returned no readable run record"; mark_unreadable; finish; }
119
+ IFS=$'\t' read -r _ head status conclusion <<< "$(printf '%s\n' "$rows" | sed -n $'s/^RUN\t/-\t/p')"
120
+ jobs=$(printf '%s\n' "$rows" | sed -n $'s/^JOB\t//p')
121
+ steps=$(printf '%s\n' "$rows" | sed -n $'s/^STEP\t//p')
96
122
  # an empty job list is an inventory like any other: serial, observed=0, every declared job missing
97
123
 
98
124
  names=$(printf '%s\n' "$jobs" | cut -f2)
@@ -113,126 +139,96 @@ while IFS= read -r name; do
113
139
  printf '%s\n' "$declared" | grep -qxF -- "$name" || defect unknown "$name"
114
140
  done < <([ -z "$jobs" ] || printf '%s\n' "$names" | awk '!seen[$0]++')
115
141
 
116
- [ -z "$jobs" ] || while IFS=$'\t' read -r id name status conclusion; do
117
- # an unknown job has no role to grade: its INVENTORY_DEFECT already made the run UNREADABLE
118
- printf '%s\n' "$declared" | grep -qxF -- "$name" || continue
119
- case "$name" in
120
- test|test-macos) role=serial ;;
121
- suite\ *) role=shard ;;
122
- single-fork\ *) role=single-fork ;;
123
- *) role=aggregate ;;
124
- esac
142
+ if [ "$form" = sharded ]; then
143
+ echo "SHARDED: UNREADABLE (the sharded proof-group selection is ANL-022 §7.4's and has not shipped)"
144
+ mark_unreadable
145
+ finish
146
+ fi
125
147
 
126
- echo "$name: role=$role status=$status conclusion=${conclusion:-none} job=$id"
127
- if [ "$status" != "completed" ]; then
128
- echo "$name: UNREADABLE (job has not reached run-end)"
129
- mark_unreadable
130
- continue
131
- fi
148
+ echo "RUN head=$head expected=$sha status=$status conclusion=$conclusion"
149
+ if [ "$head" != "$sha" ]; then
150
+ echo "RUN: UNREADABLE (the run's head is not the expected head; nothing downloaded)"
151
+ mark_unreadable
152
+ finish
153
+ fi
154
+ if [ "$status" != completed ]; then
155
+ echo "RUN: UNREADABLE (the run has not reached run-end; nothing downloaded)"
156
+ mark_unreadable
157
+ finish
158
+ fi
132
159
 
133
- log="$out_dir/CI-$tag-$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-').log"
134
- gh run view --repo "$repo" --job "$id" --log > "$log" 2>/dev/null
135
- if [ ! -s "$log" ]; then
136
- echo "$name: UNREADABLE (empty log)"
160
+ [ -z "$jobs" ] || while IFS=$'\t' read -r index name job_status job_conclusion; do
161
+ # an unknown or duplicated job has no role to grade: its INVENTORY_DEFECT already made the run UNREADABLE
162
+ [ "$(printf '%s\n' "$names" | grep -cxF -- "$name")" -eq 1 ] && printf '%s\n' "$declared" | grep -qxF -- "$name" || continue
163
+
164
+ echo "$name: status=$job_status conclusion=$job_conclusion"
165
+ if [ "$job_status" != completed ]; then
166
+ echo "$name: UNREADABLE (job has not reached run-end)"
137
167
  mark_unreadable
138
168
  continue
139
169
  fi
140
170
 
141
- files=$(grep -oE 'Test Files .*' "$log" | sed 's/[[:space:]]*$//')
142
- # OBS-1184: the log verdict comes from the ONE classifier the public CI wrapper also uses
143
- # (scripts/run-ci-vitest.sh), so the badge and this grade cannot disagree. Its rules — complete
144
- # summaries, failed/timed-out tests, coverage-threshold misses, and the RPC-timeout-only exception
145
- # to "any unhandled error is RED" (OBS-1058) — live in classify-vitest-log.sh, stated once there.
146
- # A serial job's expected count is its declared file contract: both steps' summaries must collect it all.
147
- # A shard or single-fork job carries no count: its wrapper held it to its plan in-job.
148
- if [ "$role" = serial ]; then
149
- classified=$(bash "$here/classify-vitest-log.sh" "$log" "$expected" 2>&1)
150
- else
151
- classified=$(bash "$here/classify-vitest-log.sh" "$log" 2>&1)
152
- fi
153
- log_verdict=$(field verdict)
154
- passed=$(field passed); skipped=$(field skipped); failed=$(field failed); timedout=$(field timedout)
155
- unhandled=$(field unhandled); rpc=$(field runner_rpc_timeouts); coverage=$(field coverage_misses)
156
- kills=$(field runner_kills)
157
- errors=$(grep -oE '##\[error\].*' "$log" | sort | uniq -c | sed 's/^ *//' | tr '\n' ';')
158
- # The commands' own outcomes stand, as in the wrapper: GREEN needs a success conclusion with no step
159
- # exit annotation, or — SERIAL only — a failure explained ONLY by steps that exited exactly 1 on a log
160
- # of their own (gh's step column) that the classifier calls RPC_ONLY. Any other code (2, a signal's
161
- # 137), any annotation on another role, an unexplained failure, or a cancelled, timed-out or missing
162
- # conclusion is RED.
163
- exits=$(grep -E '##\[error\]Process completed with exit code [0-9]+' "$log")
164
- case "$conclusion" in
165
- success) outcome=ok ;;
166
- failure) if [ -n "$exits" ]; then outcome=ok; else outcome=unexplained-failure; fi ;;
167
- *) outcome="conclusion-${conclusion:-none}" ;;
168
- esac
169
- if [ -n "$exits" ]; then
170
- while IFS= read -r exit_line; do
171
- code=$(printf '%s\n' "$exit_line" | sed -E 's/.*Process completed with exit code ([0-9]+).*/\1/')
172
- if [ "$code" != 1 ] || [ "$role" != serial ]; then outcome="exit-$code"; break; fi
173
- step=$(printf '%s\n' "$exit_line" | awk -F'\t' 'NF >= 3 { print $2 }')
174
- step_log "$log" "$step" > "$log.step"
175
- case $(bash "$here/classify-vitest-log.sh" "$log.step" 2>&1) in
176
- "VITEST_LOG verdict=RPC_ONLY "*) ;;
177
- *) outcome="exit-1-not-rpc-only"; break ;;
178
- esac
179
- done <<< "$exits"
180
- fi
181
-
182
- oracles=
183
- case $role in
184
- serial)
185
- oracle=$(verdict_line "$log" "$COUNT_STEP" COUNT_ORACLE "$COUNT_FORMAT")
186
- floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
187
- oracles="oracle=[$oracle] floors=[$floors] " ;;
188
- aggregate)
189
- floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
190
- set_oracle=$(verdict_line "$log" "$SET_STEP" SET_ORACLE "$SET_FORMAT")
191
- step_log "$log" "$MERGE_STEP" > "$log.merge"
192
- merge=$(bash "$here/classify-vitest-log.sh" "$log.merge" 2>&1 | sed -n 's/^VITEST_LOG verdict=\([^ ]*\) reason=\([^ ]*\) .*/\1 \2/p')
193
- oracles="floors=[$floors] set=[$set_oracle] merge=[$merge] " ;;
194
- esac
171
+ own=$(printf '%s\n' "$steps" | awk -F'\t' -v i="$index" '$1 == i')
172
+ listed=; unread=; failed=; unproven=; killed=
173
+ [ "$job_conclusion" != cancelled ] || killed=1
174
+ while IFS= read -r step; do
175
+ matches=$(printf '%s\n' "$own" | awk -F'\t' -v s="$step" '$2 == s { print $3 }')
176
+ case $(count "$matches") in
177
+ 0) outcome=MISSING; unread=1 ;;
178
+ 1) outcome=$matches ;;
179
+ *) outcome="MULTIPLE($(count "$matches"))"; unread=1 ;;
180
+ esac
181
+ case $outcome in
182
+ success|MISSING|MULTIPLE*) ;;
183
+ cancelled) killed=1 ;;
184
+ failure) failed=1 ;;
185
+ *) unproven=1 ;; # skipped, or no conclusion: never success
186
+ esac
187
+ listed="$listed${listed:+ | }$step: $outcome"
188
+ done <<< "$serial_steps"
195
189
 
196
- echo "$name: ${oracles}files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage runner_kills=${kills:-?} log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
197
- # A killed runner skips the oracle steps, so a missing verdict line is the kill's, not an unreadable log:
198
- # INFRA_KILLED unless a step's own failing exit or a failed/timed-out test dominates it (then RED).
199
- if [ "$log_verdict" = INFRA_KILLED ] && [ "${outcome#exit-}" = "$outcome" ]; then
200
- echo "$name: INFRA_KILLED"
201
- mark_killed
202
- continue
203
- elif [ "${kills:-0}" -gt 0 ] 2>/dev/null; then
204
- grade=RED
205
- elif [ -z "$log_verdict" ]; then
206
- grade=UNREADABLE
190
+ proof=
191
+ grade=GREEN
192
+ if [ -n "$unread" ]; then
193
+ grade=UNREADABLE; proof="not downloaded: a declared step is missing or duplicated"
194
+ elif [ -n "$killed" ]; then
195
+ grade=INFRA_KILLED; [ -z "$failed" ] || grade=RED; proof="not downloaded: the job was cancelled"
207
196
  else
208
- case $log_verdict in CLEAN|RPC_ONLY) log_ok=1 ;; *) log_ok= ;; esac
209
- grade=RED
210
- case $role in
211
- serial)
212
- if ! readable "$oracle" || ! readable "$floors" || [ -z "$files" ]; then
213
- grade=UNREADABLE
214
- elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] && floors_green "$floors" \
215
- && [ -n "$log_ok" ] && [ "$outcome" = ok ] && [ $((passed + skipped)) -eq "$expected" ]; then
216
- grade=GREEN
217
- fi ;;
218
- shard|single-fork)
219
- if [ "$outcome" = ok ] && [ "$conclusion" = success ] && [ -n "$log_ok" ]; then grade=GREEN; fi ;;
220
- aggregate)
221
- if ! readable "$floors" || ! readable "$set_oracle"; then
222
- grade=UNREADABLE
223
- elif [ "$outcome" = ok ] && [ "$conclusion" = success ] && floors_green "$floors" \
224
- && [ "$set_oracle" = "SET_ORACLE verdict=GREEN expected=$expected actual=$expected defects=0" ] \
225
- && { [ "${merge%% *}" = CLEAN ] || [ "${merge%% *}" = RPC_ONLY ]; }; then
226
- grade=GREEN
227
- fi ;;
228
- esac
197
+ dir=$(mktemp -d "$out_dir/CI-$(printf '%s' "$tag" | tr -c 'A-Za-z0-9._-' '-')-proof-$name.XXXXXX") \
198
+ && gh run download "$run" --repo "$gh_repo" -n "proof-$name" -D "$dir" > /dev/null 2>&1
199
+ if [ $? -ne 0 ]; then
200
+ grade=UNREADABLE; proof="MISSING: artifact proof-$name not downloaded"
201
+ else
202
+ proof_out=$("${cli[@]}" proof --head "$sha" --repo "$repo_dir" "$dir" 2> "$dir.stderr")
203
+ proof_status=$?
204
+ printf '%s\n' "$proof_out" | sed -n "s/^PROOF_DEFECT /$name: PROOF_DEFECT /p"
205
+ proof=$(printf '%s\n' "$proof_out" | grep '^PROOF ')
206
+ word=$(printf '%s\n' "$proof" | sed -n 's/^PROOF dir=.* verdict=\([A-Z]*\) head=.*/\1/p')
207
+ # fail closed: one PROOF line, and its word believed only beside its own exit status
208
+ if [ "$(count "$proof")" -ne 1 ]; then
209
+ grade=UNREADABLE; proof="UNREADABLE: cli.ts proof exited $proof_status with $(count "$proof") PROOF lines"
210
+ elif [ "$word:$proof_status" = GREEN:0 ] || [ "$word:$proof_status" = RED:1 ]; then
211
+ grade=$word
212
+ else
213
+ grade=UNREADABLE
214
+ fi
215
+ fi
216
+ if [ "$grade" != UNREADABLE ] && { [ -n "$failed$unproven" ] || [ "$job_conclusion" != success ]; }; then grade=RED; fi
229
217
  fi
218
+ echo "$name: steps=[$listed] proof=[$proof]"
230
219
  echo "$name: $grade"
231
220
  case $grade in
232
221
  RED) mark_red ;;
233
222
  UNREADABLE) mark_unreadable ;;
223
+ INFRA_KILLED) mark_killed ;;
234
224
  esac
235
225
  done <<< "$jobs"
236
226
 
237
- echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE 3=INFRA_KILLED)"
238
- exit "$verdict"
227
+ # the run's own conclusion: GREEN needs success; a cancelled run is INFRA_KILLED; any other conclusion is RED
228
+ # unless a job's grade above already accounts for it (a killed job concludes its run `failure` too)
229
+ case $conclusion in
230
+ success) ;;
231
+ cancelled) mark_killed ;;
232
+ *) if [ "$verdict" -eq 0 ]; then echo "RUN: RED (run conclusion $conclusion with every job GREEN)"; mark_red; fi ;;
233
+ esac
234
+ finish
@@ -1,133 +0,0 @@
1
- #!/bin/bash
2
- # classify-vitest-log.sh <log> [expected-files] — the ONE strict Vitest log classifier
3
- # (OBS-1184). The public CI wrapper (scripts/run-ci-vitest.sh) and grade-ci.sh both call it, so the badge
4
- # and the grade cannot disagree. Reads a raw Vitest log or a gh job log (gh's "job<TAB>step<TAB>timestamp "
5
- # prefix and ANSI styling — the raw ESC byte and gh's `^[` rendering of it — are stripped) and prints ONE line:
6
- # VITEST_LOG verdict=<CLEAN|RPC_ONLY|RED|INFRA_KILLED> reason=<why> summaries=S passed=P skipped=K failed=F
7
- # timedout=T unhandled=U runner_rpc_timeouts=R coverage_misses=C signals=G terminal_errors=E runner_kills=X
8
- # unhandled counts the unhandled errors that are NOT runner RPC timeouts. Exit 0 CLEAN or RPC_ONLY, 1 RED,
9
- # 3 INFRA_KILLED.
10
- #
11
- # INFRA_KILLED (D-897): the GitHub runner's own shutdown or cancellation annotation ("##[error]The runner
12
- # has received a shutdown signal…" / "##[error]The operation was canceled.") with no failed or timed-out
13
- # test and no coverage-threshold miss. It is never green and never passes a gate — the proof was not
14
- # finished — but it is not a test red either. Those failures dominate a kill (RED); a kill excuses only
15
- # the cut-off or short log it leaves behind (runner_kills counts the annotations either way). A failure
16
- # the reporter already printed before the kill cut its summary off counts too: a dot-reporter "x" (its
17
- # stream has no newline, so a test's "stdout | …"/"stderr | …" header may finish the same line), a
18
- # default-reporter "×" or failed module record "❯ file (N tests…)" (❯ marks a failed module even with
19
- # no failed test — a collect or hook error), a " FAIL " badge or the "Failed Tests/Suites N" banner (only
20
- # beside a kill — without one, the complete summaries' own counts decide). Only one printed after the last
21
- # complete summary counts: a complete summary's own counts settle every line before it. Test output never
22
- # counts: a console block runs from its header until the reporter speaks again — a dot run, a ⎯ banner or
23
- # a default-reporter module record (✓/❯/↓ file (N tests…)) right after a blank line, or the next summary —
24
- # so blank lines inside a test's output do not end it.
25
- # ponytail: a test that prints, after a blank line of its own, a line the reporter could have printed is
26
- # indistinguishable from the reporter; only that deliberate forgery escapes the block.
27
- #
28
- # The declared contract (D-893), when a caller supplies one: expected-files is the number of test files
29
- # the run must collect — the summaries' "(N)" totals must sum to it exactly. The caller derives it from
30
- # the declared projects' own candidate-tree discovery, so a never-started declared project is a short
31
- # count: project presence comes from that discovery, never from log text a test can print. A short count
32
- # is RED reason=partial: a complete summary of the projects that happened to start proves nothing about
33
- # the one that did not. With or without a contract, a summary whose finished files (failed + passed +
34
- # skipped + todo) fall short of its own "(N)" is RED reason=partial too: Vitest's total counts the queued
35
- # and running files an abort left unfinished.
36
- #
37
- # RED unless every Vitest summary is complete (Test Files, Tests and Duration lines, and every
38
- # "Unhandled Errors" block closed by the summary that follows it — a cut-off log is RED), with no
39
- # failed file or test, no timed-out test or hook, no coverage-threshold ERROR, no npm signal line,
40
- # no unhandled error other than a vitest-worker RPC timeout, and no terminal error: a structural
41
- # "Unhandled Error", "Startup Error" or "Collect Error" banner outside Vitest's counted block — how
42
- # Vitest reports an exception thrown after the summary, such as a coverage-reporting failure, before it
43
- # exits 1 (terminal_errors counts them; an RPC timeout never offsets one). RPC_ONLY means unhandled
44
- # errors were present and EVERY one is such a timeout (OBS-1058: the starved 2-core runner talking,
45
- # not a test).
46
- #
47
- # Which timeouts count: only one that is ITSELF an unhandled entry — the first payload line under a
48
- # per-error header, nothing later. The header is Vitest's own structural line — a run of ⎯ on each side
49
- # of "Unhandled Error", nothing else on the line — so prose that merely contains the words never opens
50
- # a window, and the payload must be Vitest's whole line (`Error: [vitest-worker]: Timeout calling
51
- # "<method>"`), so an assertion quoting it stays RED. Per-error headers are honoured only inside
52
- # Vitest's own block — after its structural "Unhandled Errors" line and "Vitest caught N" tally, until
53
- # the file summary — and each block's count never exceeds its N, so a test that prints a header and a
54
- # timeout to stdout forges nothing. The tallies must also sum to the summaries' own "Errors N" lines
55
- # (Vitest prints both from one list), so a forged or a missing block is RED as well.
56
- set -u
57
-
58
- log=${1:?log path required}
59
- fields='summaries=0 passed=0 skipped=0 failed=0 timedout=0 unhandled=0 runner_rpc_timeouts=0 coverage_misses=0 signals=0 terminal_errors=0 runner_kills=0'
60
- if [ ! -s "$log" ] || [ ! -r "$log" ]; then
61
- echo "VITEST_LOG verdict=RED reason=unreadable $fields"
62
- exit 1
63
- fi
64
-
65
- esc=$(printf '\033')
66
- awk -v esc="$esc" -v expected="${2:-}" '
67
- function num(s, word) { return match(s, "[0-9]+ " word) ? substr(s, RSTART, RLENGTH) + 0 : 0 }
68
- function close_block() { rpc += (found < cap ? found : cap); armed = 0; pending = 0 }
69
- {
70
- line = $0
71
- gsub(/\^\[\[[0-9;]*m/, "", line); gsub(esc "\\[[0-9;]*m", "", line)
72
- sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z[ ]?/, "", line)
73
- if (line ~ /^[ \t]*(·|x|\*|-)*(stdout|stderr) \| /) {
74
- if (line ~ /^[ \t]*(·|\*|-)*x/) reported++
75
- console = 1
76
- } else if (after_blank && line ~ /^[ \t]*((·|x|\*|-)*(·|x)(·|x|\*|-)*|(⎯)+( .* (⎯)+)?|(✓|❯|↓) .* [(][0-9]+ tests?( [|] [0-9]+ (failed|skipped))*[)].*)[ \t]*$/) console = 0
77
- if (!console && line ~ /^[ \t]*((·|\*|-)*x(·|x|\*|-)*|× .*|❯ .* [(][0-9]+ tests?( [|] [0-9]+ (failed|skipped))*[)].*|FAIL[ \t].*|(⎯)+ Failed (Tests|Suites) [0-9]+ (⎯)+)[ \t]*$/) reported++
78
- after_blank = line !~ /[^ \t]/
79
- }
80
- line ~ /^##\[error\](The runner has received a shutdown signal|The operation was cancell?ed)/ { kills++ }
81
- # The Vitest timeout message carries its millisecond count; a test that PRINTS a fingerprint normalized
82
- # to #ms (the daemon OBS-1106 notifications) is output, not a timed-out test. A real timeout also fails
83
- # its test, so the summary failed count still reds it.
84
- line ~ /(Test|Hook) timed out in [0-9]+ ?ms/ { timedout++ }
85
- line ~ /ERROR: Coverage for .* does not meet .*threshold/ { coverage++ }
86
- line ~ /^npm (error|ERR!) signal / { signals++ }
87
- line ~ /^(⎯)+ Unhandled Errors (⎯)+[ \t]*$/ { if (armed) close_block(); opening = 1; next }
88
- opening && line !~ /[^ \t]/ { next }
89
- opening {
90
- opening = 0
91
- if (line ~ /^Vitest caught [0-9]+ unhandled error/) { armed = 1; found = 0; cap = num(line, "unhandled"); tally += cap }
92
- else untallied++
93
- }
94
- line ~ /^[ \t]*Test Files[ \t]/ {
95
- if (armed) close_block()
96
- summaries++; in_summary = 1; saw_tests = 0; console = 0
97
- failed += num(line, "failed"); passed += num(line, "passed"); skipped += num(line, "skipped")
98
- finished = num(line, "failed") + num(line, "passed") + num(line, "skipped") + num(line, "todo")
99
- total = match(line, /\([0-9]+\)[ \t]*$/) ? substr(line, RSTART + 1) + 0 : -1
100
- if (total >= 0) collected += total
101
- if (finished != total) unfinished++
102
- next
103
- }
104
- in_summary && line ~ /^[ \t]*Tests[ \t]/ { saw_tests = 1; failed_tests += num(line, "failed"); next }
105
- in_summary && line ~ /^[ \t]*Errors[ \t]+[0-9]+ error/ { summary_errors += num(line, "error"); next }
106
- in_summary && line ~ /^[ \t]*Duration[ \t]/ { if (saw_tests) { complete++; reported = 0 }; in_summary = 0; next }
107
- armed && line ~ /^(⎯)+ Unhandled Error (⎯)+[ \t]*$/ { pending = 1; next }
108
- line ~ /^(⎯)+ (Unhandled|Startup|Collect) Error (⎯)+[ \t]*$/ { terminal++; next }
109
- pending && line ~ /[^ \t]/ { pending = 0; if (line ~ /^Error: \[vitest-worker\]: Timeout calling "[A-Za-z]+"[ \t]*$/) found++ }
110
- END {
111
- unhandled = tally + untallied
112
- if (summary_errors > unhandled) unhandled = summary_errors
113
- other = unhandled - rpc
114
- partial = unfinished || (expected != "" && collected != expected + 0)
115
- # a kill always names a class: the failure that dominates it, or the kill itself
116
- if (kills) reason = (failed || failed_tests || reported) ? "failed" : timedout ? "timed-out" : coverage ? "coverage-threshold" : "runner-shutdown"
117
- else if (summaries == 0) reason = "no-summary"
118
- else if (complete != summaries || opening || armed) reason = "truncated"
119
- else if (failed || failed_tests) reason = "failed"
120
- else if (timedout) reason = "timed-out"
121
- else if (partial) reason = "partial"
122
- else if (coverage) reason = "coverage-threshold"
123
- else if (signals) reason = "signal"
124
- else if (terminal) reason = "terminal-error"
125
- else if (other > 0) reason = "unhandled-error"
126
- else if (tally + untallied != summary_errors) reason = "unhandled-mismatch"
127
- verdict = reason == "runner-shutdown" ? "INFRA_KILLED" : (reason != "" ? "RED" : (unhandled > 0 ? "RPC_ONLY" : "CLEAN"))
128
- if (reason == "") reason = "none"
129
- printf "VITEST_LOG verdict=%s reason=%s summaries=%d passed=%d skipped=%d failed=%d timedout=%d unhandled=%d runner_rpc_timeouts=%d coverage_misses=%d signals=%d terminal_errors=%d runner_kills=%d\n", \
130
- verdict, reason, summaries, passed, skipped, failed, timedout, other, rpc, coverage, signals, terminal, kills
131
- exit (verdict == "INFRA_KILLED" ? 3 : verdict == "RED")
132
- }
133
- ' "$log"