tickmarkr 2.7.8 → 2.8.0

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.
@@ -7,7 +7,7 @@ import { TIER_RANK } from "../config/config.js";
7
7
  import { getAdapter } from "../adapters/registry.js";
8
8
  import { GATE_NAMES } from "../graph/schema.js";
9
9
  import { acceptanceGate } from "./acceptance.js";
10
- import { classifiedFileDurations, compareToBaseline, effectiveCeilingMs, waitForCalmWindow, calmWindowReady } from "./baseline.js";
10
+ import { classifiedFileDurations, compareToBaseline, effectiveCeilingMs, waitForCalmWindow, calmWindowReady, SIGNAL_EXIT_RE } from "./baseline.js";
11
11
  import { evidenceGate } from "./evidence.js";
12
12
  import { captureLlmOutput, SeatLaunchError } from "./llm.js";
13
13
  import { withLaunchGuard } from "../drivers/orca.js";
@@ -364,30 +364,36 @@ export async function reobserveTestFiles(worktree, testCmd, baseline, files, art
364
364
  return r.meta?.recovery === undefined ? r
365
365
  : { ...r, pass: false, meta: { ...r.meta, classification: "infra", infra: true, retryable: false, recoveryRefused: true } };
366
366
  }
367
- const SIGNAL_EXIT_RE = /\b(?:SIGTERM|SIGKILL|signal\s+(?:9|15)|exit(?:s|ed|\s+code)?\s+(?:137|143))\b/i;
368
367
  const FAILURE_IDENTITY_RE = /\b(?:AssertionError|FAIL\s+\S|Tests?\s+\d+\s+failed|expected\s+.+\s+to\s+)\b/i;
369
368
  /** D1: apply the daemon's signal-only rider before either battery cache read or write. Its onGate
370
369
  * classification happens after persistence, too late to keep a scripted runner's non-verdict out.
371
370
  * Keep named failures as work verdicts and preserve details for failure-policy fingerprinting.
372
371
  * A baseline-forgiven "green" whose runner was signal-killed is the same non-verdict: a killed job
373
- * completed no suite, so forgiveness never publishes or caches it green. The authoritative termination
374
- * receipt decides, never the verdict text: a signal-terminated job is killed whatever its details say, and
375
- * a green stands only on a receipt showing a normal exit — a receiptless CACHED one (an older or planted
376
- * entry) is unavailable proof, so a fresh job measures instead. A fresh row is this invocation's own: every
377
- * production runner attaches its receipt, and the manifest path's verdict already requires a clean exit
372
+ * completed no suite. The termination receipt decides first: a signal-terminated job is killed whatever its
373
+ * details say, and a green stands only on a receipt showing a normal exit — a receiptless CACHED one (an
374
+ * older or planted entry) is unavailable proof, so a fresh job measures instead. A normal exit 1 can still
375
+ * be a kill (a wrapper reports its killed worker and exits 1), and a forgiven green's details never carry
376
+ * the runner's output, so this rider cannot see it: S19m4 — the shared forgiveness decision
377
+ * (baseline.ts freshFailures) reads kill text beside a forgiven pass itself, turns one into infra that is
378
+ * never cached, and marks the green it does publish `killTextRead`. A cached forgiven green without that
379
+ * marker was written by an engine that never read kill text, so it is refused here — a normal exit 1
380
+ * receipt included — and the job runs again. A fresh row is this invocation's own: every production
381
+ * runner attaches its receipt, and the manifest path's verdict already requires a clean exit
378
382
  * (test-manifest.ts verifyManifestReport). A no-command skip ran nothing and claims none. */
379
383
  function classifySignalOnlyTest(g, cached = false) {
380
384
  const termination = g.evidenceReceipt?.termination.kind;
381
385
  const exitCode = g.evidenceReceipt?.termination.exitCode;
382
386
  const killed = termination === "signal" || exitCode === 137 || exitCode === 143 || (!g.pass && SIGNAL_EXIT_RE.test(g.details));
383
- const unproven = g.pass && g.meta?.skipped !== true && termination !== "exit" && (cached || termination !== undefined);
387
+ const killTextUnread = cached && Array.isArray(g.meta?.forgivenFingerprints) && g.meta.killTextRead !== true;
388
+ const unproven = g.pass && g.meta?.skipped !== true && (killTextUnread || termination !== "exit" && (cached || termination !== undefined));
384
389
  if (g.gate !== "test" || g.meta?.infra === true || !(killed || unproven))
385
390
  return;
386
391
  const named = Array.isArray(g.meta?.failingTests) && g.meta.failingTests.length > 0;
387
392
  if (!g.pass && (named || FAILURE_IDENTITY_RE.test(g.details)))
388
393
  return;
389
394
  g.pass = false;
390
- g.meta = { ...g.meta, classification: "infra", infra: true, retryable: false, kind: killed || termination ? "signal-exit" : "no-exit-receipt" };
395
+ g.meta = { ...g.meta, classification: "infra", infra: true, retryable: false,
396
+ kind: killed || (termination && termination !== "exit") ? "signal-exit" : termination ? "kill-text-unread" : "no-exit-receipt" };
391
397
  }
392
398
  /** OBS-1151: a criterion's comparable subject — its canonical text, the task's declared bounds and the
393
399
  * operator context. The cited files' blobs are compared separately, over the union of both citations. */
@@ -1193,7 +1199,7 @@ export async function runGates(task, ctx) {
1193
1199
  // details. The v1.1 badReviewers precedent's TIMING can't transfer: its failover lands on the NEXT
1194
1200
  // worker attempt — exactly what this fix forbids; only its meta-carries-channel pattern is mirrored.
1195
1201
  // Straight-line single `if` — NO loop/counter/knob: exactly-once by construction (a knob is a
1196
- // fail-closed weakening vector); a second garbage verdict fails the gate closed exactly as today.
1202
+ // fail-closed weakening vector); a second garbage verdict fails the gate closed as terminal infra (S23-F11).
1197
1203
  // T6: exclusion mirrors consult reroute semantics — the flaked channel's whole adapter is banned, not
1198
1204
  // just its exact channel key, so an outage window cannot re-select the vendor being routed around.
1199
1205
  // If no other adapter is live, the exclusion degrades to a channel-level reroute within the same
@@ -1226,8 +1232,11 @@ export async function runGates(task, ctx) {
1226
1232
  }
1227
1233
  // OBS-1168(b): the re-routed seat could not launch either — no seat produced a verdict, so this is
1228
1234
  // an infra park over whatever the deterministic gates proved, never a charge against the worker.
1229
- if (a.meta?.cause === "seat-launch-failed")
1235
+ // S23-F11: the same holds for a retry whose verdict is still unparseable: two seats delivered no readable
1236
+ // verdict, which judges nothing about the worker's diff, so the row is terminal infra, never a charged red.
1237
+ if (a.meta?.cause === "seat-launch-failed" || (a.meta?.judgeRetry !== undefined && a.meta.unparseable === true)) {
1230
1238
  a = { ...a, meta: { ...a.meta, classification: "infra", infra: true, retryable: false } };
1239
+ }
1231
1240
  else if (!cancelled && Array.isArray(a.meta?.judgment))
1232
1241
  a = await adjudicate(a);
1233
1242
  // No dispatch, no key: a deterministic-oracle round writes no `invocations` field rather than an
@@ -719,8 +719,8 @@ export async function evaluateManifestedTest(cmd, cwd, opts) {
719
719
  const evidenceRoot = opts.evidence?.artifactDir ?? dir;
720
720
  const stdoutPath = join(evidenceRoot, evidenceReceipt.stdout.path);
721
721
  const stderrPath = join(evidenceRoot, evidenceReceipt.stderr.path);
722
- const stdoutTail = Buffer.from(redactGateOutput(invoked.stdout, env)).subarray(-16 * 1024);
723
- const stderrTail = Buffer.from(redactGateOutput(invoked.stderr, env)).subarray(-16 * 1024);
722
+ const stdoutTail = Buffer.from(redactGateOutput(invoked.stdout, env, spawnedCommand)).subarray(-16 * 1024);
723
+ const stderrTail = Buffer.from(redactGateOutput(invoked.stderr, env, spawnedCommand)).subarray(-16 * 1024);
724
724
  const report = invoked.report?.nonce === nonce ? invoked.report : undefined;
725
725
  const neverStarted = report ? files.filter(file => !(file in report.started)).length : "unknown";
726
726
  const errors = report?.certificate?.errors;
package/dist/run/git.d.ts CHANGED
@@ -154,6 +154,8 @@ export interface ShResult {
154
154
  durationMs?: number;
155
155
  /** T7: the capacity THIS child ran under, stamped where its environment was built (see `shell`). */
156
156
  capacity?: RunCapacity;
157
+ /** Queue row 142: stdout exactly as the child wrote it, present only when the own-git caller asked for bytes. */
158
+ stdoutBytes?: Buffer;
157
159
  }
158
160
  export declare const setSpawnForTests: (fn: typeof spawn) => void;
159
161
  export declare const resetSpawnForTests: () => void;
@@ -168,6 +170,8 @@ export interface ShellOptions {
168
170
  onReceipt?: (receipt: ShellReceipt) => void;
169
171
  /** Called once per invocation (1-based), including each pre-spawn retry. */
170
172
  receiptAttribution?: (invocation: number) => CommandReceiptAttribution;
173
+ /** Queue row 142: also keep stdout as raw bytes (ShResult.stdoutBytes); only shGit's byte readers opt in. */
174
+ bytes?: boolean;
171
175
  }
172
176
  /** Shared command seam, including invocation-bound manifested runners. A PAYLOAD entry: its children keep the
173
177
  * operator's git config (hooks, fsmonitor) and get no checkout pin — ownership follows the entry, never `login`
@@ -180,7 +184,7 @@ export declare function shell(cmd: string, cwd: string, timeoutMs: number, login
180
184
  */
181
185
  export type OwnGitOptions = OwnGitCapability;
182
186
  export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options?: ShellOptions): Promise<ShResult>;
183
- export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions): Promise<ShResult>;
187
+ export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions, options?: Pick<ShellOptions, "bytes">): Promise<ShResult>;
184
188
  export declare function shOk(cmd: string, cwd: string): Promise<string>;
185
189
  export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
186
190
  /**
package/dist/run/git.js CHANGED
@@ -386,6 +386,7 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
386
386
  throw error;
387
387
  }
388
388
  let stdout = "", stderr = "";
389
+ const stdoutChunks = [];
389
390
  const stdoutDecoder = new StringDecoder("utf8");
390
391
  const stderrDecoder = new StringDecoder("utf8");
391
392
  let timedOut = false, reapedGroup = false, done = false, started = false, outputSeen = false;
@@ -419,6 +420,7 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
419
420
  timedOut,
420
421
  durationMs: Date.now() - startedAt,
421
422
  capacity,
423
+ ...(options.bytes ? { stdoutBytes: Buffer.concat(stdoutChunks) } : {}),
422
424
  ...(reapedGroup ? { reapedGroup: true } : {}),
423
425
  ...(reapError ? { reapError } : {}),
424
426
  });
@@ -485,6 +487,8 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
485
487
  p.stdout.on("data", (d) => {
486
488
  if (d.length > 0)
487
489
  outputSeen = true;
490
+ if (options.bytes)
491
+ stdoutChunks.push(d);
488
492
  stdout += stdoutDecoder.write(d);
489
493
  });
490
494
  p.stderr.on("data", (d) => {
@@ -576,9 +580,9 @@ export function sh(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, options = {})
576
580
  }
577
581
  // Git plumbing never needs an operator profile; skip login-shell startup and its side effects.
578
582
  // v2.6.8 T2: the explicit OWN-GIT entry — forced inert hooks/fsmonitor and, in a linked checkout, the trusted-path and
579
- // ref-store pin.
580
- export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, own = {}) {
581
- return shellEntry(cmd, cwd, timeoutMs, false, {}, own);
583
+ // ref-store pin. Queue row 142: `bytes` is the one opt-in raw-stdout capture; every other caller's output is unchanged.
584
+ export function shGit(cmd, cwd, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, own = {}, options = {}) {
585
+ return shellEntry(cmd, cwd, timeoutMs, false, options, own);
582
586
  }
583
587
  export async function shOk(cmd, cwd) {
584
588
  const r = await sh(cmd, cwd);
@@ -455,9 +455,9 @@ export declare const TelemetryRowSchema: z.ZodObject<{
455
455
  signalBasis: z.ZodOptional<z.ZodEnum<{
456
456
  "judge-only": "judge-only";
457
457
  skipped: "skipped";
458
+ legacy: "legacy";
458
459
  proved: "proved";
459
460
  "review-agree": "review-agree";
460
- legacy: "legacy";
461
461
  vacuous: "vacuous";
462
462
  }>>;
463
463
  kind: z.ZodOptional<z.ZodLiteral<"judge">>;
@@ -33,12 +33,34 @@ export type ProbeTimer = (fire: () => void, ms: number) => () => void;
33
33
  interface ProbeSeam {
34
34
  platform: NodeJS.Platform;
35
35
  spawn: typeof spawn;
36
+ spawnTop?: typeof spawn;
36
37
  setTimer: ProbeTimer;
37
38
  signal?: AbortSignal;
38
39
  }
39
40
  export declare const setLaunchServicesProbeForTests: (s: Partial<ProbeSeam>) => void;
40
41
  export declare const resetLaunchServicesProbeForTests: () => void;
41
42
  export declare function observeLaunchServices(s?: ProbeSeam): Promise<LaunchServicesObservation>;
43
+ export declare const LAUNCHSERVICES_PORT_ADVISORY = 100000;
44
+ export declare const LAUNCHSERVICES_PORT_STOP = 200000;
45
+ export type LaunchServicesPorts = {
46
+ status: "skipped" | "non-darwin";
47
+ } | {
48
+ status: "unknown";
49
+ pid?: number;
50
+ } | {
51
+ status: "read";
52
+ pid: number;
53
+ count: number;
54
+ };
55
+ /** Closed top format: PID COMMAND #PORTS, then the next non-empty row for this pid and launchservicesd. */
56
+ export declare function parseTopPortCount(stdout: string, pid: number): number | null;
57
+ /** One identity read and, only for a readable Darwin identity, one bounded top child. */
58
+ export declare function probeLaunchServicesPorts(options?: {
59
+ requireTop?: boolean;
60
+ }): Promise<{
61
+ observation?: LaunchServicesObservation;
62
+ ports: LaunchServicesPorts;
63
+ }>;
42
64
  export declare function launchServicesAdvisory(prior: LaunchServicesIdentity, now: LaunchServicesIdentity): string;
43
65
  /** Pure: the table row for one observation against the held record, and the record to hold next. */
44
66
  export declare function classifyLaunchServices(prior: Partial<LaunchServicesRecord> | undefined, observed: LaunchServicesObservation): {
@@ -48,7 +70,7 @@ export declare function classifyLaunchServices(prior: Partial<LaunchServicesReco
48
70
  };
49
71
  /** Observe, classify against the first record of `prior`, and hold the result on the first record of `health`.
50
72
  * Returns `health` itself when nothing changed, else a copy — the argument is never mutated. */
51
- export declare function recordLaunchServices(prior: Record<string, AuthHealth> | null, health: Record<string, AuthHealth>): Promise<{
73
+ export declare function recordLaunchServices(prior: Record<string, AuthHealth> | null, health: Record<string, AuthHealth>, observation?: LaunchServicesObservation): Promise<{
52
74
  status: LaunchServicesStatus;
53
75
  health: Record<string, AuthHealth>;
54
76
  line?: string;
@@ -27,12 +27,14 @@ export function parsePsIdentity(stdout) {
27
27
  const production = () => ({
28
28
  platform: process.platform,
29
29
  spawn,
30
+ spawnTop: spawn,
30
31
  setTimer: (fire, ms) => { const t = setTimeout(fire, ms); return () => clearTimeout(t); },
31
32
  });
32
33
  // ponytail: default-off under the test runner (the catalogRefreshAllowed precedent) so an unowned doctor
33
34
  // test never takes a real host reading or changes its doctor.json bytes; tests opt in through the seam.
35
+ // The legacy identity-only seam leaves spawnTop unset, so old /bin/ps fixtures never touch the host top.
34
36
  let seam;
35
- export const setLaunchServicesProbeForTests = (s) => { seam = { ...production(), ...s }; };
37
+ export const setLaunchServicesProbeForTests = (s) => { seam = { ...production(), ...s, spawnTop: s.spawnTop }; };
36
38
  export const resetLaunchServicesProbeForTests = () => { seam = undefined; };
37
39
  const activeSeam = () => seam ?? (process.env.VITEST === "true" ? undefined : production());
38
40
  /** One /bin/ps call that owns only its child, killed at the ceiling or on cancellation and reaped before it resolves. */
@@ -78,6 +80,88 @@ function readIdentity(s) {
78
80
  export function observeLaunchServices(s = production()) {
79
81
  return s.platform === "darwin" ? readIdentity(s) : Promise.resolve({ status: "non-darwin" });
80
82
  }
83
+ export const LAUNCHSERVICES_PORT_ADVISORY = 100_000;
84
+ export const LAUNCHSERVICES_PORT_STOP = 200_000;
85
+ const TOP_BYTES = 1024 * 1024;
86
+ /** Closed top format: PID COMMAND #PORTS, then the next non-empty row for this pid and launchservicesd. */
87
+ export function parseTopPortCount(stdout, pid) {
88
+ const lines = stdout.split(/\r?\n/);
89
+ const header = lines.findIndex((line) => line.trim().split(/\s+/).join(" ") === "PID COMMAND #PORTS");
90
+ if (header < 0)
91
+ return null;
92
+ const row = lines.slice(header + 1).find((line) => line.trim() !== "");
93
+ if (!row)
94
+ return null;
95
+ const tokens = row.trim().split(/\s+/);
96
+ if (tokens.length !== 3 || tokens[0] !== String(pid) || tokens[1] !== "launchservicesd" || !/^[0-9]+[+-]?$/.test(tokens[2]))
97
+ return null;
98
+ const count = Number(tokens[2].replace(/[+-]$/, ""));
99
+ return Number.isSafeInteger(count) ? count : null;
100
+ }
101
+ // The captured macOS top layout is closed: only a matching three-token row after the exact header
102
+ // yields a count. 100000 ports is a doctor advisory; 200000 stops standalone verify before gates.
103
+ // Unknown (including an unreadable identity, malformed top output, or a killed child) never stops.
104
+ function readPortCount(s, pid) {
105
+ return new Promise((resolve) => {
106
+ const unknown = () => resolve({ status: "unknown", pid });
107
+ if (s.signal?.aborted) {
108
+ unknown();
109
+ return;
110
+ }
111
+ if (!s.spawnTop) {
112
+ unknown();
113
+ return;
114
+ }
115
+ let child;
116
+ try {
117
+ child = s.spawnTop("/usr/bin/top", ["-l", "1", "-stats", "pid,command,ports", "-pid", String(pid)], { stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, LC_ALL: "C" } });
118
+ }
119
+ catch {
120
+ unknown();
121
+ return;
122
+ }
123
+ let out = "";
124
+ let bytes = 0;
125
+ let failed = false;
126
+ const retire = () => { if (!failed) {
127
+ failed = true;
128
+ child.kill("SIGKILL");
129
+ } };
130
+ const clear = s.setTimer(retire, LAUNCHSERVICES_PROBE_CEILING_MS);
131
+ s.signal?.addEventListener("abort", retire, { once: true });
132
+ child.stdout?.on("data", (chunk) => {
133
+ if (failed)
134
+ return;
135
+ bytes += chunk.length;
136
+ if (bytes > TOP_BYTES) {
137
+ retire();
138
+ return;
139
+ }
140
+ out += chunk.toString("utf8");
141
+ });
142
+ child.once("error", () => { failed = true; });
143
+ child.once("close", (code) => {
144
+ clear();
145
+ s.signal?.removeEventListener("abort", retire);
146
+ if (failed || code !== 0)
147
+ return unknown();
148
+ const count = parseTopPortCount(out, pid);
149
+ return count === null ? unknown() : resolve({ status: "read", pid, count });
150
+ });
151
+ });
152
+ }
153
+ /** One identity read and, only for a readable Darwin identity, one bounded top child. */
154
+ export async function probeLaunchServicesPorts(options = {}) {
155
+ const s = activeSeam();
156
+ if (!s || (options.requireTop && !s.spawnTop))
157
+ return { ports: { status: "skipped" } };
158
+ const observation = await observeLaunchServices(s);
159
+ if (observation.status === "non-darwin")
160
+ return { observation, ports: { status: "non-darwin" } };
161
+ if (observation.status === "unreadable")
162
+ return { observation, ports: { status: "unknown" } };
163
+ return { observation, ports: await readPortCount(s, observation.identity.pid) };
164
+ }
81
165
  // doctor.json is never schema-validated: a malformed held record is no identity at all.
82
166
  const knownPrior = (prior) => typeof prior?.pid === "number" && typeof prior.start === "string";
83
167
  export function launchServicesAdvisory(prior, now) {
@@ -106,10 +190,11 @@ export function classifyLaunchServices(prior, observed) {
106
190
  }
107
191
  /** Observe, classify against the first record of `prior`, and hold the result on the first record of `health`.
108
192
  * Returns `health` itself when nothing changed, else a copy — the argument is never mutated. */
109
- export async function recordLaunchServices(prior, health) {
193
+ export async function recordLaunchServices(prior, health, observation) {
110
194
  const held = prior ? Object.values(prior)[0]?.launchServices : undefined;
111
195
  const s = activeSeam();
112
- const r = s ? classifyLaunchServices(held, await observeLaunchServices(s)) : { status: "skipped", next: knownPrior(held) ? held : undefined };
196
+ const r = observation ? classifyLaunchServices(held, observation)
197
+ : s ? classifyLaunchServices(held, await observeLaunchServices(s)) : { status: "skipped", next: knownPrior(held) ? held : undefined };
113
198
  const holder = Object.keys(health)[0];
114
199
  if (!holder || !r.next || JSON.stringify(health[holder].launchServices) === JSON.stringify(r.next))
115
200
  return { ...r, health };
package/dist/run/merge.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { shq } from "../adapters/types.js";
4
- import { beginGateEvidence, ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, classifiedFileDurations, fingerprint, freshFailures, } from "../gates/baseline.js";
4
+ import { baselineEntryRefusal, beginGateEvidence, ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, classifiedFileDurations, fingerprint, freshFailures, } from "../gates/baseline.js";
5
5
  import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
6
6
  import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
7
7
  import { tickmarkrDir } from "../graph/graph.js";
@@ -216,7 +216,9 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
216
216
  // Persistent reuse requires the run's durable journal. Without it there is no
217
217
  // recorded cycle provenance to reconcile with the caller's capacity checks.
218
218
  const hit = journalFound ? store.get(id) : undefined;
219
- if (hit && id && hit.pass && !isInfraResult(hit)) {
219
+ // S19m4: a forgiven test green written before the tip read kill text never proves the suite completed.
220
+ const killTextUnread = gate === "test" && hit?.meta?.forgiven === true && hit.meta.killTextRead !== true;
221
+ if (hit && id && hit.pass && !isInfraResult(hit) && !killTextUnread) {
220
222
  results.push({
221
223
  gate,
222
224
  cmd,
@@ -339,7 +341,10 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
339
341
  });
340
342
  continue;
341
343
  }
342
- const { failing, unreadable } = freshFailures(entry, stripped);
344
+ const verdict = freshFailures(entry, stripped, gate === "test");
345
+ const { failing, unreadable } = verdict;
346
+ // S22: a missing or unreadable entry forgives nothing; the red names it rather than a bare exit code.
347
+ const refusal = baselineEntryRefusal(gate, verdict);
343
348
  const deficit = fileCountDeficit(entry, stripped);
344
349
  const cause = deficit ? "infra" : classifyFreshRunnerOutput(entry, stripped, r.code);
345
350
  const greenTeardown = classifyRunnerOutput(stripped, r.code) === "green-teardown";
@@ -354,7 +359,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
354
359
  // fingerprintable yet OBS-42-exempt from rejecting, so `failing` comes back empty, `unreadable`
355
360
  // false and the cause reads "regression" — every other guard satisfied, and a real red forgiven
356
361
  // against a capture that never finished asking the question.
357
- const baselineRed = entry !== undefined && entry.infra !== true && (entry.exitCode ?? 1) !== 0;
362
+ const baselineRed = verdict.baselineRed;
358
363
  // Battery parity on the infra rule too (T9): infrastructure-only output means the runner never
359
364
  // completed a suite — nothing was verified, so nothing is forgivable, however familiar its
360
365
  // fingerprints. Stricter-than-battery edge kept: unreadable output never forgives.
@@ -363,8 +368,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
363
368
  // often the previous session's — so a capture taken under a different resolved capacity forgives
364
369
  // nothing at the tip either. An absent capacity is a pre-T7 baseline and keeps today's verdict.
365
370
  const comparable = sameCapacity(entry?.capacity, r.capacity);
366
- const forgiven = r.code !== 0 && baselineRed && failing.length === 0 && !unreadable && cause !== "infra"
371
+ const forgivable = r.code !== 0 && baselineRed && failing.length === 0 && !unreadable && cause !== "infra"
367
372
  && comparable;
373
+ // S19m4 (battery parity): kill text beside forgivable failures means the suite never completed — red, infra.
374
+ const killLine = forgivable && !greenTeardown && !deficit ? verdict.killLine : undefined;
375
+ const forgiven = forgivable && killLine === undefined;
376
+ const rowCause = killLine !== undefined ? "infra" : cause;
368
377
  const pass = !deficit && (r.code === 0 || greenTeardown || forgiven);
369
378
  if (!pass)
370
379
  writeTipLog(artifact, raw);
@@ -375,16 +384,20 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
375
384
  evidenceReceipt, evidenceReceipts,
376
385
  exitCode: r.code,
377
386
  fingerprints: r.code !== 0 && !greenTeardown ? fingerprint(stripped) : [],
378
- details: `${cause === "infra" ? "infra; " : ""}${rerun ? `${rerun}: ` : ""}` + (deficit?.replace(/^infra; /, "") ?? (r.code === 0 ? "exit 0"
387
+ details: `${rowCause === "infra" ? "infra; " : ""}${rerun ? `${rerun}: ` : ""}` + (deficit?.replace(/^infra; /, "") ?? (r.code === 0 ? "exit 0"
379
388
  : greenTeardown ? `exit ${r.code} after a green suite summary; only the runner's teardown fingerprint followed it`
380
389
  : forgiven ? `exit ${r.code} but only baseline-recorded failures (forgiven vs baseline)`
381
- : !comparable && baselineRed && failing.length === 0
382
- ? `exit ${r.code}; every failure is baseline-recorded, but that capture ran under `
383
- + `${describeCapacity(entry?.capacity)} and this verification ran under ${describeCapacity(r.capacity)} `
384
- + `— forgiveness across a changed capacity is not evidence`
385
- : `exit ${r.code}`)),
390
+ : killLine !== undefined ? `exit ${r.code}; every failure is baseline-recorded, but the runner printed kill text — `
391
+ + `it never completed a suite, so nothing is forgiven: ${killLine}`
392
+ : !comparable && baselineRed && failing.length === 0
393
+ ? `exit ${r.code}; every failure is baseline-recorded, but that capture ran under `
394
+ + `${describeCapacity(entry?.capacity)} and this verification ran under ${describeCapacity(r.capacity)} `
395
+ + `— forgiveness across a changed capacity is not evidence`
396
+ : `exit ${r.code}`))
397
+ // S22: every red here names a missing or unreadable entry beside its own cause, deficit included
398
+ + (!pass && refusal ? `; ${refusal}` : ""),
386
399
  ...(forgiven && !deficit ? { forgiven: true, ...(baseline?.provenance ? { baselineProvenance: baseline.provenance } : {}) } : {}),
387
- ...(cause ? { cause } : {}),
400
+ ...(rowCause ? { cause: rowCause } : {}),
388
401
  ...(pass ? {} : { artifact }),
389
402
  };
390
403
  results.push(tipResult);
@@ -403,6 +416,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
403
416
  if (id && id.tree === completedTree && !isInfraResult(result)) {
404
417
  store.set(id, { ...result, meta: {
405
418
  fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
419
+ ...(result.forgiven && result.gate === "test" ? { killTextRead: true } : {}),
406
420
  reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
407
421
  artifact: result.artifact,
408
422
  evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.7.8",
3
+ "version": "2.8.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",