@tokenfactory/acc-runner 0.6.1 → 0.6.3

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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"}
@@ -1,17 +1,58 @@
1
1
  export declare function workDir(): string;
2
2
  export interface PreparedWorktree {
3
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;
4
14
  cleanup(): Promise<void>;
5
15
  }
6
16
  export type GitExec = (args: string[], cwd: string) => Promise<void>;
17
+ export type WorktreeInspector = (repoPath: string, taskId: string) => Promise<WorktreeInspection | null>;
7
18
  export interface PrepareWorktreeOptions {
8
19
  repoPath: string;
9
20
  taskId: string;
10
21
  branch: string;
11
22
  baseBranch: string;
12
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;
13
31
  }
14
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>;
15
56
  export declare function prepareTaskWorktree(opts: PrepareWorktreeOptions): Promise<PreparedWorktree>;
16
57
  export interface OrphanWorktree {
17
58
  path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"AAYA,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,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;AAErE,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;CACf;AAMD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AAyBD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;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"}
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"}
@@ -3,6 +3,18 @@
3
3
  * so parallel runners on the same machine don't race on the shared
4
4
  * clone's branch checkout. Forked off the integration branch's local
5
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.
6
18
  */
7
19
  import fs from "node:fs/promises";
8
20
  import os from "node:os";
@@ -42,10 +54,78 @@ async function removeWorktree(repoPath, target, git) {
42
54
  }
43
55
  catch { /* noop */ }
44
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
+ }
45
97
  export async function prepareTaskWorktree(opts) {
46
98
  const git = opts.git ?? defaultGitExec;
47
99
  await fs.mkdir(workDir(), { recursive: true });
48
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
+ }
49
129
  await removeWorktree(opts.repoPath, target, git);
50
130
  // `-B <branch> <path> <baseBranch>` force-creates the task branch at
51
131
  // baseBranch's tip, overwriting a stale ref from a prior crashed run.
@@ -53,6 +133,7 @@ export async function prepareTaskWorktree(opts) {
53
133
  let cleaned = false;
54
134
  return {
55
135
  path: target,
136
+ resumed: false,
56
137
  async cleanup() {
57
138
  if (cleaned)
58
139
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"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"}
@@ -69,6 +69,16 @@ export interface RunTaskDeps {
69
69
  * operator's shared clone.
70
70
  */
71
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;
72
82
  }
73
83
  export interface RunTaskController {
74
84
  taskId: string;
@@ -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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"}
@@ -16,7 +16,7 @@
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
18
  import { execa } from "execa";
19
- import { normalizeUsage, parseClaudeJson, priceUsdCents, } from "./cost-pricing.js";
19
+ import { normalizeUsage, parseClaudeJson, priceUsdCents, toCliAlias, } from "./cost-pricing.js";
20
20
  import { git as defaultGit } from "./git.js";
21
21
  import { gh as defaultGh } from "./gh.js";
22
22
  import { writeMcpConfig as defaultWriteMcpConfig, } from "./mcp-spawn.js";
@@ -186,6 +186,52 @@ export function runTask(taskId, deps) {
186
186
  process.stderr.write(`[acc-runner] release_task_locks(${taskId}) failed: ${error.message}\n`);
187
187
  }
188
188
  };
189
+ // v0.12-RESUME — periodic signal loop. After the claim succeeds we
190
+ // bump acc.tasks.last_runner_signal_at every signalIntervalMs ms so
191
+ // the v0.12 /5m sweep distinguishes "runner alive, work in flight"
192
+ // from "runner crashed, task stuck at running". Self-rescheduling
193
+ // setTimeout (not setInterval) so a slow RPC doesn't queue up
194
+ // overlapping firings; the loop stops as soon as `signalStopped`
195
+ // flips in the outer try/finally below.
196
+ const DEFAULT_SIGNAL_MS = 30_000;
197
+ const signalIntervalMs = deps.signalIntervalMs ?? DEFAULT_SIGNAL_MS;
198
+ let signalTimer = null;
199
+ let signalStopped = false;
200
+ const updateSignal = async () => {
201
+ const { error } = await deps.supabase.rpc("update_task_signal", {
202
+ p_task_id: taskId,
203
+ });
204
+ if (error) {
205
+ // Best-effort: a missed signal just means the sweep might pull
206
+ // the task back if enough of them stack up. Log and continue.
207
+ process.stderr.write(`[acc-runner] update_task_signal(${taskId}) failed: ${error.message}\n`);
208
+ }
209
+ };
210
+ const scheduleSignal = () => {
211
+ if (signalStopped)
212
+ return;
213
+ signalTimer = setTimeout(async () => {
214
+ if (signalStopped)
215
+ return;
216
+ try {
217
+ await updateSignal();
218
+ }
219
+ catch { /* logged inside */ }
220
+ scheduleSignal();
221
+ }, signalIntervalMs);
222
+ // Detach so an in-flight signal timer doesn't keep the process
223
+ // alive past `watch.ts` shutdown. The outer finally clears it
224
+ // anyway; this is belt-and-suspenders for stray timers.
225
+ if (signalTimer.unref)
226
+ signalTimer.unref();
227
+ };
228
+ const stopSignalLoop = () => {
229
+ signalStopped = true;
230
+ if (signalTimer) {
231
+ clearTimeout(signalTimer);
232
+ signalTimer = null;
233
+ }
234
+ };
189
235
  const promise = (async () => {
190
236
  // 1. Atomic claim + transition to running. v0.11-D: replaces the
191
237
  // pre-v0.6.1 raw transition_task('running') call. The RPC
@@ -234,6 +280,15 @@ export function runTask(taskId, deps) {
234
280
  // recovers from a thrown error inside runTask, so the lock
235
281
  // would otherwise leak until a future sweep job clears it.
236
282
  try {
283
+ // v0.12-RESUME: prime the signal column immediately on claim so
284
+ // the sweep window resets from "now" and the first periodic tick
285
+ // (after signalIntervalMs) refreshes it. Without this prime, a
286
+ // task whose run takes < signalIntervalMs from claim to first
287
+ // tick could race the sweep on borderline updated_at values.
288
+ // Placed inside the outer try so an unexpected throw from the
289
+ // RPC still runs the finally (stop loop, release locks).
290
+ await updateSignal();
291
+ scheduleSignal();
237
292
  // 2. Fetch task + adjacent rows.
238
293
  const fetched = await deps.supabase.rpc("fetch_task_for_runner", {
239
294
  p_task_id: taskId,
@@ -312,6 +367,25 @@ export function runTask(taskId, deps) {
312
367
  baseBranch: integrationBranch,
313
368
  });
314
369
  workdir = worktree.path;
370
+ // v0.12-RESUME: log resume-vs-fresh so an operator can audit
371
+ // how often the resume path actually fires. `resumed: true`
372
+ // means a prior runner crashed mid-task, the v0.12 sweep
373
+ // returned the task to queued, and this runner picked it
374
+ // back up with the prior worktree intact. Claude reads the
375
+ // partially-committed state and continues; the spawn is the
376
+ // same prompt either way (Claude is idempotent enough that
377
+ // re-running on a partially-edited worktree converges on
378
+ // the right final state).
379
+ if (worktree.resumed) {
380
+ await appendEvent(deps.supabase, taskId, "log", {
381
+ phase: "git",
382
+ stream: "stdout",
383
+ event: "worktree.resumed",
384
+ worktree_path: workdir,
385
+ branch,
386
+ runner_id: deps.session.runnerId,
387
+ });
388
+ }
315
389
  await git.checkout(workdir, branch);
316
390
  }
317
391
  catch (err) {
@@ -356,7 +430,12 @@ export function runTask(taskId, deps) {
356
430
  process.stderr.write(`[acc-runner] mcp .mcp.json write failed: ${err.message}\n`);
357
431
  }
358
432
  }
359
- child = spawnClaude(workdir, result.model?.id);
433
+ // v0.12-MODEL-ALIAS (REG-303/304): translate the ACC model alias
434
+ // (`claude-sonnet-4`) into the wire form `claude --model` actually
435
+ // accepts (`sonnet` or `claude-sonnet-4-6`). Unknown ids pass
436
+ // through verbatim so a future model not yet in the embedded
437
+ // table still spawns.
438
+ child = spawnClaude(workdir, toCliAlias(result.model?.id));
360
439
  if (child.stdin) {
361
440
  child.stdin.write(prompt);
362
441
  child.stdin.end();
@@ -474,6 +553,12 @@ export function runTask(taskId, deps) {
474
553
  }
475
554
  }
476
555
  finally {
556
+ // v0.12-RESUME: stop the periodic signal loop before releasing
557
+ // locks so a late-firing signal can't bump the column after the
558
+ // task transitions to a terminal status (the RPC is no-op on
559
+ // non-running rows anyway, but stopping early avoids the extra
560
+ // RPC round-trip).
561
+ stopSignalLoop();
477
562
  await releaseLocks();
478
563
  }
479
564
  })();