@tokenfactory/acc-runner 0.6.1 → 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.
- package/dist/cost-pricing.d.ts +49 -0
- package/dist/cost-pricing.d.ts.map +1 -1
- package/dist/cost-pricing.js +45 -0
- package/dist/cost-pricing.js.map +1 -1
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +71 -0
- package/dist/doctor.js.map +1 -1
- package/dist/runtime/reviewer.d.ts +70 -0
- package/dist/runtime/reviewer.d.ts.map +1 -0
- package/dist/runtime/reviewer.js +279 -0
- package/dist/runtime/reviewer.js.map +1 -0
- package/dist/runtime/worktree.d.ts +41 -0
- package/dist/runtime/worktree.d.ts.map +1 -1
- package/dist/runtime/worktree.js +81 -0
- package/dist/runtime/worktree.js.map +1 -1
- package/dist/task-runner.d.ts +10 -0
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +87 -2
- package/dist/task-runner.js.map +1 -1
- package/dist/watch.d.ts +6 -0
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +67 -0
- package/dist/watch.js.map +1 -1
- package/package.json +2 -2
|
@@ -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":"
|
|
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"}
|
package/dist/runtime/worktree.js
CHANGED
|
@@ -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":"AAAA
|
|
1
|
+
{"version":3,"file":"worktree.js","sourceRoot":"","sources":["../../src/runtime/worktree.ts"],"names":[],"mappings":"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"}
|
package/dist/task-runner.d.ts
CHANGED
|
@@ -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":"AAiBA,OAAO,EAAS,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,
|
|
1
|
+
{"version":3,"file":"task-runner.d.ts","sourceRoot":"","sources":["../src/task-runner.ts"],"names":[],"mappings":"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"}
|
package/dist/task-runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
})();
|