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.
- package/dist/adapters/codex-commit-check.d.ts +4 -3
- package/dist/adapters/codex-commit-check.js +42 -28
- package/dist/adapters/codex.d.ts +2 -2
- package/dist/adapters/codex.js +38 -32
- package/dist/adapters/grok.js +5 -9
- package/dist/adapters/model-lints.js +4 -3
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/approve.js +9 -28
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +38 -20
- package/dist/cli/commands/status.js +9 -6
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/compile/gsd.js +4 -3
- package/dist/compile/native.js +8 -6
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/baseline.js +51 -10
- package/dist/gates/cache.js +2 -4
- package/dist/graph/schema.js +3 -1
- package/dist/run/daemon.js +147 -35
- package/dist/run/git-trust.d.ts +11 -0
- package/dist/run/git-trust.js +1 -1
- package/dist/run/git.d.ts +18 -1
- package/dist/run/git.js +73 -2
- package/dist/run/journal.d.ts +15 -1
- package/dist/run/journal.js +43 -6
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +2 -2
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- 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
|
-
/**
|
|
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
|
-
/**
|
|
23
|
-
export declare const codexSandboxFor: (grant
|
|
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
|
|
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 {
|
|
10
|
+
import { renderCodexGitGrant, codex } from "./codex.js";
|
|
10
11
|
import { shq } from "./types.js";
|
|
11
12
|
const exec = promisify(execFile);
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
const hostGit = async (cwd,
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
/**
|
|
39
|
-
export const codexSandboxArgs = (p, 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
|
-
/**
|
|
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(
|
|
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((
|
|
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
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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((
|
|
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((
|
|
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
|
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -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
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
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
|
|
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()} ${
|
|
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,
|
|
311
|
-
return { command: codexExec(ctx.promptFile, a.model, a.effort, ` ${
|
|
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
|
package/dist/adapters/grok.js
CHANGED
|
@@ -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
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -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(
|
|
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.
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/adapters/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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 {};
|