tickmarkr 2.8.4 → 2.8.6

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.
Files changed (51) hide show
  1. package/dist/adapters/claude-code.js +3 -2
  2. package/dist/adapters/codex-commit-check.d.ts +4 -3
  3. package/dist/adapters/codex-commit-check.js +44 -29
  4. package/dist/adapters/codex.d.ts +2 -2
  5. package/dist/adapters/codex.js +38 -32
  6. package/dist/adapters/cursor-agent.js +2 -1
  7. package/dist/adapters/grok.js +2 -1
  8. package/dist/adapters/kimi.js +11 -9
  9. package/dist/adapters/model-lints.js +4 -3
  10. package/dist/adapters/opencode.js +2 -1
  11. package/dist/adapters/pi.js +3 -2
  12. package/dist/adapters/qwen.js +2 -1
  13. package/dist/adapters/registry.js +2 -1
  14. package/dist/adapters/types.d.ts +10 -1
  15. package/dist/adapters/types.js +18 -3
  16. package/dist/cli/commands/approve.js +9 -28
  17. package/dist/cli/commands/doctor.d.ts +49 -5
  18. package/dist/cli/commands/doctor.js +956 -94
  19. package/dist/cli/commands/fleet.js +11 -3
  20. package/dist/cli/commands/init.js +27 -19
  21. package/dist/cli/commands/status.js +9 -6
  22. package/dist/cli/commands/verify.js +12 -3
  23. package/dist/compile/gsd.js +4 -3
  24. package/dist/compile/index.js +4 -1
  25. package/dist/compile/native.js +8 -6
  26. package/dist/config/config.d.ts +15 -0
  27. package/dist/config/config.js +67 -5
  28. package/dist/drivers/host-env.d.ts +3 -0
  29. package/dist/drivers/host-env.js +47 -0
  30. package/dist/drivers/orca.js +3 -1
  31. package/dist/drivers/subprocess.d.ts +1 -1
  32. package/dist/drivers/subprocess.js +4 -3
  33. package/dist/gates/baseline.d.ts +1 -1
  34. package/dist/gates/baseline.js +4 -3
  35. package/dist/gates/cache.js +2 -4
  36. package/dist/graph/schema.js +3 -1
  37. package/dist/run/daemon.js +54 -4
  38. package/dist/run/git-trust.d.ts +11 -0
  39. package/dist/run/git-trust.js +1 -1
  40. package/dist/run/git.d.ts +13 -1
  41. package/dist/run/git.js +34 -14
  42. package/dist/run/journal.d.ts +15 -1
  43. package/dist/run/journal.js +83 -17
  44. package/dist/run/lock.d.ts +5 -0
  45. package/dist/run/lock.js +202 -9
  46. package/dist/run/protocol.d.ts +1 -1
  47. package/dist/tui/cockpit/live-store.js +12 -2
  48. package/package.json +1 -1
  49. package/skills/tickmarkr-auto/SKILL.md +65 -21
  50. package/skills/tickmarkr-loop/SKILL.md +65 -21
  51. package/skills/tickmarkr-overseer/SKILL.md +88 -32
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execSync } from "node:child_process";
3
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
4
3
  import { dirname, join, relative, resolve } from "node:path";
5
4
  import { tmpdir } from "node:os";
6
5
  import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
7
6
  import { shq } from "../adapters/types.js";
7
+ import { trustedCommonDir } from "../run/git-trust.js";
8
8
  import { titleEnvironment } from "../run/title-environment.js";
9
9
  export const DEFAULT_VERDICT_CACHE_BOUND = 128;
10
10
  let testCacheBound;
@@ -269,9 +269,7 @@ export function resolveStateDir(worktree, artifactDir) {
269
269
  return join(worktree, ".tickmarkr");
270
270
  }
271
271
  try {
272
- const commonDir = execSync("git rev-parse --git-common-dir", { cwd: worktree, encoding: "utf8" }).trim();
273
- const resolved = resolve(worktree, commonDir);
274
- const commonRoot = realpathSync(dirname(resolved));
272
+ const commonRoot = realpathSync(dirname(trustedCommonDir(worktree)));
275
273
  if (existsSync(join(commonRoot, ".tickmarkr"))) {
276
274
  return join(commonRoot, ".tickmarkr");
277
275
  }
@@ -118,7 +118,9 @@ export const RunGraphSchema = z
118
118
  source: z.enum(SPEC_SOURCES),
119
119
  paths: z.array(z.string()),
120
120
  hash: z.string(),
121
- // Q11 compile half: an author-declared ref only. Runtime Git resolution/enforcement is later.
121
+ // Q11 / queue row 220: an author-declared ref, proven at run and resume start against the run's
122
+ // starting commit (HEAD for a run, the recorded run-start base for a resume) by ancestry or patch-id;
123
+ // drifted or unresolvable, the run or resume is refused before it writes anything. Never rebased onto.
122
124
  base: z.string().min(1).optional(),
123
125
  }),
124
126
  tasks: z.array(TaskSchema).min(1),
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
34
34
  import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
35
35
  import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
36
36
  import { runEnvironment } from "./environment.js";
37
- import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
37
+ import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
38
38
  import { MASK } from "./redact.js";
39
39
  import { runInteractiveSeed } from "./interactive-seed.js";
40
40
  import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
@@ -1145,6 +1145,48 @@ export function integrationAnchor(events) {
1145
1145
  return { unreadable: `the newest merge row's from is ${show(merge.data.from)}, not an object id` };
1146
1146
  return { anchor: merge.data.commit };
1147
1147
  }
1148
+ /**
1149
+ * Queue row 220 (D-2111): prove the spec's declared base against the commit this run starts from — the
1150
+ * checkout HEAD for a fresh run (recorded as its run-start baseRef), the recorded run-start baseRef for a
1151
+ * resume, never the checkout's current HEAD: a resume never re-bases, and no run rebases onto the declared
1152
+ * ref. Drift and an unresolvable answer throw; runDaemon calls this before it writes anything.
1153
+ */
1154
+ async function proveDeclaredBase(repoRoot, runId, ref, resume) {
1155
+ const action = resume ? `resume ${runId}` : "run";
1156
+ const remedy = `Remedy: check out a branch that contains ${ref}, or correct base: in the spec and recompile.`;
1157
+ const unresolvable = (against, why) => new Error(`refusing to ${action}: declared base ${ref} is unresolvable against ${against} — ${why}. ${remedy}`);
1158
+ let start;
1159
+ let against;
1160
+ if (resume) {
1161
+ const recorded = Journal.open(repoRoot, runId).read().find((e) => e.event === "run-start")?.data.baseRef;
1162
+ if (typeof recorded !== "string" || !recorded)
1163
+ throw unresolvable("the recorded run-start base", "the journal's run-start records no baseRef");
1164
+ start = recorded;
1165
+ against = `the recorded run-start base ${short(start)}`;
1166
+ }
1167
+ else {
1168
+ const head = await shGit("git rev-parse --verify HEAD^{commit}", repoRoot);
1169
+ if (head.code !== 0)
1170
+ throw unresolvable("HEAD", `git said for HEAD: ${(head.stderr || head.stdout).trim()}`);
1171
+ start = head.stdout.trim();
1172
+ against = `HEAD ${short(start)}`;
1173
+ }
1174
+ const probe = await declaredBaseContainment(repoRoot, ref, start);
1175
+ if (probe.result === "contained")
1176
+ return { ref, via: probe.via, start };
1177
+ if (probe.result === "unresolvable")
1178
+ throw unresolvable(against, `git said for ${probe.ref}: ${probe.evidence}`);
1179
+ const lines = await Promise.all(probe.missing.map(async (m) => {
1180
+ const subject = await shGit(`git log -1 --format=%s ${shq(m.commit)}`, repoRoot);
1181
+ // a drift refusal must name the subject: an unreadable one refuses as unresolvable, quoting git
1182
+ if (subject.code !== 0)
1183
+ throw unresolvable(against, `git said for missing commit ${short(m.commit)}: ${(subject.stderr || subject.stdout).trim()}`);
1184
+ return ` ${short(m.commit)} ${subject.stdout.trim()}${m.merge ? " (merge)" : ""}`
1185
+ + ` — ${m.paths.length > 0 ? m.paths.join(", ") : "no paths"}`;
1186
+ }));
1187
+ throw new Error(`refusing to ${action}: declared base ${ref} has drifted from ${against} — `
1188
+ + `${probe.missing.length} declared commit(s) missing:\n${lines.join("\n")}\n${remedy}`);
1189
+ }
1148
1190
  /** The ref legs' details: the leg, both commits, the symbolic target and the repair text. */
1149
1191
  export function identityDetails(leg, state, branch, runId, expected) {
1150
1192
  const found = state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined;
@@ -2105,6 +2147,12 @@ export async function runDaemon(repoRoot, opts = {}) {
2105
2147
  let baselineFailed = false;
2106
2148
  try {
2107
2149
  graph = loadGraph(repoRoot);
2150
+ // Queue row 220 (D-2111): a declared base is proven HERE — before any journal row (lock-reclaimed and
2151
+ // exit-cause included), the resume anchor checks, a worktree, the integration ref or baseline capture —
2152
+ // so a refusal leaves nothing; the finally releases the lock and repository claim. No base: unchanged.
2153
+ const declaredBase = graph.spec.base === undefined ? undefined
2154
+ : await proveDeclaredBase(repoRoot, runId, graph.spec.base, opts.resume === true);
2155
+ const declaredBaseRow = declaredBase ? { declaredBase: { ref: declaredBase.ref, via: declaredBase.via } } : {};
2108
2156
  // One bounded snapshot supplies every dispatch. On resume the current journal still participates
2109
2157
  // in the fold so its task-done/ordinary-approval events can retire older evidence, but findings it
2110
2158
  // produced are suppressed: same-run feedback already replays those bytes and must not deliver them
@@ -2943,6 +2991,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2943
2991
  ...(replayedExclusions.size > 0 ? { excludedChannels: [...replayedExclusions].sort() } : {}),
2944
2992
  ...(opts.retryFailed ? { retryFailed: true } : {}),
2945
2993
  ...(opts.graphChanged ? { graphChanged: true } : {}), // OBS-1073: the release is the engagement's
2994
+ ...declaredBaseRow,
2946
2995
  });
2947
2996
  runStarted = true;
2948
2997
  await placeBoard();
@@ -2993,7 +3042,7 @@ export async function runDaemon(repoRoot, opts = {}) {
2993
3042
  }
2994
3043
  }
2995
3044
  else {
2996
- baseRef = await gitHead(repoRoot);
3045
+ baseRef = declaredBase?.start ?? await gitHead(repoRoot); // row 220: the proven commit is the run's base
2997
3046
  journal.append("baseline-start", undefined, { baseRef, commands, capacity: resolvedCapacity() });
2998
3047
  await placeBoard();
2999
3048
  baselinePending = true;
@@ -3034,6 +3083,7 @@ export async function runDaemon(repoRoot, opts = {}) {
3034
3083
  branch, graphDefinitionHash: graphDefinitionHash(graph),
3035
3084
  integrationAnchor: "journal", // 2.8.1 T2: this run creates its branch at baseRef and journals every publication with `from`
3036
3085
  ...(prior ? { supersedes: prior.runId } : {}),
3086
+ ...declaredBaseRow,
3037
3087
  }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
3038
3088
  runStarted = true;
3039
3089
  // v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
@@ -5186,7 +5236,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5186
5236
  const icmd = retryMode === "resume"
5187
5237
  ? adapter.resumeCommand(sessionId, promptFile, assignment.model, assignment.effort)
5188
5238
  : cfg.visibility.worker === "interactive" && driver.interactive
5189
- ? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort)
5239
+ ? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort, { cwd: wt, branch: taskBranch, repoRoot })
5190
5240
  : null;
5191
5241
  // v1.69 T6: adapters that declare interactiveSeed launch the real TUI and inject the prompt as a
5192
5242
  // user turn; they do NOT need the argv-seeding surface that interactiveCommand represents.
@@ -5203,7 +5253,7 @@ export async function runDaemon(repoRoot, opts = {}) {
5203
5253
  // timing can never interleave a `$(…)` with what follows it (the codex corruption class).
5204
5254
  const workerCmd = interactive
5205
5255
  ? (hasSeed ? ":" : icmd)
5206
- : adapter.invoke(t, wt, assignment, { promptFile }).command;
5256
+ : adapter.invoke(t, wt, assignment, { promptFile, branch: taskBranch, repoRoot }).command;
5207
5257
  const dispatchScript = promptFile.replace(/\.md$/, ".sh");
5208
5258
  // Approval can reset the attempt counter while the old pane remains retained.
5209
5259
  // Its ownership claim must survive a new engagement reusing the script path.
@@ -4,11 +4,21 @@ export declare class GitTrustRefusal extends Error {
4
4
  readonly detail: string;
5
5
  constructor(path: string, detail: string, options?: ErrorOptions);
6
6
  }
7
+ interface Authority {
8
+ gitdir: string;
9
+ common: string;
10
+ linked: boolean;
11
+ }
7
12
  /**
8
13
  * The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
9
14
  * a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
10
15
  */
11
16
  export declare function trustedCommonDir(cwd: string): string;
17
+ /** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
18
+ export declare function checkedAuthority(cwd: string): (Authority & {
19
+ worktree: string;
20
+ stamp?: string;
21
+ }) | undefined;
12
22
  /**
13
23
  * The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
14
24
  * (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
@@ -119,3 +129,4 @@ export declare const INERT_HOOKS_PATH = "/dev/null";
119
129
  * children (shGit/shGitOk) receive it; payload shells (gate commands, suites) keep the operator's git config.
120
130
  */
121
131
  export declare function protectedGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
132
+ export {};
@@ -151,7 +151,7 @@ catch {
151
151
  return false;
152
152
  } };
153
153
  /** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
154
- function checkedAuthority(cwd) {
154
+ export function checkedAuthority(cwd) {
155
155
  const dotGit = dotGitOf(cwd);
156
156
  if (!dotGit)
157
157
  return undefined;
package/dist/run/git.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, type SpawnSyncReturns } from "node:child_process";
3
3
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
4
4
  import { type OwnGitCapability } from "./git-trust.js";
5
5
  export { ROUTING_ENV_SEAMS };
@@ -187,6 +187,18 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options
187
187
  export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions, options?: Pick<ShellOptions, "bytes">): Promise<ShResult>;
188
188
  export declare function shOk(cmd: string, cwd: string): Promise<string>;
189
189
  export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
190
+ /**
191
+ * Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
192
+ * It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
193
+ * checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
194
+ * targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
195
+ * inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
196
+ * after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
197
+ * fail-open answer for an ordinary git failure is unchanged.
198
+ */
199
+ export declare function gitSync(dir: string, args: readonly string[], { env }?: {
200
+ env?: NodeJS.ProcessEnv;
201
+ }): SpawnSyncReturns<string>;
190
202
  /**
191
203
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
192
204
  * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
package/dist/run/git.js CHANGED
@@ -2,14 +2,15 @@ import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEn
2
2
  import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
3
3
  import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
- import { execFile, execFileSync, spawn } from "node:child_process";
6
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
6
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
7
7
  import { availableParallelism, homedir, tmpdir } from "node:os";
8
8
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
9
9
  import { StringDecoder } from "node:string_decoder";
10
10
  import { shq } from "../adapters/types.js";
11
11
  import { tickmarkrDir } from "../graph/graph.js";
12
12
  import { ROUTING_ENV_SEAMS } from "../route/router.js";
13
+ import { HOST_SESSION_VARS } from "../drivers/host-env.js";
13
14
  import { titleEnvironment } from "./title-environment.js";
14
15
  import { assertGitTrust, GitTrustRefusal, ownGitPin, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
15
16
  export { ROUTING_ENV_SEAMS };
@@ -330,6 +331,9 @@ function executeShell(cmd, cwd, timeoutMs, login, options, observation, ownGit)
330
331
  const env = ownGit ? protectedGitEnv(payloadEnv) : payloadEnv;
331
332
  for (const k of ROUTING_ENV_SEAMS)
332
333
  delete env[k];
334
+ // Queue row 193 (D-2102 add.1): no gate or agent child inherits the launching agent session's host variables.
335
+ for (const k of HOST_SESSION_VARS)
336
+ delete env[k];
333
337
  // A run freezes the operator override and cores at startup; admission can lower the round cap.
334
338
  env[FORK_CAP_ENV] = verificationBudget.getStore()
335
339
  ? String(resolvedCapacity().forkCap)
@@ -596,6 +600,27 @@ export async function shGitOk(cmd, cwd, own = {}) {
596
600
  throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
597
601
  return r.stdout;
598
602
  }
603
+ /**
604
+ * Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
605
+ * It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
606
+ * checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
607
+ * targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
608
+ * inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
609
+ * after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
610
+ * fail-open answer for an ordinary git failure is unchanged.
611
+ */
612
+ export function gitSync(dir, args, { env = process.env } = {}) {
613
+ const forced = protectedGitEnv(env);
614
+ const pin = ownGitPin(dir, forced);
615
+ try {
616
+ const r = spawnSync("git", args, { cwd: dir, env: pin?.env ?? forced, encoding: "utf8", maxBuffer: 1 << 28, stdio: ["ignore", "pipe", "pipe"] });
617
+ pin?.recheck();
618
+ return pin && typeof r.stdout === "string" ? { ...r, stdout: pin.unview(r.stdout), stderr: pin.unview(r.stderr) } : r;
619
+ }
620
+ finally {
621
+ pin?.release();
622
+ }
623
+ }
599
624
  /**
600
625
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
601
626
  * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
@@ -1089,19 +1114,14 @@ export async function probeRefsWritable(cwd = process.cwd()) {
1089
1114
  const error = (delRes.stderr || delRes.stdout).trim() || `git update-ref -d failed (${delRes.code})`;
1090
1115
  return { ok: false, path: refsDir, refsDir, error };
1091
1116
  }
1092
- // Clean up empty directories left behind by git
1093
- try {
1094
- const preflightDir = join(refsDir, "tickmarkr", "preflight");
1095
- if (existsSync(preflightDir) && readdirSync(preflightDir).length === 0) {
1096
- rmSync(preflightDir, { recursive: true, force: true });
1097
- }
1098
- const tmDir = join(refsDir, "tickmarkr");
1099
- if (existsSync(tmDir) && readdirSync(tmDir).length === 0) {
1100
- rmSync(tmDir, { recursive: true, force: true });
1117
+ // Queue row 194 (D-2102): remove only EMPTY directories git left behind, non-recursively — rmdir(2)
1118
+ // refuses (ENOTEMPTY) a ref another process writes under refs/tickmarkr at this moment, where an
1119
+ // emptiness check followed by a recursive remove deleted it. Best-effort: a refusal leaves the tree.
1120
+ for (const dir of [join(refsDir, "tickmarkr", "preflight"), join(refsDir, "tickmarkr")]) {
1121
+ try {
1122
+ rmdirSync(dir);
1101
1123
  }
1102
- }
1103
- catch {
1104
- // best-effort cleanup of empty dirs
1124
+ catch { /* absent, or no longer empty */ }
1105
1125
  }
1106
1126
  return { ok: true, path: refsDir, refsDir };
1107
1127
  }
@@ -42,7 +42,14 @@ export declare const bindingToken: (binding: DecisionBinding) => string;
42
42
  export declare function parseBindingToken(token: string): DecisionBinding | undefined;
43
43
  /** The binding a task-approved row recorded under `park` or `failure`, when well formed. */
44
44
  export declare function recordedBinding(value: unknown): DecisionBinding | undefined;
45
- /** The newest failed gate before a park row — the gate a waive of that park would satisfy. */
45
+ /**
46
+ * The newest failed gate before a park row — the gate a waive of that park would satisfy.
47
+ * Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
48
+ * unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
49
+ * review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
50
+ * an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
51
+ * does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
52
+ */
46
53
  export declare function failedGateBeforePark(events: readonly JournalEvent[], taskId: string, parkIndex: number): GateName | undefined;
47
54
  /** The physical journal line of `events[i]` (see SOURCE_LINE). */
48
55
  export declare const physicalLine: (events: readonly JournalEvent[], i: number) => number;
@@ -96,6 +103,13 @@ export interface PreservedRef {
96
103
  export declare function preservedRefsByTask(events: JournalEvent[]): Map<string, PreservedRef[]>;
97
104
  export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string, rounds?: (decided: JournalEvent[]) => JournalEvent[]): number;
98
105
  export declare function upheldFeedbackByTask(events: JournalEvent[]): Map<string, string>;
106
+ /**
107
+ * Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
108
+ * active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
109
+ * that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
110
+ * `decided` is already read through the decision fold (effectiveEvents).
111
+ */
112
+ export declare function restoresUpheldFeedback(decided: JournalEvent[], taskId: string): boolean;
99
113
  export interface StructuredFinding {
100
114
  class: string;
101
115
  path: string;
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { z } from "zod";
7
7
  import { channelKey, shq, TokenUsageSchema } from "../adapters/types.js";
@@ -11,7 +11,7 @@ import { channelRouteIdentity, modelProvider } from "../route/preference.js";
11
11
  import { buildProfile, classify } from "../route/profile.js";
12
12
  import { DecisionEventSchema, trackJournalRows, } from "./protocol.js";
13
13
  import { PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER } from "./git.js";
14
- import { assertGitTrust, protectedGitEnv } from "./git-trust.js";
14
+ import { assertGitTrust, protectedGitEnv, trustedCommonDir } from "./git-trust.js";
15
15
  import { normalizeGateOutcome } from "./outcome.js";
16
16
  import { redactSecrets } from "./redact.js";
17
17
  export function phaseForGate(gate) {
@@ -97,17 +97,44 @@ export function recordedBinding(value) {
97
97
  const { line, ts } = value;
98
98
  return typeof line === "number" && Number.isSafeInteger(line) && line > 0 && typeof ts === "string" ? { line, ts } : undefined;
99
99
  }
100
- /** The newest failed gate before a park row — the gate a waive of that park would satisfy. */
100
+ /**
101
+ * The newest failed gate before a park row — the gate a waive of that park would satisfy.
102
+ * Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
103
+ * unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
104
+ * review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
105
+ * an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
106
+ * does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
107
+ */
101
108
  export function failedGateBeforePark(events, taskId, parkIndex) {
109
+ let inherits = events[parkIndex]?.data.kind === "gate-fail";
110
+ let refused = new Set();
102
111
  for (let i = parkIndex - 1; i >= 0; i -= 1) {
103
112
  const event = events[i];
104
113
  if (event.taskId !== taskId)
105
114
  continue;
106
- if (event.event === "task-approved" || event.event === "task-human" || event.event === "task-dispatch")
115
+ if (event.event === "task-human") {
116
+ // Only a gate-fail predecessor has a failed gate to hand down; any other kind ends the walk as today.
117
+ if (!inherits || event.data.kind !== "gate-fail")
118
+ return undefined;
119
+ // The walk now reads the previous park's own span, under that park's own refusals.
120
+ refused = new Set();
121
+ }
122
+ else if (event.event === APPROVAL_REFUSED) {
123
+ if (refused !== "all")
124
+ refused = Array.isArray(event.data.lines) ? new Set([...refused, ...event.data.lines]) : "all";
125
+ }
126
+ else if (event.event === "task-approved") {
127
+ if (!inherits || (refused !== "all" && !refused.has(physicalLine(events, i))))
128
+ return undefined;
129
+ }
130
+ else if (event.event === "task-dispatch") {
107
131
  return undefined;
108
- if (event.event === "gate-result" && event.data.pass === false
109
- && typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
110
- return event.data.gate;
132
+ }
133
+ else if (event.event === "gate-result") {
134
+ if (event.data.pass === false && typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
135
+ return event.data.gate;
136
+ }
137
+ inherits = false; // a passing gate-result keeps the park's own span
111
138
  }
112
139
  }
113
140
  return undefined;
@@ -321,6 +348,16 @@ export function upheldFeedbackByTask(events) {
321
348
  }
322
349
  return upheld;
323
350
  }
351
+ /**
352
+ * Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
353
+ * active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
354
+ * that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
355
+ * `decided` is already read through the decision fold (effectiveEvents).
356
+ */
357
+ export function restoresUpheldFeedback(decided, taskId) {
358
+ const newest = (event) => decided.reduce((at, e, i) => e.taskId === taskId && e.event === event ? i : at, -1);
359
+ return newest("task-approved") > newest("task-human") && upheldFeedbackByTask(decided).has(taskId);
360
+ }
324
361
  const CONSULT_ACTIONS = ["retry", "reroute", "decompose", "human"];
325
362
  const consultAction = (action) => typeof action === "string" && CONSULT_ACTIONS.includes(action)
326
363
  ? action
@@ -1651,30 +1688,59 @@ export function engagementComparable(events, loadedHash) {
1651
1688
  return recorded === loadedHash ? { comparable: true, recorded } : { comparable: false, reason: "mismatch", recorded };
1652
1689
  }
1653
1690
  const RUN_SEQUENCE_WIDTH = 16;
1691
+ const TICKET = /^\d{16}$/;
1692
+ const ticketsIn = (dir) => {
1693
+ try {
1694
+ return readdirSync(dir).filter((name) => TICKET.test(name));
1695
+ }
1696
+ catch (error) {
1697
+ if (error.code === "ENOENT")
1698
+ return [];
1699
+ throw error;
1700
+ }
1701
+ };
1702
+ const highest = (tickets) => tickets.reduce((max, name) => (BigInt(name) > max ? BigInt(name) : max), 0n);
1654
1703
  // The daemon mints before acquiring graph.lock, and each CLI invocation has fresh module state. Claim
1655
- // a repository-state ticket with mkdir's atomic EEXIST boundary instead: a losing process advances and
1656
- // retries, while the winning directory remains as the durable high-water evidence for later invocations.
1704
+ // a ticket with mkdir's atomic EEXIST boundary: a losing process advances and retries. Queue row 194
1705
+ // (D-2102): the sequence lives at the trusted common git dir, shared by the main checkout and every
1706
+ // linked worktree (a trust refusal refuses — never a per-checkout fallback), and its first ticket sits
1707
+ // above this checkout's own pre-change sequence. Pruning keeps it bounded, and the durable high-water
1708
+ // mark is the highest ticket itself: a prune removes a ticket only after seeing a higher one, so the
1709
+ // highest ever claimed survives. A stale reader can recreate a pruned ticket, but then sees that higher
1710
+ // one and claims again above it — only a ticket that is the highest after its own mkdir is issued.
1657
1711
  function claimRunSequence(repoRoot = process.cwd()) {
1658
- const dir = join(tickmarkrDir(repoRoot), "run-id-sequence");
1712
+ const dir = join(trustedCommonDir(repoRoot), "tickmarkr-run-id-sequence");
1659
1713
  mkdirSync(dir, { recursive: true });
1660
- const allocated = readdirSync(dir).filter((name) => /^\d{16}$/.test(name));
1661
- let candidate = allocated.reduce((max, name) => {
1662
- const value = BigInt(name);
1663
- return value > max ? value : max;
1664
- }, 0n) + 1n;
1714
+ const own = highest(ticketsIn(join(repoRoot, stateDirName(repoRoot), "run-id-sequence")));
1715
+ const shared = highest(ticketsIn(dir));
1716
+ let candidate = (own > shared ? own : shared) + 1n;
1665
1717
  while (true) {
1666
1718
  const suffix = candidate.toString().padStart(RUN_SEQUENCE_WIDTH, "0");
1667
1719
  if (suffix.length > RUN_SEQUENCE_WIDTH)
1668
1720
  throw new Error("run id sequence exhausted");
1669
1721
  try {
1670
1722
  mkdirSync(join(dir, suffix));
1671
- return suffix;
1672
1723
  }
1673
1724
  catch (error) {
1674
1725
  if (error.code !== "EEXIST")
1675
1726
  throw error;
1676
1727
  candidate += 1n;
1728
+ continue;
1729
+ }
1730
+ const tickets = ticketsIn(dir);
1731
+ const top = highest(tickets);
1732
+ if (top > candidate) {
1733
+ candidate = top + 1n;
1734
+ continue;
1735
+ }
1736
+ for (const name of tickets) {
1737
+ if (BigInt(name) < candidate)
1738
+ try {
1739
+ rmdirSync(join(dir, name));
1740
+ }
1741
+ catch { /* a concurrent prune took it */ }
1677
1742
  }
1743
+ return suffix;
1678
1744
  }
1679
1745
  }
1680
1746
  export function newRunId(now = new Date()) {
@@ -1979,7 +2045,7 @@ export class Journal {
1979
2045
  const folded = !decisionRow && data.fatal === true ? [...priorEvents, { ts: new Date().toISOString(), event, data }] : priorEvents;
1980
2046
  return decisionRow ? reduced : { ...reduced, owedChecks: foldOwedChecks(folded, join(this.dir, "..", "..", "..")) };
1981
2047
  })()
1982
- : event === "resume-restore" && rowTaskId && upheldFeedbackByTask(effectiveEvents(priorEvents)).has(rowTaskId)
2048
+ : event === "resume-restore" && rowTaskId && restoresUpheldFeedback(effectiveEvents(priorEvents), rowTaskId)
1983
2049
  ? {
1984
2050
  ...inputData,
1985
2051
  upheldFeedbackRestoredFor: rowTaskId,
@@ -3,6 +3,8 @@ export declare const STALE_MS = 60000;
3
3
  export interface Inspection {
4
4
  pid?: number;
5
5
  runId?: string;
6
+ startedAt?: number;
7
+ checkout?: string;
6
8
  garbage: boolean;
7
9
  dead: boolean;
8
10
  expired: boolean;
@@ -15,6 +17,9 @@ export type RunLineEvent = {
15
17
  };
16
18
  export declare function shouldRefuse(i: Pick<Inspection, "garbage" | "dead">): boolean;
17
19
  export declare function isPidLive(pid: number): boolean;
20
+ /** The repository run claim for the checkout enclosing `repoRoot`; undefined when no checkout encloses it
21
+ * (graph.lock alone guards there, as before). Any other git trust refusal propagates: fail closed. */
22
+ export declare function repositoryClaimPath(repoRoot: string): string | undefined;
18
23
  export declare function acquireRunLock(repoRoot: string, runId: string): {
19
24
  reclaimed?: {
20
25
  pid: number;