tickmarkr 2.8.5 → 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.
Files changed (38) hide show
  1. package/dist/adapters/codex-commit-check.d.ts +4 -3
  2. package/dist/adapters/codex-commit-check.js +42 -28
  3. package/dist/adapters/codex.d.ts +2 -2
  4. package/dist/adapters/codex.js +38 -32
  5. package/dist/adapters/grok.js +5 -9
  6. package/dist/adapters/model-lints.js +4 -3
  7. package/dist/adapters/prompt.js +9 -1
  8. package/dist/adapters/types.d.ts +10 -1
  9. package/dist/adapters/types.js +12 -0
  10. package/dist/cli/commands/approve.js +9 -28
  11. package/dist/cli/commands/init.d.ts +10 -0
  12. package/dist/cli/commands/init.js +38 -20
  13. package/dist/cli/commands/status.js +9 -6
  14. package/dist/cli/commands/version.d.ts +1 -1
  15. package/dist/cli/commands/version.js +36 -2
  16. package/dist/cli/help.d.ts +1 -0
  17. package/dist/cli/help.js +6 -3
  18. package/dist/compile/gsd.js +4 -3
  19. package/dist/compile/native.js +8 -6
  20. package/dist/drivers/subprocess.js +9 -4
  21. package/dist/gates/baseline.js +51 -10
  22. package/dist/gates/cache.js +2 -4
  23. package/dist/graph/schema.js +3 -1
  24. package/dist/run/daemon.js +147 -35
  25. package/dist/run/git-trust.d.ts +11 -0
  26. package/dist/run/git-trust.js +1 -1
  27. package/dist/run/git.d.ts +18 -1
  28. package/dist/run/git.js +73 -2
  29. package/dist/run/journal.d.ts +15 -1
  30. package/dist/run/journal.js +43 -6
  31. package/dist/run/repair-disposition.d.ts +1 -1
  32. package/dist/run/repair-disposition.js +2 -2
  33. package/dist/run/stall.d.ts +2 -0
  34. package/dist/run/stall.js +12 -4
  35. package/package.json +1 -1
  36. package/skills/tickmarkr-auto/SKILL.md +10 -8
  37. package/skills/tickmarkr-loop/SKILL.md +10 -8
  38. package/skills/tickmarkr-overseer/SKILL.md +10 -8
@@ -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,19 +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";
7
+ import { assertGitTrust, GitTrustRefusal } from "../run/git-trust.js";
8
+ import { shGitOk } from "../run/git.js";
8
9
  import { withoutHostSession } from "../drivers/host-env.js";
9
- import { CODEX_GIT_GRANT, codex } from "./codex.js";
10
+ import { renderCodexGitGrant, codex } from "./codex.js";
10
11
  import { shq } from "./types.js";
11
12
  const exec = promisify(execFile);
12
- const git = async (cwd, ...args) => (await exec("git", args, { cwd })).stdout.trim();
13
- // v2.6.7 T4: host git after the sandbox has had the fixture — validation and cleanup — runs the uncached trust
14
- // check and the forced child config on every call, so seat-rewritten metadata can neither run a config program
15
- // nor answer for the probe's commit.
16
- const hostGit = async (cwd, ...args) => {
17
- assertGitTrust(cwd);
18
- return (await exec("git", args, { cwd, env: protectedGitEnv(process.env) })).stdout.trim();
19
- };
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; };
20
21
  /** The fixture checkout's trust after the sandbox returned or threw: a refusal is reported with its path. */
21
22
  const fixtureRefusal = (worktree) => {
22
23
  try {
@@ -35,14 +36,14 @@ const SCRIPT = `printf %s "$2" > "$1" && echo control=ok || echo control=fail
35
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
36
37
  (set -C; printf %s "$2" > "$3") 2>/dev/null && echo hook=written || echo hook=denied
37
38
  (set -C; printf %s "$2" > "$4") 2>/dev/null && echo root=written || echo root=denied`;
38
- /** The `sh` argv production runs at the fixture worktree: `codex sandbox` under the worker grant fragment. */
39
- 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 })) => [
40
41
  "-c",
41
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(" ")}`,
42
43
  ];
43
- /** A sandbox executing `grant`; production is CODEX_GIT_GRANT. Tests pass a mutant grant to prove discrimination. */
44
+ /** Queue row 2 (D-2112): tests may supply a mutant; production renders each fixture. */
44
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;
45
- export const codexSandbox = codexSandboxFor(CODEX_GIT_GRANT);
46
+ export const codexSandbox = codexSandboxFor();
46
47
  const holds = (path, token) => {
47
48
  try {
48
49
  return readFileSync(path, "utf8") === token;
@@ -67,18 +68,28 @@ const contradictory = (out) => {
67
68
  }
68
69
  return false;
69
70
  };
70
- 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));
71
72
  // This probe's commit and nothing else: one parent (the fixture base), its token in the subject and the control.
72
73
  const isProbeCommit = async (repo, commit, base, token) => {
73
74
  try {
74
- return await hostGit(repo, "log", "-1", "--format=%P %s", commit) === `${base} tickmarkr-probe ${token}`
75
- && 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;
76
77
  }
77
- catch {
78
- return false;
78
+ catch (e) {
79
+ return unlessRefused(false)(e);
79
80
  }
80
81
  };
81
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; };
82
93
  const token = randomUUID();
83
94
  const parent = mkdtempSync(join(tmpdir(), "tickmarkr-codex-commit-"));
84
95
  // git names the fixture's metadata directory after the checkout's basename; a unique one makes that
@@ -91,21 +102,22 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
91
102
  let hostile = [];
92
103
  let sandboxed = false;
93
104
  try {
94
- 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"]);
95
106
  lock = join(commonDir, "worktrees", basename(worktree), "index.lock");
96
107
  const hostileHook = join(commonDir, "hooks", branch), hostileRoot = join(commonDir, branch);
97
108
  hostile = [hostileHook, hostileRoot];
98
109
  if (hostile.some((m) => existsSync(m)))
99
110
  return unknown("a foreign file already holds a hostile probe path");
100
- base = await git(repoRoot, "rev-parse", "--verify", "HEAD^{commit}");
101
- await git(repoRoot, "worktree", "add", "-b", branch, worktree, base);
102
- 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)
103
115
  return unknown("the fixture worktree metadata is not where git was expected to put it");
104
116
  const control = join(worktree, CONTROL);
105
117
  if (existsSync(lock))
106
118
  return unknown("a foreign index.lock already holds the fixture worktree");
107
119
  sandboxed = true;
108
- 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 });
109
121
  // An escape on disk dominates; every other verdict needs consistent receipts AND disk — and trusted metadata.
110
122
  const escape = escaped(hostile, token);
111
123
  if (escape)
@@ -145,6 +157,8 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
145
157
  const refused = sandboxed ? fixtureRefusal(worktree) : undefined;
146
158
  if (refused)
147
159
  return refused;
160
+ if (e instanceof GitTrustRefusal)
161
+ return unknown(`fixture git refused (${e.message})`);
148
162
  return unknown(signal?.aborted ? "probe cancelled" : `probe did not run (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`);
149
163
  }
150
164
  finally {
@@ -159,7 +173,7 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
159
173
  // prune — that would take an earlier probe's preserved foreign lock (or anyone's prunable worktree).
160
174
  const foreign = lock !== undefined && existsSync(lock);
161
175
  if (lock && !foreign) {
162
- await hostGit(repoRoot, "worktree", "remove", "--force", worktree).catch(() => undefined);
176
+ await hostGit(repoRoot, ["worktree", "remove", "--force", worktree]).catch(cleanup(undefined));
163
177
  // a failed `worktree add` leaves metadata git no longer removes by path: drop this probe's own directory
164
178
  rmSync(dirname(lock), { recursive: true, force: true });
165
179
  try {
@@ -170,9 +184,9 @@ export async function probeCodexCommit(repoRoot, sandbox = codexSandbox, signal)
170
184
  // The fixture branch and its reflog go only while the tip is still this probe's (the base or its own
171
185
  // commit), and update-ref's old-value check refuses if anyone moves it between this read and the delete.
172
186
  // Metadata preserved for a foreign lock keeps its branch too: that checkout's HEAD still names it.
173
- const tip = base === undefined || foreign ? undefined : await tipOf(repoRoot, ref);
174
- if (tip && base && (tip === base || await isProbeCommit(repoRoot, tip, base, token))) {
175
- 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));
176
190
  }
177
191
  rmSync(parent, { recursive: true, force: true });
178
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
@@ -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).
@@ -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
  }
@@ -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;
@@ -187,6 +188,12 @@ export declare class SettledTrailerTracker {
187
188
  constructor(markers: readonly BusyFrameMarker[]);
188
189
  sample(paneText: string, trailer: boolean): boolean;
189
190
  }
191
+ /** Queue row 2 item a (D-2112, D-2114): context owned by the task dispatcher. */
192
+ export interface WorkerLaunchContext {
193
+ cwd: string;
194
+ branch: string;
195
+ repoRoot: string;
196
+ }
190
197
  export interface WorkerAdapter {
191
198
  id: string;
192
199
  vendor: string;
@@ -197,13 +204,15 @@ export interface WorkerAdapter {
197
204
  headlessCommand(promptFile: string, model: string, effort?: Effort): string;
198
205
  harnessBannerRows?: readonly string[];
199
206
  paneReviewBuffersOutput?: boolean;
200
- interactiveCommand(promptFile: string, model: string, effort?: Effort): string | null;
207
+ interactiveCommand(promptFile: string, model: string, effort?: Effort, ctx?: WorkerLaunchContext): string | null;
201
208
  interactiveSeed?: InteractiveSeed;
202
209
  resumeCommand?(sessionId: string, promptFile: string, model: string, effort?: Effort): string;
203
210
  sessionIdFrom?(output: string): string | undefined;
204
211
  resumeUnknownContext?: boolean;
205
212
  invoke(task: Task, cwd: string, a: Assignment, ctx: {
206
213
  promptFile: string;
214
+ branch?: string;
215
+ repoRoot?: string;
207
216
  }): Invocation;
208
217
  parse(output: string, nonce: string): WorkerResult;
209
218
  listModels?(): Promise<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,11 +1,11 @@
1
1
  import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
2
2
  import { filesGlob } from "../../graph/files-glob.js";
3
- import { execFileSync } from "node:child_process";
4
3
  import { userInfo } from "node:os";
5
4
  import { allAdapters } from "../../adapters/registry.js";
6
5
  import { channelKey } from "../../adapters/types.js";
7
6
  import { loadConfig } from "../../config/config.js";
8
7
  import { integrationBranch } from "../../run/merge.js";
8
+ import { gitSync } from "../../run/git.js";
9
9
  import { integrationAnchor } from "../../run/daemon.js";
10
10
  import { separabilityErrors, surfaceErrors, taskBudgetErrors } from "../../compile/collateral.js";
11
11
  import { GATE_NAMES } from "../../graph/schema.js";
@@ -444,15 +444,10 @@ function mergedSiblingExempt(cwd, events, status, siblingId, requesterRef) {
444
444
  if (commit === undefined || !FULL_OBJECT_ID.test(commit))
445
445
  return false;
446
446
  const ref = requesterRef();
447
- try {
448
- execFileSync("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
449
- execFileSync("git", ["merge-base", "--is-ancestor", commit, ref], { cwd, stdio: "ignore" });
450
- }
451
- catch {
452
- return false;
453
- }
454
- return true;
447
+ return gitOk(cwd, "rev-parse", "--verify", ref) && gitOk(cwd, "merge-base", "--is-ancestor", commit, ref);
455
448
  }
449
+ /** Queue row 2 item b: approve's git runs through the protected path; a trust refusal throws, never reads as false. */
450
+ const gitOk = (cwd, ...args) => gitSync(cwd, args).status === 0;
456
451
  /** The conflicting unordered pairs of separabilityErrors, in its exact emission order. */
457
452
  function separabilityPairs(tasks) {
458
453
  const byId = new Map(tasks.map((t) => [t.id, t.deps ?? []]));
@@ -531,27 +526,13 @@ function failedTaskCommitsAhead(cwd, journal, runId, taskId) {
531
526
  const preservedRef = preserved?.data.ref;
532
527
  let ref = taskRef;
533
528
  if (preservedRef) {
534
- try {
535
- execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
536
- execFileSync("git", ["merge-base", "--is-ancestor", taskRef, preservedRef], { cwd, stdio: "ignore" });
529
+ if (gitOk(cwd, "rev-parse", "--verify", taskRef) && gitOk(cwd, "merge-base", "--is-ancestor", taskRef, preservedRef))
530
+ ref = preservedRef;
531
+ else if (!gitOk(cwd, "rev-parse", "--verify", taskRef))
537
532
  ref = preservedRef;
538
- }
539
- catch {
540
- try {
541
- execFileSync("git", ["rev-parse", "--verify", taskRef], { cwd, stdio: "ignore" });
542
- }
543
- catch {
544
- ref = preservedRef;
545
- }
546
- }
547
- }
548
- try {
549
- const out = execFileSync("git", ["rev-list", "--count", `${integration.base}..${ref}`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
550
- return { ref, base, count: Number.parseInt(out.trim(), 10) || 0 };
551
- }
552
- catch {
553
- return { ref, base, count: 0 };
554
533
  }
534
+ const out = gitSync(cwd, ["rev-list", "--count", `${integration.base}..${ref}`]);
535
+ return { ref, base, count: out.status === 0 ? Number.parseInt(out.stdout.trim(), 10) || 0 : 0 };
555
536
  }
556
537
  // The status is command OUTPUT, not a typed sibling result: the registered approve function returns
557
538
  // one primitive string and the dispatcher prints those bytes unchanged. A compact sentinel followed
@@ -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 {};