tickmarkr 2.8.6 → 2.8.7

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.
@@ -115,16 +115,12 @@ export const grok = {
115
115
  return { command: this.headlessCommand(ctx.promptFile, a.model) };
116
116
  },
117
117
  parse: parseWorkerResult, // prompt.ts — last-valid-trailer, hard-wrap tolerant. No grok fork.
118
- // v1.89 T1 / OBS-414: no capture exists, so none is invented. grok 0.2.93 has never been observed
119
- // rendering a workspace/folder-trust prompt: it loads the operator's global Claude-Code-compat
120
- // config rather than keeping a per-directory trust store, and no grok dispatch in any recorded run
121
- // has stalled before launch (OBS-414's own sweep lists grok's gap as MCP suppression, not trust).
122
- // FALSIFIER, and it is cheap: dispatch grok into a never-seen worktree and read the pane. One
123
- // capture of a trust prompt replaces this with {fingerprint, key} — the capture is the only
124
- // admissible evidence, and a guess here would press a key on whatever it happened to match.
118
+ // Queue row 215 (D-2118 add.1, D-2125): recorded Grok Build 1.0.62 and 1.0.67 directory-trust
119
+ // modal, quoted in GROK_TRUST_PANE. Enter is the measured acceptance key (y also accepts).
120
+ // Accepted residual: answering persists grok's trust for the main checkout root.
125
121
  trustDialog: {
126
- kind: "none",
127
- reason: "grok 0.2.93 keeps no per-directory trust store and no recorded pane shows it rendering a workspace-trust prompt; falsified by one capture from a fresh worktree",
122
+ fingerprint: "Grok Build may run or modify contents in this directory",
123
+ key: "Enter",
128
124
  },
129
125
  // v1.5 MODEL-01: fail OPEN to [] (advisory detection, unlike gates). Parses the model LIST only —
130
126
  // the auth banner on line 1 is ignored BY CONSTRUCTION (parseGrokModels anchors on the header).
@@ -12,13 +12,21 @@ import { CAPACITY_RE } from "./types.js";
12
12
  // and the allowlist stays exactly as immutable during a run as it was before this text changed.
13
13
  export function buildTaskPrompt(task, feedback = "", nonce = "") {
14
14
  const list = (xs) => xs.map((x) => `- ${x}`).join("\n");
15
+ // Queue row 234 (D-2130 add.1, D-2131): only the worker brief carries the suite;
16
+ // the judge and the review brief keep the shared criterion text byte-equal.
17
+ const criteria = task.acceptance.map((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined
18
+ ? `${renderAcceptanceItem(item)} | suite: ${item.landing}`
19
+ : renderAcceptanceItem(item));
20
+ const suiteNote = task.acceptance.some((item) => typeof item !== "string" && item.oracle === "test" && item.landing !== undefined)
21
+ ? '\n\nFor test criteria with ` | suite: `, the test title is the text before the marker, and the test must pass in the named file.'
22
+ : "";
15
23
  return `You are an autonomous coding worker dispatched by tickmarkr into an isolated git worktree.
16
24
 
17
25
  ## Task ${task.id}: ${task.title}
18
26
  Goal: ${task.goal}
19
27
 
20
28
  ## Acceptance criteria (you will be judged against these, verbatim)
21
- ${list(task.acceptance.map(renderAcceptanceItem))}
29
+ ${list(criteria)}${suiteNote}
22
30
  ${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(task.files)}\n` : ""}${task.context.length ? `\n## Context (read these first)\n${list(task.context)}\n` : ""}${task.outOfScope?.length ? `\n## Out of scope\nStay within the task’s declared bounds; do not pursue these items:\n${list(task.outOfScope)}\n` : ""}
23
31
  ## Rules
24
32
  - Work only inside the current directory (your isolated worktree). Never push. Never switch branches.
@@ -117,6 +117,7 @@ export declare const TRUST_DIALOG_BLANK_MESSAGE = "trust-dialog fingerprint must
117
117
  export declare const CLAUDE_TRUST_PANE: string;
118
118
  export declare const CODEX_TRUST_PANE: string;
119
119
  export declare const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
120
+ export declare const GROK_TRUST_PANE: string;
120
121
  export declare const KIMI_TRUST_PANE: string;
121
122
  export declare const KIMI_TRUST_PANE_029: string;
122
123
  export declare const KIMI_MCP_TRUST_PANE: string;
@@ -59,6 +59,16 @@ export const CODEX_TRUST_PANE = [
59
59
  ].join("\n");
60
60
  // v1.22 T5 / OBS-19, cursor-agent's per-worktree dialog.
61
61
  export const CURSOR_TRUST_PANE = "Workspace Trust Required\nTrust this folder?";
62
+ // Queue row 215 (D-2118 add.1, D-2125): quoted from the recorded Grok Build 1.0.62 modal in
63
+ // tests/fixtures/grok-trust/grok-1.0.62.screen.txt; 1.0.67 renders these same modal rows centered.
64
+ export const GROK_TRUST_PANE = [
65
+ "Do you trust the contents of this directory?",
66
+ "/Users/…/repo",
67
+ "Grok Build may run or modify contents in this directory,",
68
+ "posing security risks.",
69
+ "Yes, proceed y",
70
+ "No, quit n",
71
+ ].join("\n");
62
72
  // Queue row 198, kimi 2.2.0's folder-trust modal on a fresh task worktree (run 1394 T3, read off the
63
73
  // Orca screen before its one Enter). The worktree path is shortened as above; every other row is as
64
74
  // rendered. 2.2.0 renamed the selected option, so the 0.29.0 fingerprint below never matched it and the
@@ -92,6 +102,7 @@ export const KIMI_MCP_TRUST_PANE = [
92
102
  ].join("\n");
93
103
  export const RECORDED_TRUST_PANES = [
94
104
  CLAUDE_TRUST_PANE, CODEX_TRUST_PANE, CURSOR_TRUST_PANE, KIMI_TRUST_PANE, KIMI_TRUST_PANE_029, KIMI_MCP_TRUST_PANE,
105
+ GROK_TRUST_PANE,
95
106
  ];
96
107
  // v1.89 T1 / OBS-414 round 4 — EXACT, not "leading bytes of a recorded line". Round 3 accepted any
97
108
  // prefix of a captured line, and every prefix of a trust prompt is also a substring of unrelated
@@ -111,6 +122,7 @@ export const APPROVED_TRUST_FINGERPRINTS = [
111
122
  "Workspace Trust Required", // cursor-agent, OBS-19
112
123
  "❯ Trust this folder", // kimi 0.29.0, OBS-358 + OBS-406 — the cursor glyph is load-bearing
113
124
  "❯ Trust and continue", // kimi 2.2.0, queue row 198 — the same gate with its option renamed
125
+ "Grok Build may run or modify contents in this directory", // queue row 215, D-2125
114
126
  ];
115
127
  const isApprovedFingerprint = (fingerprint) => APPROVED_TRUST_FINGERPRINTS.includes(fingerprint)
116
128
  && RECORDED_TRUST_PANES.some((pane) => pane.includes(fingerprint));
@@ -1,3 +1,12 @@
1
+ interface SkillDrift {
2
+ skill: string;
3
+ dir: string;
4
+ missing: string[];
5
+ modified: string[];
6
+ }
7
+ export declare function skillDrift(cwd: string): SkillDrift[];
8
+ export declare const docsDrift: (cwd: string) => string[];
9
+ export declare function installedAgentGuidance(cwd: string): string[];
1
10
  export type DetectedHost = "herdr" | "orca" | "subprocess";
2
11
  export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
3
12
  /** Test seam mirroring FleetIO: production callers omit it and get the process streams. */
@@ -6,3 +15,4 @@ export type InitIO = {
6
15
  output?: NodeJS.WriteStream;
7
16
  };
8
17
  export declare function init(argv: string[], cwd?: string, io?: InitIO): Promise<string>;
18
+ export {};
@@ -107,7 +107,11 @@ Outside multi-agent environments, run the loop directly.
107
107
 
108
108
  ### Version preflight
109
109
 
110
- Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, read \`package.json\` version, and stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. Tell the operator to update or link the correct binary. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
110
+ Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version --check\` and stop on a non-zero exit. In the tickmarkr source repository (\`package.json\` named \`tickmarkr\`), stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. In a consumer repository, the check compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version; refresh drifted guidance with \`tickmarkr init --agent --force\`. Tell the operator to update or link the correct binary when the source version differs. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
111
+
112
+ ### Seat hygiene
113
+
114
+ In a multi-tier session, the tiers clear each other at 50% context or more, after writing a handoff. A new spec or run is a mission boundary: write the handoff and clear before taking the next mission. Copy this duty into every brief and handoff. On a single-tier plain host, write your handoff and ask the operator to clear the session.
111
115
 
112
116
  ### Tip-verify-before-green
113
117
 
@@ -184,7 +188,7 @@ function skillFileList(root, prefix = "") {
184
188
  }
185
189
  // Drift is only computed for skills that ARE installed. A skill never installed is not drift — `--agent`
186
190
  // installs it and says so, and nagging about it would bury the case this exists for.
187
- function skillDrift(cwd) {
191
+ export function skillDrift(cwd) {
188
192
  const drifted = [];
189
193
  for (const { skillsDir } of hostTargets(cwd)) {
190
194
  for (const skill of AGENT_SKILLS) {
@@ -215,7 +219,7 @@ function skillDrift(cwd) {
215
219
  // superseded preflight rules (patch-version drift tolerated, a machine-wide process match required)
216
220
  // through the very release that fixed the tracked copy, and a consultation was launched under the stale
217
221
  // text with nobody able to see it. Same rule as skillDrift: a doc that was never written is not drift.
218
- const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
222
+ export const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
219
223
  if (!existsSync(docPath))
220
224
  return false;
221
225
  const current = readFileSync(docPath, "utf8");
@@ -227,6 +231,12 @@ const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter
227
231
  return true; // a truncated block is stale by definition
228
232
  return current.slice(begin, end + DOCS_END.length) !== AGENT_DOCS.trimEnd();
229
233
  });
234
+ export function installedAgentGuidance(cwd) {
235
+ return hostTargets(cwd).flatMap(({ skillsDir, docPath }) => [
236
+ ...AGENT_SKILLS.map((skill) => join(skillsDir, skill, "SKILL.md")).filter((path) => existsSync(path)),
237
+ ...(existsSync(docPath) && readFileSync(docPath, "utf8").includes(DOCS_BEGIN) ? [docPath] : []),
238
+ ]);
239
+ }
230
240
  const describeDrift = (d) => [
231
241
  d.missing.length ? `missing ${d.missing.join(", ")}` : "",
232
242
  d.modified.length ? `modified ${d.modified.join(", ")}` : "",
@@ -1,2 +1,2 @@
1
1
  export declare function distFingerprint(dir?: string): string;
2
- export declare function version(argv?: string[], dir?: string): Promise<string>;
2
+ export declare function version(argv?: string[], dir?: string, cwd?: string): Promise<string>;
@@ -23,7 +23,41 @@ export function distFingerprint(dir = distDir) {
23
23
  hash.update(relative(dir, file)).update("\0").update(readFileSync(file)).update("\0");
24
24
  return hash.digest("hex");
25
25
  }
26
- export async function version(argv = [], dir = distDir) {
26
+ export async function version(argv = [], dir = distDir, cwd = process.cwd()) {
27
27
  const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
28
- return argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
28
+ const stamp = argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
29
+ if (!argv.includes("--check"))
30
+ return stamp;
31
+ let manifest;
32
+ try {
33
+ manifest = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
34
+ }
35
+ catch { /* no source manifest — consumer guidance check */ }
36
+ if (manifest?.name === "tickmarkr") {
37
+ const repoVersion = manifest.version ?? "missing";
38
+ const equal = v === repoVersion;
39
+ const out = `tickmarkr binary ${stamp}: compared tickmarkr source repository package.json version ${repoVersion} — ${equal ? "entire version matches" : "versions differ; update or link the correct binary"}`;
40
+ if (!equal)
41
+ throw new Error(out);
42
+ return out;
43
+ }
44
+ const { docsDrift, installedAgentGuidance, skillDrift } = await import("./init.js");
45
+ const installed = installedAgentGuidance(cwd);
46
+ if (!installed.length)
47
+ return `tickmarkr binary ${stamp}: no installed agent guidance to compare`;
48
+ const drift = [
49
+ ...skillDrift(cwd).flatMap(({ dir: skillDir, missing, modified }) => [
50
+ ...missing.map((file) => `missing ${relative(cwd, join(skillDir, file))}`),
51
+ ...modified.map((file) => `modified ${relative(cwd, join(skillDir, file))}`),
52
+ ]),
53
+ ...docsDrift(cwd).map((file) => `stale agent-docs block in ${relative(cwd, file)}`),
54
+ ];
55
+ const out = [
56
+ `tickmarkr binary ${stamp}: compared installed agent guidance with this binary's packaged block and skills:`,
57
+ ...installed.map((file) => ` ${relative(cwd, file)}`),
58
+ ...(drift.length ? [...drift, "refresh: tickmarkr init --agent --force"] : ["installed guidance matches"]),
59
+ ].join("\n");
60
+ if (drift.length)
61
+ throw new Error(out);
62
+ return out;
29
63
  }
@@ -195,6 +195,7 @@ export declare const COMMAND_HELP: {
195
195
  description: string;
196
196
  options: {
197
197
  "--dist": string;
198
+ "--check": string;
198
199
  };
199
200
  examples: string[];
200
201
  };
package/dist/cli/help.js CHANGED
@@ -169,9 +169,12 @@ export const COMMAND_HELP = {
169
169
  ],
170
170
  },
171
171
  version: {
172
- usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
173
- options: { "--dist": "Also print the resolved build directory and distribution fingerprint." },
174
- examples: ["version", "version --dist"],
172
+ usage: "version [--dist] [--check]", description: "Print the installed package version. Top-level aliases: --version and -v.",
173
+ options: {
174
+ "--dist": "Also print the resolved build directory and distribution fingerprint.",
175
+ "--check": "Compare the entire source version when package.json is named tickmarkr; otherwise check installed agent guidance against this binary. Exit non-zero on drift; no installed guidance exits zero.",
176
+ },
177
+ examples: ["version", "version --dist", "version --check"],
175
178
  },
176
179
  verify: {
177
180
  usage: "verify [--base <ref>] [--criteria <file> | --task <id>] [options]",
@@ -49,7 +49,7 @@ export class SubprocessDriver {
49
49
  n = 0;
50
50
  async slot(cwd, name) {
51
51
  const id = `sp-${++this.n}`;
52
- this.slots.set(id, { buf: "", exited: false });
52
+ this.slots.set(id, { buf: "", receivedBytes: 0, exited: false });
53
53
  return { id, name, cwd };
54
54
  }
55
55
  state(slot) {
@@ -72,8 +72,13 @@ export class SubprocessDriver {
72
72
  detached: true,
73
73
  });
74
74
  s.proc = p;
75
- p.stdout.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
76
- p.stderr.on("data", (d) => (s.buf = (s.buf + d).slice(-MAX_BUF)));
75
+ // Queue row 233 (D-2130): count both received streams before retention truncates the tail.
76
+ const receive = (d) => {
77
+ s.receivedBytes += d.length;
78
+ s.buf = (s.buf + d).slice(-MAX_BUF);
79
+ };
80
+ p.stdout.on("data", receive);
81
+ p.stderr.on("data", receive);
77
82
  p.on("close", () => (s.exited = true));
78
83
  p.on("error", (e) => { s.buf = (s.buf + `\n[tickmarkr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
79
84
  // Queue row 108 (D-1622): THIS run's transport state. The slot's `exited` outlives an invocation (a reused slot) and
@@ -117,7 +122,7 @@ export class SubprocessDriver {
117
122
  async transportState(slot) {
118
123
  const s = this.state(slot);
119
124
  const run = s.invocation;
120
- return { closed: run?.closed ?? false, bytes: Buffer.byteLength(s.buf), exitCode: run?.exitCode ?? null, signal: run?.signal ?? null };
125
+ return { closed: run?.closed ?? false, bytes: s.receivedBytes, exitCode: run?.exitCode ?? null, signal: run?.signal ?? null };
121
126
  }
122
127
  async read(slot, lines) {
123
128
  return this.state(slot).buf.split("\n").slice(-lines).join("\n");
@@ -428,11 +428,30 @@ export function classifyFailureOutput(output) {
428
428
  // and `stderr | <task id>` (digits and underscores) when the reporter no longer knows the task
429
429
  // (dist/chunks/index.*.js, `headerText`). The stripper knew only the first form.
430
430
  const VITEST_ECHO_BLOCK_RE = /^\s*std(?:out|err)\s+\|\s+(?:unknown test\s*$|\d+_[\d_]+\s*$|\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s*$|\s+>\s+\S))/;
431
- /** Keep runner output while omitting Vitest's echoed test-owned stdout/stderr diagnostic blocks. */
431
+ // Queue row 211 (D-2118): only Jest's complete, column-zero recovery warning is noise. A missing
432
+ // neighbour, changed hint, indented copy or multi-line Watchman error remains failure evidence.
433
+ const JEST_WATCHMAN_HEADER = "jest-haste-map: Watchman crawl failed. Retrying once with node crawler.";
434
+ const JEST_WATCHMAN_HINT = " Usually this happens when watchman isn't running. Create an empty `.watchmanconfig` file in your project's root folder or initialize a git or hg repository in your project.";
435
+ const JEST_WATCHMAN_ERROR_RE = /^ {2}Error: Watchman error: [^\r\n]+\. Make sure watchman is running for this project\. See https:\/\/facebook\.github\.io\/watchman\/docs\/troubleshooting\.$/;
436
+ /** Queue row 211 (D-2118): display readers retain raw Vitest echo lines while dropping this warning. */
437
+ const withoutJestWatchmanBlocks = (output) => {
438
+ const kept = [];
439
+ const lines = output.split("\n");
440
+ for (let i = 0; i < lines.length; i++) {
441
+ const line = lines[i];
442
+ if (line === JEST_WATCHMAN_HEADER && lines[i + 1] === JEST_WATCHMAN_HINT && JEST_WATCHMAN_ERROR_RE.test(lines[i + 2] ?? "")) {
443
+ i += 2;
444
+ continue;
445
+ }
446
+ kept.push(line);
447
+ }
448
+ return kept;
449
+ };
450
+ /** Queue row 211 (D-2118): omit the recovered Jest warning alongside Vitest's test-owned echo blocks. */
432
451
  const withoutVitestEchoBlocks = (output) => {
433
452
  const outside = [];
434
453
  let inVitestEchoBlock = false;
435
- for (const line of output.split("\n")) {
454
+ for (const line of withoutJestWatchmanBlocks(output)) {
436
455
  const clean = line.replace(ANSI_RE, "");
437
456
  const runnerLine = stripTurboPrefix(clean) ?? clean;
438
457
  if (inVitestEchoBlock) {
@@ -625,8 +644,7 @@ function fingerprintOccurrences(output) {
625
644
  }
626
645
  // The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
627
646
  // verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
628
- const unrecognizedEvidence = (raw) => raw
629
- .split("\n")
647
+ const unrecognizedEvidence = (raw) => withoutJestWatchmanBlocks(raw)
630
648
  .map((l) => l.replace(ANSI_RE, "").trimEnd())
631
649
  .filter((l) => VOCAB_RE.test(l))
632
650
  .slice(0, 10)
@@ -1028,11 +1046,33 @@ export async function detectVacuousOracles(cwd, tasks) {
1028
1046
  // drop a FAIL line that fingerprint-collides with baseline noise). No headline anchors → byte-identical
1029
1047
  // fallback to today's text (non-vitest runners lose nothing). RED-pinned by tests/gates/baseline.test.ts
1030
1048
  // "HYG-08: details headlines the failing test, not the noise".
1049
+ // Queue row 224(a) (D-2118): Jest identities and their first message are display-only; Console and
1050
+ // column-zero validation bullets are not identities. Keep the existing metadata extraction separate.
1051
+ const JEST_IDENTITY_RE = /^ {2}● (?!Console\s*$)\S.*$/;
1031
1052
  function headlineDetails(raw, fresh, freshLines) {
1032
- const headlines = raw
1053
+ const namedHeadlines = raw
1033
1054
  .split("\n")
1034
1055
  .map((l) => l.replace(ANSI_RE, ""))
1035
1056
  .filter((l) => namesFailureEitherForm(l) || SUMMARY_FAIL_RE.test(l));
1057
+ const outputLines = withoutJestWatchmanBlocks(raw).map((l) => l.replace(ANSI_RE, ""));
1058
+ let inVitestEchoBlock = false;
1059
+ const headlines = outputLines.flatMap((line, index) => {
1060
+ const runnerLine = stripTurboPrefix(line) ?? line;
1061
+ const echoed = inVitestEchoBlock || VITEST_ECHO_BLOCK_RE.test(runnerLine);
1062
+ if (inVitestEchoBlock) {
1063
+ if (runnerLine.trim() === "")
1064
+ inVitestEchoBlock = false;
1065
+ }
1066
+ else if (echoed)
1067
+ inVitestEchoBlock = true;
1068
+ // Queue row 224(a) (D-2118): preserve raw FAIL/summary headlines; only Jest identities exclude echoes.
1069
+ if (namesFailureEitherForm(line) || SUMMARY_FAIL_RE.test(line))
1070
+ return [line];
1071
+ if (echoed || !JEST_IDENTITY_RE.test(line))
1072
+ return [];
1073
+ const message = withoutVitestEchoBlocks(outputLines.slice(index + 1).join("\n")).find((l) => l.trim() !== "");
1074
+ return message === undefined ? [line] : [line, message];
1075
+ });
1036
1076
  // S22: the ORIGINAL runner lines behind v2 comparison keys are the diagnostic; the keys are secondary.
1037
1077
  // A legacy entry's lines ARE its keys, so its details stay byte-identical.
1038
1078
  const lines = [...new Set(freshLines)].join("\n");
@@ -1042,7 +1082,7 @@ function headlineDetails(raw, fresh, freshLines) {
1042
1082
  return { details: same ? `new failures vs baseline:\n${lines}` : `new failures vs baseline:\n${lines}\n\n${keys}` };
1043
1083
  return {
1044
1084
  details: `failing tests:\n${headlines.join("\n")}\n\n${same ? "" : `new failure lines vs baseline:\n${lines}\n\n`}${keys}`,
1045
- meta: { failingTests: headlines.filter(namesFailureEitherForm) },
1085
+ ...(namedHeadlines.length ? { meta: { failingTests: namedHeadlines.filter(namesFailureEitherForm) } } : {}),
1046
1086
  };
1047
1087
  }
1048
1088
  export async function compareToBaseline(cwd, commands, baseline, enabled, opts = {}) {
@@ -1225,7 +1265,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
1225
1265
  if (classification === "infra") {
1226
1266
  const evidence = failingLines.length
1227
1267
  ? failingLines.slice(0, 10).join("\n")
1228
- : unrecognizedEvidence(raw) || raw.trim().split("\n").slice(0, 10).join("\n");
1268
+ : unrecognizedEvidence(raw) || withoutJestWatchmanBlocks(raw).join("\n").trim().split("\n").slice(0, 10).join("\n");
1229
1269
  record({
1230
1270
  gate: name,
1231
1271
  pass: false,
@@ -1,7 +1,7 @@
1
1
  import { HOST_PROBE_SAMPLE_MS, HOST_PROBE_SAMPLES, HostDegradedError, hostDegraded, observeHost } from "./host-health.js";
2
2
  import { runWithInterruptionSink, VITEST_CACHE_ENV, worktreeVitestCache } from "../gates/test-manifest.js";
3
3
  import { COMMAND_LEASE_TOKEN_ENV, commandLeaseEnvironment, CommandLeases, currentCommandLeaseToken, isRunnerCommand, runWithCommandLease, withCommandLease } from "./lease.js";
4
- import { execFileSync, spawn } from "node:child_process";
4
+ import { execFile, execFileSync, spawn } from "node:child_process";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { shq } from "../adapters/types.js";
7
7
  import { appendFileSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -34,10 +34,10 @@ 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, 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";
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, WorktreeAdminRaceError, 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
- import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
40
+ import { classifyRepairDisposition, namedPaths, resolveScopeHints } from "./repair-disposition.js";
41
41
  import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVEST_SOURCE, resumeHarvestAuthor, approvalAction, APPROVAL_REFUSED, bindingToken, effectiveEvents, foldDecisions, physicalLine, staleApprovals, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, readableExcerpt, receiptOrigin, outstandingReviewFindings, pendingApprovalActions, pendingRechecks, pendingRepairFindings, phaseForGate, priorJudgments, readPriorRunEvidence, recordedTaskFailureKind, reraisedReviewChains, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, standingRulings, structuredFindings, upheldFeedbackByTask } from "./journal.js";
42
42
  import { carriedAuthorVendors, gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
43
43
  import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
@@ -855,7 +855,11 @@ export function resetDeadChannelFastKillMsForTests() {
855
855
  // monotonic-tracker silence — CONCLUDES the wait. Conclude, never kill: the pane is harvested by
856
856
  // the same tail a window expiry uses, and the carried worktree goes straight to gates. Set at the
857
857
  // fast-kill's window on purpose (the OBS-264 arithmetic is "a ~36m stall + ~15m redo becomes a
858
- // ~5m gate pass"); a worker that is merely thinking still burns CPU and is never concluded here.
858
+ // ~5m gate pass"). Queue row 231 (D-2119): a headless worker waiting on its remote provider can be
859
+ // silent with flat CPU. Print mode also requires a fresh reading of no ESTABLISHED TCP connection
860
+ // in its owned process group; connected or unreadable holds the harvest. Interactive workers keep
861
+ // the triad: an idle pane holds connections too. Residual (D-2125): a truly hung headless worker
862
+ // holding a connection rides the stall window instead.
859
863
  const HARVEST_SILENT_MS = 5 * 60_000;
860
864
  let harvestSilentMs = HARVEST_SILENT_MS;
861
865
  /** Test seam — shrink the harvest silence gate without minute-long sleeps. */
@@ -3235,11 +3239,6 @@ export async function runDaemon(repoRoot, opts = {}) {
3235
3239
  ];
3236
3240
  return `${identity} — release with ${commands.map((command) => `\`${command}\``).join(" or ")}`;
3237
3241
  };
3238
- // Lexing never starts in the middle of a word. Resolution is separate so missing hints
3239
- // remain visible evidence without becoming executable approval advice.
3240
- const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
3241
- .map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
3242
- .filter((path) => !path.split("/").includes(".."));
3243
3242
  const diffPaths = async (base, wt) => {
3244
3243
  const out = await shGit(`git diff --name-only -z ${shq(base)}..HEAD`, wt);
3245
3244
  return new Set(out.code === 0 ? out.stdout.split("\0").filter(Boolean) : []);
@@ -3526,7 +3525,20 @@ export async function runDaemon(repoRoot, opts = {}) {
3526
3525
  const ref = await preserveWorktree(priorWt, producer);
3527
3526
  if (ref)
3528
3527
  journal.append("worktree-preserved", t.id, { ref, ...producerFields(producer) });
3529
- return driver.worktree(repoRoot, taskBranch, taskBase);
3528
+ // Queue row 223 (D-2118): only this typed add race buys retries, with three adds total.
3529
+ const addWorktree = () => {
3530
+ return driver.worktree(repoRoot, taskBranch, taskBase);
3531
+ };
3532
+ for (let attempt = 1;; attempt++) {
3533
+ try {
3534
+ return await addWorktree();
3535
+ }
3536
+ catch (error) {
3537
+ if (!(error instanceof WorktreeAdminRaceError) || attempt >= 3)
3538
+ throw error;
3539
+ journal.append("worktree-add-retry", t.id, { attempt, nextAttempt: attempt + 1, evidence: error.evidence });
3540
+ }
3541
+ }
3530
3542
  };
3531
3543
  // A dead worker with a clean checkout still needs a durable recovery handle: there may be no
3532
3544
  // pane left to identify even the commit it was dispatched from. preserveWorktree deliberately
@@ -5310,17 +5322,19 @@ export async function runDaemon(repoRoot, opts = {}) {
5310
5322
  let cpuFlat;
5311
5323
  let cpuAccountant;
5312
5324
  let cpuGapCount = 0;
5313
- let unmeasurableNoted = false;
5314
- // One line per attempt, whichever way the CPU leg turns out to be unmeasurable. A triad that
5325
+ const unmeasurableNoted = new Set();
5326
+ // Queue row 231 (D-2119): one line per reading per attempt, including the connection leg.
5327
+ // One CPU line per attempt, whichever way that leg turns out to be unmeasurable. A triad that
5315
5328
  // can never conclude is this feature silently ABSENT — on a host whose `ps` the probe cannot
5316
5329
  // read, every stall would ride its whole window out again with nothing saying why. Named once
5317
5330
  // per attempt, not per slice: the condition is structural, and a per-slice line would bury it.
5318
- const noteUnmeasurable = (reason) => {
5319
- if (unmeasurableNoted)
5331
+ const noteUnmeasurable = (reason, reading = "cpu") => {
5332
+ if (unmeasurableNoted.has(reading))
5320
5333
  return;
5321
- unmeasurableNoted = true;
5334
+ unmeasurableNoted.add(reading);
5322
5335
  journal.append("worker-harvest-unmeasurable", t.id, { slot: slot.name, attempt, reason });
5323
5336
  };
5337
+ let harvestHeldNoted = false;
5324
5338
  // The CPU leg needs a marker in the worker's own argv, and every launch path puts this
5325
5339
  // attempt's dispatch script there EXCEPT interactiveSeed: runInteractiveSeed launches the
5326
5340
  // TUI directly, by a command the ADAPTER owns (seed.launch(model)) which tickmarkr cannot
@@ -5382,6 +5396,39 @@ export async function runDaemon(repoRoot, opts = {}) {
5382
5396
  const carried = await commitsAheadOf(taskBase, wt);
5383
5397
  if (carried.length === 0)
5384
5398
  return false; // nothing landed: not this branch's population
5399
+ if (!interactive) {
5400
+ // Queue row 231 (D-2119): query afresh only after the other legs hold. lsof ORs selections
5401
+ // without -a; -g includes connector children in the owned group, not just the CLI root.
5402
+ // No environment inspection. One async PATH lookup, bounded from invocation like the
5403
+ // reap census (15 s); SIGKILL also retires a probe that ignores SIGTERM.
5404
+ const group = readOwnedProcessGroup(groupFile);
5405
+ const connection = group === undefined ? "unreadable" : await new Promise((resolve) => {
5406
+ try {
5407
+ execFile("lsof", ["-nP", "-a", "-g", String(group), "-iTCP", "-sTCP:ESTABLISHED", "-Fp"], { encoding: "utf8", timeout: 15_000, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
5408
+ if (!error && stdout.trim())
5409
+ resolve("connected");
5410
+ else if (error && error.code === 1 && !stdout && !stderr)
5411
+ resolve("none");
5412
+ else
5413
+ resolve("unreadable");
5414
+ });
5415
+ }
5416
+ catch {
5417
+ resolve("unreadable");
5418
+ }
5419
+ });
5420
+ if (connection === "unreadable") {
5421
+ noteUnmeasurable("the worker ESTABLISHED TCP connection reading could not be read", "connection");
5422
+ return false;
5423
+ }
5424
+ if (connection === "connected") {
5425
+ if (!harvestHeldNoted) {
5426
+ harvestHeldNoted = true;
5427
+ journal.append("worker-harvest-held", t.id, { slot: slot.name, attempt, reason: "established-tcp-connection", processGroup: group });
5428
+ }
5429
+ return false;
5430
+ }
5431
+ }
5385
5432
  journal.append("worker-harvest", t.id, {
5386
5433
  slot: slot.name, attempt, commits: carried.length,
5387
5434
  silentMs, cpuMs: leg.cpu.ms, cpuResolutionMs: leg.cpu.resolutionMs,
@@ -5860,13 +5907,17 @@ export async function runDaemon(repoRoot, opts = {}) {
5860
5907
  lastStatus = st;
5861
5908
  journal.append("worker-status", t.id, { slot: slot.name, status: st, attempt });
5862
5909
  }
5863
- if (st === "blocked" || st === "idle") {
5910
+ // Queue row 215 (D-2118 add.1): an unhooked CLI can report unknown at its startup modal.
5911
+ // Bound that status to worker-launch's startup window, including time spent reading.
5912
+ const withinTrustStartup = () => workerLaunchedAt !== undefined
5913
+ && Date.now() - workerLaunchedAt <= workerStartupWindowMs;
5914
+ if (st === "blocked" || st === "idle" || (st === "unknown" && withinTrustStartup())) {
5864
5915
  // T5: once-per-slot auto-answer when the adapter declares a trust dialog and the pane
5865
5916
  // text matches. tickmarkr created the worktree from the operator's own repo — safe by construction.
5866
5917
  if (!trustAnswered && adapter.trustDialog && driver.sendKey) {
5867
5918
  try {
5868
- const paneText = await workerTransport.read(slot, 80);
5869
- if (matchesTrustDialog(paneText, adapter.trustDialog)) {
5919
+ const paneText = await (slot.readScreen ? slot.readScreen() : workerTransport.read(slot, 80));
5920
+ if ((st !== "unknown" || withinTrustStartup()) && matchesTrustDialog(paneText, adapter.trustDialog)) {
5870
5921
  trustAnswered = true;
5871
5922
  trustAnsweredSlots.add(slot.id);
5872
5923
  // v1.25 T1: audit trail for live runs — prove the dialog appeared and was answered.
@@ -6092,9 +6143,9 @@ export async function runDaemon(repoRoot, opts = {}) {
6092
6143
  }
6093
6144
  // OBS-548: the FOURTH leg, and the one the daemon already measured. The three legs above
6094
6145
  // are all channel-side: they say a pane stopped talking. The tree's CPU says whether
6095
- // anything is still WORKING, and the harvest triad forty lines up refuses exactly this
6096
- // conclusion without it — "a worker that is merely thinking still burns CPU and is never
6097
- // concluded here". A live CPU delta is a live worker, by construction; an UNMEASURABLE
6146
+ // anything is still WORKING, and the harvest triad forty lines up refuses this
6147
+ // conclusion without it. Queue row 231 (D-2119): print-mode harvest additionally checks
6148
+ // connections because a remote model turn can have flat CPU. An UNMEASURABLE
6098
6149
  // reading keeps the fail-open contract the probe already states (undefined is never
6099
6150
  // evidence a worker stopped), so on a host whose `ps` cannot be read the fast-kill stands
6100
6151
  // down and the fixed attempt ceiling bounds the hold.
@@ -6281,13 +6332,15 @@ export async function runDaemon(repoRoot, opts = {}) {
6281
6332
  }
6282
6333
  await noteLaunched();
6283
6334
  // OBS-54: headless workers have the same output-inactivity budget as visible panes.
6284
- // v1.76: same monotonic-progress measure as the interactive site; harvest stays raw.
6335
+ // Queue row 233 (D-2130): print progress is the driver's received-byte count, never a row
6336
+ // high-water over the bounded tail; a driver reporting no count keeps the row measure.
6285
6337
  const stallWindowMs = taskTimeoutMinutes * 60_000;
6338
+ const hardDeadline = attemptStart + (attemptHardTimeoutMs ?? stallWindowMs * 4);
6286
6339
  const initialPane = await workerTransport.read(slot, 500);
6287
6340
  startupFailure = startupFailureInWindow(initialPane, workerLaunchedAt);
6288
6341
  let everHadOutput = initialPane.length > 0;
6289
6342
  const stallProgress = new StallProgressTracker();
6290
- stallProgress.observe({ paneText: initialPane, seedSubmitted: true, contextTokens });
6343
+ stallProgress.observe({ paneText: initialPane, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, seedSubmitted: true, contextTokens });
6291
6344
  let lastProgressAt = Date.now();
6292
6345
  finished = false;
6293
6346
  // T2 review (material): the exit marker proves the PROCESS EXITED, never that the worker
@@ -6297,10 +6350,10 @@ export async function runDaemon(repoRoot, opts = {}) {
6297
6350
  // worker-result-harvested row. The interactive site has always kept them apart (`finished`
6298
6351
  // there is the trailer regex; the exit marker only sets exitCode), and the cause taxonomy
6299
6352
  // already names this shape "clean-exit-no-trailer" — unreachable in print mode until now.
6300
- while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs) {
6353
+ while (!startupFailure && Date.now() - lastProgressAt < stallWindowMs && Date.now() < hardDeadline) {
6301
6354
  fatalStop.signal.throwIfAborted();
6302
6355
  executionSignal()?.throwIfAborted();
6303
- const remaining = stallWindowMs - (Date.now() - lastProgressAt);
6356
+ const remaining = Math.min(stallWindowMs - (Date.now() - lastProgressAt), hardDeadline - Date.now());
6304
6357
  let slice = Math.min(BLOCKED_POLL_MS, Math.max(100, Math.min(stallWindowMs / 2, remaining)));
6305
6358
  const startupLeft = workerLaunchedAt === undefined
6306
6359
  ? 0
@@ -6331,11 +6384,10 @@ export async function runDaemon(repoRoot, opts = {}) {
6331
6384
  break;
6332
6385
  }
6333
6386
  await sampleContext();
6334
- if (stallProgress.observe({ paneText, contextTokens }))
6387
+ if (stallProgress.observe({ paneText, receivedBytes: (await workerTransport.transportState?.(slot))?.bytes, contextTokens }))
6335
6388
  lastProgressAt = Date.now();
6336
- // T2 (OBS-264): the same liveness triad the interactive loop runs. A headless worker that
6337
- // committed and went quiet is finished work too, and before this it rode the entire
6338
- // window out before anything looked at its commits.
6389
+ // Queue row 231 (D-2119): print mode adds a fresh owned-group connection reading to the
6390
+ // silence/commits/CPU triad; a quiet remote model turn can hold a connection with flat CPU.
6339
6391
  // No nudge hold here, deliberately: print mode has no nudge surface at all (no pane to
6340
6392
  // steer, driver.nudge is never consulted on this path), so there is no pending daemon
6341
6393
  // action for the triad to preempt — the asymmetry with the interactive call site above is
@@ -6350,7 +6402,7 @@ export async function runDaemon(repoRoot, opts = {}) {
6350
6402
  // without one is finished:false with a non-null exitCode — the harvest synthesis then owns
6351
6403
  // it when the worktree carries work, and classifyWorkerResultCause names it otherwise.
6352
6404
  finished = new RegExp(trailerPattern(nonce)).test(output);
6353
- timedOut = !processExited && !finished && Date.now() - lastProgressAt >= stallWindowMs;
6405
+ timedOut = !processExited && !finished && (Date.now() - lastProgressAt >= stallWindowMs || Date.now() >= hardDeadline);
6354
6406
  }
6355
6407
  }
6356
6408
  finally {
@@ -7101,7 +7153,8 @@ export async function runDaemon(repoRoot, opts = {}) {
7101
7153
  // Budget spent means the FRESH LADDER owns this failure — including a review-only one, whose
7102
7154
  // same-channel fix retry is exactly the round the budget just declared too expensive to repeat.
7103
7155
  const repairExhausted = repairable && !repair;
7104
- if (repair && !capStep) {
7156
+ const fundedRepair = repair && !capStep;
7157
+ if (fundedRepair) {
7105
7158
  journal.append("repair-attempt", t.id, {
7106
7159
  repair: repairHistory.length + 1, charge: repairsDrawn + 1, of: MAX_REPAIRS,
7107
7160
  gates: failing.map((g) => g.gate),
@@ -7126,8 +7179,13 @@ export async function runDaemon(repoRoot, opts = {}) {
7126
7179
  });
7127
7180
  await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
7128
7181
  }
7182
+ // Queue row 224(b) (D-2118): pinned tasks park on the first unfunded owned deterministic red
7183
+ // at or after the second, so this climb fires at most once. Unpinned tasks climb on exactly
7184
+ // the second owned deterministic red, even when it funds a repair.
7129
7185
  const climbCause = repairExhausted ? "repair-exhausted"
7130
- : deterministic && ownedGateFailures === 2 ? "gate-fail" : undefined;
7186
+ : deterministic && (t.routingHints?.pin
7187
+ ? ownedGateFailures >= 2 && !fundedRepair
7188
+ : ownedGateFailures === 2) ? "gate-fail" : undefined;
7131
7189
  if (climbCause) {
7132
7190
  const climbed = await climb(climbCause, deterministic ?? failing[0], attempt + 1);
7133
7191
  if (climbed === "parked")
@@ -7240,6 +7298,10 @@ export async function runDaemon(repoRoot, opts = {}) {
7240
7298
  if (fatalStop.signal.aborted)
7241
7299
  return;
7242
7300
  const cleanupEvidence = cleanupErrors.length ? { cleanupErrors } : {};
7301
+ if (err instanceof WorktreeAdminRaceError) {
7302
+ await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { evidence: err.evidence, ...cleanupEvidence });
7303
+ return;
7304
+ }
7243
7305
  if (err instanceof HostDegradedError) {
7244
7306
  await park(t, err.message, "infra", null, 0, Date.now(), 0, 0, undefined, 0, "fresh", { disposition: "host-degraded", ...cleanupEvidence });
7245
7307
  return;
package/dist/run/git.d.ts CHANGED
@@ -199,6 +199,11 @@ export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions):
199
199
  export declare function gitSync(dir: string, args: readonly string[], { env }?: {
200
200
  env?: NodeJS.ProcessEnv;
201
201
  }): SpawnSyncReturns<string>;
202
+ /** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
203
+ export declare class WorktreeAdminRaceError extends Error {
204
+ readonly evidence: string;
205
+ constructor(evidence: string);
206
+ }
202
207
  /**
203
208
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
204
209
  * 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
@@ -621,6 +621,41 @@ export function gitSync(dir, args, { env = process.env } = {}) {
621
621
  pin?.release();
622
622
  }
623
623
  }
624
+ // Queue row 223 (D-2118): one process-wide administration queue; recreation and rollback retain
625
+ // their caller's hold, including trust rechecks and branch restoration, without queuing behind it.
626
+ let worktreeAdminTail = Promise.resolve();
627
+ const worktreeAdminHold = new AsyncLocalStorage();
628
+ async function withWorktreeAdmin(operation) {
629
+ if (worktreeAdminHold.getStore())
630
+ return operation();
631
+ const prior = worktreeAdminTail;
632
+ let release;
633
+ worktreeAdminTail = new Promise((done) => { release = done; });
634
+ await prior;
635
+ try {
636
+ return await worktreeAdminHold.run(true, operation);
637
+ }
638
+ finally {
639
+ release();
640
+ }
641
+ }
642
+ /** Queue row 223 (D-2118): git could not read a sibling's partially written admin file during add. */
643
+ export class WorktreeAdminRaceError extends Error {
644
+ evidence;
645
+ constructor(evidence) {
646
+ super(`worktree add encountered a sibling admin read race (queue row 223, D-2118): ${evidence}`);
647
+ this.evidence = evidence;
648
+ this.name = "WorktreeAdminRaceError";
649
+ }
650
+ }
651
+ const siblingAdminReadFailed = (stderr) => {
652
+ for (const line of stderr.split("\n")) {
653
+ const read = /^(?:fatal: |error: )?failed to read (?:'([^'\r\n]+)'|"([^"\r\n]+)"|([^:\r\n]+))(?::|$)/.exec(line);
654
+ if (read && /(?:^|\/)worktrees\/[^/\r\n]+\/(?:commondir|gitdir|HEAD)$/.test(read[1] ?? read[2] ?? read[3]))
655
+ return true;
656
+ }
657
+ return false;
658
+ };
624
659
  /**
625
660
  * v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
626
661
  * verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
@@ -633,6 +668,9 @@ export function gitSync(dir, args, { env = process.env } = {}) {
633
668
  * back through the trusted common directory derived here once (dropWorktree), even while that commondir stays hostile.
634
669
  */
635
670
  export async function addDetachedWorktree(cwd, dir, commitish, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS, { logHead = false } = {}) {
671
+ return withWorktreeAdmin(() => addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead));
672
+ }
673
+ async function addDetachedWorktreeHeld(cwd, dir, commitish, timeoutMs, logHead) {
636
674
  // 2.8.1 T1 (ANL-023 §7.1): `logHead` writes the new checkout's HEAD reflog whatever the repository's
637
675
  // core.logAllRefUpdates says, so a later HEAD move by any command is appended to an established witness.
638
676
  const git = logHead ? "git -c core.logAllRefUpdates=always" : "git";
@@ -682,6 +720,9 @@ function rollbackFailed(error, failure) {
682
720
  * step bounded by `timeoutMs`; every step is tried, and any that failed is thrown, never swallowed.
683
721
  */
684
722
  async function dropWorktree(store, { dir, branch }, timeoutMs = DEFAULT_SHELL_TIMEOUT_MS) {
723
+ return withWorktreeAdmin(() => dropWorktreeHeld(store, { dir, branch }, timeoutMs));
724
+ }
725
+ async function dropWorktreeHeld(store, { dir, branch }, timeoutMs) {
685
726
  const failed = await withoutExecutionBudget(async () => {
686
727
  const run = async (cmd) => {
687
728
  try {
@@ -859,6 +900,9 @@ export function checkoutIncarnation(worktree) {
859
900
  }
860
901
  }
861
902
  export async function createWorktree(repo, branch, baseRef) {
903
+ return withWorktreeAdmin(() => createWorktreeHeld(repo, branch, baseRef));
904
+ }
905
+ async function createWorktreeHeld(repo, branch, baseRef) {
862
906
  branch = await resolveTaskBranch(repo, branch);
863
907
  const dir = join(tickmarkrDir(repo), WORKTREES_DIR, sanitize(branch));
864
908
  if (existsSync(dir))
@@ -866,7 +910,13 @@ export async function createWorktree(repo, branch, baseRef) {
866
910
  // the worktree capability: a branch add, whose symbolic HEAD keeps the ref-store pin (see OwnGitCapability); a raced
867
911
  // add is rolled back, its branch restored to the value read here or deleted when it is new
868
912
  const prior = (await shGit(`git rev-parse --verify -q ${shq(`refs/heads/${branch}`)}`, repo)).stdout.trim() || undefined;
869
- await shGitOk(`git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`, repo, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
913
+ const cmd = `git worktree add -B ${shq(branch)} ${shq(dir)} ${shq(baseRef)}`;
914
+ const added = await shGit(cmd, repo, DEFAULT_SHELL_TIMEOUT_MS, { createsWorktree: "branch", rollback: { dir, branch: { name: branch, prior } } });
915
+ if (added.code !== 0) {
916
+ if (siblingAdminReadFailed(added.stderr))
917
+ throw new WorktreeAdminRaceError(added.stderr.trim());
918
+ throw new Error(`command failed (${added.code}): ${cmd}\n${added.stderr || added.stdout}`);
919
+ }
870
920
  linkNodeModules(repo, dir);
871
921
  return dir;
872
922
  }
@@ -33,7 +33,7 @@ export interface RepairDispositionInput {
33
33
  /** task tree plus diff, deleted tracked paths included */
34
34
  inventory: Iterable<string>;
35
35
  }
36
- /** Lexing never starts in the middle of a word (same lexer as the daemon's scope-red disposition). */
36
+ /** Queue row 230: tsc's position, numeric or baseline-normalized (#), ends a path; named parentheses stay. */
37
37
  export declare const namedPaths: (text: string) => string[];
38
38
  /** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
39
39
  * path-suffix. Anything unbound or bound twice comes back as a non-executable diagnostic. */
@@ -1,8 +1,8 @@
1
1
  import { filesGlob } from "../graph/files-glob.js";
2
2
  const failed = (g) => !(g.pass || g.meta?.skipped === true) || g.meta?.infra === true;
3
3
  const REFUSAL_RE = /outside|out.of.scope|allowlist|scope expansion|not (?:in|own)|unowned/i;
4
- /** Lexing never starts in the middle of a word (same lexer as the daemon's scope-red disposition). */
5
- export const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
4
+ /** Queue row 230: tsc's position, numeric or baseline-normalized (#), ends a path; named parentheses stay. */
5
+ export const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+(?:[A-Za-z0-9_@.[\]-]|\([A-Za-z0-9_@.[\]-]*\))+|(?:[A-Za-z0-9_@-]|\([A-Za-z0-9_@.[\]-]*\))+(?:\.(?:[A-Za-z0-9_-]|\([A-Za-z0-9_@.[\]-]*\))+)+)(?=$|[\s`'"),:;.!?]|\((?:\d+|#),\s*(?:\d+|#)\))/g)]
6
6
  .map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
7
7
  .filter((path) => !path.split("/").includes(".."));
8
8
  /** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
@@ -3,6 +3,7 @@
3
3
  export declare function normalizeStallSnapshot(text: string): string;
4
4
  export interface StallProgressSample {
5
5
  paneText: string;
6
+ receivedBytes?: number;
6
7
  seedSubmitted?: boolean;
7
8
  contextTokens?: number;
8
9
  }
@@ -102,6 +103,7 @@ export declare const PANE_READ_ROWS = 1000;
102
103
  */
103
104
  export declare class StallProgressTracker {
104
105
  private transcriptRows;
106
+ private receivedBytes;
105
107
  private rawWindowLines;
106
108
  private seedSubmitted;
107
109
  private contextTokens;
package/dist/run/stall.js CHANGED
@@ -613,6 +613,7 @@ export const PANE_READ_ROWS = 1000;
613
613
  */
614
614
  export class StallProgressTracker {
615
615
  transcriptRows = 0; // non-empty high-water over the bounded read — the growth signal
616
+ receivedBytes = 0;
616
617
  rawWindowLines = 0; // raw-line high-water — the SATURATION signal (see the getter)
617
618
  seedSubmitted = false;
618
619
  contextTokens;
@@ -654,6 +655,14 @@ export class StallProgressTracker {
654
655
  rowsAdvanced = true;
655
656
  this.rowGrowthAt = now; // raw signal — advances even when the report below is suppressed
656
657
  }
658
+ // Queue row 233 (D-2130): print-mode progress is the driver's received-byte count, never a
659
+ // row high-water over the bounded tail. A driver reporting no count keeps the row measure.
660
+ const bytes = sample.receivedBytes;
661
+ let outputAdvanced = rowsAdvanced;
662
+ if (bytes !== undefined && Number.isFinite(bytes) && bytes >= 0) {
663
+ outputAdvanced = bytes > this.receivedBytes;
664
+ this.receivedBytes = Math.max(this.receivedBytes, bytes);
665
+ }
657
666
  let seedAdvanced = false;
658
667
  if (sample.seedSubmitted && !this.seedSubmitted) {
659
668
  this.seedSubmitted = true;
@@ -674,10 +683,9 @@ export class StallProgressTracker {
674
683
  }
675
684
  if (tokensAdvanced || seedAdvanced)
676
685
  return true;
677
- if (rowsAdvanced) {
678
- // T1: row growth past the flat-token cap is cosmetic — the paid-work counter has not moved,
679
- // so the inactivity window must NOT re-arm on it. A tracker that never sees a token sample
680
- // (unmetered adapter) keeps the old row-growth behavior.
686
+ if (outputAdvanced) {
687
+ // Queue row 233 (D-2130): both output measures obey the existing flat-token cap;
688
+ // an unmetered adapter can keep re-arming on output indefinitely, up to its hard deadline.
681
689
  if (this.lastTokenGrowthAt !== undefined && now - this.lastTokenGrowthAt >= rowRearmTokenFlatMs)
682
690
  return false;
683
691
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.8.6",
3
+ "version": "2.8.7",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,16 +20,18 @@ Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intac
20
20
 
21
21
  ## Two-tier by default — role check before the loop
22
22
 
23
+ `<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
24
+
23
25
  When working in a multi-agent terminal environment (herdr or Orca), decide your role before starting:
24
26
 
25
- - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
27
+ - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <label>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
26
28
  - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
27
- - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
28
- - **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <version>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
29
+ - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <label>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <label>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
30
+ - **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
29
31
  - **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
30
32
  - **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
31
33
  - **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
32
- - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <version>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
34
+ - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
33
35
  - **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
34
36
 
35
37
  With neither herdr nor Orca there is no role to decide and no seat to spawn: follow [On a plain host (neither herdr nor Orca)](#on-a-plain-host-neither-herdr-nor-orca) and run the loop directly, single-tier.
@@ -83,11 +85,11 @@ Run every requested target in order without seeking routine confirmation. Stop o
83
85
 
84
86
  ## Binary preflight (before compile or run)
85
87
 
86
- Before `tickmarkr compile` or `tickmarkr run`, compare the installed binary against the repository's `package.json` version:
88
+ Before `tickmarkr compile` or `tickmarkr run`:
87
89
 
88
- 1. Run `tickmarkr version` (one line, machine-parseable).
89
- 2. Read the `version` field from the repository's `package.json`.
90
- 3. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
90
+ 1. Run `tickmarkr version --check` and **stop immediately on a non-zero exit**.
91
+ 2. In a consumer repository, it compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version. Refresh drifted guidance with `tickmarkr init --agent --force`.
92
+ 3. Only in the tickmarkr source repository (`package.json` named `tickmarkr`), the check compares its version with the binary. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
91
93
  > ⚠ **Never `npm i -g .` on the repository directory, and never link it.** npm SYMLINKS a directory install, which makes the working tree itself the machine-wide binary: every later build — including a gate's own `npm run build` — silently hot-swaps the CLI for every repository on the machine, with no version change to notice it by. Measured 2026-08-29: a verify build gate rewrote the shared binary while another repository's daemon was mid-run against it, and a positive control that rebuilds at a pre-fix ref would have installed the very defect it was proving fixed, machine-wide (OBS-771). Verify an install by comparing the global and repo **inodes** — they must DIFFER — never by `tickmarkr version`, which cannot go red when nothing is bumped.
92
94
 
93
95
  A stale binary silently skips daemon gates shipped in newer releases — the v1.38 run exposed this when a global `1.36.0` binary missed the daemon tip-verify gate entirely (OBS-38). Preflight failure is always stop-and-report; never proceed-and-hope.
@@ -9,16 +9,18 @@ Use this for any spec that `tickmarkr compile` accepts. It is SDD-agnostic: use
9
9
 
10
10
  ## Two-tier by default — role check before the loop
11
11
 
12
+ `<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
13
+
12
14
  When working in a multi-agent terminal environment (herdr or Orca), decide your role before starting:
13
15
 
14
- - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <version>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
16
+ - **Orchestrator:** your session was started to execute the mission. Rename your own tab/pane `ORCH · <label>` (short labels: ≤20 chars, `ROLE · token`) and run the loop below.
15
17
  - **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
16
- - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
17
- - **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <version>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
18
+ - **Primary session without an orchestrator:** rename your own tab `OVERSEER · <label>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <label>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
19
+ - **On herdr (`HERDR_ENV=1`)**: **spawning on current herdr is two-step** — the one-shot `agent start --cwd/--tab/--no-focus` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138). First create the pane: `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` (tab create does not steal focus unless `--focus` is passed; parse `result.root_pane.pane_id` from its JSON), then start the agent in it:
18
20
  - **Claude Code:** `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions`
19
21
  - **Codex:** `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
20
22
  - **Auxiliary seats run as the CLI's interactive TUI in their visible pane — never headless** (`claude -p` / `codex exec`): headless buffers output until exit so the pane renders idle for the entire run, is blind to SessionStart hook errors (a broken and a fixed hook both return green), and gives a stall watcher no midpoint — silent-time equals lifetime. Headless is for exit-code probes only (a quota check that wants `rc`), never for work anyone must watch.
21
- - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <version>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
23
+ - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: spawn it with `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json` and parse `result.terminal.handle` from the create receipt. **Auxiliary seats run as the CLI's interactive TUI in their visible terminal — never headless**, for the reasons the herdr branch gives.
22
24
  - **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
23
25
 
24
26
  With neither herdr nor Orca there is no role to decide and no seat to spawn: follow [On a plain host (neither herdr nor Orca)](#on-a-plain-host-neither-herdr-nor-orca) and run the loop directly, single-tier.
@@ -69,11 +71,11 @@ Proceed through the loop without seeking routine confirmation. Stop only for a b
69
71
 
70
72
  ## Binary preflight (before compile or run)
71
73
 
72
- Before `tickmarkr compile` or `tickmarkr run`, compare the installed binary against the repository's `package.json` version:
74
+ Before `tickmarkr compile` or `tickmarkr run`:
73
75
 
74
- 1. Run `tickmarkr version` (one line, machine-parseable).
75
- 2. Read the `version` field from the repository's `package.json`.
76
- 3. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
76
+ 1. Run `tickmarkr version --check` and **stop immediately on a non-zero exit**.
77
+ 2. In a consumer repository, it compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version. Refresh drifted guidance with `tickmarkr init --agent --force`.
78
+ 3. Only in the tickmarkr source repository (`package.json` named `tickmarkr`), the check compares its version with the binary. If the binary and repository do not **agree on the entire version** (including the patch; e.g. binary `2.1.0` vs repo `2.1.1`), **stop immediately** and tell the operator to update the global install (`npm i -g tickmarkr@latest`), or to install this repository's build as a REAL COPY — `npm pack`, then `npm i -g ./<tarball>`. Do not compile, plan, or run on hope.
77
79
  > ⚠ **Never `npm i -g .` on the repository directory, and never link it.** npm SYMLINKS a directory install, which makes the working tree itself the machine-wide binary: every later build — including a gate's own `npm run build` — silently hot-swaps the CLI for every repository on the machine, with no version change to notice it by. Measured 2026-08-29: a verify build gate rewrote the shared binary while another repository's daemon was mid-run against it, and a positive control that rebuilds at a pre-fix ref would have installed the very defect it was proving fixed, machine-wide (OBS-771). Verify an install by comparing the global and repo **inodes** — they must DIFFER — never by `tickmarkr version`, which cannot go red when nothing is bumped.
78
80
 
79
81
  A stale binary silently skips daemon gates shipped in newer releases — the v1.38 run exposed this when a global `1.36.0` binary missed the daemon tip-verify gate entirely (OBS-38). Preflight failure is always stop-and-report; never proceed-and-hope.
@@ -5,6 +5,8 @@ description: "Use when the user asks to oversee/supervise/babysit an autonomous
5
5
 
6
6
  # Overseer (tickmarkr)
7
7
 
8
+ `<label>` means the version only in the tickmarkr source repository (`package.json` named `tickmarkr`); elsewhere it means the mission or run, never the installed binary version.
9
+
8
10
  Become the OVERSEER for this workspace or session. Do no heavy work directly — build and supervise a two-tier
9
11
  hierarchy of VISIBLE agents (you → orchestrator → tickmarkr's own worker fleet), and route human decisions
10
12
  to the user with evidence.
@@ -203,15 +205,15 @@ replaces the two-tier setup.
203
205
  **Live tab labels (standing operator rule, 2026-07-12):** on every decision or state change (role
204
206
  handoff, task done/merged, run end) rename the affected tabs — and keep labels SHORT: the role as the
205
207
  main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
206
- fraction (`ORCH · v1.19 4/5`, updated on every task-done); tickmarkr opens ONE TAB PER TASK, labelled
208
+ fraction (`ORCH · <label> 4/5`, updated on every task-done); tickmarkr opens ONE TAB PER TASK, labelled
207
209
  with the task id and holding that task's worker plus its judge/review/consult panes (tickmarkr
208
210
  updates it). Never long context strings or ✓-chains.
209
- - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Do not load the `herdr` skill and do not map a Herdr workspace. Work from the current Orca terminal context. Keep the overseer in the launching terminal, name it in the same act — `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <version>"` (tab title; see the seat-name law under Seat-spawn recipes) — and inspect terminals with `orca terminal list --json`. Launch the daemon detached at the orchestrator terminal's recorded handle using the detached launch below — no pane of its own; its self-placed watch board stays in the ORCH tab.
211
+ - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Do not load the `herdr` skill and do not map a Herdr workspace. Work from the current Orca terminal context. Keep the overseer in the launching terminal, name it in the same act — `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <label>"` (tab title; see the seat-name law under Seat-spawn recipes) — and inspect terminals with `orca terminal list --json`. Launch the daemon detached at the orchestrator terminal's recorded handle using the detached launch below — no pane of its own; its self-placed watch board stays in the ORCH tab.
210
212
  2. **Orchestrator**: First consume a successful `tickmarkr fleet --pick consult` in the seat repository, per the Fleet selection contract below; then launch the returned adapter/model with your agent host.
211
- - **On herdr (`HERDR_ENV=1`)**: Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <version>"` and parse `result.root_pane.pane_id` from its JSON, then start the agent in it. For Claude Code, use `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions` (append `--model <picked-model>` after the `--` from the Fleet receipt). For Codex, use `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` (add `--model <picked-model>` from the Fleet receipt). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do spawn (consultants, reviewers, scouts) follow the same forms: never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions --settings '{"promptSuggestionEnabled":false}'`. **For a codex consultant, use `-a never --sandbox workspace-write` — NOT `--sandbox read-only`.** ⚠ **`--sandbox read-only` CONTRADICTS this skill's own completion protocol and will hang the seat.** Every seat you spawn is told to deliver an ARTIFACT ending in a terminal MARKER, because that is the only completion signal the artifact watcher can key on (`done` is turn end). A read-only sandbox cannot write that artifact, so codex blocks on `Would you like to make the following edits?` for its OWN report — and the report exists ONLY in the pending edit, so abandoning the prompt destroys the work rather than merely delaying it. Measured 2026-08-28: a consultant spawned `--sandbox read-only` finished a 14,604-byte verdict, sat blocked on the write, and the operator saw the prompt before the supervising tier did. `read-only` is correct ONLY for a seat that writes nothing at all — which, under the artifact+marker rule, is no seat this skill tells you to spawn. When the prompt does appear, answer **"Yes, and don't ask again for these files"** rather than plain yes: plain yes re-blocks on the next write of the same file. **That `--settings` pair is not cosmetic and it is not optional:** claude-code's AUTOSUGGEST renders context-plausible ghost text into an idle seat's prompt line that is BYTE-IDENTICAL to a typed draft in text-format reads (OBS-482), so a supervising tier cannot tell a seat's own unsent work from a rendering artifact without `agent read --format ansi`. Turning the suggester off at spawn removes the ambiguity at its source instead of paying for the discrimination at every read. Verified against the shipped binary: `claude --settings '{"promptSuggestionEnabled":false}' -p …` exits 0 with a real response, and the key appears in the binary's own settings schema. **For kimi, pass `-y`** (`herdr agent start <name> --kind kimi --pane <id> -- -y`) — the adapter already launches its own workers that way (`src/adapters/kimi.ts:204`), and a kimi seat spawned without it sits on an approval prompt having done nothing. **Herdr cannot see that state**: it reports a kimi pane as `agent_status: working` with `screen_detection_skipped: true` while the prompt is up, so the BLOCKED-STATE watcher below is blind on this vendor and the spawn flag is the ONLY control. Every vendor you spawn needs its auto-approve form named here; a vendor absent from this list is a seat that will hang.
213
+ - **On herdr (`HERDR_ENV=1`)**: Spawning on current herdr is two-step — the one-shot `agent start --cwd` form was removed in the herdr CLI redesign and now fails with `unknown option` (OBS-138): first create the pane with `herdr tab create --workspace <ws> --cwd <repo> --label "ORCH · <label>"` and parse `result.root_pane.pane_id` from its JSON, then start the agent in it. For Claude Code, use `herdr agent start orchestrator --kind claude --pane <root-pane-id> -- --permission-mode bypassPermissions` (append `--model <picked-model>` after the `--` from the Fleet receipt). For Codex, use `herdr agent start orchestrator --kind codex --pane <root-pane-id> -- --dangerously-bypass-approvals-and-sandbox` (add `--model <picked-model>` from the Fleet receipt). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do spawn (consultants, reviewers, scouts) follow the same forms: never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions --settings '{"promptSuggestionEnabled":false}'`. **For a codex consultant, use `-a never --sandbox workspace-write` — NOT `--sandbox read-only`.** ⚠ **`--sandbox read-only` CONTRADICTS this skill's own completion protocol and will hang the seat.** Every seat you spawn is told to deliver an ARTIFACT ending in a terminal MARKER, because that is the only completion signal the artifact watcher can key on (`done` is turn end). A read-only sandbox cannot write that artifact, so codex blocks on `Would you like to make the following edits?` for its OWN report — and the report exists ONLY in the pending edit, so abandoning the prompt destroys the work rather than merely delaying it. Measured 2026-08-28: a consultant spawned `--sandbox read-only` finished a 14,604-byte verdict, sat blocked on the write, and the operator saw the prompt before the supervising tier did. `read-only` is correct ONLY for a seat that writes nothing at all — which, under the artifact+marker rule, is no seat this skill tells you to spawn. When the prompt does appear, answer **"Yes, and don't ask again for these files"** rather than plain yes: plain yes re-blocks on the next write of the same file. **That `--settings` pair is not cosmetic and it is not optional:** claude-code's AUTOSUGGEST renders context-plausible ghost text into an idle seat's prompt line that is BYTE-IDENTICAL to a typed draft in text-format reads (OBS-482), so a supervising tier cannot tell a seat's own unsent work from a rendering artifact without `agent read --format ansi`. Turning the suggester off at spawn removes the ambiguity at its source instead of paying for the discrimination at every read. Verified against the shipped binary: `claude --settings '{"promptSuggestionEnabled":false}' -p …` exits 0 with a real response, and the key appears in the binary's own settings schema. **For kimi, pass `-y`** (`herdr agent start <name> --kind kimi --pane <id> -- -y`) — the adapter already launches its own workers that way (`src/adapters/kimi.ts:204`), and a kimi seat spawned without it sits on an approval prompt having done nothing. **Herdr cannot see that state**: it reports a kimi pane as `agent_status: working` with `screen_detection_skipped: true` while the prompt is up, so the BLOCKED-STATE watcher below is blind on this vendor and the spawn flag is the ONLY control. Every vendor you spawn needs its auto-approve form named here; a vendor absent from this list is a seat that will hang.
212
214
  - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Seats spawn with `orca terminal create` on a path worktree selector with a command:
213
- `orca terminal create --worktree path:<repo> --title "ORCH · <version>" --command "<agent-cmd>" --json`
214
- For Claude Code: `orca terminal create --worktree path:<repo> --title "ORCH · <version>" --command "claude --model <picked-model> --permission-mode bypassPermissions --settings '{\"promptSuggestionEnabled\":false}'" --json`. For Codex: `orca terminal create --worktree path:<repo> --title "ORCH · <version>" --command "codex --model <picked-model> --dangerously-bypass-approvals-and-sandbox" --json`. Parse `result.terminal.handle` from the create receipt.
215
+ `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "<agent-cmd>" --json`
216
+ For Claude Code: `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "claude --model <picked-model> --permission-mode bypassPermissions --settings '{\"promptSuggestionEnabled\":false}'" --json`. For Codex: `orca terminal create --worktree path:<repo> --title "ORCH · <label>" --command "codex --model <picked-model> --dangerously-bypass-approvals-and-sandbox" --json`. Parse `result.terminal.handle` from the create receipt.
215
217
  Require the orchestrator to use the host-owned daemon and detached-beat lifecycle below for both run and resume, and to report the lock pid ancestry check.
216
218
  3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
217
219
  truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
@@ -221,7 +223,7 @@ replaces the two-tier setup.
221
223
  - **On Orca (`TERM_PROGRAM=Orca` and non-empty `ORCA_TERMINAL_HANDLE`)**: Briefs travel as a file announced by `terminal send` with enter and a submit wait whose receipt is read for observed submission rather than input acceptance:
222
224
  `orca terminal send --terminal <handle> --text "Read .tickmarkr/overseer/ORCH-BRIEF.md and follow it exactly." --enter --wait-submit 15 --json`
223
225
  Read `result.send.prompt.stages` and treat the brief as delivered only when a `turn_started` stage is present; `accepted: true` proves input acceptance, not a started turn; never resend on `accepted` alone.
224
- The brief MUST contain: the mission, the five-tab canon (on herdr) or seat layout (on Orca), the pane mechanics below, rules 1–2, the GSD-leg rules
226
+ The brief MUST contain: **Seat hygiene — in a multi-tier session the tiers clear each other at 50% context or more after writing a handoff; a new spec or run is a mission boundary, so hand off and clear before the next mission; copy this duty into every brief and handoff; on a single-tier plain host write your handoff and ask the operator to clear the session**, then the mission, the five-tab canon (on herdr) or seat layout (on Orca), the pane mechanics below, rules 1–2, the GSD-leg rules
225
227
  (below) whenever the mission dispatches `/gsd:*` legs, and require a verbatim one-sentence
226
228
  acknowledgment of the human-checkpoint rule before anything is dispatched.
227
229
  *method guards* in that brief (how to know a thing, not what is true of this spec), and deleting them
@@ -329,7 +331,7 @@ Kimi `-y`, artifact plus terminal marker, and verified file-brief delivery. Flee
329
331
  a `git worktree add` the overseer made for a spec branch, a throwaway clone — `terminal create` hangs and
330
332
  returns "Timed out waiting for terminal handle after creation". The lawful form for such a checkout is a
331
333
  terminal in a managed worktree with a cwd change in the command:
332
- `orca terminal create --worktree path:<managed-repo> --title "ORCH · <version>" --command "cd <checkout> && <agent-cmd>" --json`.
334
+ `orca terminal create --worktree path:<managed-repo> --title "ORCH · <label>" --command "cd <checkout> && <agent-cmd>" --json`.
333
335
  **An Orca-hosted run's checkout is created by Orca** (`orca worktree create --repo id:<repo> --base-branch <spec-branch>`),
334
336
  never by `git worktree add`: the orca driver asks `orca worktree current` from inside every task worktree and refuses
335
337
  (`selector_not_found`) on an unmanaged checkout — every task fails pre-launch and the board cannot place (OBS-1061,
@@ -353,7 +355,7 @@ Kimi `-y`, artifact plus terminal marker, and verified file-brief delivery. Flee
353
355
  Code's turn summary, Codex's last prompt) and is NOT the seat's name. This is vendor-neutral by
354
356
  construction — no per-vendor env var, no agent command, no launch-form ritual — which is what makes
355
357
  it the same law as Herdr's tab label. So: **the overseer names its OWN seat at Setup 1** with
356
- `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <version>"` (the seat
358
+ `orca terminal rename --terminal "$ORCA_TERMINAL_HANDLE" --title "OVERSEER · <label>"` (the seat
357
359
  it was invoked in is the one seat nobody else launches), every seat it spawns is named by
358
360
  `create --title`, and a live-label change (progress fraction, hot-state token) is `rename` again.
359
361
  **Prove a name by the field that holds:** `tabs[].title` in