tickmarkr 2.8.5 → 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.
- 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/model-lints.js +4 -3
- package/dist/adapters/types.d.ts +9 -1
- package/dist/cli/commands/approve.js +9 -28
- package/dist/cli/commands/init.js +25 -17
- package/dist/cli/commands/status.js +9 -6
- package/dist/compile/gsd.js +4 -3
- package/dist/compile/native.js +8 -6
- package/dist/gates/baseline.js +4 -3
- package/dist/gates/cache.js +2 -4
- package/dist/graph/schema.js +3 -1
- package/dist/run/daemon.js +54 -4
- package/dist/run/git-trust.d.ts +11 -0
- package/dist/run/git-trust.js +1 -1
- package/dist/run/git.d.ts +13 -1
- package/dist/run/git.js +22 -1
- package/dist/run/journal.d.ts +15 -1
- package/dist/run/journal.js +43 -6
- package/package.json +1 -1
|
@@ -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
|
|
@@ -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/types.d.ts
CHANGED
|
@@ -187,6 +187,12 @@ export declare class SettledTrailerTracker {
|
|
|
187
187
|
constructor(markers: readonly BusyFrameMarker[]);
|
|
188
188
|
sample(paneText: string, trailer: boolean): boolean;
|
|
189
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
|
+
}
|
|
190
196
|
export interface WorkerAdapter {
|
|
191
197
|
id: string;
|
|
192
198
|
vendor: string;
|
|
@@ -197,13 +203,15 @@ export interface WorkerAdapter {
|
|
|
197
203
|
headlessCommand(promptFile: string, model: string, effort?: Effort): string;
|
|
198
204
|
harnessBannerRows?: readonly string[];
|
|
199
205
|
paneReviewBuffersOutput?: boolean;
|
|
200
|
-
interactiveCommand(promptFile: string, model: string, effort?: Effort): string | null;
|
|
206
|
+
interactiveCommand(promptFile: string, model: string, effort?: Effort, ctx?: WorkerLaunchContext): string | null;
|
|
201
207
|
interactiveSeed?: InteractiveSeed;
|
|
202
208
|
resumeCommand?(sessionId: string, promptFile: string, model: string, effort?: Effort): string;
|
|
203
209
|
sessionIdFrom?(output: string): string | undefined;
|
|
204
210
|
resumeUnknownContext?: boolean;
|
|
205
211
|
invoke(task: Task, cwd: string, a: Assignment, ctx: {
|
|
206
212
|
promptFile: string;
|
|
213
|
+
branch?: string;
|
|
214
|
+
repoRoot?: string;
|
|
207
215
|
}): Invocation;
|
|
208
216
|
parse(output: string, nonce: string): WorkerResult;
|
|
209
217
|
listModels?(): Promise<string[]>;
|
|
@@ -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,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,7 +10,8 @@ import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
|
11
10
|
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
12
11
|
import { stateDirName, tickmarkrDir } from "../../graph/graph.js";
|
|
13
12
|
import { orcaHostDetected } from "../../drivers/index.js";
|
|
14
|
-
import { WORKTREES_DIR } from "../../run/git.js";
|
|
13
|
+
import { gitSync, WORKTREES_DIR } from "../../run/git.js";
|
|
14
|
+
import { GitTrustRefusal } from "../../run/git-trust.js";
|
|
15
15
|
import { Journal } from "../../run/journal.js";
|
|
16
16
|
import { runLockOwner, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
17
17
|
import { doctor } from "./doctor.js";
|
|
@@ -256,13 +256,8 @@ function gitTracked(cwd, path) {
|
|
|
256
256
|
const rel = relative(cwd, path);
|
|
257
257
|
if (rel.startsWith(".."))
|
|
258
258
|
return false;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
259
|
+
// queue row 2 item b: protected git; a trust refusal throws, an ordinary failure still reads as untracked
|
|
260
|
+
return gitSync(cwd, ["ls-files", "--error-unmatch", "--", rel]).status === 0;
|
|
266
261
|
}
|
|
267
262
|
async function installAgentFiles(cwd, force, docs, notes) {
|
|
268
263
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
@@ -375,19 +370,29 @@ function isPathInside(parentDir, candidatePath) {
|
|
|
375
370
|
}
|
|
376
371
|
/**
|
|
377
372
|
* The ONLY way the --remove path runs git: GIT_DIR, GIT_WORK_TREE and GIT_COMMON_DIR are stripped so the repository acted on
|
|
378
|
-
* is always cwd's own, and LC_ALL=C keeps stderr parseable.
|
|
373
|
+
* is always cwd's own, and LC_ALL=C keeps stderr parseable. Queue row 2 item b: it runs through the protected path (gitSync),
|
|
374
|
+
* whose GitTrustRefusal propagates unchanged; a failed git throws an error carrying its status and stderr.
|
|
379
375
|
*/
|
|
380
376
|
function removeGit(cwd, args) {
|
|
381
377
|
const { GIT_DIR: _dir, GIT_WORK_TREE: _workTree, GIT_COMMON_DIR: _commonDir, ...env } = process.env;
|
|
382
|
-
|
|
378
|
+
const r = gitSync(cwd, args, { env: { ...env, LC_ALL: "C" } });
|
|
379
|
+
if (r.error)
|
|
380
|
+
throw r.error;
|
|
381
|
+
if (r.status !== 0)
|
|
382
|
+
throw Object.assign(new Error(`git ${args.join(" ")} failed (${r.status ?? r.signal})`), { status: r.status, stderr: r.stderr });
|
|
383
|
+
return r.stdout;
|
|
383
384
|
}
|
|
385
|
+
/** A trust refusal is never one of the --remove path's fail-open answers. */
|
|
386
|
+
const rethrowRefusal = (error) => { if (error instanceof GitTrustRefusal)
|
|
387
|
+
throw error; };
|
|
384
388
|
function isLinkedWorktree(cwd) {
|
|
385
389
|
try {
|
|
386
390
|
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
387
391
|
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
388
392
|
return resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir);
|
|
389
393
|
}
|
|
390
|
-
catch {
|
|
394
|
+
catch (error) {
|
|
395
|
+
rethrowRefusal(error);
|
|
391
396
|
return false;
|
|
392
397
|
}
|
|
393
398
|
}
|
|
@@ -405,7 +410,8 @@ function checkLiveLock(cwd) {
|
|
|
405
410
|
return mainOwner;
|
|
406
411
|
}
|
|
407
412
|
}
|
|
408
|
-
catch {
|
|
413
|
+
catch (error) {
|
|
414
|
+
rethrowRefusal(error);
|
|
409
415
|
// not a git repo
|
|
410
416
|
}
|
|
411
417
|
return undefined;
|
|
@@ -420,6 +426,7 @@ function isGitRepo(cwd) {
|
|
|
420
426
|
return true;
|
|
421
427
|
}
|
|
422
428
|
catch (err) {
|
|
429
|
+
rethrowRefusal(err);
|
|
423
430
|
const e = err;
|
|
424
431
|
const stderr = String(e.stderr ?? "").trim();
|
|
425
432
|
if (e.status === 128 && stderr.startsWith("fatal: not a git repository (or any of the parent directories)"))
|
|
@@ -433,6 +440,7 @@ function git(cwd, args, message) {
|
|
|
433
440
|
return removeGit(cwd, args);
|
|
434
441
|
}
|
|
435
442
|
catch (err) {
|
|
443
|
+
rethrowRefusal(err);
|
|
436
444
|
const stderr = String(err.stderr ?? "").trim();
|
|
437
445
|
throw new Error(stderr ? `${message}: ${stderr}` : message);
|
|
438
446
|
}
|
|
@@ -522,16 +530,16 @@ function handleRemove(cwd) {
|
|
|
522
530
|
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/tickmarkr/"]);
|
|
523
531
|
branches = out.split("\n").map((b) => b.trim()).filter(Boolean);
|
|
524
532
|
}
|
|
525
|
-
catch {
|
|
526
|
-
|
|
533
|
+
catch (error) {
|
|
534
|
+
rethrowRefusal(error);
|
|
527
535
|
}
|
|
528
536
|
let preservedRefs = [];
|
|
529
537
|
try {
|
|
530
538
|
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/tickmarkr/preserved/"]);
|
|
531
539
|
preservedRefs = out.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
532
540
|
}
|
|
533
|
-
catch {
|
|
534
|
-
|
|
541
|
+
catch (error) {
|
|
542
|
+
rethrowRefusal(error);
|
|
535
543
|
}
|
|
536
544
|
for (const b of branches) {
|
|
537
545
|
notes.push(`left branch: ${b}`);
|
|
@@ -13,7 +13,7 @@ import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
|
13
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
14
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
15
15
|
import { currentOwed, endToEndFirstPass, firstPassText, leadLines, leadRows, needsYouText, wallText } from "./report.js";
|
|
16
|
-
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, fatalRunEndCause, recordedTaskFailureKind, runHasEnded,
|
|
16
|
+
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, fatalRunEndCause, recordedTaskFailureKind, runHasEnded, restoresUpheldFeedback, visibleControls, } from "../../run/journal.js";
|
|
17
17
|
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
18
18
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
19
19
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
@@ -1070,15 +1070,18 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1070
1070
|
}).map(sanitizeTaskText)
|
|
1071
1071
|
: [];
|
|
1072
1072
|
// OBS-738: recovery facts stay on the journal's two established reducers. The prefix fold keeps an
|
|
1073
|
-
// older journal equally readable by asking
|
|
1073
|
+
// older journal equally readable by asking restoresUpheldFeedback what was funded at that exact
|
|
1074
1074
|
// restore boundary; current resume-restore rows additionally carry the same task for narration.
|
|
1075
1075
|
// OBS-1178: both folds read decisions through the one decision fold over the whole journal.
|
|
1076
|
+
// Queue row 225 (D-2111): only each task's NEWEST restore speaks — never a historical set.
|
|
1076
1077
|
const decided = effectiveEvents(events);
|
|
1077
1078
|
const preservedRefs = preservedRefsByTask(events);
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1079
|
+
const newestRestores = new Map();
|
|
1080
|
+
decided.forEach((event, index) => { if (event.event === "resume-restore" && event.taskId)
|
|
1081
|
+
newestRestores.set(event.taskId, index); });
|
|
1082
|
+
const restoredUpheldFeedback = new Set([...newestRestores]
|
|
1083
|
+
.filter(([taskId, index]) => restoresUpheldFeedback(decided.slice(0, index), taskId))
|
|
1084
|
+
.map(([taskId]) => taskId));
|
|
1082
1085
|
// OBS-1178: a failed task's newest task-failed row is the token its recheck binds to. Only an
|
|
1083
1086
|
// effective decision consumes it — a refused or unsound recheck leaves the task failed and the token shown.
|
|
1084
1087
|
const failureTokens = new Map();
|
package/dist/compile/gsd.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
3
|
import { parse as parseYaml } from "yaml";
|
|
5
4
|
import { AcceptanceItemSchema, TIERS, validateGraph } from "../graph/schema.js";
|
|
6
5
|
import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
|
|
6
|
+
import { gitSync } from "../run/git.js";
|
|
7
7
|
import { classifyContextPath } from "./native.js";
|
|
8
8
|
// GSD artifact front-end (spec v1.3): one GSD *plan* is one tickmarkr *task* — a plan is
|
|
9
9
|
// worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
|
|
@@ -306,9 +306,10 @@ export function compileGsd(src, root) {
|
|
|
306
306
|
// Match native context reachability against the committed tree. This block only obtains the HEAD
|
|
307
307
|
// snapshot; classifyContextPath remains the single authority for missing, untracked, and glob refs.
|
|
308
308
|
const gitDir = dirname(files[0]);
|
|
309
|
-
|
|
309
|
+
// queue row 2 item b: protected git; a trust refusal throws instead of reading as "no repository"
|
|
310
|
+
const top = gitSync(gitDir, ["rev-parse", "--show-toplevel"]);
|
|
310
311
|
// -z (queue row 71): without it a non-ASCII path is C-quoted and a tracked ref reads as untracked
|
|
311
|
-
const tree =
|
|
312
|
+
const tree = gitSync(gitDir, ["ls-tree", "--full-tree", "-r", "--name-only", "-z", "HEAD"]);
|
|
312
313
|
const repoRoot = typeof top.stdout === "string" ? top.stdout.trim() : "";
|
|
313
314
|
// `root` is the repository whose worktrees will consume these refs. A source nested inside some
|
|
314
315
|
// unrelated repository (as with vendored fixtures) must not be judged against that outer tree.
|
package/dist/compile/native.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
5
4
|
import picomatch from "picomatch";
|
|
6
5
|
import { GATE_NAMES, GRAPH_ROUTING_MODES, ORACLES, renderAcceptanceItem, SHAPES, TIERS, validateGraph, } from "../graph/schema.js";
|
|
6
|
+
import { gitSync } from "../run/git.js";
|
|
7
7
|
import { criterionReadsOnly } from "./collateral.js";
|
|
8
8
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
9
9
|
// OBS-170/OBS-184: `context:` is a promise to the worker, and nothing ever checked it could be kept.
|
|
@@ -18,7 +18,8 @@ import { CompileError, inferShape, sha256 } from "./common.js";
|
|
|
18
18
|
// `ls-tree -r HEAD` enumerates that tree once; per-path `cat-file -e HEAD:<path>` is the same oracle
|
|
19
19
|
// one path at a time, and one spawn beats N.
|
|
20
20
|
function trackedAtHead(dir) {
|
|
21
|
-
|
|
21
|
+
// queue row 2 item b: every compile git runs through the protected path; a trust refusal throws, never fails open
|
|
22
|
+
const git = (...args) => gitSync(dir, args);
|
|
22
23
|
const top = git("rev-parse", "--show-toplevel");
|
|
23
24
|
// --full-tree is load-bearing: without it `ls-tree` scopes to the CWD's subtree and prints paths
|
|
24
25
|
// relative to it, so a spec in specs/ would see only specs/-relative names and misjudge every
|
|
@@ -146,7 +147,7 @@ function symlinkScopeWarnings(file, tasks, base) {
|
|
|
146
147
|
if (!scoped.length || !repositoryRoot(file))
|
|
147
148
|
return [];
|
|
148
149
|
const tree = base ?? "HEAD";
|
|
149
|
-
const git = (...args) =>
|
|
150
|
+
const git = (...args) => gitSync(dirname(file), args);
|
|
150
151
|
const failed = (r) => r.error || r.signal || r.status !== 0 || typeof r.stdout !== "string";
|
|
151
152
|
const why = (r) => r.signal ? `signal ${r.signal}` : r.status === null ? "git did not run" : `git exit ${r.status}`;
|
|
152
153
|
const advisory = (what, r) => `tickmarkr: advisory: files[] symlink inspection could not read ${what} in the selected git tree ${JSON.stringify(tree)} (${why(r)}) — symlink scope is unverified for this compile.`;
|
|
@@ -270,7 +271,7 @@ function testsAtHead(root) {
|
|
|
270
271
|
const cached = headTestCache.get(root);
|
|
271
272
|
if (cached)
|
|
272
273
|
return cached;
|
|
273
|
-
const grep =
|
|
274
|
+
const grep = gitSync(root, ["grep", "-I", "-n", "-e", "", "HEAD", "--", "tests/*.test.ts", "tests/**/*.test.ts"]);
|
|
274
275
|
if (grep.status !== 0 || typeof grep.stdout !== "string") {
|
|
275
276
|
headTestCache.set(root, []);
|
|
276
277
|
return [];
|
|
@@ -389,7 +390,7 @@ function criterionScopeFinding(task, text, criterion, id, tests) {
|
|
|
389
390
|
// 20-task spec compiled in seconds and the committed-spec corpus test timed out. Fail-open like
|
|
390
391
|
// testsAtHead: no git answer means no corpus, and the lint stays quiet rather than wrong.
|
|
391
392
|
function repoFileList(root) {
|
|
392
|
-
const ls =
|
|
393
|
+
const ls = gitSync(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
393
394
|
if (ls.status !== 0 || typeof ls.stdout !== "string")
|
|
394
395
|
return [];
|
|
395
396
|
return ls.stdout.split("\0").filter(Boolean).sort();
|
|
@@ -949,7 +950,8 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
949
950
|
mode: partner-led | risk-based | staff-led — this engagement's routing mode
|
|
950
951
|
(loses only to an explicit \`run --mode\` flag)
|
|
951
952
|
base: commit-or-ref — optional declared source base; the declaration must begin at column zero
|
|
952
|
-
(
|
|
953
|
+
(proven against the run's starting commit at run and resume start; a drifted
|
|
954
|
+
or unresolvable base is refused before anything is written)
|
|
953
955
|
|
|
954
956
|
Fields available per task:
|
|
955
957
|
goal: outcome the task must achieve (defaults to the title if omitted)
|
package/dist/gates/baseline.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
2
|
import { EVICTION_TOMBSTONE_LIMIT, EVICTION_TOMBSTONES_FILE, parseEvictionTombstones } from "../run/receipt-resolver.js";
|
|
4
3
|
import { existsSync, readFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, rmdirSync } from "node:fs";
|
|
5
4
|
import { availableParallelism, loadavg, tmpdir } from "node:os";
|
|
6
5
|
import { basename, join } from "node:path";
|
|
7
6
|
import { DEFAULT_EVIDENCE_QUOTA_BYTES } from "../config/config.js";
|
|
8
|
-
import { dependencyLinkRefusal, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
7
|
+
import { dependencyLinkRefusal, gitSync, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
9
8
|
import { executionSignal } from "../run/execution-budget.js";
|
|
10
9
|
import { isVitestTestCommand, manifestChildEnv, manifestFileCount } from "./test-manifest.js";
|
|
11
10
|
export const EVIDENCE_TAIL_BYTES = 16 * 1024;
|
|
@@ -121,7 +120,9 @@ export function beginGateEvidence(cwd, gate, command, opts = {}, nonce) {
|
|
|
121
120
|
let subjectCommit = opts.subjectCommit ?? null;
|
|
122
121
|
if (!subjectCommit) {
|
|
123
122
|
try {
|
|
124
|
-
|
|
123
|
+
const result = gitSync(cwd, ["rev-parse", "HEAD"]);
|
|
124
|
+
if (result.status === 0 && result.stdout.trim())
|
|
125
|
+
subjectCommit = result.stdout.trim();
|
|
125
126
|
}
|
|
126
127
|
catch { /* unknown subject */ }
|
|
127
128
|
}
|
package/dist/gates/cache.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
6
|
import { shq } from "../adapters/types.js";
|
|
7
|
+
import { trustedCommonDir } from "../run/git-trust.js";
|
|
8
8
|
import { titleEnvironment } from "../run/title-environment.js";
|
|
9
9
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
10
10
|
let testCacheBound;
|
|
@@ -269,9 +269,7 @@ export function resolveStateDir(worktree, artifactDir) {
|
|
|
269
269
|
return join(worktree, ".tickmarkr");
|
|
270
270
|
}
|
|
271
271
|
try {
|
|
272
|
-
const
|
|
273
|
-
const resolved = resolve(worktree, commonDir);
|
|
274
|
-
const commonRoot = realpathSync(dirname(resolved));
|
|
272
|
+
const commonRoot = realpathSync(dirname(trustedCommonDir(worktree)));
|
|
275
273
|
if (existsSync(join(commonRoot, ".tickmarkr"))) {
|
|
276
274
|
return join(commonRoot, ".tickmarkr");
|
|
277
275
|
}
|
package/dist/graph/schema.js
CHANGED
|
@@ -118,7 +118,9 @@ export const RunGraphSchema = z
|
|
|
118
118
|
source: z.enum(SPEC_SOURCES),
|
|
119
119
|
paths: z.array(z.string()),
|
|
120
120
|
hash: z.string(),
|
|
121
|
-
// Q11
|
|
121
|
+
// Q11 / queue row 220: an author-declared ref, proven at run and resume start against the run's
|
|
122
|
+
// starting commit (HEAD for a run, the recorded run-start base for a resume) by ancestry or patch-id;
|
|
123
|
+
// drifted or unresolvable, the run or resume is refused before it writes anything. Never rebased onto.
|
|
122
124
|
base: z.string().min(1).optional(),
|
|
123
125
|
}),
|
|
124
126
|
tasks: z.array(TaskSchema).min(1),
|
package/dist/run/daemon.js
CHANGED
|
@@ -34,7 +34,7 @@ import { executionSignal, remainingExecutionMs, withExecutionBudget, withoutExec
|
|
|
34
34
|
import { repairSelectionDecision, repairSelectionEnabled } from "./repair-selection.js";
|
|
35
35
|
import { failureDisposition, reserveInfrastructureRetry } from "./recovery.js";
|
|
36
36
|
import { runEnvironment } from "./environment.js";
|
|
37
|
-
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
37
|
+
import { addDetachedWorktree, changeRepresented, cleanupRunWorktrees, declaredBaseContainment, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, PRESERVE_COMMIT_SUBJECT, PRESERVE_PRODUCER_TRAILER, preserveWorktree, producerFields, reflogWitness, removeWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sameVerification, sh, shell, shGit, SUITE_PARENT_ENV, verificationProtocol, WORKTREE_LAYOUT_CONTRACT, WORKTREES_DIR, worktreePath } from "./git.js";
|
|
38
38
|
import { MASK } from "./redact.js";
|
|
39
39
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
40
40
|
import { classifyRepairDisposition, resolveScopeHints } from "./repair-disposition.js";
|
|
@@ -1145,6 +1145,48 @@ export function integrationAnchor(events) {
|
|
|
1145
1145
|
return { unreadable: `the newest merge row's from is ${show(merge.data.from)}, not an object id` };
|
|
1146
1146
|
return { anchor: merge.data.commit };
|
|
1147
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Queue row 220 (D-2111): prove the spec's declared base against the commit this run starts from — the
|
|
1150
|
+
* checkout HEAD for a fresh run (recorded as its run-start baseRef), the recorded run-start baseRef for a
|
|
1151
|
+
* resume, never the checkout's current HEAD: a resume never re-bases, and no run rebases onto the declared
|
|
1152
|
+
* ref. Drift and an unresolvable answer throw; runDaemon calls this before it writes anything.
|
|
1153
|
+
*/
|
|
1154
|
+
async function proveDeclaredBase(repoRoot, runId, ref, resume) {
|
|
1155
|
+
const action = resume ? `resume ${runId}` : "run";
|
|
1156
|
+
const remedy = `Remedy: check out a branch that contains ${ref}, or correct base: in the spec and recompile.`;
|
|
1157
|
+
const unresolvable = (against, why) => new Error(`refusing to ${action}: declared base ${ref} is unresolvable against ${against} — ${why}. ${remedy}`);
|
|
1158
|
+
let start;
|
|
1159
|
+
let against;
|
|
1160
|
+
if (resume) {
|
|
1161
|
+
const recorded = Journal.open(repoRoot, runId).read().find((e) => e.event === "run-start")?.data.baseRef;
|
|
1162
|
+
if (typeof recorded !== "string" || !recorded)
|
|
1163
|
+
throw unresolvable("the recorded run-start base", "the journal's run-start records no baseRef");
|
|
1164
|
+
start = recorded;
|
|
1165
|
+
against = `the recorded run-start base ${short(start)}`;
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const head = await shGit("git rev-parse --verify HEAD^{commit}", repoRoot);
|
|
1169
|
+
if (head.code !== 0)
|
|
1170
|
+
throw unresolvable("HEAD", `git said for HEAD: ${(head.stderr || head.stdout).trim()}`);
|
|
1171
|
+
start = head.stdout.trim();
|
|
1172
|
+
against = `HEAD ${short(start)}`;
|
|
1173
|
+
}
|
|
1174
|
+
const probe = await declaredBaseContainment(repoRoot, ref, start);
|
|
1175
|
+
if (probe.result === "contained")
|
|
1176
|
+
return { ref, via: probe.via, start };
|
|
1177
|
+
if (probe.result === "unresolvable")
|
|
1178
|
+
throw unresolvable(against, `git said for ${probe.ref}: ${probe.evidence}`);
|
|
1179
|
+
const lines = await Promise.all(probe.missing.map(async (m) => {
|
|
1180
|
+
const subject = await shGit(`git log -1 --format=%s ${shq(m.commit)}`, repoRoot);
|
|
1181
|
+
// a drift refusal must name the subject: an unreadable one refuses as unresolvable, quoting git
|
|
1182
|
+
if (subject.code !== 0)
|
|
1183
|
+
throw unresolvable(against, `git said for missing commit ${short(m.commit)}: ${(subject.stderr || subject.stdout).trim()}`);
|
|
1184
|
+
return ` ${short(m.commit)} ${subject.stdout.trim()}${m.merge ? " (merge)" : ""}`
|
|
1185
|
+
+ ` — ${m.paths.length > 0 ? m.paths.join(", ") : "no paths"}`;
|
|
1186
|
+
}));
|
|
1187
|
+
throw new Error(`refusing to ${action}: declared base ${ref} has drifted from ${against} — `
|
|
1188
|
+
+ `${probe.missing.length} declared commit(s) missing:\n${lines.join("\n")}\n${remedy}`);
|
|
1189
|
+
}
|
|
1148
1190
|
/** The ref legs' details: the leg, both commits, the symbolic target and the repair text. */
|
|
1149
1191
|
export function identityDetails(leg, state, branch, runId, expected) {
|
|
1150
1192
|
const found = state.kind === "regular" || state.kind === "symbolic" ? state.commit : undefined;
|
|
@@ -2105,6 +2147,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2105
2147
|
let baselineFailed = false;
|
|
2106
2148
|
try {
|
|
2107
2149
|
graph = loadGraph(repoRoot);
|
|
2150
|
+
// Queue row 220 (D-2111): a declared base is proven HERE — before any journal row (lock-reclaimed and
|
|
2151
|
+
// exit-cause included), the resume anchor checks, a worktree, the integration ref or baseline capture —
|
|
2152
|
+
// so a refusal leaves nothing; the finally releases the lock and repository claim. No base: unchanged.
|
|
2153
|
+
const declaredBase = graph.spec.base === undefined ? undefined
|
|
2154
|
+
: await proveDeclaredBase(repoRoot, runId, graph.spec.base, opts.resume === true);
|
|
2155
|
+
const declaredBaseRow = declaredBase ? { declaredBase: { ref: declaredBase.ref, via: declaredBase.via } } : {};
|
|
2108
2156
|
// One bounded snapshot supplies every dispatch. On resume the current journal still participates
|
|
2109
2157
|
// in the fold so its task-done/ordinary-approval events can retire older evidence, but findings it
|
|
2110
2158
|
// produced are suppressed: same-run feedback already replays those bytes and must not deliver them
|
|
@@ -2943,6 +2991,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2943
2991
|
...(replayedExclusions.size > 0 ? { excludedChannels: [...replayedExclusions].sort() } : {}),
|
|
2944
2992
|
...(opts.retryFailed ? { retryFailed: true } : {}),
|
|
2945
2993
|
...(opts.graphChanged ? { graphChanged: true } : {}), // OBS-1073: the release is the engagement's
|
|
2994
|
+
...declaredBaseRow,
|
|
2946
2995
|
});
|
|
2947
2996
|
runStarted = true;
|
|
2948
2997
|
await placeBoard();
|
|
@@ -2993,7 +3042,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2993
3042
|
}
|
|
2994
3043
|
}
|
|
2995
3044
|
else {
|
|
2996
|
-
baseRef = await gitHead(repoRoot);
|
|
3045
|
+
baseRef = declaredBase?.start ?? await gitHead(repoRoot); // row 220: the proven commit is the run's base
|
|
2997
3046
|
journal.append("baseline-start", undefined, { baseRef, commands, capacity: resolvedCapacity() });
|
|
2998
3047
|
await placeBoard();
|
|
2999
3048
|
baselinePending = true;
|
|
@@ -3034,6 +3083,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3034
3083
|
branch, graphDefinitionHash: graphDefinitionHash(graph),
|
|
3035
3084
|
integrationAnchor: "journal", // 2.8.1 T2: this run creates its branch at baseRef and journals every publication with `from`
|
|
3036
3085
|
...(prior ? { supersedes: prior.runId } : {}),
|
|
3086
|
+
...declaredBaseRow,
|
|
3037
3087
|
}); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
|
|
3038
3088
|
runStarted = true;
|
|
3039
3089
|
// v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
|
|
@@ -5186,7 +5236,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5186
5236
|
const icmd = retryMode === "resume"
|
|
5187
5237
|
? adapter.resumeCommand(sessionId, promptFile, assignment.model, assignment.effort)
|
|
5188
5238
|
: cfg.visibility.worker === "interactive" && driver.interactive
|
|
5189
|
-
? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort)
|
|
5239
|
+
? adapter.interactiveCommand(promptFile, assignment.model, assignment.effort, { cwd: wt, branch: taskBranch, repoRoot })
|
|
5190
5240
|
: null;
|
|
5191
5241
|
// v1.69 T6: adapters that declare interactiveSeed launch the real TUI and inject the prompt as a
|
|
5192
5242
|
// user turn; they do NOT need the argv-seeding surface that interactiveCommand represents.
|
|
@@ -5203,7 +5253,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
5203
5253
|
// timing can never interleave a `$(…)` with what follows it (the codex corruption class).
|
|
5204
5254
|
const workerCmd = interactive
|
|
5205
5255
|
? (hasSeed ? ":" : icmd)
|
|
5206
|
-
: adapter.invoke(t, wt, assignment, { promptFile }).command;
|
|
5256
|
+
: adapter.invoke(t, wt, assignment, { promptFile, branch: taskBranch, repoRoot }).command;
|
|
5207
5257
|
const dispatchScript = promptFile.replace(/\.md$/, ".sh");
|
|
5208
5258
|
// Approval can reset the attempt counter while the old pane remains retained.
|
|
5209
5259
|
// Its ownership claim must survive a new engagement reusing the script path.
|
package/dist/run/git-trust.d.ts
CHANGED
|
@@ -4,11 +4,21 @@ export declare class GitTrustRefusal extends Error {
|
|
|
4
4
|
readonly detail: string;
|
|
5
5
|
constructor(path: string, detail: string, options?: ErrorOptions);
|
|
6
6
|
}
|
|
7
|
+
interface Authority {
|
|
8
|
+
gitdir: string;
|
|
9
|
+
common: string;
|
|
10
|
+
linked: boolean;
|
|
11
|
+
}
|
|
7
12
|
/**
|
|
8
13
|
* The canonical, independently expected common git directory for the checkout enclosing `cwd` — never what
|
|
9
14
|
* a mutable commondir says. Throws GitTrustRefusal naming the authority path when it cannot be established.
|
|
10
15
|
*/
|
|
11
16
|
export declare function trustedCommonDir(cwd: string): string;
|
|
17
|
+
/** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
|
|
18
|
+
export declare function checkedAuthority(cwd: string): (Authority & {
|
|
19
|
+
worktree: string;
|
|
20
|
+
stamp?: string;
|
|
21
|
+
}) | undefined;
|
|
12
22
|
/**
|
|
13
23
|
* The uncached pre-spawn check for a covered git invocation at `cwd`. Outside any checkout it is a no-op
|
|
14
24
|
* (unrelated shell commands stay compatible). Inside one it refuses, naming the file, when the commondir git
|
|
@@ -119,3 +129,4 @@ export declare const INERT_HOOKS_PATH = "/dev/null";
|
|
|
119
129
|
* children (shGit/shGitOk) receive it; payload shells (gate commands, suites) keep the operator's git config.
|
|
120
130
|
*/
|
|
121
131
|
export declare function protectedGitEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
132
|
+
export {};
|
package/dist/run/git-trust.js
CHANGED
|
@@ -151,7 +151,7 @@ catch {
|
|
|
151
151
|
return false;
|
|
152
152
|
} };
|
|
153
153
|
/** The checked authority for `cwd` (see assertGitTrust), with the canonical worktree root the gitfile sits in. */
|
|
154
|
-
function checkedAuthority(cwd) {
|
|
154
|
+
export function checkedAuthority(cwd) {
|
|
155
155
|
const dotGit = dotGitOf(cwd);
|
|
156
156
|
if (!dotGit)
|
|
157
157
|
return undefined;
|
package/dist/run/git.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommandReceiptAttribution, ShellReceipt } from "./protocol.js";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, type SpawnSyncReturns } from "node:child_process";
|
|
3
3
|
import { ROUTING_ENV_SEAMS } from "../route/router.js";
|
|
4
4
|
import { type OwnGitCapability } from "./git-trust.js";
|
|
5
5
|
export { ROUTING_ENV_SEAMS };
|
|
@@ -187,6 +187,18 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number, options
|
|
|
187
187
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number, own?: OwnGitOptions, options?: Pick<ShellOptions, "bytes">): Promise<ShResult>;
|
|
188
188
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
189
189
|
export declare function shGitOk(cmd: string, cwd: string, own?: OwnGitOptions): Promise<string>;
|
|
190
|
+
/**
|
|
191
|
+
* Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
|
|
192
|
+
* It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
|
|
193
|
+
* checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
|
|
194
|
+
* targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
|
|
195
|
+
* inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
|
|
196
|
+
* after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
|
|
197
|
+
* fail-open answer for an ordinary git failure is unchanged.
|
|
198
|
+
*/
|
|
199
|
+
export declare function gitSync(dir: string, args: readonly string[], { env }?: {
|
|
200
|
+
env?: NodeJS.ProcessEnv;
|
|
201
|
+
}): SpawnSyncReturns<string>;
|
|
190
202
|
/**
|
|
191
203
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
192
204
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
package/dist/run/git.js
CHANGED
|
@@ -2,7 +2,7 @@ import { observeVerificationReceipt, observeVerificationSpawn, verificationJobEn
|
|
|
2
2
|
import { executionSignal, withoutExecutionBudget } from "./execution-budget.js";
|
|
3
3
|
import { commandLeaseEnvironment, repositoryLeaseEnvironment, registerRepositoryChild, withCommandLease } from "./lease.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { availableParallelism, homedir, tmpdir } from "node:os";
|
|
8
8
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -600,6 +600,27 @@ export async function shGitOk(cmd, cwd, own = {}) {
|
|
|
600
600
|
throw new Error(`command failed (${r.code}): ${cmd}\n${r.stderr || r.stdout}`);
|
|
601
601
|
return r.stdout;
|
|
602
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Queue row 2 item b (D-1042, D-2112, D-2114): the SYNCHRONOUS own-git entry for operator commands — argv, no shell.
|
|
605
|
+
* It applies what shGit's own-git entry applies, from the same sources: the uncached trust check and, in a linked
|
|
606
|
+
* checkout, ownGitPin's pin, post-exit recheck and ref-view release, each derived from `dir` — the directory git
|
|
607
|
+
* targets (a site's -C directory), never the caller's cwd — and protectedGitEnv's forced core.fsmonitor=false and
|
|
608
|
+
* inert core.hooksPath appended last. A trust refusal throws GitTrustRefusal naming the path (before the spawn, or
|
|
609
|
+
* after it for a commondir changed while the child ran); every other outcome is spawnSync's own result, so a caller's
|
|
610
|
+
* fail-open answer for an ordinary git failure is unchanged.
|
|
611
|
+
*/
|
|
612
|
+
export function gitSync(dir, args, { env = process.env } = {}) {
|
|
613
|
+
const forced = protectedGitEnv(env);
|
|
614
|
+
const pin = ownGitPin(dir, forced);
|
|
615
|
+
try {
|
|
616
|
+
const r = spawnSync("git", args, { cwd: dir, env: pin?.env ?? forced, encoding: "utf8", maxBuffer: 1 << 28, stdio: ["ignore", "pipe", "pipe"] });
|
|
617
|
+
pin?.recheck();
|
|
618
|
+
return pin && typeof r.stdout === "string" ? { ...r, stdout: pin.unview(r.stdout), stderr: pin.unview(r.stderr) } : r;
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
pin?.release();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
603
624
|
/**
|
|
604
625
|
* v2.6.8 T2: the DETACHED worktree capability, declared by calling it (the daemon's baseline recapture and standalone
|
|
605
626
|
* verify's base worktree). Under the ref-store pin git stubs a detached add's new HEAD as `ref: refs/heads/.invalid`
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -42,7 +42,14 @@ export declare const bindingToken: (binding: DecisionBinding) => string;
|
|
|
42
42
|
export declare function parseBindingToken(token: string): DecisionBinding | undefined;
|
|
43
43
|
/** The binding a task-approved row recorded under `park` or `failure`, when well formed. */
|
|
44
44
|
export declare function recordedBinding(value: unknown): DecisionBinding | undefined;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The newest failed gate before a park row — the gate a waive of that park would satisfy.
|
|
47
|
+
* Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
|
|
48
|
+
* unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
|
|
49
|
+
* review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
|
|
50
|
+
* an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
|
|
51
|
+
* does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
|
|
52
|
+
*/
|
|
46
53
|
export declare function failedGateBeforePark(events: readonly JournalEvent[], taskId: string, parkIndex: number): GateName | undefined;
|
|
47
54
|
/** The physical journal line of `events[i]` (see SOURCE_LINE). */
|
|
48
55
|
export declare const physicalLine: (events: readonly JournalEvent[], i: number) => number;
|
|
@@ -96,6 +103,13 @@ export interface PreservedRef {
|
|
|
96
103
|
export declare function preservedRefsByTask(events: JournalEvent[]): Map<string, PreservedRef[]>;
|
|
97
104
|
export declare function reviewRoundsSinceApproval(events: JournalEvent[], taskId: string, rounds?: (decided: JournalEvent[]) => JournalEvent[]): number;
|
|
98
105
|
export declare function upheldFeedbackByTask(events: JournalEvent[]): Map<string, string>;
|
|
106
|
+
/**
|
|
107
|
+
* Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
|
|
108
|
+
* active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
|
|
109
|
+
* that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
|
|
110
|
+
* `decided` is already read through the decision fold (effectiveEvents).
|
|
111
|
+
*/
|
|
112
|
+
export declare function restoresUpheldFeedback(decided: JournalEvent[], taskId: string): boolean;
|
|
99
113
|
export interface StructuredFinding {
|
|
100
114
|
class: string;
|
|
101
115
|
path: string;
|
package/dist/run/journal.js
CHANGED
|
@@ -97,17 +97,44 @@ export function recordedBinding(value) {
|
|
|
97
97
|
const { line, ts } = value;
|
|
98
98
|
return typeof line === "number" && Number.isSafeInteger(line) && line > 0 && typeof ts === "string" ? { line, ts } : undefined;
|
|
99
99
|
}
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The newest failed gate before a park row — the gate a waive of that park would satisfy.
|
|
102
|
+
* Queue row 225 (D-2111): a gate-fail park with no task-dispatch, gate-result (passing or failing) or
|
|
103
|
+
* unvoided task-approved row of its task since its previous task-human — a resume's re-park at the same
|
|
104
|
+
* review round cap — inherits that park's failed gate, the walk continuing past it. A task-approved row
|
|
105
|
+
* an approval-refused row of the task voids before the newer park (its lines name it, or it names none)
|
|
106
|
+
* does not end the walk; refusals are read here directly, never through foldDecisions (which calls this).
|
|
107
|
+
*/
|
|
101
108
|
export function failedGateBeforePark(events, taskId, parkIndex) {
|
|
109
|
+
let inherits = events[parkIndex]?.data.kind === "gate-fail";
|
|
110
|
+
let refused = new Set();
|
|
102
111
|
for (let i = parkIndex - 1; i >= 0; i -= 1) {
|
|
103
112
|
const event = events[i];
|
|
104
113
|
if (event.taskId !== taskId)
|
|
105
114
|
continue;
|
|
106
|
-
if (event.event === "task-
|
|
115
|
+
if (event.event === "task-human") {
|
|
116
|
+
// Only a gate-fail predecessor has a failed gate to hand down; any other kind ends the walk as today.
|
|
117
|
+
if (!inherits || event.data.kind !== "gate-fail")
|
|
118
|
+
return undefined;
|
|
119
|
+
// The walk now reads the previous park's own span, under that park's own refusals.
|
|
120
|
+
refused = new Set();
|
|
121
|
+
}
|
|
122
|
+
else if (event.event === APPROVAL_REFUSED) {
|
|
123
|
+
if (refused !== "all")
|
|
124
|
+
refused = Array.isArray(event.data.lines) ? new Set([...refused, ...event.data.lines]) : "all";
|
|
125
|
+
}
|
|
126
|
+
else if (event.event === "task-approved") {
|
|
127
|
+
if (!inherits || (refused !== "all" && !refused.has(physicalLine(events, i))))
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
else if (event.event === "task-dispatch") {
|
|
107
131
|
return undefined;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
}
|
|
133
|
+
else if (event.event === "gate-result") {
|
|
134
|
+
if (event.data.pass === false && typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
|
|
135
|
+
return event.data.gate;
|
|
136
|
+
}
|
|
137
|
+
inherits = false; // a passing gate-result keeps the park's own span
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
return undefined;
|
|
@@ -321,6 +348,16 @@ export function upheldFeedbackByTask(events) {
|
|
|
321
348
|
}
|
|
322
349
|
return upheld;
|
|
323
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Queue row 225 (D-2111): a restore names upheld feedback only when it is FUNDED — the task carries an
|
|
353
|
+
* active uphold brief AND an effective task-approved row newer than its newest task-human. A restore
|
|
354
|
+
* that only re-parks at the same cap announces nothing, so a spent uphold is never re-announced.
|
|
355
|
+
* `decided` is already read through the decision fold (effectiveEvents).
|
|
356
|
+
*/
|
|
357
|
+
export function restoresUpheldFeedback(decided, taskId) {
|
|
358
|
+
const newest = (event) => decided.reduce((at, e, i) => e.taskId === taskId && e.event === event ? i : at, -1);
|
|
359
|
+
return newest("task-approved") > newest("task-human") && upheldFeedbackByTask(decided).has(taskId);
|
|
360
|
+
}
|
|
324
361
|
const CONSULT_ACTIONS = ["retry", "reroute", "decompose", "human"];
|
|
325
362
|
const consultAction = (action) => typeof action === "string" && CONSULT_ACTIONS.includes(action)
|
|
326
363
|
? action
|
|
@@ -2008,7 +2045,7 @@ export class Journal {
|
|
|
2008
2045
|
const folded = !decisionRow && data.fatal === true ? [...priorEvents, { ts: new Date().toISOString(), event, data }] : priorEvents;
|
|
2009
2046
|
return decisionRow ? reduced : { ...reduced, owedChecks: foldOwedChecks(folded, join(this.dir, "..", "..", "..")) };
|
|
2010
2047
|
})()
|
|
2011
|
-
: event === "resume-restore" && rowTaskId &&
|
|
2048
|
+
: event === "resume-restore" && rowTaskId && restoresUpheldFeedback(effectiveEvents(priorEvents), rowTaskId)
|
|
2012
2049
|
? {
|
|
2013
2050
|
...inputData,
|
|
2014
2051
|
upheldFeedbackRestoredFor: rowTaskId,
|