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.
package/dist/run/git.d.ts CHANGED
@@ -198,16 +198,29 @@ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions):
198
198
  * A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
199
199
  * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
200
200
  */
201
- export declare function addDetachedWorktree(cwd: string, dir: string, commitish: string, timeoutMs?: number): Promise<ShResult>;
201
+ export declare function addDetachedWorktree(cwd: string, dir: string, commitish: string, timeoutMs?: number, { logHead }?: {
202
+ logHead?: boolean | undefined;
203
+ }): Promise<ShResult>;
204
+ /**
205
+ * 2.8.1 T1 (ANL-023 §7.1): the reflog witness W(dir, ref) — read-only, the newest entry first. It is valid for a commit
206
+ * x only when the exit is 0, at least one line came back and the first is x: exit 0 with no lines is git's answer for
207
+ * a missing or unreadable log, so an empty witness is a refusal, never an accepted empty.
208
+ */
209
+ export declare function reflogWitness(dir: string, ref: string): Promise<{
210
+ code: number;
211
+ lines: string[];
212
+ }>;
202
213
  export declare function npmDependencyManifestChanged(cwd: string, baselineRef: string): Promise<boolean>;
203
214
  export declare function npmDependencyInstallCommand(cwd: string): string;
204
215
  export declare function gitHead(cwd: string): Promise<string>;
205
216
  export declare const sanitizeBranch: (branch: string) => string;
206
217
  export declare const WORKTREES_DIR = "worktrees.noindex";
207
218
  export declare const worktreePath: (repo: string, branch: string) => string;
208
- /** OBS-28: remove this run's recorded worktrees; tolerates already-gone paths. */
219
+ /**
220
+ * OBS-28: remove this run's recorded task worktrees; tolerates already-gone paths. 2.8.1 T2: a run keeps no
221
+ * integration checkout, and a 2.8.0 run's legacy one is never removed here (it may hold the only view of a merge).
222
+ */
209
223
  export declare function cleanupRunWorktrees(repo: string, branch: string, opts: {
210
- removeIntegration: boolean;
211
224
  removeTaskIds: string[];
212
225
  }): Promise<void>;
213
226
  export declare function resolveIntegrationBranch(_repo: string, branch: string): Promise<string>;
package/dist/run/git.js CHANGED
@@ -607,7 +607,10 @@ export async function shGitOk(cmd, cwd, own = {}) {
607
607
  * A step raced by a commondir change is refused by name before the next one runs, and what the add created is rolled
608
608
  * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
609
609
  */
610
- export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
610
+ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, { logHead = false } = {}) {
611
+ // 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
612
+ // core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
613
+ const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
611
614
  // the authority this operation keeps, rollback included: derived once from the trusted layout, never a commondir
612
615
  const store = trustedCommonDir(cwd);
613
616
  const resolved = await shGit(`git rev-parse --verify --end-of-options ${shq(`${commitish}^{commit}`)}`, cwd, timeoutMs);
@@ -616,12 +619,12 @@ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAU
616
619
  const oid = resolved.stdout.trim();
617
620
  // under a ref payload git also resolves its new-HEAD stub into a shared refs/heads/.invalid; it is deleted again
618
621
  const rollback = { dir, branch: { name: ".invalid" } };
619
- const added = await shellEntry(`git worktree add --no-checkout --detach ${shq(dir)} ${shq(oid)}`, cwd, timeoutMs, false, {}, { createsWorktree: "detached", rollback });
622
+ const added = await shellEntry(`${git} worktree add --no-checkout --detach ${shq(dir)} ${shq(oid)}`, cwd, timeoutMs, false, {}, { createsWorktree: "detached", rollback });
620
623
  if (added.code !== 0)
621
624
  return added;
622
625
  let repaired;
623
626
  try {
624
- repaired = await shGit(`git update-ref --no-deref HEAD ${shq(oid)} && git update-ref -d refs/heads/.invalid && git reset -q --hard --no-recurse-submodules`, dir, timeoutMs);
627
+ repaired = await shGit(`${git} update-ref --no-deref HEAD ${shq(oid)} && ${git} update-ref -d refs/heads/.invalid && ${git} reset -q --hard --no-recurse-submodules`, dir, timeoutMs);
625
628
  }
626
629
  catch (error) {
627
630
  await dropWorktree(store, rollback, timeoutMs).catch((failure) => { throw rollbackFailed(error, failure); });
@@ -631,6 +634,15 @@ export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAU
631
634
  await dropWorktree(store, rollback, timeoutMs);
632
635
  return repaired;
633
636
  }
637
+ /**
638
+ * 2.8.1 T1 (ANL-023 §7.1): the reflog witness W(dir, ref) — read-only, the newest entry first. It is valid for a commit
639
+ * x only when the exit is 0, at least one line came back and the first is x: exit 0 with no lines is git's answer for
640
+ * a missing or unreadable log, so an empty witness is a refusal, never an accepted empty.
641
+ */
642
+ export async function reflogWitness(dir, ref) {
643
+ const r = await shGit(`git reflog show --no-abbrev --format=%H ${shq(ref)}`, dir);
644
+ return { code: r.code, lines: r.stdout.split("\n").map((line) => line.trim()).filter(Boolean) };
645
+ }
634
646
  /** A failed rollback is never swallowed: it rides on the error the operation already throws (a refusal stays one). */
635
647
  function rollbackFailed(error, failure) {
636
648
  const why = `; its rollback failed, so what it created may remain: ${failure instanceof Error ? failure.message : String(failure)}`;
@@ -738,10 +750,11 @@ const sanitize = sanitizeBranch;
738
750
  // once after this rename — the same one-time per-install cost as the v1.38 state-dir rename.
739
751
  export const WORKTREES_DIR = "worktrees.noindex";
740
752
  export const worktreePath = (repo, branch) => join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
741
- /** OBS-28: remove this run's recorded worktrees; tolerates already-gone paths. */
753
+ /**
754
+ * OBS-28: remove this run's recorded task worktrees; tolerates already-gone paths. 2.8.1 T2: a run keeps no
755
+ * integration checkout, and a 2.8.0 run's legacy one is never removed here (it may hold the only view of a merge).
756
+ */
742
757
  export async function cleanupRunWorktrees(repo, branch, opts) {
743
- if (opts.removeIntegration)
744
- await removeWorktree(repo, worktreePath(repo, branch));
745
758
  for (const id of opts.removeTaskIds)
746
759
  await removeWorktree(repo, worktreePath(repo, `${branch}--${id}`));
747
760
  }
@@ -1,5 +1,6 @@
1
1
  import type { TickmarkrConfig } from "../config/config.js";
2
2
  import { type Baseline, type BaselineProvenance, type GateEvidenceOptions, type FailureClassification } from "../gates/baseline.js";
3
+ import { type VerificationIdentity } from "../gates/cache.js";
3
4
  import type { GateEvidenceReceipt } from "./protocol.js";
4
5
  export interface TipVerifyResult {
5
6
  evidenceReceipt?: GateEvidenceReceipt;
@@ -30,18 +31,124 @@ export interface TipVerifyResult {
30
31
  * for a kill, the shared runner classifier for everything else. `infra` means nothing was verified.
31
32
  */
32
33
  cause?: FailureClassification;
34
+ /** 2.8.1 T1: with `deferVerdicts`, the verdict-store write this result would have made, left to the caller. */
35
+ deferredVerdict?: {
36
+ id: VerificationIdentity;
37
+ verdict: TipVerifyResult & {
38
+ meta: Record<string, unknown>;
39
+ };
40
+ };
33
41
  }
34
42
  /** Carry execution evidence without minting an invocation or rebasing its artifact paths. */
35
43
  export declare function reusedTipEvidence(source: Partial<TipVerifyResult>): Partial<TipVerifyResult>;
36
44
  export declare function integrationBranch(cfg: TickmarkrConfig, runId: string): string;
45
+ /**
46
+ * 2.8.1 T2 (ANL-023 §7.2): creates the integration ref create-only at `baseRef` (the empty old value means "must not
47
+ * exist") and returns the published tip E. The run keeps no checkout of the branch: every later publication is a
48
+ * compare-and-swap of the ref from `repo` (mergeTask). An existing or uncreatable ref throws — the fatal path's text.
49
+ */
37
50
  export declare function ensureIntegration(repo: string, branch: string, baseRef: string): Promise<string>;
38
- export declare function integrationHead(intWt: string): Promise<string>;
39
- export declare function mergeTask(intWt: string, taskBranch: string, message: string, gatedCommit: string): Promise<{
40
- ok: boolean;
41
- conflict?: string;
42
- tipMoved?: {
51
+ /** 2.8.1 T1 (ANL-023 §7.1): the closed states of the integration ref, as R(ref) reads them. */
52
+ export type IntegrationRefState = {
53
+ kind: "regular";
54
+ commit: string;
55
+ } | {
56
+ kind: "missing";
57
+ } | {
58
+ kind: "symbolic";
59
+ target: string;
60
+ commit?: string;
61
+ } | {
62
+ kind: "unreadable";
63
+ read: string;
64
+ };
65
+ export type IntegrationLeg = "symbolic" | "missing" | "value" | "unreadable";
66
+ /**
67
+ * R(ref): the integration ref read from `repo` through the own-git runner, never its reflog. The exact ref only —
68
+ * a descendant ref (`refs/heads/<branch>/x`) is not this ref, and a ref that exists but names no commit is unreadable.
69
+ */
70
+ export declare function readIntegrationRef(repo: string, branch: string): Promise<IntegrationRefState>;
71
+ /** I(x): holds iff the ref is regular at x; otherwise the leg, with the state the repair text is chosen by. */
72
+ export declare function checkIntegrationIdentity(repo: string, branch: string, expected: string): Promise<{
73
+ ok: true;
74
+ state: IntegrationRefState;
75
+ } | {
76
+ ok: false;
77
+ leg: IntegrationLeg;
78
+ state: IntegrationRefState;
79
+ }>;
80
+ /**
81
+ * The repair text (shared by run end's failure row, the merge park and the resume refusal): backticked commands run
82
+ * from the repository root in order, stopping at the first that fails. A found commit is preserved create-only under
83
+ * refs/tickmarkr/found/<runId>/<F> before a no-deref compare-and-swap restores the ref; a symbolic found ref or one
84
+ * already holding another commit stops the sequence before the restore. Dangling and unreadable print no command.
85
+ */
86
+ export declare function integrationRepairText(state: IntegrationRefState, branch: string, runId: string, expected: string): string;
87
+ type GitResult = {
88
+ code: number;
89
+ stdout: string;
90
+ stderr: string;
91
+ };
92
+ /** The merge's git steps after the identity check, in order; `publish` is the compare-and-swap. */
93
+ export type MergeStep = "ancestry" | "merge-tree" | "commit-tree" | "publish";
94
+ /** Test-only: runs before each step; a returned result replaces that step's git result (never `publish`'s). */
95
+ export type MergeSeam = (step: MergeStep) => Promise<GitResult | void> | GitResult | void;
96
+ export interface IntegrationMove {
97
+ branch: string;
98
+ expected: string;
99
+ found: string | null;
100
+ leg: IntegrationLeg;
101
+ target?: string;
102
+ stage: "check" | "publish";
103
+ }
104
+ export type MergeOutcome = {
105
+ ok: true;
106
+ commit: string;
107
+ from: string;
108
+ } | {
109
+ ok: true;
110
+ alreadyMerged: string;
111
+ } | {
112
+ ok: false;
113
+ tipMoved: {
43
114
  gatedCommit: string;
44
115
  branchTip: string;
45
116
  };
46
- }>;
47
- export declare function verifyIntegrationTip(intWt: string, commands: Record<string, string>, runDir: string, baseline?: Baseline, evidenceOptions?: GateEvidenceOptions): Promise<TipVerifyResult[]>;
117
+ } | {
118
+ ok: false;
119
+ conflict: string;
120
+ } | {
121
+ ok: false;
122
+ integrationMoved: IntegrationMove;
123
+ state: IntegrationRefState;
124
+ } | {
125
+ ok: false;
126
+ alreadyIntegrated: {
127
+ gated: string;
128
+ tip: string;
129
+ };
130
+ } | {
131
+ ok: false;
132
+ infra: {
133
+ step: MergeStep;
134
+ message: string;
135
+ };
136
+ };
137
+ /**
138
+ * 2.8.1 T2 (ANL-023 §7.2): publishes `gated` onto the integration ref by compare-and-swap from the stored tip
139
+ * `expected`, all git from `repo` — no checkout. (1) the task-tip check; (2) I(expected); (3) ancestry; (4) merge-tree;
140
+ * (5) commit-tree; (6) `update-ref --no-deref` from `expected`; (7) `onPublished(M)` in the same synchronous
141
+ * continuation as (6)'s result, so the caller's rows land in publication order. `merged` is the task's recorded merge
142
+ * commits, newest first: a gated commit already in the history is done only through its own recorded merge.
143
+ * `ancestorOnly` (a replay that ran no gates): a gated commit not in E is refused `infra`, never published.
144
+ */
145
+ export declare function mergeTask(repo: string, { branch, expected, merged, ancestorOnly }: {
146
+ branch: string;
147
+ expected: string;
148
+ merged?: readonly string[];
149
+ ancestorOnly?: boolean;
150
+ }, taskBranch: string, message: string, gated: string, onPublished: (commit: string) => void, seam?: MergeSeam): Promise<MergeOutcome>;
151
+ export declare function verifyIntegrationTip(intWt: string, commands: Record<string, string>, runDir: string, baseline?: Baseline, evidenceOptions?: GateEvidenceOptions, { deferVerdicts }?: {
152
+ deferVerdicts?: boolean;
153
+ }): Promise<TipVerifyResult[]>;
154
+ export {};
package/dist/run/merge.js CHANGED
@@ -4,8 +4,7 @@ import { shq } from "../adapters/types.js";
4
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
- import { tickmarkrDir } from "../graph/graph.js";
8
- import { dependencyLinkRefusal, describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
7
+ import { dependencyLinkRefusal, describeCapacity, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit } from "./git.js";
9
8
  import { executionSignal } from "./execution-budget.js";
10
9
  // Diagnostic convenience logs are observational, just like receipt persistence.
11
10
  function writeTipLog(path, text) {
@@ -32,42 +31,131 @@ export function reusedTipEvidence(source) {
32
31
  export function integrationBranch(cfg, runId) {
33
32
  return `${cfg.integrationBranchPrefix}${runId}`;
34
33
  }
35
- const sanitize = (branch) => branch.replace(/[^\w.-]+/g, "-");
34
+ /**
35
+ * 2.8.1 T2 (ANL-023 §7.2): creates the integration ref create-only at `baseRef` (the empty old value means "must not
36
+ * exist") and returns the published tip E. The run keeps no checkout of the branch: every later publication is a
37
+ * compare-and-swap of the ref from `repo` (mergeTask). An existing or uncreatable ref throws — the fatal path's text.
38
+ */
36
39
  export async function ensureIntegration(repo, branch, baseRef) {
37
40
  branch = await resolveIntegrationBranch(repo, branch);
38
- const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
39
- if (!existsSync(join(dir, ".git"))) {
40
- const exists = (await shGit(`git rev-parse --verify refs/heads/${shq(branch)}`, repo)).code === 0;
41
- if (exists) {
42
- await shGitOk(`git worktree add ${shq(dir)} ${shq(branch)}`, repo, { createsWorktree: "branch", rollback: { dir } });
43
- }
44
- else {
45
- await shGitOk(`git worktree add -b ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo, { createsWorktree: "branch", rollback: { dir, branch: { name: branch } } });
46
- }
47
- }
48
- linkNodeModules(repo, dir);
49
- return dir;
41
+ // ponytail: no --create-reflog (ANL-023 §7.2 names it): the repository's own core.logAllRefUpdates decides, because
42
+ // nothing reads this reflog (X10) and run-end-integration's E-P10 builds its no-reflog legacy branch through a fresh
43
+ // run with logging disabled. Add the flag back once that fixture builds its legacy branch by hand.
44
+ const r = await shGit(`git update-ref --no-deref -m ${shq("tickmarkr: run start")} ${shq(`refs/heads/${branch}`)} ${shq(baseRef)} ""`, repo);
45
+ if (r.code !== 0)
46
+ throw new Error(`integration branch ${branch} already exists or cannot be created: ${(r.stderr || r.stdout).trim()}`);
47
+ return baseRef;
48
+ }
49
+ /**
50
+ * R(ref): the integration ref read from `repo` through the own-git runner, never its reflog. The exact ref only —
51
+ * a descendant ref (`refs/heads/<branch>/x`) is not this ref, and a ref that exists but names no commit is unreadable.
52
+ */
53
+ export async function readIntegrationRef(repo, branch) {
54
+ const ref = `refs/heads/${branch}`;
55
+ const unreadable = (cmd, r) => ({ kind: "unreadable", read: `${cmd} exited ${r.code}${(r.stderr || r.stdout).trim() ? `: ${(r.stderr || r.stdout).trim()}` : ""}` });
56
+ const symbolic = await shGit(`git symbolic-ref -q ${shq(ref)}`, repo);
57
+ if (symbolic.code !== 0 && symbolic.code !== 1)
58
+ return unreadable("git symbolic-ref", symbolic);
59
+ const commit = await shGit(`git rev-parse --verify -q ${shq(`${ref}^{commit}`)}`, repo);
60
+ if (commit.code !== 0 && commit.code !== 1)
61
+ return unreadable("git rev-parse", commit);
62
+ const found = commit.code === 0 ? commit.stdout.trim() : undefined;
63
+ if (symbolic.code === 0)
64
+ return { kind: "symbolic", target: symbolic.stdout.trim(), ...(found ? { commit: found } : {}) };
65
+ if (found)
66
+ return { kind: "regular", commit: found };
67
+ const exists = await shGit(`git show-ref --verify -q ${shq(ref)}`, repo);
68
+ return exists.code === 1 ? { kind: "missing" } : unreadable("git show-ref", exists);
50
69
  }
51
- export function integrationHead(intWt) {
52
- return gitHead(intWt);
70
+ /** I(x): holds iff the ref is regular at x; otherwise the leg, with the state the repair text is chosen by. */
71
+ export async function checkIntegrationIdentity(repo, branch, expected) {
72
+ const state = await readIntegrationRef(repo, branch);
73
+ if (state.kind === "regular" && state.commit === expected)
74
+ return { ok: true, state };
75
+ return { ok: false, leg: state.kind === "regular" ? "value" : state.kind, state };
53
76
  }
54
- export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
55
- const tip = await shGit(`git rev-parse --verify ${shq(`refs/heads/${taskBranch}`)}`, intWt);
77
+ /**
78
+ * The repair text (shared by run end's failure row, the merge park and the resume refusal): backticked commands run
79
+ * from the repository root in order, stopping at the first that fails. A found commit is preserved create-only under
80
+ * refs/tickmarkr/found/<runId>/<F> before a no-deref compare-and-swap restores the ref; a symbolic found ref or one
81
+ * already holding another commit stops the sequence before the restore. Dangling and unreadable print no command.
82
+ */
83
+ export function integrationRepairText(state, branch, runId, expected) {
84
+ const ref = `refs/heads/${branch}`;
85
+ const byHand = `inspect ${ref} and restore it to ${expected} by hand`;
86
+ const found = state.kind === "regular" ? state.commit : state.kind === "symbolic" ? state.commit : undefined;
87
+ if (found) {
88
+ const keep = `refs/tickmarkr/found/${runId}/${found}`;
89
+ return `\`test -z "$(git symbolic-ref -q ${keep})"\` then \`test "$(git rev-parse -q --verify ${keep})" = ${found} || git update-ref --no-deref ${keep} ${found} ""\` then \`git update-ref --no-deref ${ref} ${expected} ${found}\``;
90
+ }
91
+ if (state.kind === "missing")
92
+ return `\`git update-ref --no-deref ${ref} ${expected} ""\``;
93
+ if (state.kind === "symbolic")
94
+ return `symbolic to ${state.target}, which names no commit; ${byHand}`;
95
+ return state.kind === "unreadable" ? `${ref} is unreadable (${state.read}); ${byHand}` : byHand;
96
+ }
97
+ /**
98
+ * 2.8.1 T2 (ANL-023 §7.2): publishes `gated` onto the integration ref by compare-and-swap from the stored tip
99
+ * `expected`, all git from `repo` — no checkout. (1) the task-tip check; (2) I(expected); (3) ancestry; (4) merge-tree;
100
+ * (5) commit-tree; (6) `update-ref --no-deref` from `expected`; (7) `onPublished(M)` in the same synchronous
101
+ * continuation as (6)'s result, so the caller's rows land in publication order. `merged` is the task's recorded merge
102
+ * commits, newest first: a gated commit already in the history is done only through its own recorded merge.
103
+ * `ancestorOnly` (a replay that ran no gates): a gated commit not in E is refused `infra`, never published.
104
+ */
105
+ export async function mergeTask(repo, { branch, expected, merged = [], ancestorOnly = false }, taskBranch, message, gated, onPublished, seam) {
106
+ const tip = await shGit(`git rev-parse --verify ${shq(`refs/heads/${taskBranch}`)}`, repo);
56
107
  if (tip.code !== 0)
57
108
  return { ok: false, conflict: tip.stderr || tip.stdout };
58
109
  const branchTip = tip.stdout.trim();
59
- if (branchTip !== gatedCommit)
60
- return { ok: false, tipMoved: { gatedCommit, branchTip } };
61
- // Merge the verified hash, not the mutable branch name: a move after the comparison cannot land ungated content.
62
- const r = await shGit(`git merge --no-ff ${shq(gatedCommit)} -m ${shq(message)}`, intWt);
63
- if (r.code === 0)
64
- return { ok: true };
65
- const conflict = (await shGit("git status --porcelain", intWt)).stdout
66
- .split("\n")
67
- .filter((l) => l.startsWith("UU") || l.startsWith("AA"))
68
- .join("\n") || r.stderr || r.stdout;
69
- await shGit("git merge --abort", intWt);
70
- return { ok: false, conflict };
110
+ if (branchTip !== gated)
111
+ return { ok: false, tipMoved: { gatedCommit: gated, branchTip } };
112
+ const moved = (leg, state, stage) => ({
113
+ ok: false, state, integrationMoved: { branch, expected, leg, stage,
114
+ found: (state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined) ?? null,
115
+ ...(state.kind === "symbolic" ? { target: state.target } : {}) },
116
+ });
117
+ const infra = (step, r) => ({ ok: false, infra: { step, message: (r.stderr || r.stdout).trim() || `exit ${r.code}` } });
118
+ const run = async (step, cmd) => (await seam?.(step)) || shGit(cmd, repo);
119
+ const held = await checkIntegrationIdentity(repo, branch, expected);
120
+ if (!held.ok)
121
+ return moved(held.leg, held.state, "check");
122
+ const ancestry = await run("ancestry", `git merge-base --is-ancestor ${shq(gated)} ${shq(expected)}`);
123
+ if (ancestry.code === 0) {
124
+ // Ancestor or equal tip: only the task's own recorded merge (second parent = gated) makes it done. A resume replay
125
+ // hands the task's own gated commit here, never a tip recreated on E (runDaemon's resume gate replay).
126
+ for (const commit of merged) {
127
+ const second = await shGit(`git rev-parse --verify -q ${shq(`${commit}^2`)}`, repo);
128
+ if (second.code !== 0)
129
+ break; // a failed read (no second parent, no such object) is never passed over: fail closed
130
+ if (second.stdout.trim() === gated)
131
+ return { ok: true, alreadyMerged: commit };
132
+ }
133
+ return { ok: false, alreadyIntegrated: { gated, tip: expected } };
134
+ }
135
+ if (ancestry.code !== 1)
136
+ return infra("ancestry", ancestry);
137
+ // A caller that skipped the gates on its own ancestry proof (a resume replay) never reaches construction.
138
+ if (ancestorOnly)
139
+ return infra("ancestry", { code: 1, stdout: "", stderr: `${gated} is not in ${expected}; an ungated replay publishes nothing` });
140
+ const tree = await run("merge-tree", `git merge-tree --write-tree ${shq(expected)} ${shq(gated)}`);
141
+ if (tree.code === 1)
142
+ return { ok: false, conflict: (tree.stdout || tree.stderr).trim() };
143
+ if (tree.code !== 0)
144
+ return infra("merge-tree", tree);
145
+ const made = await run("commit-tree", `git commit-tree ${shq(tree.stdout.split("\n")[0].trim())} -p ${shq(expected)} -p ${shq(gated)} -m ${shq(message)}`);
146
+ if (made.code !== 0)
147
+ return infra("commit-tree", made);
148
+ const commit = made.stdout.trim();
149
+ await seam?.("publish");
150
+ const published = await shGit(`git update-ref --no-deref -m ${shq(message)} ${shq(`refs/heads/${branch}`)} ${shq(commit)} ${shq(expected)}`, repo);
151
+ if (published.code === 0) {
152
+ onPublished(commit); // no await between the compare-and-swap's result and the caller's rows
153
+ return { ok: true, commit, from: expected };
154
+ }
155
+ const state = await readIntegrationRef(repo, branch);
156
+ if (state.kind === "regular" && state.commit === expected)
157
+ return infra("publish", published);
158
+ return moved(state.kind === "regular" ? "value" : state.kind, state, "publish");
71
159
  }
72
160
  // OBS-34 ruled strict exit-code verify; Q121s (TRIAL T-OBS-1) narrows it: a red whose failure
73
161
  // fingerprints are ALL recorded in the run's baseline is forgiven with the battery's own math
@@ -75,7 +163,7 @@ export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
75
163
  // terminus unreachable by construction and misattributed the red to the last merged task.
76
164
  // Fail-closed edges kept: no baseline → strict; baseline green for that gate → strict; any fresh
77
165
  // fingerprint, or output with no recognizable failure shape, → failed.
78
- export async function verifyIntegrationTip(intWt, commands, runDir, baseline, evidenceOptions = {}) {
166
+ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, evidenceOptions = {}, { deferVerdicts = false } = {}) {
79
167
  const results = [];
80
168
  let runStartCommands;
81
169
  let journalFound = false;
@@ -414,7 +502,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
414
502
  }
415
503
  const id = pending.get(result.gate);
416
504
  if (id && id.tree === completedTree && !isInfraResult(result)) {
417
- store.set(id, { ...result, meta: {
505
+ const verdict = { ...result, meta: {
418
506
  fingerprints: result.fingerprints, forgiven: result.forgiven, baselineProvenance: result.baselineProvenance,
419
507
  ...(result.forgiven && result.gate === "test" ? { killTextRead: true } : {}),
420
508
  reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
@@ -422,7 +510,12 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline, ev
422
510
  evidenceReceipt: result.evidenceReceipt, evidenceReceipts: result.evidenceReceipts,
423
511
  evidenceAbsence: result.evidenceAbsence, originRunRoot: result.originRunRoot,
424
512
  nonce: result.nonce, stdoutPath: result.stdoutPath, stderrPath: result.stderrPath,
425
- } });
513
+ } };
514
+ // 2.8.1 T1: a deferring caller (run end) writes the verdict itself, only after its closing identity check.
515
+ if (deferVerdicts)
516
+ result.deferredVerdict = { id, verdict };
517
+ else
518
+ store.set(id, verdict);
426
519
  }
427
520
  }
428
521
  return results;
@@ -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.0",
3
+ "version": "2.8.2",
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",
@@ -503,6 +503,14 @@ journal tail to decide what happens next, or sweeping orphans — you have taken
503
503
  - **the INTEGRATION subject** — the integration-branch commit that worktree was created from, named by
504
504
  sha from the journal's `task-dispatch` row or `tickmarkr status`; never "the run", and never the
505
505
  branch name alone, which moves while you are reading it;
506
+ - **the operator view of that branch** — a live run has NO integration checkout: the daemon publishes
507
+ each merge onto `tickmarkr/<runId>` by compare-and-swap from the repository root. View it through a
508
+ detached snapshot (`git worktree add --detach <dir> tickmarkr/<runId>`), refreshed by removing it
509
+ (`git worktree remove <dir>`) and re-adding it; check the branch out attached only with the daemon
510
+ stopped (no live `graph.lock`) — an attached checkout taken while the run is live goes stale at the next
511
+ publication, and any write to the ref parks the next merge infra. Run `tickmarkr verify` and `report`
512
+ from the main checkout or a snapshot;
513
+ a legacy attached view from a 2.8.0 run is stale from the first publication;
506
514
  - **and, where the fix is a FILE the tree must hold** — a new test, a fixture, a schema — its presence
507
515
  at that commit: `git -C <task-worktree> cat-file -e <task-head>:<path>`. A fix that lives only in the
508
516
  daemon's `dist` cannot make a worker's missing oracle appear, and a worker red on that missing file is
@@ -683,12 +691,13 @@ subject of a release proof is never *"the run"* and never *"the fix leg"* — it
683
691
  the one commit that carries the run's work AND every leg merged into it. Name that commit's sha in the
684
692
  proof, in the same act as the grade, and the proof is bound to that sha alone. **When the subject moves — a
685
693
  later merge, a new integration tip, an amended or re-cut commit, a fresh export, a re-tag — the proof is
686
- VOID, and the new subject is graded from scratch**, count oracle included, since the oracle is derived from
687
- the merged tree and not from either parent. Two greens assembled into one verdict are two claims about two
688
- trees that never contained each other: the union's own conflict resolutions, its ordering and its
689
- collected-test total are exactly what neither green observed — which is why `RELEASING.md` step 4 matches
690
- the run's head SHA to the mirror's `HEAD` before a single job log is graded. This is the void-condition duty
691
- 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.**
692
701
 
693
702
  **THE RECIPROCAL DUTY, and it is yours because you write both documents:** when you issue a ruling that
694
703
  re-scopes, narrows, splits or re-owns anything, **the ruling must name every sealed document its subject
@@ -1051,7 +1060,8 @@ they are left implicit:
1051
1060
  test exists — the acceptance oracle runs the sentence as vitest `-t` and a paraphrase matches zero tests.
1052
1061
  - Every fix-leg verify carries `--author <the seat that wrote it>`; without it the author is HUMAN, every
1053
1062
  vendor stays eligible, and a same-vendor approval is not the cross-vendor review the criterion names.
1054
- - 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.
1055
1065
  - A public-CI red is classified in a throttled Linux container first (`docker run --cpus=2 node:22`),
1056
1066
  by measurement: starvation on the base tree is not a defect on the branch.
1057
1067
 
@@ -1595,19 +1605,31 @@ Create specialist seats using `orca terminal create --worktree path:<repo> --com
1595
1605
  - **RUN BOTH RELEASE PROOFS BEFORE STANDING DOWN.** The first is the exported-tree suite, which
1596
1606
  maintainers run from the private repo with its `verify:export` script — that tooling is deliberately
1597
1607
  not part of this package, so the command does not exist in a public checkout; it must install,
1598
- build, lint, and only then run the suite in the exported tree; then
1599
- `TICKMARKR_E2E=1 npx vitest run tests/e2e/orca-smoke.e2e.test.ts` must exercise the installed-Orca
1600
- smoke. A skipped smoke is not proof. Record both results before declaring the release complete.
1601
-
1602
- - **GRADE PUBLIC CI FROM JOB LOGS AT RUN-END ONLY (law 48).** A badge is not the grade, and
1603
- `gh run view --job --log` is empty while the run is in progress. At run-end execute
1604
- `scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from every job of the
1605
- declared inventory it prints first — serial `test` and `test-macos`, or sharded, per OS, the four
1606
- `suite <os> k/4` shards, `single-fork <os>` and `aggregate <os>`; a missing, duplicated or unknown
1607
- job reads UNREADABLE, never a skip. Its UNREADABLE state is a hard stop, not a green default.
1608
- Derive the expected glob pathspec count with `git ls-files ':(glob)tests/**/*.test.ts'`
1609
- (including the `.tsx` sibling where present), never a bare
1610
- `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.
1611
1633
 
1612
1634
  - **RE-READ THE REGISTRY BEFORE RULING (law 51).** A successful Release run and the first
1613
1635
  `npm view` are separate observations: registry propagation can briefly return the prior version.