tickmarkr 2.8.0 → 2.8.2

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
@@ -5,6 +5,7 @@ import { allAdapters } from "../../adapters/registry.js";
5
5
  import { channelKey } from "../../adapters/types.js";
6
6
  import { loadConfig } from "../../config/config.js";
7
7
  import { integrationBranch } from "../../run/merge.js";
8
+ import { integrationAnchor } from "../../run/daemon.js";
8
9
  import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
9
10
  import { GATE_NAMES } from "../../graph/schema.js";
10
11
  import { applyScopeAmendments, bindingToken, captureOwedCheck, engagementComparable, engagementReleased, failedGateBeforePark, parseBindingToken, physicalLine, ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
@@ -168,6 +169,11 @@ export async function approve(argv, cwd = process.cwd()) {
168
169
  const serialization = await acquireApprovalSerialization(cwd, runId);
169
170
  try {
170
171
  const journal = Journal.open(cwd, runId);
172
+ // 2.8.1 T3 (ANL-023 §7.3): a marked run's recheck counts against its journal anchor; an unreadable one admits nothing.
173
+ const anchor = integrationAnchor(journal.read());
174
+ if (recheck && anchor && "unreadable" in anchor) {
175
+ throw new Error(`refusing to recheck ${taskId} in ${runId}: its journal names no readable published tip (${anchor.unreadable})`);
176
+ }
171
177
  const status = journal.replayStatuses().get(taskId);
172
178
  if (status === undefined) {
173
179
  throw new Error(`task ${taskId} has no events in run ${runId} — unknown task or never dispatched`);
@@ -308,12 +314,12 @@ export async function approve(argv, cwd = process.cwd()) {
308
314
  // daemon's boundary never observes the decision without its obligation (or a second write).
309
315
  // The seats a review discharge may resolve to are this repository's declared channels, recorded on
310
316
  // the row so every later fold reads the same table whatever process or checkout it runs in.
311
- let integrationRef = "unknown";
317
+ let byName = "unknown";
312
318
  let declared;
313
319
  let owedTask;
314
320
  try {
315
321
  const cfg = loadConfig(cwd);
316
- integrationRef = integrationBranch(cfg, runId);
322
+ byName = integrationBranch(cfg, runId);
317
323
  declared = allAdapters().flatMap((a) => a.channels(cfg)).map((c) => ({ key: channelKey(c), vendor: c.vendor }));
318
324
  }
319
325
  catch { /* an unreadable config binds no range and declares no seat: unknown */ }
@@ -321,11 +327,15 @@ export async function approve(argv, cwd = process.cwd()) {
321
327
  owedTask = loadGraph(cwd).tasks.find((t) => t.id === taskId);
322
328
  }
323
329
  catch { /* recorded as unknown criteria and scope */ }
324
- const obligation = captureOwedCheck({
330
+ const integration = integrationBase(journal.read(), () => byName);
331
+ const owed = captureOwedCheck({
325
332
  cwd, runId, taskId, gate: failedGate, events, acceptance: owedTask?.acceptance, files: owedTask?.files, declared,
326
333
  cause: reason ?? `operator waived failed ${failedGate} gate`, // the row's own park field binds the park
327
- taskRef: `${integrationRef}--${taskId}`, integrationRef,
334
+ taskRef: `${integration.branch}--${taskId}`, integrationRef: integration.base,
328
335
  });
336
+ // An unreadable anchor still records the waive, its obligation unknown: the debt is never silently known.
337
+ const obligation = integration.unreadable === undefined ? owed
338
+ : { ...owed, known: false, unknownReason: `its journal names no readable published tip (${integration.unreadable})` };
329
339
  journal.append("task-approved", taskId, {
330
340
  by,
331
341
  ...(reason ? { reason } : {}),
@@ -370,6 +380,20 @@ export async function approve(argv, cwd = process.cwd()) {
370
380
  serialization.release();
371
381
  }
372
382
  }
383
+ /**
384
+ * 2.8.1 T3 (ANL-023 §7.3): the integration branch and base a decision reads. A marked run (run-start
385
+ * `integrationAnchor: "journal"`) takes its branch from run-start, never the current config, and its base is the
386
+ * journal anchor A (`unreadable` when the journal names none); an unmarked run reads the branch by name, as before.
387
+ */
388
+ function integrationBase(events, byName) {
389
+ const anchor = integrationAnchor(events);
390
+ if (!anchor) {
391
+ const branch = byName();
392
+ return { branch, base: branch };
393
+ }
394
+ const branch = String(events.find((e) => e.event === "run-start").data.branch);
395
+ return "anchor" in anchor ? { branch, base: anchor.anchor } : { branch, base: "unknown", unreadable: anchor.unreadable };
396
+ }
373
397
  /** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
374
398
  function openDecision(journal, taskId) {
375
399
  const { events, sourceIndexes } = readJournalEvents(journal);
@@ -391,11 +415,12 @@ function requireBound(runId, taskId, bound, open, what) {
391
415
  * OBS-1022: what a failed task has landed. A preserved snapshot is authoritative only when it contains
392
416
  * the current task-branch tip; an older preservation can remain in the journal after a later retry has
393
417
  * advanced/rebuilt the branch. Missing task branches may still recover from a preserved ref. The selected
394
- * ref is counted against the run's integration branch; unreadable refs count as zero and fail closed.
418
+ * ref is counted against the run's integration base (integrationBase); unreadable refs count as zero and fail closed.
419
+ * `base` names the branch the text prints; the count reads `integration.base`.
395
420
  */
396
421
  function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
397
- const cfg = loadConfig(cwd);
398
- const base = integrationBranch(cfg, runId);
422
+ const integration = integrationBase(journal.read(), () => integrationBranch(loadConfig(cwd), runId));
423
+ const base = integration.branch;
399
424
  const taskRef = `${base}--${taskId}`;
400
425
  const preserved = [...journal.read()].reverse().find((e) => e.taskId === taskId && e.event === "worktree-preserved" && typeof e.data.ref === "string");
401
426
  const preservedRef = preserved?.data.ref;
@@ -416,7 +441,7 @@ function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
416
441
  }
417
442
  }
418
443
  try {
419
- const out = execFileSync("git", ["rev-list", "--count", `${base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
444
+ const out = execFileSync("git", ["rev-list", "--count", `${integration.base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
420
445
  return { ref, base, count: Number.parseInt(out.trim(), 10) || 0 };
421
446
  }
422
447
  catch {
@@ -13,7 +13,7 @@ import { runGates } from "../../gates/run-gates.js";
13
13
  import { getTask, loadGraph } from "../../graph/graph.js";
14
14
  import { GATE_NAMES } from "../../graph/schema.js";
15
15
  import { executionSignal } from "../../run/execution-budget.js";
16
- import { addDetachedWorktree, linkNodeModules, removeWorktree, shGit, shGitOk } from "../../run/git.js";
16
+ import { addDetachedWorktree, linkNodeModules, reflogWitness, removeWorktree, shGit, shGitOk } from "../../run/git.js";
17
17
  import { foldOwedChecks, integrationMapped, owedCriteria, Journal, OWED_DISCHARGE_EVENT } from "../../run/journal.js";
18
18
  import { describeLeaseWait, withRepositoryLease } from "../../run/lease.js";
19
19
  import { isPidLive } from "../../run/lock.js";
@@ -200,16 +200,24 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
200
200
  }
201
201
  const cfg = loadConfig(fileRoot("config.yaml"));
202
202
  const head = (await shGitOk("git rev-parse HEAD", cwd)).trim();
203
- // Git appends every move of HEAD to its reflog — one a gate command makes and undoes itself included —
204
- // so the log as of this capture is the subject's witness: any later difference is a move (fail closed).
205
- const headLogPath = resolve(cwd, (await shGitOk("git rev-parse --git-path logs/HEAD", cwd)).trim());
206
- const headLog = () => { try {
207
- return readFileSync(headLogPath, "utf8");
203
+ // The latch reads HEAD's reflog through git as a validated witness, refuses before capture when it
204
+ // is absent or invalid, and compares it line for line at every subject check.
205
+ const witnessFault = (witness, commit) => {
206
+ if (witness.code !== 0)
207
+ return `exit ${witness.code}`;
208
+ const newest = witness.lines[0];
209
+ if (newest === undefined)
210
+ return "no reflog";
211
+ if (newest !== commit)
212
+ return `newest is ${newest.slice(0, 12)}`;
213
+ return undefined;
214
+ };
215
+ const capturedWitness = await reflogWitness(cwd, "HEAD");
216
+ const fault = witnessFault(capturedWitness, head);
217
+ if (fault) {
218
+ throw new Error(`verify refused: HEAD's reflog cannot witness the candidate (${fault}) — record HEAD moves in this checkout, then verify again`);
208
219
  }
209
- catch {
210
- return "";
211
- } };
212
- const headLogAtCapture = headLog();
220
+ const headReflogAtCapture = capturedWitness.lines.join("\n");
213
221
  const baseTip = (await shGitOk(`git rev-parse '${values.base}'`, cwd).catch(() => {
214
222
  throw new Error(`--base ${values.base} is not a resolvable ref — pass --base <ref> naming the branch this diff targets`);
215
223
  })).trim();
@@ -501,9 +509,13 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
501
509
  let subjectLatch;
502
510
  const subjectChange = async () => {
503
511
  const now = await shGitOk("git rev-parse HEAD", cwd).then((out) => out.trim(), () => undefined);
512
+ // The latch reads HEAD's reflog through git as a validated witness, refuses before capture when it
513
+ // is absent or invalid, and compares it line for line at every subject check.
514
+ const witness = now === head ? await reflogWitness(cwd, "HEAD") : undefined;
515
+ const reflogMoved = witness !== undefined && (witness.code !== 0 || witness.lines.join("\n") !== headReflogAtCapture || witness.lines[0] !== head);
504
516
  const why = now === undefined ? "HEAD is unreadable"
505
517
  : now !== head ? `HEAD moved from ${head.slice(0, 12)} to ${now.slice(0, 12)}`
506
- : headLog() !== headLogAtCapture ? `HEAD moved and was restored to ${head.slice(0, 12)} (its reflog recorded the move)`
518
+ : reflogMoved ? `HEAD moved and was restored to ${head.slice(0, 12)} (its reflog recorded the move)`
507
519
  : await candidateDirt();
508
520
  if (why)
509
521
  subjectLatch ??= why;
@@ -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
@@ -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;
@@ -437,6 +437,49 @@ export async function judgeContradictions(worktree, head, fresh, priors) {
437
437
  }
438
438
  return found;
439
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
+ }
440
483
  export async function runGates(task, ctx) {
441
484
  const results = [];
442
485
  const evidence = {
@@ -1147,6 +1190,11 @@ export async function runGates(task, ctx) {
1147
1190
  // reused. Only a criterion that REVERSES the newest prior ruling on the same subject key over
1148
1191
  // identical cited blobs needs a second, distinct judge; agreement stands (a sound FAIL included),
1149
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.
1150
1198
  const adjudicate = async (fresh) => {
1151
1199
  const primary = String(fresh.meta?.judge ?? channelKey({ adapter: ctx.cfg.judge.adapter, model: ctx.cfg.judge.model }));
1152
1200
  const head = await shGit("git rev-parse HEAD", ctx.worktree);
@@ -1179,7 +1227,19 @@ export async function runGates(task, ctx) {
1179
1227
  const seatVia = via
1180
1228
  ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("judge", seatAdapter.id) + "-r2", label: via.labelFor("judge") }
1181
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);
1182
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);
1183
1243
  const rulings = Array.isArray(second.meta?.judgment) ? second.meta.judgment : undefined;
1184
1244
  // A citation-less ruling cannot be compared, so it confirms nothing (invented evidence is already unparseable,
1185
1245
  // and an internally inconsistent verdict carries no judgment rows at all).
@@ -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;