tickmarkr 2.8.4 → 2.8.5

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.
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { withoutHostSession } from "../drivers/host-env.js";
5
6
  import { parseWorkerResult } from "./prompt.js";
6
7
  import { channelsFromConfig, declareInputBox, MODEL_ID_RE, promptFitsArgv, shq, TokenUsageSchema } from "./types.js";
7
8
  // SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
@@ -188,7 +189,7 @@ export function probeClaudeAliasIdentity(cwd, alias) {
188
189
  "--strict-mcp-config",
189
190
  "--mcp-config", '{"mcpServers":{}}',
190
191
  "--output-format", "text",
191
- ], { cwd, encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"] });
192
+ ], { cwd, encoding: "utf8", timeout: 60000, stdio: ["ignore", "pipe", "pipe"], env: withoutHostSession() });
192
193
  if (r.error || r.status !== 0)
193
194
  return undefined;
194
195
  const identity = (r.stdout || "").trim();
@@ -202,7 +203,7 @@ export function resolveClaudeAliasIdentity(cwd, alias, probe = probeClaudeAliasI
202
203
  return readClaudeAliasIdentity(cwd, alias) ?? probe(cwd, alias);
203
204
  }
204
205
  export function probeVersion(bin) {
205
- const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10000 });
206
+ const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10000, env: withoutHostSession() });
206
207
  if (r.error || r.status !== 0)
207
208
  return { installed: false, authed: false, models: [] };
208
209
  return {
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { assertGitTrust, GitTrustRefusal, protectedGitEnv } from "../run/git-trust.js";
8
+ import { withoutHostSession } from "../drivers/host-env.js";
8
9
  import { CODEX_GIT_GRANT, codex } from "./codex.js";
9
10
  import { shq } from "./types.js";
10
11
  const exec = promisify(execFile);
@@ -40,7 +41,7 @@ export const codexSandboxArgs = (p, grant = CODEX_GIT_GRANT) => [
40
41
  `codex sandbox -c 'sandbox_mode="workspace-write"' ${grant} -- sh -c ${shq(SCRIPT)} sh ${[p.control, p.token, p.hostileHook, p.hostileRoot].map(shq).join(" ")}`,
41
42
  ];
42
43
  /** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
43
- export const codexSandboxFor = (grant) => async (p) => (await exec("sh", codexSandboxArgs(p, grant), { cwd: p.worktree, timeout: 20_000, signal: p.signal })).stdout;
44
+ export const codexSandboxFor = (grant) => async (p) => (await exec("sh", codexSandboxArgs(p, grant), { cwd: p.worktree, timeout: 20_000, signal: p.signal, env: withoutHostSession() })).stdout;
44
45
  export const codexSandbox = codexSandboxFor(CODEX_GIT_GRANT);
45
46
  const holds = (path, token) => {
46
47
  try {
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { withoutHostSession } from "../drivers/host-env.js";
2
3
  import { probeVersion } from "./claude-code.js";
3
4
  import { parseWorkerResult } from "./prompt.js";
4
5
  import { channelsFromConfig, MODEL_ID_RE, shq } from "./types.js";
@@ -58,7 +59,7 @@ export const cursorAgent = {
58
59
  // network (1.35s wall vs 0.34s CPU, RESEARCH A1); 15s timeout + fail-open covers offline.
59
60
  // Live-verified 2026-07-10, cursor-agent 2026.07.08.
60
61
  listModels: async () => {
61
- const r = spawnSync("cursor-agent", ["--list-models"], { encoding: "utf8", timeout: 15000 });
62
+ const r = spawnSync("cursor-agent", ["--list-models"], { encoding: "utf8", timeout: 15000, env: withoutHostSession() });
62
63
  return r.error || r.status !== 0 ? [] : parseCursorModels(r.stdout || "");
63
64
  },
64
65
  };
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { withoutHostSession } from "../drivers/host-env.js";
5
6
  import { probeVersion } from "./claude-code.js";
6
7
  import { parseWorkerResult } from "./prompt.js";
7
8
  import { channelsFromConfig, MODEL_ID_RE, shq } from "./types.js";
@@ -130,7 +131,7 @@ export const grok = {
130
131
  // Advisory/doctor-only — never in the dispatch or auth path (types.ts:56-60).
131
132
  // Live-verified 2026-07-11 format, grok 0.2.93, 40-RESEARCH F-1.
132
133
  listModels: async () => {
133
- const r = spawnSync("grok", ["models"], { encoding: "utf8", timeout: 15000 });
134
+ const r = spawnSync("grok", ["models"], { encoding: "utf8", timeout: 15000, env: withoutHostSession() });
134
135
  return r.error || r.status !== 0 ? [] : parseGrokModels(r.stdout || "");
135
136
  },
136
137
  };
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { withoutHostSession } from "../drivers/host-env.js";
5
6
  import { probeVersion } from "./claude-code.js";
6
7
  import { parseWorkerResult } from "./prompt.js";
7
8
  import { runInteractiveSeed } from "../run/interactive-seed.js";
@@ -191,7 +192,7 @@ const kimiTuiLaunch = (model) => {
191
192
  let neverAsk = false;
192
193
  try {
193
194
  const help = spawnSync("kimi", ["--help"], {
194
- encoding: "utf8", timeout: KIMI_HELP_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"],
195
+ encoding: "utf8", timeout: KIMI_HELP_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"], env: withoutHostSession(),
195
196
  });
196
197
  neverAsk = !help.error && help.status === 0 && kimiHasNeverAskOption(help.stdout);
197
198
  }
@@ -287,8 +288,12 @@ const KIMI_SEED = {
287
288
  // cursor glyph; prose never does. Enter accepts the highlighted "Trust this folder" option, and the
288
289
  // two captures above are the reason the cursor+label pair is the fingerprint rather than either
289
290
  // prompt's heading: the same gate ships two different headings.
291
+ // Queue row 198: kimi 2.2.0 renders that gate as one modal whose selected option is "Trust and continue"
292
+ // (KIMI_TRUST_PANE in types.ts), so the 0.29.0 pair above no longer appears and the seat waited out its
293
+ // readiness budget on an unanswered modal. The declaration follows the installed kimi, which updates
294
+ // itself; a kimi older than 2.2.0 renders the 0.29.0 option, which this declaration no longer answers.
290
295
  export const KIMI_TRUST_DIALOG = {
291
- fingerprint: "❯ Trust this folder",
296
+ fingerprint: "❯ Trust and continue",
292
297
  key: "Enter",
293
298
  };
294
299
  // v1.69 T7 / v1.71 T2: thin wrapper over the daemon's generic dispatch path for kimi-only tests.
@@ -317,13 +322,10 @@ export const kimi = {
317
322
  // v1.65 T3: every flag the command builders below hardcode (-S is resumeCommand's) — verified in
318
323
  // `kimi --help` 2026-07-22.
319
324
  hardcodedFlags: { binary: "kimi", flags: ["-p", "--model", "--output-format", "-S"] },
320
- // KNOWN GAP, stated rather than implied: this declaration is consumed by the daemon's trust loop,
321
- // which is entered only AFTER runInteractiveSeed returns — and that seed waits for
322
- // `readinessMatch` for the whole task timeout, which is exactly the window this prompt blocks in
323
- // (run …215). So on a kimi startup pane the declaration is correct and still unreached: the seed
324
- // reports `readiness pattern not seen` with zero keypresses. Answering it during the readiness
325
- // wait is a change to src/run/interactive-seed.ts — WHEN and HOW OFTEN a declaration is answered
326
- // is T19's half of this contract, and src/run may not appear in this task's diff.
325
+ // The seed's readiness wait answers this declaration once per slot before the banner appears
326
+ // (src/run/interactive-seed.ts awaitReadiness); the daemon's post-seed trust loop is the second
327
+ // consumer. Queue row 198: a declaration whose fingerprint the installed kimi no longer renders is
328
+ // never answered, and the seat waits out its readiness budget on the modal (row 199).
327
329
  trustDialog: KIMI_TRUST_DIALOG,
328
330
  // KIMI-02 headless. -p prompt mode + explicit model, NO permission flag: kimi 0.26.0 rejects
329
331
  // -p combined with -y/--auto at argument parse time ("Cannot combine --prompt with --yolo",
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { withoutHostSession } from "../drivers/host-env.js";
5
6
  import { probeVersion } from "./claude-code.js";
6
7
  import { parseWorkerResult } from "./prompt.js";
7
8
  import { channelsFromConfig, MODEL_ID_RE, OPENCODE_BUSY_FRAME_MARKERS, shq, TokenUsageSchema } from "./types.js";
@@ -52,7 +53,7 @@ export const opencode = {
52
53
  // opencode reads its own cache offline; --refresh adds an avoidable network dependency (RESEARCH
53
54
  // anti-pattern). Live-verified 2026-07-10, opencode 1.17.15.
54
55
  listModels: async () => {
55
- const r = spawnSync("opencode", ["models"], { encoding: "utf8", timeout: 15000 });
56
+ const r = spawnSync("opencode", ["models"], { encoding: "utf8", timeout: 15000, env: withoutHostSession() });
56
57
  return r.error || r.status !== 0 ? [] : parseOpencodeModels(r.stdout || "");
57
58
  },
58
59
  // SPEND-09: opencode 1.x writes per-message token deltas to SQLite opencode.db (NOT the dead 0.7.x
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { withoutHostSession } from "../drivers/host-env.js";
5
6
  import { probeVersion } from "./claude-code.js";
6
7
  import { parseWorkerResult } from "./prompt.js";
7
8
  import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
@@ -142,7 +143,7 @@ export const pi = {
142
143
  // The store is local and independently useful: preserve its advisory even when the network/auth
143
144
  // capability call below fails. A failed model listing must not erase evidence of a substitution.
144
145
  const drift = servedModelNote();
145
- const r = spawnSync("pi", ["--list-models"], { encoding: "utf8", timeout: 15000 });
146
+ const r = spawnSync("pi", ["--list-models"], { encoding: "utf8", timeout: 15000, env: withoutHostSession() });
146
147
  if (r.error || r.status !== 0) {
147
148
  return drift ? { ...h, note: `${h.note ? `${h.note}; ` : ""}${drift}` } : h;
148
149
  }
@@ -183,7 +184,7 @@ export const pi = {
183
184
  // v1.5 MODEL-01: fail OPEN to [] (detection is advisory — unlike gates' fail-closed posture).
184
185
  // spawnSync mirrors probeVersion; 15s timeout. Live-verified 2026-07-10, pi 0.80.3.
185
186
  listModels: async () => {
186
- const r = spawnSync("pi", ["--list-models"], { encoding: "utf8", timeout: 15000 });
187
+ const r = spawnSync("pi", ["--list-models"], { encoding: "utf8", timeout: 15000, env: withoutHostSession() });
187
188
  return r.error || r.status !== 0 ? [] : parsePiModels(r.stdout || "");
188
189
  },
189
190
  collectUsage(cwd, sinceMs) {
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { withoutHostSession } from "../drivers/host-env.js";
2
3
  import { parseWorkerResult } from "./prompt.js";
3
4
  import { channelsFromConfig, shq, } from "./types.js";
4
5
  export const QWEN_VERSION_IDENTITY = /^\d+\.\d+\.\d+/;
@@ -149,7 +150,7 @@ export function parseQwenResult(raw, nonce) {
149
150
  return { ...parseWorkerResult(decoded.assistantText, nonce), raw };
150
151
  }
151
152
  function probeQwen() {
152
- const result = spawnSync("qwen", ["--version"], { encoding: "utf8", timeout: 10_000 });
153
+ const result = spawnSync("qwen", ["--version"], { encoding: "utf8", timeout: 10_000, env: withoutHostSession() });
153
154
  if (result.error || result.status !== 0)
154
155
  return { installed: false, authed: false, models: [] };
155
156
  const version = (result.stdout || result.stderr).trim().split("\n")[0] ?? "";
@@ -10,6 +10,7 @@ import { sh } from "../run/git.js";
10
10
  import { FakeAdapter } from "./fake.js";
11
11
  import { parseWorkerResult } from "./prompt.js";
12
12
  import { sealHerdrEnv } from "../drivers/subprocess.js";
13
+ import { withoutHostSession } from "../drivers/host-env.js";
13
14
  import { catalogEntries, isNativeCliDrive, projectCliEntries, SHIPPED_CLI_CATALOG, } from "./catalog.js";
14
15
  import { channelKey, channelsFromConfig, modelAuthed, MODEL_ID_RE, MODEL_PROBE_ERRORS, QUOTA_PHRASE_RE, shq, STATUS_LABEL, } from "./types.js";
15
16
  // Compatibility projection for callers/tests that only need shipped advisory names. The literal
@@ -180,7 +181,7 @@ export function formatCandidateCliRow({ binary, path }) {
180
181
  // so no drift verdict piles onto the existing auth report.
181
182
  export function probeHelpText(bin) {
182
183
  try {
183
- const r = spawnSync(bin, ["--help"], { encoding: "utf8", timeout: 10000 });
184
+ const r = spawnSync(bin, ["--help"], { encoding: "utf8", timeout: 10000, env: withoutHostSession() });
184
185
  if (r.error || r.status !== 0)
185
186
  return undefined;
186
187
  return `${r.stdout || ""}\n${r.stderr || ""}`;
@@ -118,6 +118,7 @@ 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
120
  export declare const KIMI_TRUST_PANE: string;
121
+ export declare const KIMI_TRUST_PANE_029: string;
121
122
  export declare const KIMI_MCP_TRUST_PANE: string;
122
123
  export declare const RECORDED_TRUST_PANES: readonly string[];
123
124
  export declare const APPROVED_TRUST_FINGERPRINTS: readonly string[];
@@ -59,11 +59,25 @@ 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 198, kimi 2.2.0's folder-trust modal on a fresh task worktree (run 1394 T3, read off the
63
+ // Orca screen before its one Enter). The worktree path is shortened as above; every other row is as
64
+ // rendered. 2.2.0 renamed the selected option, so the 0.29.0 fingerprint below never matched it and the
65
+ // seat sat on this modal for 21 minutes.
66
+ export const KIMI_TRUST_PANE = [
67
+ "Trust this folder?",
68
+ "↑↓ navigate · Enter select · Esc exit",
69
+ "/Users/…/.tickmarkr/worktrees.noindex/tickmarkr-run-20261017-122301-…1394--T3",
70
+ "No project integrations or instructions to activate.",
71
+ "Trust is remembered for this folder, including future project config changes.",
72
+ "Tool approvals follow your permission settings.",
73
+ " ❯ Trust and continue",
74
+ " Exit",
75
+ ].join("\n");
62
76
  // OBS-358, live pane wW:p2TB of run-20260805-121252 — the dialog that cost 30 minutes — quoted from
63
77
  // the record holding all five lines, .overseer/REPAIR-v186/GATE-T28-1.md (OBSERVATIONS.md abridges
64
78
  // it to the first three). "← highlighted" is the observer's annotation of the selected row, kept
65
- // exactly as recorded rather than tidied away.
66
- export const KIMI_TRUST_PANE = [
79
+ // exactly as recorded rather than tidied away. kimi 0.29.0; superseded by the 2.2.0 capture above.
80
+ export const KIMI_TRUST_PANE_029 = [
67
81
  "Trust this folder?",
68
82
  " /Users/…/.tickmarkr/worktrees.noindex/tickmarkr-run-20260805-121252--T29",
69
83
  "❯ Trust this folder ← highlighted",
@@ -77,7 +91,7 @@ export const KIMI_MCP_TRUST_PANE = [
77
91
  " ❯ Trust this folder / Don't trust",
78
92
  ].join("\n");
79
93
  export const RECORDED_TRUST_PANES = [
80
- CLAUDE_TRUST_PANE, CODEX_TRUST_PANE, CURSOR_TRUST_PANE, KIMI_TRUST_PANE, KIMI_MCP_TRUST_PANE,
94
+ CLAUDE_TRUST_PANE, CODEX_TRUST_PANE, CURSOR_TRUST_PANE, KIMI_TRUST_PANE, KIMI_TRUST_PANE_029, KIMI_MCP_TRUST_PANE,
81
95
  ];
82
96
  // v1.89 T1 / OBS-414 round 4 — EXACT, not "leading bytes of a recorded line". Round 3 accepted any
83
97
  // prefix of a captured line, and every prefix of a trust prompt is also a substring of unrelated
@@ -96,6 +110,7 @@ export const APPROVED_TRUST_FINGERPRINTS = [
96
110
  "Do you trust the contents of this directory?", // codex 0.144.6, OBS-137
97
111
  "Workspace Trust Required", // cursor-agent, OBS-19
98
112
  "❯ Trust this folder", // kimi 0.29.0, OBS-358 + OBS-406 — the cursor glyph is load-bearing
113
+ "❯ Trust and continue", // kimi 2.2.0, queue row 198 — the same gate with its option renamed
99
114
  ];
100
115
  const isApprovedFingerprint = (fingerprint) => APPROVED_TRUST_FINGERPRINTS.includes(fingerprint)
101
116
  && RECORDED_TRUST_PANES.some((pane) => pane.includes(fingerprint));
@@ -52,6 +52,23 @@ type OrcaCapability = {
52
52
  * stderr (Electron timestamps), so the row is byte-stable across runs.
53
53
  */
54
54
  export declare function probeOrcaCapability(cwd: string, opts?: Pick<DoctorOpts, "orcaStatusProbe" | "orcaHooksStatusProbe" | "resolveOrcaBinary" | "orcaEnv">): Promise<OrcaCapability>;
55
+ /** D-2102 add.39: the seams for a Node without stripTypeScriptTypes, and for a child that does not answer in time. */
56
+ export type ParseSeam = {
57
+ strip?: "absent";
58
+ timeoutMs?: number;
59
+ };
60
+ /**
61
+ * D-2102 add.37 and add.39: the config's text with its TypeScript types erased (offsets unchanged), once a real
62
+ * parser accepts it — or why not. The source reaches PARSE_CHILD on stdin through process.execPath with no shell
63
+ * and no temp file. A spawn error, a timeout, a non-zero exit, a missing or malformed answer, or erased text of
64
+ * another length reads "doctor could not check <file>'s syntax"; a TypeScript config on a Node without
65
+ * stripTypeScriptTypes (below 22.13) reads unverifiable; nothing here throws.
66
+ */
67
+ export declare function realParse(source: string, configPath: string, seam?: ParseSeam): {
68
+ text: string;
69
+ } | {
70
+ why: string;
71
+ };
55
72
  export declare function runnerIgnoreFinding(cwd: string): {
56
73
  verdict: "pass" | "warn";
57
74
  detail: string;
@@ -61,16 +78,43 @@ export declare function runnerIgnoreFinding(cwd: string): {
61
78
  * repos a hand-edit (SentioQ run 2 paid a whole run first); --fix applies the ignore
62
79
  * where a safe textual edit exists and refuses loudly otherwise.
63
80
  *
64
- * Safety contract: only three write shapes, each verified by re-running
65
- * runnerIgnoreFinding afterward — a write that does not turn the row green is
66
- * REVERTED and reported as manual. Never touches package.json (jest block → manual).
67
- * ponytail: text-level edits, same ceilings as the finding; the verify-then-revert
68
- * loop is what makes them safe to ship.
81
+ * Safety contract (queue row 197; D-2102 add.8 and add.30): the canonical entries the config's own literal
82
+ * ignore list lacks go into that array as complete string-literal elements (never a coverage array or a
83
+ * scalar), beside the state dir's canonical entry, else first; a jest element that is exactly an unanchored
84
+ * entry for either directory (the SentioQ run-2 shape) is replaced by its canonical twin; a jest config with
85
+ * no testPathIgnorePatterns gets the property, preserving jest's default /node_modules/ ignore, as before
86
+ * this change (a JSON config with its key quoted), and a vitest config without test.exclude gets one that
87
+ * keeps Vitest 3.2.7's default excludes ahead of the two entries — inside a new test object where it has
88
+ * none (D-2102 add.31). A config doctor could not read or any other element is manual with the file
89
+ * byte-unchanged. The write is verified by
90
+ * re-running the same rule on the written file (and JSON.parse for a .json config); a write that does not
91
+ * turn the row green is REVERTED byte-for-byte and reported as manual. Never touches package.json.
69
92
  */
70
93
  export declare function applyRunnerIgnore(cwd: string): {
71
94
  action: "none" | "wrote" | "manual";
72
95
  detail: string;
73
96
  };
97
+ type EnvLike = NodeJS.ProcessEnv | Record<string, string | undefined>;
98
+ /**
99
+ * Queue row 196 (D-2102 add.1): the host decides where seats run. herdr or Orca markers (classifyHost)
100
+ * name that driver; with neither, seats run headless on the subprocess driver with no daemon board.
101
+ * Only the exact claude-desktop entry point names Claude Desktop — a cli entry point is a terminal seat
102
+ * and no Claude variable alone is a host. Panes configured where no pane host exists cannot be honored.
103
+ */
104
+ export declare function hostRows(env: EnvLike, llm: "pane" | "headless"): {
105
+ verdict: "pass" | "warn";
106
+ key: string;
107
+ detail: string;
108
+ }[];
109
+ /**
110
+ * Queue row 196 (D-2102 add.1): gates inherit the launching locale. The effective value is LC_ALL, else
111
+ * LC_CTYPE, else LANG (an empty value counts as unset). Unset, C or POSIX is the C-locale fallback;
112
+ * C.UTF-8 is a UTF-8 locale and passes. Advisory only — doctor never sets or writes a locale.
113
+ */
114
+ export declare function localeFinding(env: EnvLike): {
115
+ verdict: "pass" | "warn";
116
+ detail: string;
117
+ };
74
118
  /**
75
119
  * Q140s(b) / operator directive (D-OBS-10): the repo's package manager must resolve in
76
120
  * THIS process's environment — the same class of environment the daemon spawns gates