@towles/tool 0.0.62 → 0.0.63

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.
Files changed (83) hide show
  1. package/package.json +50 -57
  2. package/src/commands/agentboard.ts +176 -0
  3. package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
  4. package/src/commands/auto-claude/list.ts +114 -0
  5. package/src/commands/auto-claude/retry.test.ts +138 -0
  6. package/src/commands/auto-claude/retry.ts +139 -0
  7. package/src/commands/auto-claude/status.test.ts +147 -0
  8. package/src/commands/auto-claude/status.ts +123 -0
  9. package/src/commands/base.ts +7 -2
  10. package/src/commands/config.ts +5 -7
  11. package/src/commands/doctor.ts +111 -12
  12. package/src/commands/gh/branch.ts +4 -4
  13. package/src/commands/gh/pr.ts +1 -0
  14. package/src/commands/graph/index.ts +169 -0
  15. package/src/commands/graph.test.ts +1 -1
  16. package/src/commands/install.ts +40 -68
  17. package/src/commands/journal/daily-notes.ts +3 -3
  18. package/src/commands/journal/meeting.ts +3 -3
  19. package/src/commands/journal/note.ts +3 -3
  20. package/src/lib/auto-claude/claude-cli.ts +183 -0
  21. package/src/lib/auto-claude/config.test.ts +6 -8
  22. package/src/lib/auto-claude/config.ts +3 -4
  23. package/src/lib/auto-claude/index.ts +2 -3
  24. package/src/lib/auto-claude/labels.test.ts +85 -0
  25. package/src/lib/auto-claude/labels.ts +42 -0
  26. package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
  27. package/src/lib/auto-claude/pipeline.test.ts +2 -2
  28. package/src/lib/auto-claude/pipeline.ts +120 -36
  29. package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
  30. package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
  31. package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
  32. package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
  33. package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
  34. package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
  35. package/src/lib/auto-claude/run-claude.test.ts +48 -68
  36. package/src/lib/auto-claude/shell.ts +6 -0
  37. package/src/lib/auto-claude/steps/create-pr.ts +89 -25
  38. package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
  39. package/src/lib/auto-claude/steps/implement.ts +9 -16
  40. package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
  41. package/src/lib/auto-claude/steps/steps.test.ts +68 -63
  42. package/src/lib/auto-claude/templates.test.ts +91 -0
  43. package/src/lib/auto-claude/templates.ts +34 -0
  44. package/src/lib/auto-claude/test-helpers.ts +2 -1
  45. package/src/lib/auto-claude/utils-execution.test.ts +9 -57
  46. package/src/lib/auto-claude/utils.test.ts +5 -9
  47. package/src/lib/auto-claude/utils.ts +27 -253
  48. package/src/lib/graph/analyzer.test.ts +451 -0
  49. package/src/lib/graph/analyzer.ts +165 -0
  50. package/src/lib/graph/index.ts +24 -0
  51. package/src/lib/graph/labels.ts +87 -0
  52. package/src/lib/graph/parser.test.ts +150 -0
  53. package/src/lib/graph/parser.ts +65 -0
  54. package/src/lib/graph/render.ts +25 -0
  55. package/src/lib/graph/server.ts +70 -0
  56. package/src/lib/graph/sessions.ts +104 -0
  57. package/src/lib/graph/tools.ts +90 -0
  58. package/src/lib/graph/treemap.ts +211 -0
  59. package/src/lib/graph/types.ts +80 -0
  60. package/src/lib/install/claude-settings.ts +64 -0
  61. package/src/lib/journal/editor.ts +33 -0
  62. package/src/lib/journal/fs.ts +13 -0
  63. package/src/lib/journal/index.ts +11 -0
  64. package/src/lib/journal/paths.ts +106 -0
  65. package/src/lib/journal/{utils.ts → templates.ts} +3 -151
  66. package/src/utils/fs.ts +19 -0
  67. package/src/utils/git/exec.ts +18 -0
  68. package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
  69. package/src/utils/git/gh-cli-wrapper.ts +31 -19
  70. package/src/utils/render.ts +3 -1
  71. package/src/commands/graph.ts +0 -970
  72. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
  73. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
  74. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
  75. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
  76. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
  77. package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
  78. package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
  79. package/src/lib/auto-claude/steps/plan.ts +0 -14
  80. package/src/lib/auto-claude/steps/refresh.ts +0 -114
  81. package/src/lib/auto-claude/steps/remove-label.ts +0 -22
  82. package/src/lib/auto-claude/steps/research.ts +0 -21
  83. package/src/lib/auto-claude/steps/review.ts +0 -14
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
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: "01_research.prompt.md",
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: "01_research.prompt.md",
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: "01_research.prompt.md",
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: "01_research.prompt.md",
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
- // ── stepResearch ──
123
+ // ── stepPlan ──
127
124
 
128
- describe("stepResearch", () => {
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 research.md exists and is > 200 chars", async () => {
141
- const { stepResearch } = await import("./research");
137
+ it("skips when plan.md already exists", async () => {
138
+ const { stepPlan } = await import("./simple-steps");
142
139
 
143
- writeFileSync(join(ctx.issueDir, ARTIFACTS.research), "x".repeat(250));
140
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.plan), "# Existing plan");
144
141
 
145
- const result = await stepResearch(ctx);
142
+ const result = await stepPlan(ctx);
146
143
  expect(result).toBe(true);
147
144
  });
148
145
 
149
- it("does NOT skip when research.md exists but is < 200 chars", async () => {
150
- const { stepResearch } = await import("./research");
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
- claudeCalled = true;
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 stepResearch(ctx);
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("calls ensureBranch (real git branch creation)", async () => {
168
- const { stepResearch } = await import("./research");
165
+ it("returns false when Claude fails", async () => {
166
+ const { stepPlan } = await import("./simple-steps");
169
167
 
170
- const researchPath = join(ctx.issueDir, ARTIFACTS.research);
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 branches = execSync("git branch", { cwd: repo.dir, encoding: "utf-8" });
179
- expect(branches).toContain(ctx.branch.split("/").pop());
170
+ const result = await stepPlan(ctx);
171
+ expect(result).toBe(false);
180
172
  });
181
173
  });
182
174
 
183
- // ── stepPlanAnnotations ──
175
+ // ── stepSimplify ──
184
176
 
185
- describe("stepPlanAnnotations", () => {
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("returns true when no plan-annotations.md exists", async () => {
198
- const { stepPlanAnnotations } = await import("./plan-annotations");
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 stepPlanAnnotations(ctx);
197
+ const result = await stepSimplify(ctx);
201
198
  expect(result).toBe(true);
202
199
  });
203
200
 
204
- it("skips when plan-annotations-addressed.md already exists", async () => {
205
- const { stepPlanAnnotations } = await import("./plan-annotations");
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
- writeFileSync(join(ctx.issueDir, ARTIFACTS.planAnnotations), "# Annotations");
208
- writeFileSync(join(ctx.issueDir, ARTIFACTS.planAnnotationsAddressed), "# Addressed");
205
+ mockClaudeImpl = () => {
206
+ writeFileSync(artifactPath, "# Simplify Summary\n\nCode simplified.");
207
+ return { stdout: successClaudeJson(), exitCode: 0 };
208
+ };
209
209
 
210
- const result = await stepPlanAnnotations(ctx);
210
+ const result = await stepSimplify(ctx);
211
211
  expect(result).toBe(true);
212
212
  });
213
213
 
214
- it("renames file after Claude runs successfully", async () => {
215
- const { stepPlanAnnotations } = await import("./plan-annotations");
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
- const result = await stepPlanAnnotations(ctx);
217
+ mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
224
218
 
225
- expect(result).toBe(true);
226
- expect(existsSync(addressedPath)).toBe(true);
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
- // ── stepCreatePR / stepRemoveLabel -- skip in CI (needs gh) ──
304
+ // ── createPr / label helpers -- skip in CI (needs gh) ──
296
305
 
297
- describe.skipIf(!!process.env.CI)("stepCreatePR (requires gh)", () => {
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, IssueContext } from "./utils";
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("./utils");
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("./utils");
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("./utils");
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, git } = await import("./utils");
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, git } = await import("./utils");
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, git } = await import("./utils");
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
- buildContextFromArtifacts,
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("01_research.prompt.md", tokens, issueDir);
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("01_research.prompt.md");
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, "01_research.prompt.md"), "utf-8");
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}}");