tickmarkr 2.7.9 → 2.8.1

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";
@@ -19,7 +19,7 @@ import { discoverTestManifest, evaluateManifestedTest, isVitestTestCommand, mani
19
19
  import { RUNNER_INFRA_DIAGNOSTIC_RE, timeoutShaped } from "./timeout-shaped.js";
20
20
  import { executionSignal } from "../run/execution-budget.js";
21
21
  import { failureDisposition } from "../run/recovery.js";
22
- import { dependencyLinkRefusal, preserveWorktree, producerFields, readCapacity, shGit, resolvedCapacity, sameCapacity, verificationProtocol } from "../run/git.js";
22
+ import { dependencyLinkRefusal, preserveWorktree, producerFields, readCapacity, reflogWitness, shGit, resolvedCapacity, sameCapacity, verificationProtocol } from "../run/git.js";
23
23
  import { withJudgeInvocationEvidence } from "../run/journal.js";
24
24
  import { computeVerificationIdentity, verificationIdentityKey, formatReusedRow, getVerdictStore, isInfraResult, resolveStateDir, reusedIdentity, } from "./cache.js";
25
25
  const productionLoadProvider = () => loadavg()[0] ?? 0;
@@ -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. */
@@ -431,6 +437,49 @@ export async function judgeContradictions(worktree, head, fresh, priors) {
431
437
  }
432
438
  return found;
433
439
  }
440
+ const SUBJECT_LEG_TEXT = {
441
+ moved: "moved", restored: "moved and restored", witness: "could not be witnessed", unreadable: "is unreadable",
442
+ };
443
+ /** ANL-023 §7.4 S0, read-only: HEAD's symbolic state and its reflog W(HEAD) — plus, when attached, its branch's
444
+ * W(R) — each valid for the sampled `commit`. A move made inside the checkout writes both logs, a branch move from
445
+ * another worktree only the branch's, a same-commit retarget only HEAD's, so both are kept. */
446
+ async function openSubjectWitness(dir, commit) {
447
+ const sym = await shGit("git symbolic-ref -q HEAD", dir);
448
+ if (sym.code !== 0 && sym.code !== 1)
449
+ return { leg: "unreadable" };
450
+ const ref = sym.code === 0 ? sym.stdout.trim() : undefined;
451
+ const logs = [];
452
+ for (const name of ref ? ["HEAD", ref] : ["HEAD"]) {
453
+ const w = await reflogWitness(dir, name);
454
+ if (w.code !== 0 || w.lines[0] !== commit)
455
+ return { leg: "witness" };
456
+ logs.push(w.lines.join("\n"));
457
+ }
458
+ return { ref, logs };
459
+ }
460
+ /** ANL-023 §7.4, after the seat returns: HEAD = `commit`, its symbolic state and referent unchanged, and both logs
461
+ * line for line as S0 found them — or the leg that changed, with the HEAD it found. */
462
+ async function adjudicationSubjectChange(dir, commit, s0) {
463
+ const head = await shGit("git rev-parse HEAD", dir);
464
+ if (head.code !== 0)
465
+ return { leg: "unreadable", found: "unreadable" };
466
+ const found = head.stdout.trim();
467
+ if (found !== commit)
468
+ return { leg: "moved", found };
469
+ const sym = await shGit("git symbolic-ref -q HEAD", dir);
470
+ if (sym.code !== 0 && sym.code !== 1)
471
+ return { leg: "unreadable", found: "unreadable" };
472
+ if ((sym.code === 0 ? sym.stdout.trim() : undefined) !== s0.ref)
473
+ return { leg: "moved", found };
474
+ for (const [i, name] of (s0.ref ? ["HEAD", s0.ref] : ["HEAD"]).entries()) {
475
+ const w = await reflogWitness(dir, name);
476
+ if (w.code !== 0)
477
+ return { leg: "unreadable", found: "unreadable" };
478
+ if (w.lines.join("\n") !== s0.logs[i])
479
+ return { leg: "restored", found };
480
+ }
481
+ return undefined;
482
+ }
434
483
  export async function runGates(task, ctx) {
435
484
  const results = [];
436
485
  const evidence = {
@@ -1141,6 +1190,11 @@ export async function runGates(task, ctx) {
1141
1190
  // reused. Only a criterion that REVERSES the newest prior ruling on the same subject key over
1142
1191
  // identical cited blobs needs a second, distinct judge; agreement stands (a sound FAIL included),
1143
1192
  // and a split, no distinct eligible seat or an unreadable adjudication parks for the operator.
1193
+ // 2.8.1 T4 (ANL-023 §7.4): the record names the commit it judged, under a validated subject witness. Only when
1194
+ // a second seat is about to run, the subject witness (HEAD's reflog and, when attached, its branch's reflog) is
1195
+ // validated for the sampled HEAD before the second seat runs, read-only, and re-read after it returns with HEAD
1196
+ // and its symbolic state; a move, a move-and-restore or an unwitnessable (or unreadable) subject parks infra with
1197
+ // no record stamped and no worker charge.
1144
1198
  const adjudicate = async (fresh) => {
1145
1199
  const primary = String(fresh.meta?.judge ?? channelKey({ adapter: ctx.cfg.judge.adapter, model: ctx.cfg.judge.model }));
1146
1200
  const head = await shGit("git rev-parse HEAD", ctx.worktree);
@@ -1173,7 +1227,19 @@ export async function runGates(task, ctx) {
1173
1227
  const seatVia = via
1174
1228
  ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("judge", seatAdapter.id) + "-r2", label: via.labelFor("judge") }
1175
1229
  : undefined;
1230
+ const subjectPark = (leg, found, ran) => ({
1231
+ gate: "acceptance", pass: false,
1232
+ details: `the adjudication subject ${SUBJECT_LEG_TEXT[leg]}: sampled ${commit.slice(0, 12)}, found ${found.slice(0, 12)} (${leg}); parked for an operator ruling, no worker charge`,
1233
+ meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary,
1234
+ adjudication: { primary, ...(ran ? { adjudicator: ran } : {}), sampled: commit, found, leg } },
1235
+ });
1236
+ const s0 = await openSubjectWitness(ctx.worktree, commit);
1237
+ if ("leg" in s0)
1238
+ return subjectPark(s0.leg, s0.leg === "unreadable" ? "unreadable" : commit);
1176
1239
  const second = await invokeJudge(seatAdapter, seat.model, seatVia, configuredEffort(ctx.cfg, seat));
1240
+ const changed = await adjudicationSubjectChange(ctx.worktree, commit, s0);
1241
+ if (changed)
1242
+ return subjectPark(changed.leg, changed.found, adjudicator);
1177
1243
  const rulings = Array.isArray(second.meta?.judgment) ? second.meta.judgment : undefined;
1178
1244
  // A citation-less ruling cannot be compared, so it confirms nothing (invented evidence is already unparseable,
1179
1245
  // and an internally inconsistent verdict carries no judgment rows at all).
@@ -1193,7 +1259,7 @@ export async function runGates(task, ctx) {
1193
1259
  // details. The v1.1 badReviewers precedent's TIMING can't transfer: its failover lands on the NEXT
1194
1260
  // worker attempt — exactly what this fix forbids; only its meta-carries-channel pattern is mirrored.
1195
1261
  // 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.
1262
+ // fail-closed weakening vector); a second garbage verdict fails the gate closed as terminal infra (S23-F11).
1197
1263
  // T6: exclusion mirrors consult reroute semantics — the flaked channel's whole adapter is banned, not
1198
1264
  // just its exact channel key, so an outage window cannot re-select the vendor being routed around.
1199
1265
  // If no other adapter is live, the exclusion degrades to a channel-level reroute within the same
@@ -1226,8 +1292,11 @@ export async function runGates(task, ctx) {
1226
1292
  }
1227
1293
  // OBS-1168(b): the re-routed seat could not launch either — no seat produced a verdict, so this is
1228
1294
  // an infra park over whatever the deterministic gates proved, never a charge against the worker.
1229
- if (a.meta?.cause === "seat-launch-failed")
1295
+ // S23-F11: the same holds for a retry whose verdict is still unparseable: two seats delivered no readable
1296
+ // verdict, which judges nothing about the worker's diff, so the row is terminal infra, never a charged red.
1297
+ if (a.meta?.cause === "seat-launch-failed" || (a.meta?.judgeRetry !== undefined && a.meta.unparseable === true)) {
1230
1298
  a = { ...a, meta: { ...a.meta, classification: "infra", infra: true, retryable: false } };
1299
+ }
1231
1300
  else if (!cancelled && Array.isArray(a.meta?.judgment))
1232
1301
  a = await adjudicate(a);
1233
1302
  // No dispatch, no key: a deterministic-oracle round writes no `invocations` field rather than an
@@ -5,8 +5,9 @@ import { type ExecutorDriver, type Slot } from "../drivers/types.js";
5
5
  import { type Baseline, type BaselineProvenance, type GateEvidenceOptions } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
7
  import { type RunGraph, type Task } from "../graph/schema.js";
8
- import { type PreserveProducer } from "./git.js";
8
+ import { type PreserveProducer, type RunCapacity } from "./git.js";
9
9
  import { Journal, type JournalEvent } from "./journal.js";
10
+ import { type IntegrationLeg, type IntegrationRefState, type TipVerifyResult } from "./merge.js";
10
11
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
11
12
  export declare function setContactUnreadableDeadlineMsForTests(ms: number): void;
12
13
  export declare function resetContactUnreadableDeadlineMsForTests(): void;
@@ -246,23 +247,86 @@ export interface TipProof {
246
247
  export declare function runEndTipProof(events: readonly JournalEvent[]): TipProof;
247
248
  /** The close notification's statement of the proof — one clause per kind. */
248
249
  export declare function formatTipProof(p: TipProof): string;
250
+ /** 2.8.1 T1 (ANL-023 §7.1): what run end checks the integration ref against; `expected` absent = the opening read. */
251
+ export interface TipIdentity {
252
+ repo: string;
253
+ branch: string;
254
+ expected?: string;
255
+ }
256
+ export type IdentityLeg = IntegrationLeg | "setup" | "witness" | "checkout";
257
+ type DeferredVerdict = NonNullable<TipVerifyResult["deferredVerdict"]>;
258
+ /** One close under an identity: `expected` is what the closing check must find; `identityFailed` skips it. */
259
+ export interface TipClose {
260
+ failed: boolean;
261
+ expected?: string;
262
+ identityFailed: boolean;
263
+ deferred: DeferredVerdict[];
264
+ }
265
+ interface TipVerifyOptions {
266
+ lastMergedTask?: string;
267
+ baseline?: Baseline;
268
+ signal?: AbortSignal;
269
+ evidence?: GateEvidenceOptions;
270
+ }
271
+ /**
272
+ * 2.8.1 T3 (ANL-023 §7.3): anchor A of a run whose run-start carries `integrationAnchor: "journal"` — the newest merge
273
+ * row's commit (that row must carry a `from`), else the run-start base; any of them not an object id is unreadable.
274
+ * No marker: undefined (legacy-ref). Resume and approve both read it.
275
+ */
276
+ export declare function integrationAnchor(events: readonly JournalEvent[]): {
277
+ anchor: string;
278
+ } | {
279
+ unreadable: string;
280
+ } | undefined;
281
+ /** The ref legs' details: the leg, both commits, the symbolic target and the repair text. */
282
+ export declare function identityDetails(leg: IntegrationLeg, state: IntegrationRefState, branch: string, runId: string, expected: string | undefined): string;
283
+ /**
284
+ * The one closed outcome row for every identity leg: an integration `tip-verify-failed` (preceded by a start row
285
+ * with no gates when this close appended none), which runEndTipProof and every consumer already read as failure.
286
+ */
287
+ export declare function appendIdentityFailure(journal: Journal, f: {
288
+ started: boolean;
289
+ branch: string;
290
+ expected?: string;
291
+ leg: IdentityLeg;
292
+ details: string;
293
+ state?: IntegrationRefState;
294
+ cmdHash: string;
295
+ capacity: RunCapacity;
296
+ infra?: boolean;
297
+ }): void;
298
+ /** O: I(expected) — or, with no expected value, the ref must be regular and its commit becomes the expected. */
299
+ export declare function openingIdentity(identity: TipIdentity, journal: Journal): Promise<{
300
+ ok: true;
301
+ expected: string;
302
+ } | {
303
+ ok: false;
304
+ leg: IntegrationLeg;
305
+ state: IntegrationRefState;
306
+ expected?: string;
307
+ details: string;
308
+ }>;
249
309
  /**
250
310
  * OBS-34's strict tip verify, but it stops re-paying for an unmoved tip (~334m corpus-wide; 69.5m in
251
311
  * one park-heavy run whose 18 resume cycles merged nothing new). The verify journals the SHA it
252
312
  * verified and the hash of the command set, so a later run-end can recognize the same verified state:
253
- * head equals the LAST green verified SHA, commands unchanged, tree clean → journal
254
- * `tip-verify-cached` and skip. ANY doubt — moved head, changed commands, dirty tree, a gate missing
255
- * from that cycle's green set, a failure recorded in it, a cycle older than the last one — runs the
256
- * full verify. The tip-verify-before-green law is untouched: a cached green is a verified green OF
257
- * THAT EXACT COMMIT, established by the most recent real run of the same commands.
258
- * Returns whether the tip is failing.
313
+ * the expected commit equals the LAST green verified SHA and the commands are unchanged → journal
314
+ * `tip-verify-cached` and skip, with no checkout. ANY doubt — moved tip, changed commands, a gate
315
+ * missing from that cycle's green set, a failure recorded in it, a cycle older than the last one —
316
+ * runs the full verify. 2.8.1 T1 (ANL-023 §7.1): with `identity` (the daemon always passes it) run end
317
+ * verifies a fresh detached checkout of the expected commit under a validated HEAD reflog, never the
318
+ * integration checkout; it checks tracked content after the battery (the battery-end check also proves
319
+ * the checkout's HEAD and its reflog unchanged), defers verdict-store writes to the caller, and the
320
+ * caller closes on an identity check taken after the approval serialization with no await before the
321
+ * run-end row. The opening check reads the ref before the cycle decision, so a cached green is carried
322
+ * only for the commit the ref holds now. A caller without `identity` keeps the integration checkout's
323
+ * head and clean tree as the cache key, exactly as before.
324
+ * Returns whether the tip is failing (without `identity`) or the close's TipClose (with it).
259
325
  */
260
- export declare function verifyIntegrationTipCached(intWt: string, commands: Record<string, string>, journal: Journal, opts?: {
261
- lastMergedTask?: string;
262
- baseline?: Baseline;
263
- signal?: AbortSignal;
264
- evidence?: GateEvidenceOptions;
265
- }): Promise<boolean>;
326
+ export declare function verifyIntegrationTipCached(intWt: string, commands: Record<string, string>, journal: Journal, opts: TipVerifyOptions & {
327
+ identity: TipIdentity;
328
+ }): Promise<TipClose>;
329
+ export declare function verifyIntegrationTipCached(intWt: string, commands: Record<string, string>, journal: Journal, opts?: TipVerifyOptions): Promise<boolean>;
266
330
  type WorkerProcessTree = "empty" | "running" | "unmeasurable";
267
331
  export declare function observeWorkerProcessTree(marker: string, cwd: string): Promise<WorkerProcessTree>;
268
332
  type SuitePidProbe = (pid: number) => number | undefined;