@tokenfactory/acc-runner 0.6.0 → 0.6.2

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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * v0.12-REVIEW-LOCAL — Runner-side reviewer-agent runtime.
3
+ *
4
+ * Triggered by a `review_assigned` realtime broadcast on
5
+ * `runner:<id>`. Spawns `claude --print` with the reviewer prompt
6
+ * (mirrors v0.11-B's spawnClaude for task-runner.ts) against the
7
+ * operator's CLI subscription, parses the JSON decision, and posts
8
+ * it back via acc.submit_review. Cost-cap enforcement is identical
9
+ * to api/_lib/reviewer-agent.ts so the cron's downstream merge
10
+ * gate sees consistent ReviewerOutcome shape across the queue and
11
+ * fallback paths.
12
+ *
13
+ * Never throws. Every failure surfaces through acc.submit_review
14
+ * with decision='reviewer_error' so the queue row settles and the
15
+ * cron's poll-merge loop can advance.
16
+ */
17
+ import { execa } from "execa";
18
+ import { parseClaudeJson, priceUsdCents, normalizeUsage, } from "../cost-pricing.js";
19
+ // Same template as prompts/reviewer-agent.md. The runner does not have
20
+ // the markdown file at runtime (the package ships without prompts/) so
21
+ // the prompt is embedded. Keep this string in sync with the markdown
22
+ // version on every prompt edit; tests/integration/v0.12-REVIEW-LOCAL
23
+ // asserts both render the same outline.
24
+ const REVIEWER_PROMPT_TEMPLATE = [
25
+ "You are reviewing a pull request opened by another Claude (an ACC",
26
+ "task runner). Be skeptical. Bias toward rejecting when in doubt.",
27
+ "",
28
+ "Task spec:",
29
+ "{{task_description}}",
30
+ "",
31
+ "Acceptance criteria:",
32
+ "{{acceptance_criteria}}",
33
+ "",
34
+ "PR title: {{pr_title}}",
35
+ "PR body:",
36
+ "{{pr_body}}",
37
+ "",
38
+ "PR diff:",
39
+ "{{pr_diff}}",
40
+ "",
41
+ "Answer in this exact JSON shape, no surrounding prose:",
42
+ '{"decision":"approve"|"reject","reasons":["..."],"confidence":0.0}',
43
+ ].join("\n");
44
+ const MAX_DIFF_CHARS = 60_000;
45
+ export function renderReviewerPrompt(args) {
46
+ const diff = args.pr_diff.length > MAX_DIFF_CHARS
47
+ ? args.pr_diff.slice(0, MAX_DIFF_CHARS) +
48
+ `\n\n... [diff truncated at ${MAX_DIFF_CHARS} chars; full length ${args.pr_diff.length}] ...\n`
49
+ : args.pr_diff;
50
+ const acceptance = (args.acceptance_criteria ?? [])
51
+ .map((a, i) => `${i + 1}. ${a}`)
52
+ .join("\n");
53
+ return REVIEWER_PROMPT_TEMPLATE
54
+ .replace("{{task_description}}", args.task_description ?? "")
55
+ .replace("{{acceptance_criteria}}", acceptance)
56
+ .replace("{{pr_title}}", args.pr_title ?? "")
57
+ .replace("{{pr_body}}", args.pr_body ?? "")
58
+ .replace("{{pr_diff}}", diff);
59
+ }
60
+ export function extractReviewerDecision(text) {
61
+ if (!text)
62
+ return null;
63
+ const first = text.indexOf("{");
64
+ const last = text.lastIndexOf("}");
65
+ if (first === -1 || last <= first)
66
+ return null;
67
+ const candidates = [text.slice(first, last + 1), text.trim()];
68
+ for (const candidate of candidates) {
69
+ try {
70
+ const obj = JSON.parse(candidate);
71
+ if (obj.decision !== "approve" && obj.decision !== "reject")
72
+ continue;
73
+ const reasons = Array.isArray(obj.reasons)
74
+ ? obj.reasons.filter((r) => typeof r === "string")
75
+ : [];
76
+ const confidence = typeof obj.confidence === "number" &&
77
+ obj.confidence >= 0 && obj.confidence <= 1
78
+ ? obj.confidence
79
+ : 0;
80
+ return { decision: obj.decision, reasons, confidence };
81
+ }
82
+ catch { /* try next candidate */ }
83
+ }
84
+ return null;
85
+ }
86
+ async function defaultFetchPRMeta(repo, prNumber) {
87
+ const { stdout } = await execa("gh", ["pr", "view", String(prNumber), "-R", repo, "--json", "title,body"], { env: process.env });
88
+ const parsed = JSON.parse(stdout);
89
+ return { title: parsed.title ?? "", body: parsed.body ?? "" };
90
+ }
91
+ async function defaultFetchPRDiff(repo, prNumber) {
92
+ const { stdout } = await execa("gh", ["pr", "diff", String(prNumber), "-R", repo], { env: process.env });
93
+ return stdout;
94
+ }
95
+ function defaultSpawnClaude(modelId) {
96
+ const args = ["--print", "--dangerously-skip-permissions", "--output-format=json"];
97
+ const trimmed = (modelId ?? "").trim();
98
+ if (trimmed)
99
+ args.push("--model", trimmed);
100
+ return execa("claude", args, {
101
+ stdin: "pipe",
102
+ stdout: "pipe",
103
+ stderr: "pipe",
104
+ reject: false,
105
+ env: process.env,
106
+ });
107
+ }
108
+ function parseEnvelope(stdout, fallbackModel) {
109
+ const parsed = parseClaudeJson(stdout);
110
+ if (!parsed) {
111
+ return { result: stdout.trim(), session_id: null, cost_usd: 0, model: fallbackModel };
112
+ }
113
+ const model = parsed.model ?? fallbackModel;
114
+ const usage = normalizeUsage(parsed.usage);
115
+ const cents = priceUsdCents(model, usage);
116
+ return {
117
+ result: parsed.result ?? "",
118
+ // any-allowed: parseClaudeJson keeps additional fields on the
119
+ // record; session_id isn't on the typed shape but appears in
120
+ // every observed `--output-format=json` envelope from Claude CLI.
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ session_id: parsed.session_id ?? null,
123
+ cost_usd: cents / 100,
124
+ model,
125
+ };
126
+ }
127
+ /**
128
+ * Drive one review end-to-end: fetch task spec + PR meta + diff →
129
+ * render prompt → spawn claude → parse decision → submit_review.
130
+ * Always submits a row even on internal failure (reviewer_error) so
131
+ * the cron's poll-merge loop can advance.
132
+ */
133
+ export async function runReview(assignment, deps) {
134
+ const fetchMeta = deps.fetchPRMeta ?? defaultFetchPRMeta;
135
+ const fetchDiff = deps.fetchPRDiff ?? defaultFetchPRDiff;
136
+ const spawnFn = deps.spawnClaude ?? defaultSpawnClaude;
137
+ let outcome;
138
+ try {
139
+ outcome = await driveReview(assignment, deps, fetchMeta, fetchDiff, spawnFn);
140
+ }
141
+ catch (err) {
142
+ outcome = {
143
+ decision: "reviewer_error",
144
+ reasons: [`runtime_threw: ${err.message?.slice(0, 200)}`],
145
+ confidence: 0,
146
+ session_id: null,
147
+ cost_usd: 0,
148
+ };
149
+ }
150
+ const { error } = await deps.supabase.rpc("submit_review", {
151
+ p_review_id: assignment.review_id,
152
+ p_decision: outcome.decision,
153
+ p_reasons: outcome.reasons,
154
+ p_confidence: outcome.confidence,
155
+ p_session_id: outcome.session_id,
156
+ p_cost_usd: outcome.cost_usd,
157
+ });
158
+ if (error) {
159
+ process.stderr.write(`[acc-runner] submit_review(${assignment.review_id}) failed: ${error.message}\n`);
160
+ }
161
+ return outcome;
162
+ }
163
+ async function driveReview(assignment, deps, fetchMeta, fetchDiff, spawnFn) {
164
+ const { data, error } = await deps.supabase.rpc("fetch_task_for_runner", {
165
+ p_task_id: assignment.task_id,
166
+ });
167
+ if (error || !data) {
168
+ return {
169
+ decision: "reviewer_error",
170
+ reasons: [`fetch_task_failed: ${error?.message ?? "no data"}`],
171
+ confidence: 0,
172
+ session_id: null,
173
+ cost_usd: 0,
174
+ };
175
+ }
176
+ const result = data;
177
+ const task = result.task;
178
+ const repo = task.repo?.trim();
179
+ if (!repo) {
180
+ return {
181
+ decision: "reviewer_error",
182
+ reasons: ["task has no repo to target gh against"],
183
+ confidence: 0,
184
+ session_id: null,
185
+ cost_usd: 0,
186
+ };
187
+ }
188
+ // Look up the org's reviewer policy. The runner-side reviewer needs
189
+ // confidence_threshold + max_cost_usd_per_review to stay consistent
190
+ // with the cron-side gate, since the cron now just lifts the decision
191
+ // out of the queue row without re-checking the cap.
192
+ const policy = deps.loadPolicy
193
+ ? await deps.loadPolicy("")
194
+ : await loadReviewerPolicy(deps.supabase);
195
+ const [meta, diff] = await Promise.all([
196
+ fetchMeta(repo, assignment.pr_number),
197
+ fetchDiff(repo, assignment.pr_number),
198
+ ]);
199
+ const prompt = renderReviewerPrompt({
200
+ task_description: task.description ?? "",
201
+ acceptance_criteria: task.acceptance ?? [],
202
+ pr_title: meta.title,
203
+ pr_body: meta.body,
204
+ pr_diff: diff,
205
+ });
206
+ const child = spawnFn(policy?.model_id ?? "claude-sonnet-4-6");
207
+ if (child.stdin) {
208
+ child.stdin.write(prompt);
209
+ child.stdin.end();
210
+ }
211
+ const stdout = await collectStream(child.stdout);
212
+ const exit = await child;
213
+ if (exit.exitCode !== 0) {
214
+ return {
215
+ decision: "reviewer_error",
216
+ reasons: [`claude exited ${exit.exitCode}`],
217
+ confidence: 0,
218
+ session_id: null,
219
+ cost_usd: 0,
220
+ };
221
+ }
222
+ const envelope = parseEnvelope(stdout, policy?.model_id ?? "claude-sonnet-4-6");
223
+ // Cost cap parity with api/_lib/reviewer-agent.ts. A cap of 0
224
+ // disables the gate.
225
+ if (policy &&
226
+ policy.max_cost_usd_per_review > 0 &&
227
+ envelope.cost_usd > policy.max_cost_usd_per_review) {
228
+ return {
229
+ decision: "cost_cap_exceeded",
230
+ reasons: [
231
+ `reviewer cost $${envelope.cost_usd.toFixed(4)} exceeded cap $${policy.max_cost_usd_per_review.toFixed(2)}`,
232
+ ],
233
+ confidence: 0,
234
+ session_id: envelope.session_id,
235
+ cost_usd: envelope.cost_usd,
236
+ };
237
+ }
238
+ const decision = extractReviewerDecision(envelope.result);
239
+ if (!decision) {
240
+ return {
241
+ decision: "reviewer_error",
242
+ reasons: [
243
+ "could not parse reviewer JSON from claude output",
244
+ `output_head: ${envelope.result.slice(0, 200)}`,
245
+ ],
246
+ confidence: 0,
247
+ session_id: envelope.session_id,
248
+ cost_usd: envelope.cost_usd,
249
+ };
250
+ }
251
+ return {
252
+ decision: decision.decision,
253
+ reasons: decision.reasons,
254
+ confidence: decision.confidence,
255
+ session_id: envelope.session_id,
256
+ cost_usd: envelope.cost_usd,
257
+ };
258
+ }
259
+ async function loadReviewerPolicy(supabase) {
260
+ // any-allowed: org_settings rows aren't in the runner's generated
261
+ // Database type yet — same pattern as task-runner.ts cost-event sink.
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ const { data } = await supabase.from("org_settings")
264
+ .select("automerge_policy")
265
+ .limit(1)
266
+ .maybeSingle();
267
+ const row = data;
268
+ return row?.automerge_policy?.reviewer ?? null;
269
+ }
270
+ async function collectStream(stream) {
271
+ if (!stream)
272
+ return "";
273
+ let captured = "";
274
+ for await (const raw of stream) {
275
+ captured += typeof raw === "string" ? raw : raw.toString("utf8");
276
+ }
277
+ return captured;
278
+ }
279
+ //# sourceMappingURL=reviewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewer.js","sourceRoot":"","sources":["../../src/runtime/reviewer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ export declare function workDir(): string;
2
+ export interface PreparedWorktree {
3
+ path: string;
4
+ /**
5
+ * v0.12-RESUME — true when a pre-existing registered worktree on
6
+ * the expected branch was reused instead of force-recreated. The
7
+ * caller logs this so an operator can audit how often the resume
8
+ * path actually fires vs. force-recreate (which is the path taken
9
+ * on every fresh task and on every stale/mismatched leftover).
10
+ * Optional so v0.11-F-era test stubs that omit the flag remain
11
+ * valid; absent or false both mean "fresh-recreate path".
12
+ */
13
+ resumed?: boolean;
14
+ cleanup(): Promise<void>;
15
+ }
16
+ export type GitExec = (args: string[], cwd: string) => Promise<void>;
17
+ export type WorktreeInspector = (repoPath: string, taskId: string) => Promise<WorktreeInspection | null>;
18
+ export interface PrepareWorktreeOptions {
19
+ repoPath: string;
20
+ taskId: string;
21
+ branch: string;
22
+ baseBranch: string;
23
+ git?: GitExec;
24
+ /**
25
+ * v0.12-RESUME — override worktree-state inspection for tests.
26
+ * Defaults to the real `inspectExistingWorktree` which shells out
27
+ * via `execa`. Tests inject a stub so the resume-vs-fresh branch is
28
+ * exercisable without standing up a real git repo on disk.
29
+ */
30
+ inspect?: WorktreeInspector;
31
+ }
32
+ export declare function worktreePath(taskId: string): string;
33
+ /**
34
+ * v0.12-RESUME — capture the state of `worktreePath(taskId)` so the
35
+ * caller can decide between resume (reuse the dir + branch) and
36
+ * force-recreate (the legacy v0.11-F behaviour). Returns null when
37
+ * the directory does not exist.
38
+ *
39
+ * `dirty` is set when `git status --porcelain` inside the worktree
40
+ * lists at least one entry — meaning the prior (crashed) runner left
41
+ * uncommitted edits Claude can pick up from. `branch` is the result
42
+ * of `git rev-parse --abbrev-ref HEAD` — when it matches the
43
+ * task's expected branch the worktree is a valid resume candidate.
44
+ *
45
+ * Best-effort: failures (corrupt worktree, missing .git/worktrees
46
+ * entry, etc.) return `registered: false, branch: null` so the
47
+ * caller falls back to force-recreate rather than crashing.
48
+ */
49
+ export interface WorktreeInspection {
50
+ path: string;
51
+ registered: boolean;
52
+ branch: string | null;
53
+ dirty: boolean;
54
+ }
55
+ export declare function inspectExistingWorktree(repoPath: string, taskId: string): Promise<WorktreeInspection | null>;
56
+ export declare function prepareTaskWorktree(opts: PrepareWorktreeOptions): Promise<PreparedWorktree>;
57
+ export interface OrphanWorktree {
58
+ path: string;
59
+ taskId: string;
60
+ }
61
+ /** Doctor helper: dirs under workDir() not registered in `git worktree list`. */
62
+ export declare function listOrphanWorktrees(repoPath: string): Promise<OrphanWorktree[]>;
63
+ //# sourceMappingURL=worktree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"AAwBA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACrE,MAAM,MAAM,iBAAiB,GAAG,CAC9B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;AAExC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAMD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AAyBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAgDpC;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAkD3B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iFAAiF;AACjF,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,cAAc,EAAE,CAAC,CA+B3B"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Per-task isolated `git worktree` at `~/.cache/acc-runner/work/<task_id>/`
3
+ * so parallel runners on the same machine don't race on the shared
4
+ * clone's branch checkout. Forked off the integration branch's local
5
+ * tip; cleaned up on every task completion path.
6
+ *
7
+ * v0.12-RESUME — when a prior runner crashed mid-task and the
8
+ * v0.12 stale-running sweep returned the task to queued, a fresh
9
+ * runner that picks the same task back up will find a leftover
10
+ * worktree directory + git registration. The pre-v0.12 behaviour
11
+ * was to force-remove and recreate, throwing away the dead
12
+ * runner's in-progress edits. We now detect this case
13
+ * (`inspectExistingWorktree`), preserve the directory when it is
14
+ * still a valid registration on the expected task branch, and
15
+ * mark the returned `PreparedWorktree` as `resumed: true` so the
16
+ * task-runner can log the resume and Claude picks up from the
17
+ * partially-committed state instead of restarting from baseBranch.
18
+ */
19
+ import fs from "node:fs/promises";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ import { execa } from "execa";
23
+ // Lazy so tests can override HOME between cases.
24
+ export function workDir() {
25
+ return path.join(os.homedir(), ".cache", "acc-runner", "work");
26
+ }
27
+ function defaultGitExec(args, cwd) {
28
+ return execa("git", args, { cwd, env: process.env }).then(() => undefined);
29
+ }
30
+ export function worktreePath(taskId) {
31
+ return path.join(workDir(), taskId);
32
+ }
33
+ async function pathExists(p) {
34
+ try {
35
+ await fs.access(p);
36
+ return true;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
42
+ async function removeWorktree(repoPath, target, git) {
43
+ if (await pathExists(target)) {
44
+ // Best-effort: deregister, then nuke residue. Partial cleanup
45
+ // shouldn't fail the surrounding task lifecycle.
46
+ try {
47
+ await git(["worktree", "remove", "--force", target], repoPath);
48
+ }
49
+ catch { /* noop */ }
50
+ await fs.rm(target, { recursive: true, force: true });
51
+ }
52
+ try {
53
+ await git(["worktree", "prune"], repoPath);
54
+ }
55
+ catch { /* noop */ }
56
+ }
57
+ export async function inspectExistingWorktree(repoPath, taskId) {
58
+ const target = worktreePath(taskId);
59
+ if (!(await pathExists(target)))
60
+ return null;
61
+ // `git worktree list --porcelain` runs from the *parent* clone so
62
+ // the registration check sees the canonical list. A target dir
63
+ // that exists on disk but is absent here is an orphan — the
64
+ // caller will treat it as not-registered and force-recreate.
65
+ let registered = false;
66
+ try {
67
+ const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], { cwd: repoPath, env: process.env });
68
+ for (const line of stdout.split(/\r?\n/)) {
69
+ const m = /^worktree (.+)$/.exec(line);
70
+ if (m && path.resolve(m[1]) === path.resolve(target)) {
71
+ registered = true;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ registered = false;
78
+ }
79
+ let branch = null;
80
+ try {
81
+ const { stdout } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: target, env: process.env });
82
+ branch = stdout.trim() || null;
83
+ }
84
+ catch {
85
+ branch = null;
86
+ }
87
+ let dirty = false;
88
+ try {
89
+ const { stdout } = await execa("git", ["status", "--porcelain"], { cwd: target, env: process.env });
90
+ dirty = stdout.trim().length > 0;
91
+ }
92
+ catch {
93
+ dirty = false;
94
+ }
95
+ return { path: target, registered, branch, dirty };
96
+ }
97
+ export async function prepareTaskWorktree(opts) {
98
+ const git = opts.git ?? defaultGitExec;
99
+ await fs.mkdir(workDir(), { recursive: true });
100
+ const target = worktreePath(opts.taskId);
101
+ // v0.12-RESUME — if a leftover worktree is registered on the
102
+ // expected branch, preserve it so Claude can pick up where the
103
+ // dead runner left off. Otherwise (orphan dir, wrong branch,
104
+ // corrupt state) fall through to the v0.11-F force-recreate
105
+ // path so the new task starts from a known baseBranch tip.
106
+ //
107
+ // We only enter the resume path when the inspector reports
108
+ // registered=true on the expected branch. The real inspector
109
+ // shells to `git worktree list --porcelain`; tests inject a stub.
110
+ // When `opts.repoPath` is not a real git checkout (test fixtures
111
+ // like "/repo"), the default inspector swallows the `git` error
112
+ // and returns registered=false, preserving v0.11-F's force-
113
+ // recreate semantics for the existing test suite.
114
+ const inspect = opts.inspect ?? inspectExistingWorktree;
115
+ const existing = await inspect(opts.repoPath, opts.taskId);
116
+ if (existing && existing.registered && existing.branch === opts.branch) {
117
+ let cleaned = false;
118
+ return {
119
+ path: target,
120
+ resumed: true,
121
+ async cleanup() {
122
+ if (cleaned)
123
+ return;
124
+ cleaned = true;
125
+ await removeWorktree(opts.repoPath, target, git);
126
+ },
127
+ };
128
+ }
129
+ await removeWorktree(opts.repoPath, target, git);
130
+ // `-B <branch> <path> <baseBranch>` force-creates the task branch at
131
+ // baseBranch's tip, overwriting a stale ref from a prior crashed run.
132
+ await git(["worktree", "add", "-B", opts.branch, target, opts.baseBranch], opts.repoPath);
133
+ let cleaned = false;
134
+ return {
135
+ path: target,
136
+ resumed: false,
137
+ async cleanup() {
138
+ if (cleaned)
139
+ return;
140
+ cleaned = true;
141
+ await removeWorktree(opts.repoPath, target, git);
142
+ },
143
+ };
144
+ }
145
+ /** Doctor helper: dirs under workDir() not registered in `git worktree list`. */
146
+ export async function listOrphanWorktrees(repoPath) {
147
+ let entries;
148
+ try {
149
+ entries = await fs.readdir(workDir());
150
+ }
151
+ catch (err) {
152
+ if (err.code === "ENOENT")
153
+ return [];
154
+ throw err;
155
+ }
156
+ const registered = new Set();
157
+ try {
158
+ const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], { cwd: repoPath, env: process.env });
159
+ for (const line of stdout.split(/\r?\n/)) {
160
+ const m = /^worktree (.+)$/.exec(line);
161
+ if (m)
162
+ registered.add(path.resolve(m[1]));
163
+ }
164
+ }
165
+ catch {
166
+ // Without a usable repo we can't distinguish; surface everything.
167
+ }
168
+ const orphans = [];
169
+ for (const name of entries) {
170
+ const p = path.join(workDir(), name);
171
+ try {
172
+ const stat = await fs.stat(p);
173
+ if (!stat.isDirectory())
174
+ continue;
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ if (!registered.has(p))
180
+ orphans.push({ path: p, taskId: name });
181
+ }
182
+ return orphans;
183
+ }
184
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,8 @@ import { type NormalizedUsage } from "./cost-pricing.js";
4
4
  import { type GitRunner } from "./git.js";
5
5
  import { type GhRunner } from "./gh.js";
6
6
  import { type McpConfigCleanup, type McpSpawnOptions } from "./mcp-spawn.js";
7
+ import { type TaskLock } from "./runtime/locks.js";
8
+ import { type PreparedWorktree, type PrepareWorktreeOptions } from "./runtime/worktree.js";
7
9
  import type { RunnerSupabaseClient } from "./supabase.js";
8
10
  export interface CostEventPayload extends NormalizedUsage {
9
11
  task_id: string;
@@ -40,11 +42,12 @@ export interface RunTaskDeps {
40
42
  postCostEvent?: (event: CostEventPayload) => Promise<void>;
41
43
  /**
42
44
  * Runner session coordinates threaded down so the per-task
43
- * .mcp.json can be authored before Claude Code spawns. When absent,
44
- * the MCP server is not provisioned (back-compat for tests + older
45
- * watch.ts wiring).
45
+ * .mcp.json can be authored before Claude Code spawns AND so the
46
+ * v0.6.1 claim_task_with_locks RPC can attribute the lock to the
47
+ * caller's runner row. v0.6.1 (REG-309 sibling): required now —
48
+ * watch.ts has always passed it; tests must stub it.
46
49
  */
47
- session?: {
50
+ session: {
48
51
  accessToken: string;
49
52
  runnerId: string;
50
53
  };
@@ -52,6 +55,30 @@ export interface RunTaskDeps {
52
55
  publicUrl?: string;
53
56
  /** Override for tests; defaults to writing a real .mcp.json. */
54
57
  writeMcpConfig?: (opts: McpSpawnOptions) => Promise<McpConfigCleanup>;
58
+ /**
59
+ * v0.11-F: per-task PID lock at `~/.cache/acc-runner/locks/<task_id>.pid`.
60
+ * Throws `TaskLockHeldError` when another live runner holds the lock;
61
+ * the caller surfaces `phase: "worktree_lock"` and bails before any
62
+ * worktree side-effect lands.
63
+ */
64
+ acquireLock?: (taskId: string) => Promise<TaskLock>;
65
+ /**
66
+ * v0.11-F: provision an isolated git worktree at
67
+ * `~/.cache/acc-runner/work/<task_id>/`. All subsequent git ops +
68
+ * spawnClaude run with the worktree's path as cwd, never the
69
+ * operator's shared clone.
70
+ */
71
+ prepareWorktree?: (opts: PrepareWorktreeOptions) => Promise<PreparedWorktree>;
72
+ /**
73
+ * v0.12-RESUME: cadence (ms) for the periodic
74
+ * `acc.update_task_signal` RPC that refreshes
75
+ * `acc.tasks.last_runner_signal_at` while the task is in flight.
76
+ * The 5-minute cron sweep uses a 5-minute cutoff, so 30s gives
77
+ * ~10 missed signals of margin before a still-alive runner gets
78
+ * its task pulled back to queued. Tests override to a sub-second
79
+ * cadence so the loop is observable without real-time waits.
80
+ */
81
+ signalIntervalMs?: number;
55
82
  }
56
83
  export interface RunTaskController {
57
84
  taskId: string;
@@ -63,7 +90,7 @@ export interface RunTaskController {
63
90
  * line (REG-294) so an operator can tell whether a multi-minute task
64
91
  * died at fetch, git, claude, or push.
65
92
  */
66
- export type RunTaskPhase = "transition_to_running" | "fetch" | "git" | "claude_exit" | "push";
93
+ export type RunTaskPhase = "claim_locks" | "transition_to_running" | "fetch" | "worktree_lock" | "git" | "claude_exit" | "push";
67
94
  export interface RunTaskOutcome {
68
95
  taskId: string;
69
96
  status: "ok" | "failed" | "cancelled";
@@ -1 +1 @@
1
- {"version":3,"file":"task-runner.d.ts","sourceRoot":"","sources":["../src/task-runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-runner.d.ts","sourceRoot":"","sources":["../src/task-runner.ts"],"names":[],"mappings":"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"}