@towles/tool 0.0.62 → 0.0.64
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 +50 -57
- package/src/commands/agentboard.ts +176 -0
- package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
- package/src/commands/auto-claude/list.ts +114 -0
- package/src/commands/auto-claude/retry.test.ts +138 -0
- package/src/commands/auto-claude/retry.ts +139 -0
- package/src/commands/auto-claude/status.test.ts +147 -0
- package/src/commands/auto-claude/status.ts +123 -0
- package/src/commands/base.ts +7 -2
- package/src/commands/config.ts +5 -7
- package/src/commands/doctor.ts +111 -12
- package/src/commands/gh/branch.ts +4 -4
- package/src/commands/gh/pr.ts +1 -0
- package/src/commands/graph/index.ts +169 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +40 -68
- package/src/commands/journal/daily-notes.ts +3 -3
- package/src/commands/journal/meeting.ts +3 -3
- package/src/commands/journal/note.ts +3 -3
- package/src/lib/auto-claude/claude-cli.ts +183 -0
- package/src/lib/auto-claude/config.test.ts +6 -8
- package/src/lib/auto-claude/config.ts +3 -4
- package/src/lib/auto-claude/index.ts +2 -3
- package/src/lib/auto-claude/labels.test.ts +85 -0
- package/src/lib/auto-claude/labels.ts +42 -0
- package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
- package/src/lib/auto-claude/pipeline.test.ts +2 -2
- package/src/lib/auto-claude/pipeline.ts +120 -36
- package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
- package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
- package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
- package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
- package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
- package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
- package/src/lib/auto-claude/run-claude.test.ts +48 -68
- package/src/lib/auto-claude/shell.ts +6 -0
- package/src/lib/auto-claude/steps/create-pr.ts +89 -25
- package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
- package/src/lib/auto-claude/steps/implement.ts +9 -16
- package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
- package/src/lib/auto-claude/steps/steps.test.ts +68 -63
- package/src/lib/auto-claude/templates.test.ts +91 -0
- package/src/lib/auto-claude/templates.ts +34 -0
- package/src/lib/auto-claude/test-helpers.ts +2 -1
- package/src/lib/auto-claude/utils-execution.test.ts +9 -57
- package/src/lib/auto-claude/utils.test.ts +5 -9
- package/src/lib/auto-claude/utils.ts +27 -253
- package/src/lib/graph/analyzer.test.ts +451 -0
- package/src/lib/graph/analyzer.ts +165 -0
- package/src/lib/graph/index.ts +24 -0
- package/src/lib/graph/labels.ts +87 -0
- package/src/lib/graph/parser.test.ts +150 -0
- package/src/lib/graph/parser.ts +65 -0
- package/src/lib/graph/render.ts +25 -0
- package/src/lib/graph/server.ts +70 -0
- package/src/lib/graph/sessions.ts +104 -0
- package/src/lib/graph/tools.ts +90 -0
- package/src/lib/graph/treemap.ts +211 -0
- package/src/lib/graph/types.ts +80 -0
- package/src/lib/install/claude-settings.ts +64 -0
- package/src/lib/journal/editor.ts +33 -0
- package/src/lib/journal/fs.ts +13 -0
- package/src/lib/journal/index.ts +11 -0
- package/src/lib/journal/paths.ts +106 -0
- package/src/lib/journal/{utils.ts → templates.ts} +3 -151
- package/src/utils/fs.ts +19 -0
- package/src/utils/git/exec.ts +18 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
- package/src/utils/git/gh-cli-wrapper.ts +31 -19
- package/src/utils/render.ts +3 -1
- package/src/commands/graph.ts +0 -970
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
- package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
- package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
- package/src/lib/auto-claude/steps/plan.ts +0 -14
- package/src/lib/auto-claude/steps/refresh.ts +0 -114
- package/src/lib/auto-claude/steps/remove-label.ts +0 -22
- package/src/lib/auto-claude/steps/research.ts +0 -21
- package/src/lib/auto-claude/steps/review.ts +0 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import consola from "consola";
|
|
@@ -62,7 +62,7 @@ describe("runStepWithArtifact", () => {
|
|
|
62
62
|
stepName: "Test Step",
|
|
63
63
|
ctx,
|
|
64
64
|
artifactPath,
|
|
65
|
-
templateName: "
|
|
65
|
+
templateName: "01_plan.prompt.md",
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
expect(result).toBe(true);
|
|
@@ -78,7 +78,7 @@ describe("runStepWithArtifact", () => {
|
|
|
78
78
|
stepName: "Test Step",
|
|
79
79
|
ctx,
|
|
80
80
|
artifactPath,
|
|
81
|
-
templateName: "
|
|
81
|
+
templateName: "01_plan.prompt.md",
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
expect(result).toBe(false);
|
|
@@ -94,7 +94,7 @@ describe("runStepWithArtifact", () => {
|
|
|
94
94
|
stepName: "Test Step",
|
|
95
95
|
ctx,
|
|
96
96
|
artifactPath,
|
|
97
|
-
templateName: "
|
|
97
|
+
templateName: "01_plan.prompt.md",
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
expect(result).toBe(false);
|
|
@@ -113,19 +113,16 @@ describe("runStepWithArtifact", () => {
|
|
|
113
113
|
stepName: "Test Step",
|
|
114
114
|
ctx,
|
|
115
115
|
artifactPath,
|
|
116
|
-
templateName: "
|
|
116
|
+
templateName: "01_plan.prompt.md",
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
expect(result).toBe(true);
|
|
120
|
-
|
|
121
|
-
const log = execSync("git log --oneline", { cwd: repo.dir, encoding: "utf-8" });
|
|
122
|
-
expect(log).toContain("chore(auto-claude)");
|
|
123
120
|
});
|
|
124
121
|
});
|
|
125
122
|
|
|
126
|
-
// ──
|
|
123
|
+
// ── stepPlan ──
|
|
127
124
|
|
|
128
|
-
describe("
|
|
125
|
+
describe("stepPlan", () => {
|
|
129
126
|
let originalCwd: string;
|
|
130
127
|
let repo: TestRepo;
|
|
131
128
|
let ctx: IssueContext;
|
|
@@ -137,52 +134,47 @@ describe("stepResearch", () => {
|
|
|
137
134
|
|
|
138
135
|
afterEach(() => teardownStepTest(originalCwd, repo));
|
|
139
136
|
|
|
140
|
-
it("skips when
|
|
141
|
-
const {
|
|
137
|
+
it("skips when plan.md already exists", async () => {
|
|
138
|
+
const { stepPlan } = await import("./simple-steps");
|
|
142
139
|
|
|
143
|
-
writeFileSync(join(ctx.issueDir, ARTIFACTS.
|
|
140
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.plan), "# Existing plan");
|
|
144
141
|
|
|
145
|
-
const result = await
|
|
142
|
+
const result = await stepPlan(ctx);
|
|
146
143
|
expect(result).toBe(true);
|
|
147
144
|
});
|
|
148
145
|
|
|
149
|
-
it("
|
|
150
|
-
const {
|
|
151
|
-
|
|
152
|
-
const researchPath = join(ctx.issueDir, ARTIFACTS.research);
|
|
153
|
-
writeFileSync(researchPath, "short");
|
|
146
|
+
it("calls ensureBranch and creates plan.md on success", async () => {
|
|
147
|
+
const { stepPlan } = await import("./simple-steps");
|
|
148
|
+
const planPath = join(ctx.issueDir, ARTIFACTS.plan);
|
|
154
149
|
|
|
155
|
-
let claudeCalled = false;
|
|
156
150
|
mockClaudeImpl = () => {
|
|
157
|
-
|
|
158
|
-
writeFileSync(researchPath, "x".repeat(250));
|
|
151
|
+
writeFileSync(planPath, "# Plan\n\nDetailed plan.");
|
|
159
152
|
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
160
153
|
};
|
|
161
154
|
|
|
162
|
-
const result = await
|
|
163
|
-
expect(claudeCalled).toBe(true);
|
|
155
|
+
const result = await stepPlan(ctx);
|
|
164
156
|
expect(result).toBe(true);
|
|
157
|
+
|
|
158
|
+
// Verify we ended up on the branch (ensureBranch was called)
|
|
159
|
+
const currentBranch = execSync("git branch --show-current", { cwd: repo.dir })
|
|
160
|
+
.toString()
|
|
161
|
+
.trim();
|
|
162
|
+
expect(currentBranch).toBe(ctx.branch);
|
|
165
163
|
});
|
|
166
164
|
|
|
167
|
-
it("
|
|
168
|
-
const {
|
|
165
|
+
it("returns false when Claude fails", async () => {
|
|
166
|
+
const { stepPlan } = await import("./simple-steps");
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
mockClaudeImpl = () => {
|
|
172
|
-
writeFileSync(researchPath, "x".repeat(250));
|
|
173
|
-
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
await stepResearch(ctx);
|
|
168
|
+
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
177
169
|
|
|
178
|
-
const
|
|
179
|
-
expect(
|
|
170
|
+
const result = await stepPlan(ctx);
|
|
171
|
+
expect(result).toBe(false);
|
|
180
172
|
});
|
|
181
173
|
});
|
|
182
174
|
|
|
183
|
-
// ──
|
|
175
|
+
// ── stepSimplify ──
|
|
184
176
|
|
|
185
|
-
describe("
|
|
177
|
+
describe("stepSimplify", () => {
|
|
186
178
|
let originalCwd: string;
|
|
187
179
|
let repo: TestRepo;
|
|
188
180
|
let ctx: IssueContext;
|
|
@@ -190,41 +182,42 @@ describe("stepPlanAnnotations", () => {
|
|
|
190
182
|
beforeEach(async () => {
|
|
191
183
|
({ originalCwd, repo, ctx } = setupStepTest());
|
|
192
184
|
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
185
|
+
|
|
186
|
+
// stepSimplify doesn't switch branches, but we need to be on one
|
|
187
|
+
execSync(`git checkout -b ${ctx.branch}`, { cwd: repo.dir, stdio: "ignore" });
|
|
193
188
|
});
|
|
194
189
|
|
|
195
190
|
afterEach(() => teardownStepTest(originalCwd, repo));
|
|
196
191
|
|
|
197
|
-
it("
|
|
198
|
-
const {
|
|
192
|
+
it("skips when simplify-summary.md already exists", async () => {
|
|
193
|
+
const { stepSimplify } = await import("./simple-steps");
|
|
194
|
+
|
|
195
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.simplifySummary), "# Simplified");
|
|
199
196
|
|
|
200
|
-
const result = await
|
|
197
|
+
const result = await stepSimplify(ctx);
|
|
201
198
|
expect(result).toBe(true);
|
|
202
199
|
});
|
|
203
200
|
|
|
204
|
-
it("
|
|
205
|
-
const {
|
|
201
|
+
it("creates simplify-summary.md on success", async () => {
|
|
202
|
+
const { stepSimplify } = await import("./simple-steps");
|
|
203
|
+
const artifactPath = join(ctx.issueDir, ARTIFACTS.simplifySummary);
|
|
206
204
|
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
mockClaudeImpl = () => {
|
|
206
|
+
writeFileSync(artifactPath, "# Simplify Summary\n\nCode simplified.");
|
|
207
|
+
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
208
|
+
};
|
|
209
209
|
|
|
210
|
-
const result = await
|
|
210
|
+
const result = await stepSimplify(ctx);
|
|
211
211
|
expect(result).toBe(true);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
-
it("
|
|
215
|
-
const {
|
|
216
|
-
|
|
217
|
-
const annotationsPath = join(ctx.issueDir, ARTIFACTS.planAnnotations);
|
|
218
|
-
const addressedPath = join(ctx.issueDir, ARTIFACTS.planAnnotationsAddressed);
|
|
219
|
-
writeFileSync(annotationsPath, "# Annotations\n\nSome feedback here.");
|
|
220
|
-
|
|
221
|
-
mockClaudeImpl = () => ({ stdout: successClaudeJson(), exitCode: 0 });
|
|
214
|
+
it("returns false when Claude fails", async () => {
|
|
215
|
+
const { stepSimplify } = await import("./simple-steps");
|
|
222
216
|
|
|
223
|
-
|
|
217
|
+
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
224
218
|
|
|
225
|
-
|
|
226
|
-
expect(
|
|
227
|
-
expect(existsSync(annotationsPath)).toBe(false);
|
|
219
|
+
const result = await stepSimplify(ctx);
|
|
220
|
+
expect(result).toBe(false);
|
|
228
221
|
});
|
|
229
222
|
});
|
|
230
223
|
|
|
@@ -272,6 +265,22 @@ describe("stepImplement", () => {
|
|
|
272
265
|
expect(callCount).toBe(3);
|
|
273
266
|
});
|
|
274
267
|
|
|
268
|
+
it("passes review feedback when review.md exists", async () => {
|
|
269
|
+
const { stepImplement } = await import("./implement");
|
|
270
|
+
|
|
271
|
+
// Write a review.md to simulate review feedback
|
|
272
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.review), "# Review\n\nFix the tests.");
|
|
273
|
+
|
|
274
|
+
const completedPath = join(ctx.issueDir, ARTIFACTS.completedSummary);
|
|
275
|
+
mockClaudeImpl = () => {
|
|
276
|
+
writeFileSync(completedPath, "# Done");
|
|
277
|
+
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = await stepImplement(ctx);
|
|
281
|
+
expect(result).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
275
284
|
it("stops looping when completed-summary.md appears", async () => {
|
|
276
285
|
const { stepImplement } = await import("./implement");
|
|
277
286
|
|
|
@@ -292,13 +301,9 @@ describe("stepImplement", () => {
|
|
|
292
301
|
});
|
|
293
302
|
});
|
|
294
303
|
|
|
295
|
-
// ──
|
|
304
|
+
// ── createPr / label helpers -- skip in CI (needs gh) ──
|
|
296
305
|
|
|
297
|
-
describe.skipIf(!!process.env.CI)("
|
|
306
|
+
describe.skipIf(!!process.env.CI)("createPr (requires gh)", () => {
|
|
298
307
|
it.todo("skips when open PR already exists");
|
|
299
308
|
it.todo("creates PR and writes pr-url.txt");
|
|
300
309
|
});
|
|
301
|
-
|
|
302
|
-
describe.skipIf(!!process.env.CI)("stepRemoveLabel (requires gh)", () => {
|
|
303
|
-
it.todo("calls gh with correct args");
|
|
304
|
-
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { resolveTemplate } from "./templates";
|
|
5
|
+
import type { TokenValues } from "./templates";
|
|
6
|
+
|
|
7
|
+
vi.mock("node:fs", () => ({
|
|
8
|
+
readFileSync: vi.fn(),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
mkdirSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockedReadFileSync = vi.mocked(readFileSync);
|
|
14
|
+
const mockedWriteFileSync = vi.mocked(writeFileSync);
|
|
15
|
+
const mockedMkdirSync = vi.mocked(mkdirSync);
|
|
16
|
+
|
|
17
|
+
describe("resolveTemplate", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const tokens: TokenValues = {
|
|
23
|
+
SCOPE_PATH: "/home/user/project",
|
|
24
|
+
ISSUE_DIR: "/tmp/issues/42",
|
|
25
|
+
MAIN_BRANCH: "main",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
it("replaces all token placeholders in template", () => {
|
|
29
|
+
mockedReadFileSync.mockReturnValue(
|
|
30
|
+
"Scope: {{SCOPE_PATH}}\nDir: {{ISSUE_DIR}}\nBranch: {{MAIN_BRANCH}}",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
resolveTemplate("plan.md", tokens, "/tmp/issues/42");
|
|
34
|
+
|
|
35
|
+
const writtenContent = mockedWriteFileSync.mock.calls[0][1];
|
|
36
|
+
expect(writtenContent).toContain("/home/user/project");
|
|
37
|
+
expect(writtenContent).toContain("/tmp/issues/42");
|
|
38
|
+
expect(writtenContent).toContain("main");
|
|
39
|
+
expect(writtenContent).not.toContain("{{");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("replaces REVIEW_FEEDBACK token when provided", () => {
|
|
43
|
+
const tokensWithFeedback: TokenValues = {
|
|
44
|
+
...tokens,
|
|
45
|
+
REVIEW_FEEDBACK: "Needs more tests",
|
|
46
|
+
};
|
|
47
|
+
mockedReadFileSync.mockReturnValue("Feedback: {{REVIEW_FEEDBACK}}");
|
|
48
|
+
|
|
49
|
+
resolveTemplate("review.md", tokensWithFeedback, "/tmp/issues/42");
|
|
50
|
+
|
|
51
|
+
const writtenContent = mockedWriteFileSync.mock.calls[0][1];
|
|
52
|
+
expect(writtenContent).toContain("Needs more tests");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("creates output directory recursively", () => {
|
|
56
|
+
mockedReadFileSync.mockReturnValue("template content");
|
|
57
|
+
|
|
58
|
+
resolveTemplate("plan.md", tokens, "/tmp/issues/42");
|
|
59
|
+
|
|
60
|
+
expect(mockedMkdirSync).toHaveBeenCalledWith("/tmp/issues/42", { recursive: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("writes resolved template to issue dir", () => {
|
|
64
|
+
mockedReadFileSync.mockReturnValue("simple content");
|
|
65
|
+
|
|
66
|
+
resolveTemplate("plan.md", tokens, "/tmp/issues/42");
|
|
67
|
+
|
|
68
|
+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
69
|
+
"/tmp/issues/42/plan.md",
|
|
70
|
+
"simple content",
|
|
71
|
+
"utf-8",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns relative path from cwd", () => {
|
|
76
|
+
mockedReadFileSync.mockReturnValue("content");
|
|
77
|
+
|
|
78
|
+
const result = resolveTemplate("plan.md", tokens, "/tmp/issues/42");
|
|
79
|
+
// Should be a relative path (not starting with /)
|
|
80
|
+
expect(result).not.toMatch(/^\/tmp/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("handles multiple occurrences of the same token", () => {
|
|
84
|
+
mockedReadFileSync.mockReturnValue("{{MAIN_BRANCH}} and {{MAIN_BRANCH}} again");
|
|
85
|
+
|
|
86
|
+
resolveTemplate("plan.md", tokens, "/tmp/issues/42");
|
|
87
|
+
|
|
88
|
+
const writtenContent = mockedWriteFileSync.mock.calls[0][1];
|
|
89
|
+
expect(writtenContent).toBe("main and main again");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export const TEMPLATES_DIR = join(__dirname, "prompt-templates");
|
|
7
|
+
|
|
8
|
+
// ── Template resolution ──
|
|
9
|
+
|
|
10
|
+
export interface TokenValues {
|
|
11
|
+
SCOPE_PATH: string;
|
|
12
|
+
ISSUE_DIR: string;
|
|
13
|
+
MAIN_BRANCH: string;
|
|
14
|
+
REVIEW_FEEDBACK?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveTemplate(
|
|
18
|
+
templateName: string,
|
|
19
|
+
tokens: TokenValues,
|
|
20
|
+
issueDir: string,
|
|
21
|
+
): string {
|
|
22
|
+
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
23
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
24
|
+
|
|
25
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
26
|
+
template = template.replaceAll(`{{${key}}}`, value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resolvedPath = join(issueDir, templateName);
|
|
30
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
31
|
+
writeFileSync(resolvedPath, template, "utf-8");
|
|
32
|
+
|
|
33
|
+
return relative(process.cwd(), resolvedPath);
|
|
34
|
+
}
|
|
@@ -8,7 +8,8 @@ import { PassThrough } from "node:stream";
|
|
|
8
8
|
|
|
9
9
|
import { vi } from "vitest";
|
|
10
10
|
|
|
11
|
-
import type { ClaudeResult
|
|
11
|
+
import type { ClaudeResult } from "./claude-cli";
|
|
12
|
+
import type { IssueContext } from "./utils";
|
|
12
13
|
|
|
13
14
|
export interface TestRepo {
|
|
14
15
|
dir: string;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
2
|
|
|
5
3
|
import consola from "consola";
|
|
6
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -30,20 +28,20 @@ describe("shell helpers (real execution)", () => {
|
|
|
30
28
|
});
|
|
31
29
|
|
|
32
30
|
it("execSafe returns ok:true for a successful command", async () => {
|
|
33
|
-
const { execSafe } = await import("./
|
|
31
|
+
const { execSafe } = await import("./shell");
|
|
34
32
|
const result = await execSafe("echo", ["hello"]);
|
|
35
33
|
expect(result.ok).toBe(true);
|
|
36
34
|
expect(result.stdout).toBe("hello");
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
it("execSafe returns ok:false for a failing command", async () => {
|
|
40
|
-
const { execSafe } = await import("./
|
|
38
|
+
const { execSafe } = await import("./shell");
|
|
41
39
|
const result = await execSafe("git", ["checkout", "nonexistent-branch-xyz"]);
|
|
42
40
|
expect(result.ok).toBe(false);
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
it("git() runs real git commands", async () => {
|
|
46
|
-
const { git } = await import("./
|
|
44
|
+
const { git } = await import("./shell");
|
|
47
45
|
const status = await git(["status", "--porcelain"]);
|
|
48
46
|
expect(typeof status).toBe("string");
|
|
49
47
|
});
|
|
@@ -68,7 +66,8 @@ describe("ensureBranch (real git)", () => {
|
|
|
68
66
|
});
|
|
69
67
|
|
|
70
68
|
it("creates a new branch from main when branch doesn't exist", async () => {
|
|
71
|
-
const { ensureBranch
|
|
69
|
+
const { ensureBranch } = await import("./utils");
|
|
70
|
+
const { git } = await import("./shell");
|
|
72
71
|
|
|
73
72
|
await ensureBranch("feature/42-new-branch");
|
|
74
73
|
|
|
@@ -77,7 +76,8 @@ describe("ensureBranch (real git)", () => {
|
|
|
77
76
|
});
|
|
78
77
|
|
|
79
78
|
it("checks out an existing local branch", async () => {
|
|
80
|
-
const { ensureBranch
|
|
79
|
+
const { ensureBranch } = await import("./utils");
|
|
80
|
+
const { git } = await import("./shell");
|
|
81
81
|
|
|
82
82
|
execSync("git checkout -b feature/existing-branch", { cwd: repo.dir, stdio: "ignore" });
|
|
83
83
|
execSync("git checkout main", { cwd: repo.dir, stdio: "ignore" });
|
|
@@ -89,7 +89,8 @@ describe("ensureBranch (real git)", () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("can checkout after branch creation", async () => {
|
|
92
|
-
const { ensureBranch
|
|
92
|
+
const { ensureBranch } = await import("./utils");
|
|
93
|
+
const { git } = await import("./shell");
|
|
93
94
|
|
|
94
95
|
await ensureBranch("feature/99-test-checkout");
|
|
95
96
|
const branch1 = await git(["branch", "--show-current"]);
|
|
@@ -101,52 +102,3 @@ describe("ensureBranch (real git)", () => {
|
|
|
101
102
|
expect(branch2).toBe("feature/99-test-checkout");
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
|
-
|
|
105
|
-
// ── commitArtifacts: real git ──
|
|
106
|
-
|
|
107
|
-
describe("commitArtifacts (real git)", () => {
|
|
108
|
-
let originalCwd: string;
|
|
109
|
-
let repo: TestRepo;
|
|
110
|
-
|
|
111
|
-
beforeEach(async () => {
|
|
112
|
-
originalCwd = process.cwd();
|
|
113
|
-
repo = createTestRepo();
|
|
114
|
-
process.chdir(repo.dir);
|
|
115
|
-
await initConfig({ repo: "test/repo", mainBranch: "main" });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
afterEach(() => {
|
|
119
|
-
process.chdir(originalCwd);
|
|
120
|
-
repo.cleanup();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("commits staged files in the issue directory", async () => {
|
|
124
|
-
const { commitArtifacts, buildIssueContext } = await import("./utils");
|
|
125
|
-
|
|
126
|
-
const ctx = buildIssueContext({ number: 1, title: "Test", body: "body" }, "test/repo", ".");
|
|
127
|
-
|
|
128
|
-
const issueDir = join(repo.dir, ctx.issueDirRel);
|
|
129
|
-
mkdirSync(issueDir, { recursive: true });
|
|
130
|
-
writeFileSync(join(issueDir, "test.md"), "# Test artifact");
|
|
131
|
-
|
|
132
|
-
await commitArtifacts(ctx, "test commit");
|
|
133
|
-
|
|
134
|
-
const log = execSync("git log --oneline", { cwd: repo.dir, encoding: "utf-8" });
|
|
135
|
-
expect(log).toContain("test commit");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("does not commit when no changes are staged", async () => {
|
|
139
|
-
const { commitArtifacts, buildIssueContext } = await import("./utils");
|
|
140
|
-
|
|
141
|
-
const ctx = buildIssueContext({ number: 2, title: "Test", body: "body" }, "test/repo", ".");
|
|
142
|
-
|
|
143
|
-
const issueDir = join(repo.dir, ctx.issueDirRel);
|
|
144
|
-
mkdirSync(issueDir, { recursive: true });
|
|
145
|
-
|
|
146
|
-
const logBefore = execSync("git log --oneline", { cwd: repo.dir, encoding: "utf-8" });
|
|
147
|
-
await commitArtifacts(ctx, "should not appear");
|
|
148
|
-
const logAfter = execSync("git log --oneline", { cwd: repo.dir, encoding: "utf-8" });
|
|
149
|
-
|
|
150
|
-
expect(logAfter).toBe(logBefore);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -5,12 +5,8 @@ import { join } from "node:path";
|
|
|
5
5
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { initConfig } from "./config";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
buildIssueContext,
|
|
11
|
-
buildTokens,
|
|
12
|
-
resolveTemplate,
|
|
13
|
-
} from "./utils";
|
|
8
|
+
import { resolveTemplate } from "./templates";
|
|
9
|
+
import { buildContextFromArtifacts, buildIssueContext, buildTokens } from "./utils";
|
|
14
10
|
|
|
15
11
|
// Initialize config once for tests that need getConfig()
|
|
16
12
|
beforeAll(async () => {
|
|
@@ -69,14 +65,14 @@ describe("resolveTemplate", () => {
|
|
|
69
65
|
const issueDir = join(tmpDir, "issue-99");
|
|
70
66
|
mkdirSync(issueDir, { recursive: true });
|
|
71
67
|
|
|
72
|
-
const result = resolveTemplate("
|
|
68
|
+
const result = resolveTemplate("01_plan.prompt.md", tokens, issueDir);
|
|
73
69
|
|
|
74
70
|
// Should return a relative path
|
|
75
71
|
expect(result).toContain("issue-99");
|
|
76
|
-
expect(result).toContain("
|
|
72
|
+
expect(result).toContain("01_plan.prompt.md");
|
|
77
73
|
|
|
78
74
|
// Resolved file should exist and have tokens replaced
|
|
79
|
-
const content = readFileSync(join(issueDir, "
|
|
75
|
+
const content = readFileSync(join(issueDir, "01_plan.prompt.md"), "utf-8");
|
|
80
76
|
expect(content).toContain("src/");
|
|
81
77
|
expect(content).toContain(".auto-claude/issue-99");
|
|
82
78
|
expect(content).not.toContain("{{SCOPE_PATH}}");
|