@towles/tool 0.0.66 → 0.0.68
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/package.json +1 -1
- package/src/commands/auto-claude/index.ts +41 -3
- package/src/commands/auto-claude/retry.test.ts +8 -11
- package/src/commands/auto-claude/retry.ts +4 -2
- package/src/lib/auto-claude/claude-cli.ts +35 -16
- package/src/lib/auto-claude/index.ts +1 -0
- package/src/lib/auto-claude/labels.test.ts +10 -14
- package/src/lib/auto-claude/labels.ts +22 -23
- package/src/lib/auto-claude/pipeline-execution.test.ts +33 -39
- package/src/lib/auto-claude/pipeline.ts +42 -20
- package/src/lib/auto-claude/run-claude.test.ts +40 -28
- package/src/lib/auto-claude/spawn-claude.ts +2 -0
- package/src/lib/auto-claude/steps/create-pr.ts +58 -55
- package/src/lib/auto-claude/steps/implement.ts +3 -1
- package/src/lib/auto-claude/steps/simple-steps.ts +7 -3
- package/src/lib/auto-claude/steps/steps.test.ts +58 -31
- package/src/lib/auto-claude/templates.test.ts +28 -29
- package/src/lib/auto-claude/templates.ts +21 -4
- package/src/lib/auto-claude/utils.ts +4 -1
- package/src/lib/graph/parser.test.ts +21 -25
- package/src/lib/graph/parser.ts +14 -5
- package/src/utils/git/gh-cli-wrapper.test.ts +16 -15
- package/src/utils/git/gh-cli-wrapper.ts +18 -5
|
@@ -8,16 +8,29 @@ import { createPr } from "./steps/create-pr.js";
|
|
|
8
8
|
import { stepImplement } from "./steps/implement.js";
|
|
9
9
|
import { stepPlan, stepReview, stepSimplify } from "./steps/simple-steps.js";
|
|
10
10
|
import { LABELS, ensureLabelsExist, removeLabel, setLabel } from "./labels.js";
|
|
11
|
+
import type { ExecSafeFn } from "./labels.js";
|
|
11
12
|
import { ensureDir, fileExists, readFile, writeFile } from "../../utils/fs.js";
|
|
12
13
|
import { execSafe, git } from "../../utils/git/exec.js";
|
|
13
14
|
import { ghRaw } from "../../utils/git/gh-cli-wrapper.js";
|
|
14
15
|
import { log } from "./utils.js";
|
|
15
16
|
import type { IssueContext } from "./utils.js";
|
|
17
|
+
import type { SpawnClaudeFn } from "./spawn-claude.js";
|
|
16
18
|
|
|
17
19
|
export { type StepName, STEP_NAMES } from "./prompt-templates/index.js";
|
|
18
20
|
|
|
19
|
-
export
|
|
21
|
+
export interface PipelineDeps {
|
|
22
|
+
spawnFn?: SpawnClaudeFn;
|
|
23
|
+
exec?: ExecSafeFn;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runPipeline(
|
|
27
|
+
ctx: IssueContext,
|
|
28
|
+
untilStep?: StepName,
|
|
29
|
+
deps?: PipelineDeps,
|
|
30
|
+
): Promise<void> {
|
|
20
31
|
const cfg = getConfig();
|
|
32
|
+
const exec = deps?.exec;
|
|
33
|
+
const spawnFn = deps?.spawnFn;
|
|
21
34
|
log(`Pipeline starting for ${ctx.repo}#${ctx.number}: ${ctx.title}`);
|
|
22
35
|
|
|
23
36
|
ensureDir(ctx.issueDir);
|
|
@@ -29,14 +42,14 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
// Label management
|
|
32
|
-
await ensureLabelsExist(ctx.repo);
|
|
33
|
-
await removeLabel(ctx.repo, ctx.number, cfg.triggerLabel);
|
|
34
|
-
await setLabel(ctx.repo, ctx.number, LABELS.inProgress);
|
|
45
|
+
await ensureLabelsExist(ctx.repo, exec);
|
|
46
|
+
await removeLabel(ctx.repo, ctx.number, cfg.triggerLabel, exec);
|
|
47
|
+
await setLabel(ctx.repo, ctx.number, LABELS.inProgress, exec);
|
|
35
48
|
|
|
36
49
|
try {
|
|
37
50
|
// Step 1: Plan (runs once)
|
|
38
|
-
if (!(await stepPlan(ctx))) {
|
|
39
|
-
await handleFailure(ctx, "plan");
|
|
51
|
+
if (!(await stepPlan(ctx, spawnFn))) {
|
|
52
|
+
await handleFailure(ctx, "plan", undefined, exec);
|
|
40
53
|
return;
|
|
41
54
|
}
|
|
42
55
|
if (untilStep === "plan") {
|
|
@@ -55,8 +68,8 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
// Implement
|
|
58
|
-
if (!(await stepImplement(ctx))) {
|
|
59
|
-
await handleFailure(ctx, "implement");
|
|
71
|
+
if (!(await stepImplement(ctx, spawnFn))) {
|
|
72
|
+
await handleFailure(ctx, "implement", undefined, exec);
|
|
60
73
|
return;
|
|
61
74
|
}
|
|
62
75
|
if (untilStep === "implement") {
|
|
@@ -65,8 +78,8 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
// Simplify
|
|
68
|
-
if (!(await stepSimplify(ctx))) {
|
|
69
|
-
await handleFailure(ctx, "simplify");
|
|
81
|
+
if (!(await stepSimplify(ctx, spawnFn))) {
|
|
82
|
+
await handleFailure(ctx, "simplify", undefined, exec);
|
|
70
83
|
return;
|
|
71
84
|
}
|
|
72
85
|
if (untilStep === "simplify") {
|
|
@@ -75,8 +88,8 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
// Review
|
|
78
|
-
if (!(await stepReview(ctx))) {
|
|
79
|
-
await handleFailure(ctx, "review");
|
|
91
|
+
if (!(await stepReview(ctx, spawnFn))) {
|
|
92
|
+
await handleFailure(ctx, "review", undefined, exec);
|
|
80
93
|
return;
|
|
81
94
|
}
|
|
82
95
|
if (untilStep === "review") {
|
|
@@ -86,10 +99,10 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
86
99
|
|
|
87
100
|
// Check review result
|
|
88
101
|
if (isReviewPass(ctx)) {
|
|
89
|
-
const prUrl = await createPr(ctx);
|
|
90
|
-
await removeLabel(ctx.repo, ctx.number, LABELS.inProgress);
|
|
91
|
-
await setLabel(ctx.repo, ctx.number, LABELS.success);
|
|
92
|
-
await setLabel(ctx.repo, ctx.number, LABELS.review);
|
|
102
|
+
const prUrl = await createPr(ctx, exec);
|
|
103
|
+
await removeLabel(ctx.repo, ctx.number, LABELS.inProgress, exec);
|
|
104
|
+
await setLabel(ctx.repo, ctx.number, LABELS.success, exec);
|
|
105
|
+
await setLabel(ctx.repo, ctx.number, LABELS.review, exec);
|
|
93
106
|
log(`Pipeline complete for ${ctx.repo}#${ctx.number} — ${prUrl}`);
|
|
94
107
|
return;
|
|
95
108
|
}
|
|
@@ -107,6 +120,7 @@ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Prom
|
|
|
107
120
|
ctx,
|
|
108
121
|
"review",
|
|
109
122
|
`auto-claude: review did not pass after ${maxRetries + 1} attempts. Labelled \`${LABELS.failed}\`.`,
|
|
123
|
+
exec,
|
|
110
124
|
);
|
|
111
125
|
} finally {
|
|
112
126
|
await checkoutMain();
|
|
@@ -125,11 +139,19 @@ function isReviewPass(ctx: IssueContext): boolean {
|
|
|
125
139
|
return firstLine === "PASS";
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
async function handleFailure(
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
async function handleFailure(
|
|
143
|
+
ctx: IssueContext,
|
|
144
|
+
stepName: string,
|
|
145
|
+
comment?: string,
|
|
146
|
+
exec?: ExecSafeFn,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
await removeLabel(ctx.repo, ctx.number, LABELS.inProgress, exec);
|
|
149
|
+
await setLabel(ctx.repo, ctx.number, LABELS.failed, exec);
|
|
131
150
|
if (comment) {
|
|
132
|
-
await ghRaw(
|
|
151
|
+
await ghRaw(
|
|
152
|
+
["issue", "comment", String(ctx.number), "--repo", ctx.repo, "--body", comment],
|
|
153
|
+
exec,
|
|
154
|
+
);
|
|
133
155
|
}
|
|
134
156
|
log(`Pipeline stopped at "${stepName}" for ${ctx.repo}#${ctx.number}`);
|
|
135
157
|
}
|
|
@@ -2,15 +2,14 @@ import consola from "consola";
|
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { initConfig } from "./config";
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
5
|
+
import { createMockClaudeProcess, createTestRepo } from "./test-helpers";
|
|
6
|
+
import type { TestRepo } from "./test-helpers";
|
|
7
|
+
import type { SpawnClaudeFn } from "./spawn-claude";
|
|
8
|
+
import type { ClaudeLogger } from "./claude-cli";
|
|
7
9
|
|
|
8
10
|
consola.level = -999;
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
vi.mock("./spawn-claude", () => createSpawnClaudeMock(() => mockSpawnImpl));
|
|
12
|
-
|
|
13
|
-
describe("runClaude (mocked spawn-claude)", () => {
|
|
12
|
+
describe("runClaude (injected spawnFn)", () => {
|
|
14
13
|
let originalCwd: string;
|
|
15
14
|
let repo: TestRepo;
|
|
16
15
|
|
|
@@ -26,19 +25,20 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
beforeEach(() => {
|
|
29
|
-
mockSpawnImpl = null;
|
|
30
28
|
vi.clearAllMocks();
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
it("parses stream-json result event", async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn((args: string[]) => {
|
|
33
|
+
return createMockClaudeProcess(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
result: "All done",
|
|
36
|
+
is_error: false,
|
|
37
|
+
total_cost_usd: 0.05,
|
|
38
|
+
num_turns: 3,
|
|
39
|
+
}),
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
@@ -48,14 +48,14 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
48
48
|
const result = await runClaude({
|
|
49
49
|
promptFile: "test-prompt.md",
|
|
50
50
|
maxTurns: 10,
|
|
51
|
+
spawnFn: mockSpawnClaude,
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
expect(result.result).toBe("All done");
|
|
54
55
|
expect(result.is_error).toBe(false);
|
|
55
56
|
expect(result.num_turns).toBe(3);
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
expect(spawnClaude).toHaveBeenCalledWith(
|
|
58
|
+
expect(mockSpawnClaude).toHaveBeenCalledWith(
|
|
59
59
|
expect.arrayContaining([
|
|
60
60
|
"-p",
|
|
61
61
|
"--output-format",
|
|
@@ -71,7 +71,13 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it("logs tool names from stream_event and shows thinking indicator", async () => {
|
|
74
|
-
const
|
|
74
|
+
const mockLogger: ClaudeLogger = {
|
|
75
|
+
info: vi.fn(),
|
|
76
|
+
warn: vi.fn(),
|
|
77
|
+
error: vi.fn(),
|
|
78
|
+
success: vi.fn(),
|
|
79
|
+
log: vi.fn(),
|
|
80
|
+
};
|
|
75
81
|
|
|
76
82
|
const thinkingEvent = {
|
|
77
83
|
type: "stream_event",
|
|
@@ -94,30 +100,35 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
94
100
|
num_turns: 1,
|
|
95
101
|
};
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn(() => {
|
|
104
|
+
return createMockClaudeProcess(
|
|
105
|
+
[thinkingEvent, toolEvent, resultEvent].map((e) => JSON.stringify(e)).join("\n"),
|
|
106
|
+
0,
|
|
107
|
+
);
|
|
100
108
|
});
|
|
101
109
|
|
|
102
110
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
103
111
|
|
|
104
112
|
const { runClaude } = await import("./claude-cli");
|
|
105
|
-
const result = await runClaude({
|
|
113
|
+
const result = await runClaude({
|
|
114
|
+
promptFile: "test.md",
|
|
115
|
+
spawnFn: mockSpawnClaude,
|
|
116
|
+
logger: mockLogger,
|
|
117
|
+
});
|
|
106
118
|
|
|
107
119
|
expect(result.result).toBe("Done");
|
|
108
120
|
expect(result.num_turns).toBe(1);
|
|
109
121
|
|
|
110
|
-
const infoCalls =
|
|
122
|
+
const infoCalls = (mockLogger.info as ReturnType<typeof vi.fn>).mock.calls.map((c) =>
|
|
123
|
+
String(c[0]),
|
|
124
|
+
);
|
|
111
125
|
expect(infoCalls.some((msg) => msg.includes("thinking"))).toBe(true);
|
|
112
126
|
expect(infoCalls.some((msg) => msg.includes("Edit"))).toBe(true);
|
|
113
|
-
|
|
114
|
-
infoSpy.mockRestore();
|
|
115
127
|
});
|
|
116
128
|
|
|
117
129
|
it("returns fallback when no result event in stream", async () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
exitCode: 0,
|
|
130
|
+
const mockSpawnClaude: SpawnClaudeFn = vi.fn(() => {
|
|
131
|
+
return createMockClaudeProcess('{"type":"system","message":"starting"}', 0);
|
|
121
132
|
});
|
|
122
133
|
|
|
123
134
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
@@ -126,6 +137,7 @@ describe("runClaude (mocked spawn-claude)", () => {
|
|
|
126
137
|
|
|
127
138
|
const result = await runClaude({
|
|
128
139
|
promptFile: "test.md",
|
|
140
|
+
spawnFn: mockSpawnClaude,
|
|
129
141
|
});
|
|
130
142
|
|
|
131
143
|
expect(result.result).toBe("");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { ChildProcess } from "node:child_process";
|
|
3
3
|
|
|
4
|
+
export type SpawnClaudeFn = (args: string[]) => ChildProcess;
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Spawns the Claude CLI as a child process.
|
|
6
8
|
* Extracted into its own module so tests can mock it cleanly
|
|
@@ -9,11 +9,12 @@ import { getConfig } from "../config.js";
|
|
|
9
9
|
import { ARTIFACTS } from "../prompt-templates/index.js";
|
|
10
10
|
import { log } from "../utils.js";
|
|
11
11
|
import type { IssueContext } from "../utils.js";
|
|
12
|
+
import type { ExecSafeFn } from "../labels.js";
|
|
12
13
|
|
|
13
|
-
export async function createPr(ctx: IssueContext): Promise<string> {
|
|
14
|
+
export async function createPr(ctx: IssueContext, exec?: ExecSafeFn): Promise<string> {
|
|
14
15
|
const cfg = getConfig();
|
|
15
16
|
|
|
16
|
-
const existingUrl = await getExistingPrUrl(ctx.branch);
|
|
17
|
+
const existingUrl = await getExistingPrUrl(ctx.branch, exec);
|
|
17
18
|
if (existingUrl) {
|
|
18
19
|
log(`PR already exists: ${existingUrl}`);
|
|
19
20
|
return existingUrl;
|
|
@@ -48,18 +49,21 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
48
49
|
"Generated by auto-claude pipeline",
|
|
49
50
|
].join("\n");
|
|
50
51
|
|
|
51
|
-
const prUrl = await ghRaw(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
const prUrl = await ghRaw(
|
|
53
|
+
[
|
|
54
|
+
"pr",
|
|
55
|
+
"create",
|
|
56
|
+
"--repo",
|
|
57
|
+
cfg.repo,
|
|
58
|
+
"--head",
|
|
59
|
+
ctx.branch,
|
|
60
|
+
"--title",
|
|
61
|
+
ctx.title,
|
|
62
|
+
"--body",
|
|
63
|
+
body,
|
|
64
|
+
],
|
|
65
|
+
exec,
|
|
66
|
+
);
|
|
63
67
|
|
|
64
68
|
if (!prUrl) {
|
|
65
69
|
throw new Error("Failed to create PR — gh returned empty output");
|
|
@@ -69,7 +73,7 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
69
73
|
log(`PR created: ${prUrl}`);
|
|
70
74
|
|
|
71
75
|
try {
|
|
72
|
-
await attachArtifacts(ctx, prUrl);
|
|
76
|
+
await attachArtifacts(ctx, prUrl, exec);
|
|
73
77
|
} catch (e) {
|
|
74
78
|
consola.warn(`Artifact upload failed (non-blocking): ${e}`);
|
|
75
79
|
}
|
|
@@ -77,7 +81,7 @@ export async function createPr(ctx: IssueContext): Promise<string> {
|
|
|
77
81
|
return prUrl;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void> {
|
|
84
|
+
async function attachArtifacts(ctx: IssueContext, prUrl: string, exec?: ExecSafeFn): Promise<void> {
|
|
81
85
|
const archivePath = join(ctx.issueDir, "artifacts.tar.gz");
|
|
82
86
|
const tag = `ac-issue-${ctx.number}`;
|
|
83
87
|
const cfg = getConfig();
|
|
@@ -94,31 +98,27 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void>
|
|
|
94
98
|
]);
|
|
95
99
|
|
|
96
100
|
log("Uploading artifacts to GitHub release…");
|
|
97
|
-
await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo]);
|
|
98
|
-
|
|
99
|
-
await ghRaw(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"--json",
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
".assets[0].url",
|
|
119
|
-
"--repo",
|
|
120
|
-
cfg.repo,
|
|
121
|
-
]);
|
|
101
|
+
await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo], exec);
|
|
102
|
+
|
|
103
|
+
await ghRaw(
|
|
104
|
+
[
|
|
105
|
+
"release",
|
|
106
|
+
"create",
|
|
107
|
+
tag,
|
|
108
|
+
"--prerelease",
|
|
109
|
+
"--title",
|
|
110
|
+
`Artifacts: #${ctx.number}`,
|
|
111
|
+
"--repo",
|
|
112
|
+
cfg.repo,
|
|
113
|
+
archivePath,
|
|
114
|
+
],
|
|
115
|
+
exec,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const assetUrl = await ghRaw(
|
|
119
|
+
["release", "view", tag, "--json", "assets", "--jq", ".assets[0].url", "--repo", cfg.repo],
|
|
120
|
+
exec,
|
|
121
|
+
);
|
|
122
122
|
|
|
123
123
|
if (!assetUrl) {
|
|
124
124
|
consola.warn("Could not get artifact download URL — skipping PR comment");
|
|
@@ -133,22 +133,25 @@ async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void>
|
|
|
133
133
|
"Contains: plan.md, completed-summary.md, simplify-summary.md, review.md",
|
|
134
134
|
].join("\n");
|
|
135
135
|
|
|
136
|
-
await ghRaw(["pr", "comment", prUrl, "--body", comment]);
|
|
136
|
+
await ghRaw(["pr", "comment", prUrl, "--body", comment], exec);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async function getExistingPrUrl(branch: string): Promise<string | null> {
|
|
140
|
-
const out = await ghRaw(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
async function getExistingPrUrl(branch: string, exec?: ExecSafeFn): Promise<string | null> {
|
|
140
|
+
const out = await ghRaw(
|
|
141
|
+
[
|
|
142
|
+
"pr",
|
|
143
|
+
"list",
|
|
144
|
+
"--repo",
|
|
145
|
+
getConfig().repo,
|
|
146
|
+
"--head",
|
|
147
|
+
branch,
|
|
148
|
+
"--state",
|
|
149
|
+
"open",
|
|
150
|
+
"--json",
|
|
151
|
+
"url",
|
|
152
|
+
],
|
|
153
|
+
exec,
|
|
154
|
+
);
|
|
152
155
|
try {
|
|
153
156
|
const prs = JSON.parse(out) as Array<{ url: string }>;
|
|
154
157
|
return prs.length > 0 ? prs[0].url : null;
|
|
@@ -10,8 +10,9 @@ import { runClaude } from "../claude-cli.js";
|
|
|
10
10
|
import { resolveTemplate } from "../templates.js";
|
|
11
11
|
import { buildTokens, log, logStep } from "../utils.js";
|
|
12
12
|
import type { IssueContext } from "../utils.js";
|
|
13
|
+
import type { SpawnClaudeFn } from "../spawn-claude.js";
|
|
13
14
|
|
|
14
|
-
export async function stepImplement(ctx: IssueContext): Promise<boolean> {
|
|
15
|
+
export async function stepImplement(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
15
16
|
const completedPath = join(ctx.issueDir, ARTIFACTS.completedSummary);
|
|
16
17
|
const maxIterations = getConfig().maxImplementIterations;
|
|
17
18
|
|
|
@@ -36,6 +37,7 @@ export async function stepImplement(ctx: IssueContext): Promise<boolean> {
|
|
|
36
37
|
const result = await runClaude({
|
|
37
38
|
promptFile,
|
|
38
39
|
maxTurns: getConfig().maxTurns,
|
|
40
|
+
spawnFn,
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
if (result.is_error) {
|
|
@@ -3,8 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
import { ARTIFACTS, STEP_LABELS, TEMPLATES } from "../prompt-templates/index.js";
|
|
4
4
|
import { ensureBranch, runStepWithArtifact } from "../utils.js";
|
|
5
5
|
import type { IssueContext } from "../utils.js";
|
|
6
|
+
import type { SpawnClaudeFn } from "../spawn-claude.js";
|
|
6
7
|
|
|
7
|
-
export async function stepPlan(ctx: IssueContext): Promise<boolean> {
|
|
8
|
+
export async function stepPlan(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
8
9
|
await ensureBranch(ctx.branch);
|
|
9
10
|
|
|
10
11
|
return runStepWithArtifact({
|
|
@@ -12,23 +13,26 @@ export async function stepPlan(ctx: IssueContext): Promise<boolean> {
|
|
|
12
13
|
ctx,
|
|
13
14
|
artifactPath: join(ctx.issueDir, ARTIFACTS.plan),
|
|
14
15
|
templateName: TEMPLATES.plan,
|
|
16
|
+
spawnFn,
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export async function stepSimplify(ctx: IssueContext): Promise<boolean> {
|
|
20
|
+
export async function stepSimplify(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
19
21
|
return runStepWithArtifact({
|
|
20
22
|
stepName: STEP_LABELS.simplify,
|
|
21
23
|
ctx,
|
|
22
24
|
artifactPath: join(ctx.issueDir, ARTIFACTS.simplifySummary),
|
|
23
25
|
templateName: TEMPLATES.simplify,
|
|
26
|
+
spawnFn,
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export async function stepReview(ctx: IssueContext): Promise<boolean> {
|
|
30
|
+
export async function stepReview(ctx: IssueContext, spawnFn?: SpawnClaudeFn): Promise<boolean> {
|
|
28
31
|
return runStepWithArtifact({
|
|
29
32
|
stepName: STEP_LABELS.review,
|
|
30
33
|
ctx,
|
|
31
34
|
artifactPath: join(ctx.issueDir, ARTIFACTS.review),
|
|
32
35
|
templateName: TEMPLATES.review,
|
|
36
|
+
spawnFn,
|
|
33
37
|
});
|
|
34
38
|
}
|