tickmarkr 2.8.4 → 2.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/adapters/claude-code.js +3 -2
  2. package/dist/adapters/codex-commit-check.d.ts +4 -3
  3. package/dist/adapters/codex-commit-check.js +44 -29
  4. package/dist/adapters/codex.d.ts +2 -2
  5. package/dist/adapters/codex.js +38 -32
  6. package/dist/adapters/cursor-agent.js +2 -1
  7. package/dist/adapters/grok.js +2 -1
  8. package/dist/adapters/kimi.js +11 -9
  9. package/dist/adapters/model-lints.js +4 -3
  10. package/dist/adapters/opencode.js +2 -1
  11. package/dist/adapters/pi.js +3 -2
  12. package/dist/adapters/qwen.js +2 -1
  13. package/dist/adapters/registry.js +2 -1
  14. package/dist/adapters/types.d.ts +10 -1
  15. package/dist/adapters/types.js +18 -3
  16. package/dist/cli/commands/approve.js +9 -28
  17. package/dist/cli/commands/doctor.d.ts +49 -5
  18. package/dist/cli/commands/doctor.js +956 -94
  19. package/dist/cli/commands/fleet.js +11 -3
  20. package/dist/cli/commands/init.js +27 -19
  21. package/dist/cli/commands/status.js +9 -6
  22. package/dist/cli/commands/verify.js +12 -3
  23. package/dist/compile/gsd.js +4 -3
  24. package/dist/compile/index.js +4 -1
  25. package/dist/compile/native.js +8 -6
  26. package/dist/config/config.d.ts +15 -0
  27. package/dist/config/config.js +67 -5
  28. package/dist/drivers/host-env.d.ts +3 -0
  29. package/dist/drivers/host-env.js +47 -0
  30. package/dist/drivers/orca.js +3 -1
  31. package/dist/drivers/subprocess.d.ts +1 -1
  32. package/dist/drivers/subprocess.js +4 -3
  33. package/dist/gates/baseline.d.ts +1 -1
  34. package/dist/gates/baseline.js +4 -3
  35. package/dist/gates/cache.js +2 -4
  36. package/dist/graph/schema.js +3 -1
  37. package/dist/run/daemon.js +54 -4
  38. package/dist/run/git-trust.d.ts +11 -0
  39. package/dist/run/git-trust.js +1 -1
  40. package/dist/run/git.d.ts +13 -1
  41. package/dist/run/git.js +34 -14
  42. package/dist/run/journal.d.ts +15 -1
  43. package/dist/run/journal.js +83 -17
  44. package/dist/run/lock.d.ts +5 -0
  45. package/dist/run/lock.js +202 -9
  46. package/dist/run/protocol.d.ts +1 -1
  47. package/dist/tui/cockpit/live-store.js +12 -2
  48. package/package.json +1 -1
  49. package/skills/tickmarkr-auto/SKILL.md +65 -21
  50. package/skills/tickmarkr-loop/SKILL.md +65 -21
  51. package/skills/tickmarkr-overseer/SKILL.md +88 -32
@@ -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 {
@@ -10,6 +10,7 @@ export interface CodexCommitProbe {
10
10
  hostileRoot: string;
11
11
  token: string;
12
12
  signal?: AbortSignal;
13
+ repoRoot?: string;
13
14
  }
14
15
  /** Runs the probe script inside the Codex sandbox and resolves its stdout. Injected in tests. */
15
16
  export type CodexSandbox = (probe: CodexCommitProbe) => Promise<string>;
@@ -17,10 +18,10 @@ export interface CodexCommitResult {
17
18
  status: CodexCommitStatus;
18
19
  detail: string;
19
20
  }
20
- /** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
21
+ /** Queue row 2 (D-2112, D-2114): the fixture uses the same literal grant as a worker. */
21
22
  export declare const codexSandboxArgs: (p: CodexCommitProbe, grant?: string) => string[];
22
- /** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
23
- export declare const codexSandboxFor: (grant: string) => CodexSandbox;
23
+ /** Queue row 2 (D-2112): tests may supply a mutant; production renders each fixture. */
24
+ export declare const codexSandboxFor: (grant?: string) => CodexSandbox;
24
25
  export declare const codexSandbox: CodexSandbox;
25
26
  export declare function probeCodexCommit(repoRoot: string, sandbox?: CodexSandbox, signal?: AbortSignal): Promise<CodexCommitResult>;
26
27
  /** A persisted verdict outside the closed table (an older or newer doctor.json) reads unknown, never allowed. */
@@ -4,18 +4,20 @@ import { existsSync, mkdtempSync, readFileSync, rmdirSync, rmSync } from "node:f
4
4
  import { tmpdir } from "node:os";
5
5
  import { basename, dirname, join } from "node:path";
6
6
  import { promisify } from "node:util";
7
- import { assertGitTrust, GitTrustRefusal, protectedGitEnv } from "../run/git-trust.js";
8
- import { CODEX_GIT_GRANT, codex } from "./codex.js";
7
+ import { assertGitTrust, GitTrustRefusal } from "../run/git-trust.js";
8
+ import { shGitOk } from "../run/git.js";
9
+ import { withoutHostSession } from "../drivers/host-env.js";
10
+ import { renderCodexGitGrant, codex } from "./codex.js";
9
11
  import { shq } from "./types.js";
10
12
  const exec = promisify(execFile);
11
- const git = async (cwd, ...args) => (await exec("git", args, { cwd })).stdout.trim();
12
- // v2.6.7 T4: host git after the sandbox has had the fixture — validation and cleanup — runs the uncached trust
13
- // check and the forced child config on every call, so seat-rewritten metadata can neither run a config program
14
- // nor answer for the probe's commit.
15
- const hostGit = async (cwd, ...args) => {
16
- assertGitTrust(cwd);
17
- return (await exec("git", args, { cwd, env: protectedGitEnv(process.env) })).stdout.trim();
18
- };
13
+ // v2.6.7 T4 → queue row 2 item b (D-2112): every fixture git — setup, validation and cleanup — is tickmarkr's own git
14
+ // (shGitOk): the uncached trust check, the forced inert fsmonitor and hooks and, in a linked checkout, the pin, so
15
+ // seat-rewritten metadata can neither run a config program or hook nor answer for the probe's commit. A trust refusal
16
+ // rejects with GitTrustRefusal naming the path and is reported as an unknown verdict, never folded into another answer.
17
+ const hostGit = async (cwd, args, own) => (await shGitOk(["git", ...args].map(shq).join(" "), cwd, own)).trim();
18
+ /** An ordinary git failure is `fallback`; a trust refusal propagates. */
19
+ const unlessRefused = (fallback) => (error) => { if (error instanceof GitTrustRefusal)
20
+ throw error; return fallback; };
19
21
  /** The fixture checkout's trust after the sandbox returned or threw: a refusal is reported with its path. */
20
22
  const fixtureRefusal = (worktree) => {
21
23
  try {
@@ -34,14 +36,14 @@ const SCRIPT = `printf %s "$2" > "$1" && echo control=ok || echo control=fail
34
36
  git add -- "$1" && git -c core.hooksPath=/dev/null -c core.logAllRefUpdates=true -c commit.gpgsign=false -c user.name=tickmarkr -c user.email=probe@tickmarkr.invalid commit -q -m "tickmarkr-probe $2" && echo commit=ok || echo commit=fail
35
37
  (set -C; printf %s "$2" > "$3") 2>/dev/null && echo hook=written || echo hook=denied
36
38
  (set -C; printf %s "$2" > "$4") 2>/dev/null && echo root=written || echo root=denied`;
37
- /** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
38
- export const codexSandboxArgs = (p, grant = CODEX_GIT_GRANT) => [
39
+ /** Queue row 2 (D-2112, D-2114): the fixture uses the same literal grant as a worker. */
40
+ export const codexSandboxArgs = (p, grant = renderCodexGitGrant({ cwd: p.worktree, branch: p.branch, repoRoot: p.repoRoot ?? p.commonDir })) => [
39
41
  "-c",
40
42
  `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
43
  ];
42
- /** 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 codexSandbox = codexSandboxFor(CODEX_GIT_GRANT);
44
+ /** Queue row 2 (D-2112): tests may supply a mutant; production renders each fixture. */
45
+ export const codexSandboxFor = (grant) => async (p) => (await exec("sh", codexSandboxArgs(p, grant), { cwd: p.worktree, timeout: 20_000, signal: p.signal, env: withoutHostSession() })).stdout;
46
+ export const codexSandbox = codexSandboxFor();
45
47
  const holds = (path, token) => {
46
48
  try {
47
49
  return readFileSync(path, "utf8") === token;
@@ -66,18 +68,28 @@ const contradictory = (out) => {
66
68
  }
67
69
  return false;
68
70
  };
69
- const tipOf = async (repo, ref) => hostGit(repo, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`).catch(() => undefined);
71
+ const tipOf = async (repo, ref) => hostGit(repo, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(unlessRefused(undefined));
70
72
  // This probe's commit and nothing else: one parent (the fixture base), its token in the subject and the control.
71
73
  const isProbeCommit = async (repo, commit, base, token) => {
72
74
  try {
73
- return await hostGit(repo, "log", "-1", "--format=%P %s", commit) === `${base} tickmarkr-probe ${token}`
74
- && await hostGit(repo, "cat-file", "blob", `${commit}:${CONTROL}`) === token;
75
+ return await hostGit(repo, ["log", "-1", "--format=%P %s", commit]) === `${base} tickmarkr-probe ${token}`
76
+ && await hostGit(repo, ["cat-file", "blob", `${commit}:${CONTROL}`]) === token;
75
77
  }
76
- catch {
77
- return false;
78
+ catch (e) {
79
+ return unlessRefused(false)(e);
78
80
  }
79
81
  };
80
82
  export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal) {
83
+ // queue row 2 item b: cleanup stays best effort for ordinary git failures, but a trust refusal there is never
84
+ // folded into the verdict already reached — it is reported as unknown naming its path (an escape still dominates)
85
+ let refusal;
86
+ const result = await probeFixture(repoRoot, sandbox, signal, (e) => { refusal ??= e; });
87
+ return refusal && result.status !== "escape" ? unknown(`fixture git refused (${refusal.message})`) : result;
88
+ }
89
+ async function probeFixture(repoRoot, sandbox, signal, refused) {
90
+ /** A cleanup git's ordinary failure is `fallback`; its trust refusal is reported to the caller, then `fallback`. */
91
+ const cleanup = (fallback) => (e) => { if (e instanceof GitTrustRefusal)
92
+ refused(e); return fallback; };
81
93
  const token = randomUUID();
82
94
  const parent = mkdtempSync(join(tmpdir(), "tickmarkr-codex-commit-"));
83
95
  // git names the fixture's metadata directory after the checkout's basename; a unique one makes that
@@ -90,21 +102,22 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
90
102
  let hostile = [];
91
103
  let sandboxed = false;
92
104
  try {
93
- const commonDir = await git(repoRoot, "rev-parse", "--path-format=absolute", "--git-common-dir");
105
+ const commonDir = await hostGit(repoRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
94
106
  lock = join(commonDir, "worktrees", basename(worktree), "index.lock");
95
107
  const hostileHook = join(commonDir, "hooks", branch), hostileRoot = join(commonDir, branch);
96
108
  hostile = [hostileHook, hostileRoot];
97
109
  if (hostile.some((m) => existsSync(m)))
98
110
  return unknown("a foreign file already holds a hostile probe path");
99
- base = await git(repoRoot, "rev-parse", "--verify", "HEAD^{commit}");
100
- await git(repoRoot, "worktree", "add", "-b", branch, worktree, base);
101
- if (join(await git(worktree, "rev-parse", "--absolute-git-dir"), "index.lock") !== lock)
111
+ base = await hostGit(repoRoot, ["rev-parse", "--verify", "HEAD^{commit}"]);
112
+ // the add declares the branch capability: a result refused after its child ran is rolled back (checkout and branch)
113
+ await hostGit(repoRoot, ["worktree", "add", "-b", branch, worktree, base], { createsWorktree: "branch", rollback: { dir: worktree, branch: { name: branch } } });
114
+ if (join(await hostGit(worktree, ["rev-parse", "--absolute-git-dir"]), "index.lock") !== lock)
102
115
  return unknown("the fixture worktree metadata is not where git was expected to put it");
103
116
  const control = join(worktree, CONTROL);
104
117
  if (existsSync(lock))
105
118
  return unknown("a foreign index.lock already holds the fixture worktree");
106
119
  sandboxed = true;
107
- const out = await sandbox({ worktree, commonDir, branch, control, lock, hostileHook, hostileRoot, token, signal });
120
+ const out = await sandbox({ worktree, commonDir, branch, control, lock, hostileHook, hostileRoot, token, signal, repoRoot });
108
121
  // An escape on disk dominates; every other verdict needs consistent receipts AND disk — and trusted metadata.
109
122
  const escape = escaped(hostile, token);
110
123
  if (escape)
@@ -144,6 +157,8 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
144
157
  const refused = sandboxed ? fixtureRefusal(worktree) : undefined;
145
158
  if (refused)
146
159
  return refused;
160
+ if (e instanceof GitTrustRefusal)
161
+ return unknown(`fixture git refused (${e.message})`);
147
162
  return unknown(signal?.aborted ? "probe cancelled" : `probe did not run (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`);
148
163
  }
149
164
  finally {
@@ -158,7 +173,7 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
158
173
  // prune — that would take an earlier probe's preserved foreign lock (or anyone's prunable worktree).
159
174
  const foreign = lock !== undefined && existsSync(lock);
160
175
  if (lock && !foreign) {
161
- await hostGit(repoRoot, "worktree", "remove", "--force", worktree).catch(() => undefined);
176
+ await hostGit(repoRoot, ["worktree", "remove", "--force", worktree]).catch(cleanup(undefined));
162
177
  // a failed `worktree add` leaves metadata git no longer removes by path: drop this probe's own directory
163
178
  rmSync(dirname(lock), { recursive: true, force: true });
164
179
  try {
@@ -169,9 +184,9 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
169
184
  // The fixture branch and its reflog go only while the tip is still this probe's (the base or its own
170
185
  // commit), and update-ref's old-value check refuses if anyone moves it between this read and the delete.
171
186
  // Metadata preserved for a foreign lock keeps its branch too: that checkout's HEAD still names it.
172
- const tip = base === undefined || foreign ? undefined : await tipOf(repoRoot, ref);
173
- if (tip && base && (tip === base || await isProbeCommit(repoRoot, tip, base, token))) {
174
- await hostGit(repoRoot, "update-ref", "-d", ref, tip).catch(() => undefined);
187
+ const tip = base === undefined || foreign ? undefined : await tipOf(repoRoot, ref).catch(cleanup(undefined));
188
+ if (tip && base && (tip === base || await isProbeCommit(repoRoot, tip, base, token).catch(cleanup(false)))) {
189
+ await hostGit(repoRoot, ["update-ref", "-d", ref, tip]).catch(cleanup(undefined));
175
190
  }
176
191
  rmSync(parent, { recursive: true, force: true });
177
192
  }
@@ -1,11 +1,11 @@
1
- import { type ModelRetirement, type TrustDialog, type TrustVerdict, type WorkerAdapter } from "./types.js";
1
+ import { type ModelRetirement, type TrustDialog, type TrustVerdict, type WorkerAdapter, type WorkerLaunchContext } from "./types.js";
2
2
  export declare function readCodexModelsCache(path?: string): {
3
3
  models: string[];
4
4
  fetchedAt?: string;
5
5
  retirements?: Record<string, ModelRetirement | null>;
6
6
  hidden?: string[];
7
7
  };
8
- export declare const CODEX_GIT_GRANT = "-c \"sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=${tkr_common%?/} && tkr_gitdir=${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=${tkr_head%/*};; (*) tkr_leaf=;; esac && [ \"$(printf %s \"$tkr_common$tkr_gitdir$tkr_leaf\" | LC_ALL=C tr -d '\\0-\\37\\177\"\\\\')\" = \"$tkr_common$tkr_gitdir$tkr_leaf\" ] && { [ \"$tkr_gitdir\" = \"$tkr_common\" ] || printf '\"%s\",' \"$tkr_gitdir\"; } && printf '\"%s/objects\"' \"$tkr_common\" && { [ -z \"$tkr_leaf\" ] || printf ',\"%s/%s\",\"%s/logs/%s\"' \"$tkr_common\" \"$tkr_leaf\" \"$tkr_common\" \"$tkr_leaf\"; })]\"";
8
+ export declare function renderCodexGitGrant(ctx?: Partial<WorkerLaunchContext>): string;
9
9
  export declare const CODEX_TRUST_DIALOG: TrustDialog;
10
10
  export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
11
11
  export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
+ import { checkedAuthority } from "../run/git-trust.js";
4
5
  import { probeVersion } from "./claude-code.js";
5
6
  import { parseWorkerResult } from "./prompt.js";
6
7
  import { channelsFromConfig, declareInputBox, MODEL_ID_RE, promptFitsArgv, shq, TokenUsageSchema } from "./types.js";
@@ -100,32 +101,37 @@ function codexRetirement(upgrade) {
100
101
  // the successor id comes from an external file and is rendered in lints — same charset gate as slugs
101
102
  return typeof u.model === "string" && MODEL_ID_RE.test(u.model) ? { retiresAt: u.retirement_at, successor: u.model } : undefined;
102
103
  }
103
- // tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
104
- // workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
105
- // P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the launching shell at the
106
- // worktree cwd, for Codex WORKERS only: v2.6.7 T4 (D-1046) — invoke() (the headless worker form, including
107
- // the interactive size fallback) and interactiveCommand carry it; headlessCommand, which judge, review,
108
- // consult and the model probe launch, carries no writable_roots at all (those seats never commit).
109
- // v2.6.6 T9 (K, D-912) → v2.8.0 T1 (rows 17/18, D-910): the grant is a LEAF of what a linked-worktree branch
110
- // commit writes, never the shared hooks/config (the shipped [common] and D-910's [common, gitdir] let a seat
111
- // write <common>/hooks and <common>/config, which unsandboxed git later runs) and never whole common/refs and
112
- // common/logs (which let a seat move main, any branch or tag, or the stash). The closed root table, in order:
113
- // - the worktree's own gitdir, only when it differs from the common dir (an ordinary checkout gets none);
114
- // - <common>/objects;
115
- // - only when HEAD is a symbolic ref refs/heads/<B>: <common>/refs/heads/<D> and <common>/logs/refs/heads/<D>,
116
- // <D> being <B> without its last slash-separated segment (refs/heads itself when <B> holds no slash);
117
- // a detached HEAD gets no refs or reflog root.
118
- // A directory outside any repository, any failed git read, or any root holding a character of the closed
119
- // forbidden set (U+0000 to U+001F, U+007F DEL, the double quote, the backslash) yields the EMPTY array `[]`:
120
- // no git write root, so a commit fails closed — never a partial array, an escaped string or codex's
121
- // literal-string fallback. Every emitted root is therefore a TOML basic string that parses back to the exact
122
- // path (git's own trailing newline is stripped through a sentinel, never a path's). No common-root fallback;
123
- // packed-refs is not granted (packed-ref ops fail closed); stash, tags, notes and branches outside the leaf
124
- // are denied. Declared (queue row 151): sibling refs in the same leaf (other task branches, the run's integration
125
- // branch under the default prefix) stay writable; since 2.8.1 a write to the integration ref parks the merge and is
126
- // never published.
127
- // doctor's probe (codex-commit-check.ts) executes these exact bytes; it never rebuilds the grant.
128
- export const CODEX_GIT_GRANT = `-c "sandbox_workspace_write.writable_roots=[$(tkr_common=$(git rev-parse --path-format=absolute --git-common-dir && echo /) && tkr_gitdir=$(git rev-parse --absolute-git-dir && echo /) && tkr_common=\${tkr_common%?/} && tkr_gitdir=\${tkr_gitdir%?/} && { tkr_head=$(git symbolic-ref -q HEAD); [ $? -le 1 ]; } && case $tkr_head in (refs/heads/*) tkr_leaf=\${tkr_head%/*};; (*) tkr_leaf=;; esac && [ "$(printf %s "$tkr_common$tkr_gitdir$tkr_leaf" | LC_ALL=C tr -d '\\0-\\37\\177"\\\\')" = "$tkr_common$tkr_gitdir$tkr_leaf" ] && { [ "$tkr_gitdir" = "$tkr_common" ] || printf '"%s",' "$tkr_gitdir"; } && printf '"%s/objects"' "$tkr_common" && { [ -z "$tkr_leaf" ] || printf ',"%s/%s","%s/logs/%s"' "$tkr_common" "$tkr_leaf" "$tkr_common" "$tkr_leaf"; })]"`;
104
+ // Queue row 2 item a (D-2112, D-2114): tickmarkr renders literal roots from the checked authority,
105
+ // never a launch-shell git lookup. The worker's common dir must be the run repository's trusted one.
106
+ // Closed table: gitdir only when distinct, common objects, and the assigned branch's ref/reflog leaf
107
+ // only while its HEAD still names that branch. Ordinary checkouts omit gitdir; packed-refs stays denied.
108
+ // Any refusal, absent context or forbidden byte yields []; permitted bytes survive TOML and shq exactly.
109
+ // Queue row 151: sibling refs inside the assigned leaf remain writable.
110
+ export function renderCodexGitGrant(ctx) {
111
+ let roots = [];
112
+ try {
113
+ if (!ctx?.cwd || !ctx.branch || !ctx.repoRoot)
114
+ throw new Error("missing launch context");
115
+ const authority = checkedAuthority(ctx.cwd);
116
+ const run = checkedAuthority(ctx.repoRoot);
117
+ if (!authority || !run || authority.common !== run.common)
118
+ throw new Error("foreign git authority");
119
+ const { gitdir, common } = authority;
120
+ roots = [...(gitdir === common ? [] : [gitdir]), `${common}/objects`];
121
+ const head = readFileSync(join(gitdir, "HEAD"), "utf8");
122
+ if (head === `ref: refs/heads/${ctx.branch}\n` || head === `ref: refs/heads/${ctx.branch}`) {
123
+ const ref = `refs/heads/${ctx.branch}`;
124
+ const leaf = ref.slice(0, ref.lastIndexOf("/"));
125
+ roots.push(`${common}/${leaf}`, `${common}/logs/${leaf}`);
126
+ }
127
+ if (roots.some(root => /[\x00-\x1f\x7f"\\]/.test(root)))
128
+ roots = [];
129
+ }
130
+ catch {
131
+ roots = [];
132
+ }
133
+ return `-c ${shq(`sandbox_workspace_write.writable_roots=[${roots.map(root => `"${root}"`).join(",")}]`)}`;
134
+ }
129
135
  // OBS-125: codex 0.144.x gates enabled hooks behind PER-PROJECT-PATH hook trust — a NEW gate distinct
130
136
  // from the directory trust seedCodexTrust seeds. Every worker runs in a fresh ephemeral worktree, an
131
137
  // untrusted project, so codex re-prompts "Hooks need review" and the interactive worker stalls forever
@@ -275,7 +281,7 @@ export function codexMcpSuppressionFlags(configPath) {
275
281
  }
276
282
  return flags.join(" ");
277
283
  }
278
- // The one headless form: `grant` is "" for every non-worker seat and ` ${CODEX_GIT_GRANT}` for the worker.
284
+ // Queue row 2 (D-2112): non-worker seats omit the grant; workers carry the rendered argument.
279
285
  const codexExec = (promptFile, model, effort, grant) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()}${grant}${codexEffortFlag(effort)} --model ${shq(model)} - < ${shq(promptFile)}`;
280
286
  export const codex = {
281
287
  id: "codex",
@@ -286,7 +292,7 @@ export const codex = {
286
292
  probe: async () => probeVersion("codex"),
287
293
  channels: (cfg) => channelsFromConfig("codex", cfg),
288
294
  // v1.65 T3: every flag the command builder below hardcodes (incl. codexMcpSuppressionFlags' -c/
289
- // --disable and CODEX_GIT_GRANT's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
295
+ // --disable and renderCodexGitGrant's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
290
296
  hardcodedFlags: { binary: "codex", flags: ["--sandbox", "-a", "-s", "--model", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
291
297
  // --sandbox workspace-write is the autonomous sandbox mode (codex v0.144.1+)
292
298
  // MCP suppression built per dispatch (config can change between runs) — see codexMcpSuppressionFlags.
@@ -304,11 +310,11 @@ export const codex = {
304
310
  // autonomous approval policy (exec has no approvals to configure).
305
311
  // OBS-930 (Linux): the inlined prompt is ONE argv string and Linux caps one at 131072 bytes, so a
306
312
  // prompt over promptArgvCeiling() returns null → worker-mode-fallback → the headless form (types.ts).
307
- interactiveCommand: (promptFile, model, effort) => promptFitsArgv(promptFile)
308
- ? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${CODEX_GIT_GRANT}${codexEffortFlag(effort)} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
313
+ interactiveCommand: (promptFile, model, effort, ctx) => promptFitsArgv(promptFile)
314
+ ? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${renderCodexGitGrant(ctx)}${codexEffortFlag(effort)} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
309
315
  : null,
310
- invoke(task, _cwd, a, ctx) {
311
- return { command: codexExec(ctx.promptFile, a.model, a.effort, ` ${CODEX_GIT_GRANT}`) };
316
+ invoke(task, cwd, a, ctx) {
317
+ return { command: codexExec(ctx.promptFile, a.model, a.effort, ` ${renderCodexGitGrant({ ...ctx, cwd })}`) };
312
318
  },
313
319
  parse: parseWorkerResult,
314
320
  // v1.22 T5: seed [projects."<repoRoot>"] trust_level="trusted" so fresh worktrees never stall on
@@ -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",
@@ -1,8 +1,8 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import { join } from "node:path";
4
3
  import { DEFAULT_CONFIG, TIER_RANK } from "../config/config.js";
5
4
  import { filesGlob } from "../graph/files-glob.js";
5
+ import { gitSync } from "../run/git.js";
6
6
  import { piModelVendor } from "./pi.js";
7
7
  import { buildTaskPrompt } from "./prompt.js";
8
8
  import { channelKey, MODEL_ID_RE } from "./types.js";
@@ -249,7 +249,8 @@ const GLOB_CHARS = /[*?{[]/;
249
249
  // and `-z` is load-bearing correctness, not economy: without it git quotes any non-ASCII path, so
250
250
  // every Arabic/UTF-8 tracked file read as absent.
251
251
  function baseTreeAtHead(dir) {
252
- const git = (...args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
252
+ // queue row 2 item b: protected git; a trust refusal throws instead of reading as "no base tree"
253
+ const git = (...args) => gitSync(dir, args);
253
254
  const top = git("rev-parse", "--show-toplevel");
254
255
  const tree = git("ls-tree", "--full-tree", "-r", "-l", "-z", "HEAD");
255
256
  if (top.status !== 0 || tree.status !== 0 || typeof top.stdout !== "string" || typeof tree.stdout !== "string")
@@ -287,7 +288,7 @@ function measureBytes(tree, rel) {
287
288
  return covered ? total : undefined;
288
289
  }
289
290
  function absentContextReason(root, path) {
290
- if (spawnSync("git", ["-C", root, "check-ignore", "-q", "--", path]).status === 0)
291
+ if (gitSync(root, ["check-ignore", "-q", "--", path]).status === 0)
291
292
  return "gitignored";
292
293
  return existsSync(join(root, path)) ? "untracked" : "absent";
293
294
  }
@@ -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[];
@@ -186,6 +187,12 @@ export declare class SettledTrailerTracker {
186
187
  constructor(markers: readonly BusyFrameMarker[]);
187
188
  sample(paneText: string, trailer: boolean): boolean;
188
189
  }
190
+ /** Queue row 2 item a (D-2112, D-2114): context owned by the task dispatcher. */
191
+ export interface WorkerLaunchContext {
192
+ cwd: string;
193
+ branch: string;
194
+ repoRoot: string;
195
+ }
189
196
  export interface WorkerAdapter {
190
197
  id: string;
191
198
  vendor: string;
@@ -196,13 +203,15 @@ export interface WorkerAdapter {
196
203
  headlessCommand(promptFile: string, model: string, effort?: Effort): string;
197
204
  harnessBannerRows?: readonly string[];
198
205
  paneReviewBuffersOutput?: boolean;
199
- interactiveCommand(promptFile: string, model: string, effort?: Effort): string | null;
206
+ interactiveCommand(promptFile: string, model: string, effort?: Effort, ctx?: WorkerLaunchContext): string | null;
200
207
  interactiveSeed?: InteractiveSeed;
201
208
  resumeCommand?(sessionId: string, promptFile: string, model: string, effort?: Effort): string;
202
209
  sessionIdFrom?(output: string): string | undefined;
203
210
  resumeUnknownContext?: boolean;
204
211
  invoke(task: Task, cwd: string, a: Assignment, ctx: {
205
212
  promptFile: string;
213
+ branch?: string;
214
+ repoRoot?: string;
206
215
  }): Invocation;
207
216
  parse(output: string, nonce: string): WorkerResult;
208
217
  listModels?(): Promise<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));