@towles/tool 0.0.59 → 0.0.60

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 (29) hide show
  1. package/package.json +1 -1
  2. package/src/commands/gh/branch.test.ts +107 -108
  3. package/src/commands/gh/branch.ts +39 -36
  4. package/src/lib/auto-claude/pipeline-execution.test.ts +195 -0
  5. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +21 -0
  6. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +27 -0
  7. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +15 -0
  8. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +35 -0
  9. package/src/lib/auto-claude/prompt-templates/05_implement.prompt.md +36 -0
  10. package/src/lib/auto-claude/prompt-templates/06_review.prompt.md +32 -0
  11. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +30 -0
  12. package/src/lib/auto-claude/prompt-templates/CLAUDE.md +12 -0
  13. package/src/lib/auto-claude/prompt-templates/index.test.ts +2 -2
  14. package/src/lib/auto-claude/prompt-templates/index.ts +7 -7
  15. package/src/lib/auto-claude/run-claude.test.ts +160 -0
  16. package/src/lib/auto-claude/steps/steps.test.ts +330 -0
  17. package/src/lib/auto-claude/test-helpers.ts +86 -0
  18. package/src/lib/auto-claude/utils-execution.test.ts +152 -0
  19. package/src/lib/auto-claude/utils.test.ts +7 -7
  20. package/src/lib/auto-claude/utils.ts +2 -1
  21. package/src/utils/git/branch-name.test.ts +83 -0
  22. package/src/utils/git/branch-name.ts +10 -0
  23. package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +0 -28
  24. package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +0 -28
  25. package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +0 -21
  26. package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +0 -33
  27. package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +0 -31
  28. package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +0 -30
  29. package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "description": "CLI tool with auto-claude pipeline, developer tools, and journaling via markdown.",
5
5
  "keywords": [
6
6
  "auto-claude",
@@ -1,124 +1,123 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import stripAnsi from "strip-ansi";
2
3
  import type { Issue } from "../../utils/git/gh-cli-wrapper";
3
- import GhBranch from "./branch";
4
+ import { buildIssueChoices, computeColumnLayout } from "./branch";
4
5
 
5
- const createBranchNameFromIssue = GhBranch.createBranchNameFromIssue;
6
+ const issues: Issue[] = [
7
+ {
8
+ number: 4,
9
+ title: "Short bug",
10
+ state: "open",
11
+ labels: [{ name: "bug", color: "d73a4a" }],
12
+ },
13
+ {
14
+ number: 123,
15
+ title: "Add authentication flow with OAuth",
16
+ state: "open",
17
+ labels: [
18
+ { name: "enhancement", color: "a2eeef" },
19
+ { name: "priority", color: "ff0000" },
20
+ ],
21
+ },
22
+ {
23
+ number: 7,
24
+ title: "Docs update",
25
+ state: "open",
26
+ labels: [],
27
+ },
28
+ ];
6
29
 
7
- describe("gh-branch", () => {
8
- describe("createBranchNameFromIssue", () => {
9
- it("creates branch name from issue with basic title", () => {
10
- const issue: Issue = {
11
- number: 4,
12
- title: "Long Issue Title - with a lot of words and stuff ",
13
- state: "open",
14
- labels: [],
15
- };
16
- const branchName = createBranchNameFromIssue(issue);
17
- expect(branchName).toBe("feature/4-long-issue-title-with-a-lot-of-words-and-stuff");
18
- });
30
+ describe("computeColumnLayout", () => {
31
+ it("computes longestNumber from widest issue number", () => {
32
+ const layout = computeColumnLayout(issues, 100);
33
+ // "123" is 3 chars
34
+ expect(layout.longestNumber).toBe(3);
35
+ });
36
+
37
+ it("computes longestLabels from widest joined label string", () => {
38
+ const layout = computeColumnLayout(issues, 100);
39
+ // "enhancement, priority" is 21 chars
40
+ expect(layout.longestLabels).toBe("enhancement, priority".length);
41
+ });
19
42
 
20
- it("handles special characters in title", () => {
21
- const issue: Issue = {
22
- number: 123,
23
- title: "Fix bug: @user reported $100 issue!",
24
- state: "open",
25
- labels: [],
26
- };
27
- const branchName = createBranchNameFromIssue(issue);
28
- expect(branchName).toBe("feature/123-fix-bug-user-reported-100-issue");
29
- });
43
+ it("caps line width at 130", () => {
44
+ const narrow = computeColumnLayout(issues, 80);
45
+ const wide = computeColumnLayout(issues, 200);
46
+ // descriptionLength = min(cols, 130) - longestNumber - longestLabels - 15
47
+ expect(narrow.descriptionLength).toBe(80 - 3 - 21 - 15);
48
+ expect(wide.descriptionLength).toBe(130 - 3 - 21 - 15);
49
+ });
30
50
 
31
- it("handles title with only numbers", () => {
32
- const issue: Issue = {
33
- number: 42,
34
- title: "123 456",
35
- state: "open",
36
- labels: [],
37
- };
38
- const branchName = createBranchNameFromIssue(issue);
39
- expect(branchName).toBe("feature/42-123-456");
40
- });
51
+ it("handles single issue", () => {
52
+ const single: Issue[] = [{ number: 1, title: "t", state: "open", labels: [] }];
53
+ const layout = computeColumnLayout(single, 80);
54
+ expect(layout.longestNumber).toBe(1);
55
+ expect(layout.longestLabels).toBe(0);
56
+ expect(layout.descriptionLength).toBe(80 - 1 - 0 - 15);
57
+ });
58
+
59
+ it("handles issues with no labels", () => {
60
+ const noLabels: Issue[] = [
61
+ { number: 42, title: "No labels here", state: "open", labels: [] },
62
+ { number: 100, title: "Also no labels", state: "open", labels: [] },
63
+ ];
64
+ const layout = computeColumnLayout(noLabels, 100);
65
+ expect(layout.longestLabels).toBe(0);
66
+ expect(layout.longestNumber).toBe(3);
67
+ });
68
+ });
41
69
 
42
- it("trims trailing dashes", () => {
43
- const issue: Issue = {
44
- number: 7,
45
- title: "Update docs ---",
46
- state: "open",
47
- labels: [],
48
- };
49
- const branchName = createBranchNameFromIssue(issue);
50
- expect(branchName).toBe("feature/7-update-docs");
51
- });
70
+ describe("buildIssueChoices", () => {
71
+ const layout = computeColumnLayout(issues, 100);
52
72
 
53
- it("handles unicode characters", () => {
54
- const issue: Issue = {
55
- number: 99,
56
- title: "Fix für Übersetzung",
57
- state: "open",
58
- labels: [],
59
- };
60
- const branchName = createBranchNameFromIssue(issue);
61
- expect(branchName).toBe("feature/99-fix-f-r-bersetzung");
62
- });
73
+ it("returns one choice per issue plus a Cancel option", () => {
74
+ const choices = buildIssueChoices(issues, layout);
75
+ expect(choices).toHaveLength(issues.length + 1);
76
+ });
63
77
 
64
- it("handles empty-ish title", () => {
65
- const issue: Issue = {
66
- number: 1,
67
- title: " ",
68
- state: "open",
69
- labels: [],
70
- };
71
- const branchName = createBranchNameFromIssue(issue);
72
- expect(branchName).toBe("feature/1-");
73
- });
78
+ it("last choice is Cancel", () => {
79
+ const choices = buildIssueChoices(issues, layout);
80
+ const last = choices[choices.length - 1];
81
+ expect(last.title).toBe("Cancel");
82
+ expect(last.value).toBe("cancel");
83
+ });
74
84
 
75
- it("handles title with underscores", () => {
76
- const issue: Issue = {
77
- number: 50,
78
- title: "snake_case_title",
79
- state: "open",
80
- labels: [],
81
- };
82
- const branchName = createBranchNameFromIssue(issue);
83
- expect(branchName).toBe("feature/50-snake_case_title");
84
- });
85
+ it("uses issue number as title and value", () => {
86
+ const choices = buildIssueChoices(issues, layout);
87
+ expect(choices[0].title).toBe("4");
88
+ expect(choices[0].value).toBe(4);
89
+ expect(choices[1].title).toBe("123");
90
+ expect(choices[1].value).toBe(123);
91
+ });
85
92
 
86
- it("handles very long titles", () => {
87
- const issue: Issue = {
88
- number: 200,
89
- title: "This is a very long issue title that goes on and on with many words",
90
- state: "open",
91
- labels: [],
92
- };
93
- const branchName = createBranchNameFromIssue(issue);
94
- expect(branchName).toBe(
95
- "feature/200-this-is-a-very-long-issue-title-that-goes-on-and-on-with-many-words",
96
- );
97
- });
93
+ it("includes issue title text in description", () => {
94
+ const choices = buildIssueChoices(issues, layout);
95
+ const desc = stripAnsi(choices[0].description!);
96
+ expect(desc).toContain("Short bug");
97
+ });
98
98
 
99
- it("collapses multiple consecutive dashes", () => {
100
- const issue: Issue = {
101
- number: 15,
102
- title: "Fix multiple spaces",
103
- state: "open",
104
- labels: [],
105
- };
106
- const branchName = createBranchNameFromIssue(issue);
107
- expect(branchName).toBe("feature/15-fix-multiple-spaces");
108
- });
99
+ it("includes label names in description", () => {
100
+ const choices = buildIssueChoices(issues, layout);
101
+ const desc = stripAnsi(choices[1].description!);
102
+ expect(desc).toContain("enhancement");
103
+ expect(desc).toContain("priority");
104
+ });
109
105
 
110
- it("handles title with brackets and parentheses", () => {
111
- const issue: Issue = {
112
- number: 33,
113
- title: "[Bug] Fix (critical) issue",
114
- state: "open",
115
- labels: [],
116
- };
117
- const branchName = createBranchNameFromIssue(issue);
118
- expect(branchName).toBe("feature/33--bug-fix-critical-issue");
119
- });
106
+ it("handles issues with no labels", () => {
107
+ const choices = buildIssueChoices(issues, layout);
108
+ // Issue #7 has no labels — description should still contain the title
109
+ const desc = stripAnsi(choices[2].description!);
110
+ expect(desc).toContain("Docs update");
120
111
  });
121
112
 
122
- // TODO: Integration tests for githubBranchCommand require module mocking
123
- // which works differently in bun:test vs vitest
113
+ it("works with empty issue list", () => {
114
+ const emptyLayout = computeColumnLayout(
115
+ [{ number: 0, title: "", state: "open", labels: [] }],
116
+ 80,
117
+ );
118
+ const choices = buildIssueChoices([], emptyLayout);
119
+ // Only the Cancel choice
120
+ expect(choices).toHaveLength(1);
121
+ expect(choices[0].value).toBe("cancel");
122
+ });
124
123
  });
@@ -6,11 +6,46 @@ import { Fzf } from "fzf";
6
6
  import consola from "consola";
7
7
 
8
8
  import { BaseCommand } from "../base.js";
9
- import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
10
9
  import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
10
+ import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
11
11
  import { createBranch } from "../../utils/git/git-wrapper.js";
12
+ import { createBranchNameFromIssue } from "../../utils/git/branch-name.js";
12
13
  import { getTerminalColumns, limitText, printWithHexColor } from "../../utils/render.js";
13
14
 
15
+ export interface ColumnLayout {
16
+ longestNumber: number;
17
+ longestLabels: number;
18
+ descriptionLength: number;
19
+ }
20
+
21
+ export function computeColumnLayout(issues: Issue[], terminalColumns: number): ColumnLayout {
22
+ const longestNumber = Math.max(...issues.map((i) => i.number.toString().length));
23
+ const longestLabels = Math.max(
24
+ ...issues.map((i) => i.labels.map((x) => x.name).join(", ").length),
25
+ );
26
+ const lineMaxLength = Math.min(terminalColumns, 130);
27
+ const descriptionLength = lineMaxLength - longestNumber - longestLabels - 15;
28
+
29
+ return { longestNumber, longestLabels, descriptionLength };
30
+ }
31
+
32
+ export function buildIssueChoices(issues: Issue[], layout: ColumnLayout): Choice[] {
33
+ const choices: Choice[] = issues.map((i) => {
34
+ const labelText = i.labels
35
+ .map((l) => printWithHexColor({ msg: l.name, hex: l.color }))
36
+ .join(", ");
37
+ const labelTextNoColor = i.labels.map((l) => l.name).join(", ");
38
+ const labelStartpad = layout.longestLabels - labelTextNoColor.length;
39
+ return {
40
+ title: i.number.toString(),
41
+ value: i.number,
42
+ description: `${limitText(i.title, layout.descriptionLength).padEnd(layout.descriptionLength)} ${"".padStart(labelStartpad)}${labelText}`,
43
+ } as Choice;
44
+ });
45
+ choices.push({ title: "Cancel", value: "cancel" });
46
+ return choices;
47
+ }
48
+
14
49
  /**
15
50
  * Create a git branch from a GitHub issue
16
51
  */
@@ -55,29 +90,8 @@ export default class GhBranch extends BaseCommand {
55
90
  consola.log(colors.green(`${currentIssues.length} Issues found assigned to you`));
56
91
  }
57
92
 
58
- // Format table with nice labels
59
- let lineMaxLength = getTerminalColumns();
60
- const longestNumber = Math.max(...currentIssues.map((i) => i.number.toString().length));
61
- const longestLabels = Math.max(
62
- ...currentIssues.map((i) => i.labels.map((x) => x.name).join(", ").length),
63
- );
64
-
65
- lineMaxLength = lineMaxLength > 130 ? 130 : lineMaxLength;
66
- const descriptionLength = lineMaxLength - longestNumber - longestLabels - 15;
67
-
68
- const choices: Choice[] = currentIssues.map((i) => {
69
- const labelText = i.labels
70
- .map((l) => printWithHexColor({ msg: l.name, hex: l.color }))
71
- .join(", ");
72
- const labelTextNoColor = i.labels.map((l) => l.name).join(", ");
73
- const labelStartpad = longestLabels - labelTextNoColor.length;
74
- return {
75
- title: i.number.toString(),
76
- value: i.number,
77
- description: `${limitText(i.title, descriptionLength).padEnd(descriptionLength)} ${"".padStart(labelStartpad)}${labelText}`,
78
- } as Choice;
79
- });
80
- choices.push({ title: "Cancel", value: "cancel" });
93
+ const layout = computeColumnLayout(currentIssues, getTerminalColumns());
94
+ const choices = buildIssueChoices(currentIssues, layout);
81
95
 
82
96
  const fzf = new Fzf(choices, {
83
97
  selector: (item) => `${item.value} ${item.description}`,
@@ -115,21 +129,10 @@ export default class GhBranch extends BaseCommand {
115
129
  `Selected issue ${colors.green(selectedIssue.number)} - ${colors.green(selectedIssue.title)}`,
116
130
  );
117
131
 
118
- const branchName = GhBranch.createBranchNameFromIssue(selectedIssue);
132
+ const branchName = createBranchNameFromIssue(selectedIssue);
119
133
  createBranch({ branchName });
120
134
  } catch {
121
135
  this.exit(1);
122
136
  }
123
137
  }
124
-
125
- static createBranchNameFromIssue(selectedIssue: Issue): string {
126
- let slug = selectedIssue.title.toLowerCase();
127
- slug = slug.trim();
128
- slug = slug.replaceAll(" ", "-");
129
- slug = slug.replace(/[^0-9a-zA-Z_-]/g, "-");
130
- slug = slug.replace(/-+/g, "-");
131
- slug = slug.replace(/-+$/, "");
132
-
133
- return `feature/${selectedIssue.number}-${slug}`;
134
- }
135
138
  }
@@ -0,0 +1,195 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import consola from "consola";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+
8
+ import { initConfig } from "./config";
9
+ import { ARTIFACTS } from "./prompt-templates/index";
10
+ import {
11
+ buildTestContext,
12
+ createTestRepoWithRemote,
13
+ errorClaudeJson,
14
+ successClaudeJson,
15
+ } from "./test-helpers";
16
+ import type { TestRepo } from "./test-helpers";
17
+ import type { IssueContext } from "./utils";
18
+
19
+ consola.level = -999;
20
+
21
+ // ── Mock tinyexec: intercept "claude" and "gh" calls, pass through git ──
22
+
23
+ let mockClaudeImpl: ((args: string[]) => Promise<{ stdout: string; exitCode: number }>) | null =
24
+ null;
25
+ let mockGhImpl: ((args: string[]) => Promise<{ stdout: string; exitCode: number }>) | null = null;
26
+
27
+ vi.mock("tinyexec", async (importOriginal) => {
28
+ const original = await importOriginal<typeof import("tinyexec")>();
29
+ return {
30
+ ...original,
31
+ x: vi.fn(
32
+ async (
33
+ cmd: string,
34
+ args: string[],
35
+ opts?: Record<string, unknown>,
36
+ ): Promise<{ stdout: string; exitCode: number }> => {
37
+ if (cmd === "claude" && mockClaudeImpl) {
38
+ return mockClaudeImpl(args);
39
+ }
40
+ if (cmd === "claude") {
41
+ throw new Error("Unexpected claude call -- set mockClaudeImpl");
42
+ }
43
+ if (cmd === "gh" && mockGhImpl) {
44
+ return mockGhImpl(args);
45
+ }
46
+ if (cmd === "gh") {
47
+ throw new Error("Unexpected gh call -- set mockGhImpl");
48
+ }
49
+ return original.x(cmd, args, opts as never) as unknown as Promise<{
50
+ stdout: string;
51
+ exitCode: number;
52
+ }>;
53
+ },
54
+ ),
55
+ };
56
+ });
57
+
58
+ describe("runPipeline", () => {
59
+ let originalCwd: string;
60
+ let repo: TestRepo;
61
+ let ctx: IssueContext;
62
+
63
+ beforeEach(async () => {
64
+ originalCwd = process.cwd();
65
+ repo = createTestRepoWithRemote();
66
+ process.chdir(repo.dir);
67
+ await initConfig({
68
+ repo: "test/repo",
69
+ mainBranch: "main",
70
+ maxImplementIterations: 2,
71
+ });
72
+ ctx = buildTestContext(repo.dir);
73
+ mockClaudeImpl = null;
74
+ mockGhImpl = null;
75
+ });
76
+
77
+ afterEach(() => {
78
+ process.chdir(originalCwd);
79
+ repo.cleanup();
80
+ });
81
+
82
+ it("writes initial-ramblings.md on first run", async () => {
83
+ const { runPipeline } = await import("./pipeline");
84
+
85
+ mockClaudeImpl = async () => ({ stdout: errorClaudeJson(), exitCode: 0 });
86
+
87
+ await runPipeline(ctx);
88
+
89
+ const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
90
+ expect(existsSync(ramblingsPath)).toBe(true);
91
+
92
+ const content = readFileSync(ramblingsPath, "utf-8");
93
+ expect(content).toContain(ctx.title);
94
+ expect(content).toContain(`${ctx.repo}#${ctx.number}`);
95
+ });
96
+
97
+ it("skips writing initial-ramblings.md if already present", async () => {
98
+ const { runPipeline } = await import("./pipeline");
99
+
100
+ mkdirSync(ctx.issueDir, { recursive: true });
101
+ const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
102
+ writeFileSync(ramblingsPath, "# Existing ramblings");
103
+
104
+ mockClaudeImpl = async () => ({ stdout: errorClaudeJson(), exitCode: 0 });
105
+
106
+ await runPipeline(ctx);
107
+
108
+ const content = readFileSync(ramblingsPath, "utf-8");
109
+ expect(content).toBe("# Existing ramblings");
110
+ });
111
+
112
+ it("stops after --until step", async () => {
113
+ const { runPipeline } = await import("./pipeline");
114
+
115
+ let claudeCallCount = 0;
116
+ const researchPath = join(ctx.issueDir, ARTIFACTS.research);
117
+
118
+ mockClaudeImpl = async () => {
119
+ claudeCallCount++;
120
+ mkdirSync(ctx.issueDir, { recursive: true });
121
+ writeFileSync(researchPath, "x".repeat(250));
122
+ return { stdout: successClaudeJson(), exitCode: 0 };
123
+ };
124
+
125
+ await runPipeline(ctx, "research");
126
+
127
+ expect(claudeCallCount).toBe(1);
128
+ });
129
+
130
+ it("stops and checks out main on step failure", async () => {
131
+ const { runPipeline } = await import("./pipeline");
132
+
133
+ mockClaudeImpl = async () => ({ stdout: errorClaudeJson(), exitCode: 0 });
134
+
135
+ await runPipeline(ctx);
136
+
137
+ const currentBranch = execSync("git branch --show-current", {
138
+ cwd: repo.dir,
139
+ encoding: "utf-8",
140
+ }).trim();
141
+ expect(currentBranch).toBe("main");
142
+ });
143
+
144
+ it("runs all steps in order when all succeed", async () => {
145
+ const { runPipeline } = await import("./pipeline");
146
+
147
+ let claudeCallCount = 0;
148
+ mockClaudeImpl = async () => {
149
+ claudeCallCount++;
150
+ mkdirSync(ctx.issueDir, { recursive: true });
151
+
152
+ switch (claudeCallCount) {
153
+ case 1:
154
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.research), "x".repeat(250));
155
+ break;
156
+ case 2:
157
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.plan), "# Plan");
158
+ break;
159
+ case 3:
160
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.planImplementation), "# Impl Plan");
161
+ break;
162
+ case 4:
163
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.completedSummary), "# Done");
164
+ break;
165
+ case 5:
166
+ writeFileSync(join(ctx.issueDir, ARTIFACTS.review), "# Review\nLooks good.");
167
+ break;
168
+ }
169
+ return { stdout: successClaudeJson(), exitCode: 0 };
170
+ };
171
+
172
+ let ghCallCount = 0;
173
+ mockGhImpl = async (args: string[]) => {
174
+ ghCallCount++;
175
+ if (args[0] === "pr" && args[1] === "list") {
176
+ return { stdout: "[]", exitCode: 0 };
177
+ }
178
+ if (args[0] === "pr" && args[1] === "create") {
179
+ return { stdout: "https://github.com/test/repo/pull/1", exitCode: 0 };
180
+ }
181
+ return { stdout: "", exitCode: 0 };
182
+ };
183
+
184
+ await runPipeline(ctx);
185
+
186
+ const prUrlPath = join(ctx.issueDir, ARTIFACTS.prUrl);
187
+ if (existsSync(prUrlPath)) {
188
+ const prUrl = readFileSync(prUrlPath, "utf-8");
189
+ expect(prUrl).toContain("github.com");
190
+ }
191
+
192
+ expect(claudeCallCount).toBe(5);
193
+ expect(ghCallCount).toBeGreaterThanOrEqual(2);
194
+ });
195
+ });
@@ -0,0 +1,21 @@
1
+ You are a senior developer researching a codebase to prepare for implementing a GitHub issue.
2
+
3
+ Read the issue in @{{ISSUE_DIR}}/initial-ramblings.md and **research the codebase** to understand what implementing it involves.
4
+
5
+ **CRITICAL:** Do **NOT** implement. Your **ONLY** deliverable is @{{ISSUE_DIR}}/research.md.
6
+
7
+ If the issue is vague or trivial — research it anyway. Note what's ambiguous and list assumptions.
8
+
9
+ ## Where to look
10
+
11
+ Start at `{{SCOPE_PATH}}/`. Follow imports, check test files, trace types and schemas. Read every relevant file in full. Do not skim.
12
+
13
+ ## What to write in @{{ISSUE_DIR}}/research.md
14
+
15
+ 1. **Relevant files** — every file to read or modify, with brief descriptions
16
+ 2. **Existing patterns** — how similar features are implemented in this codebase
17
+ 3. **Dependencies** — libraries, utilities, shared code that are relevant
18
+ 4. **Potential impact areas** — what else might break (tests, types, imports, configs)
19
+ 5. **Existing test coverage** — which test files cover affected modules, what gaps exist. Run the project's test command to confirm the suite passes.
20
+ 6. **Edge cases and constraints** — anything tricky
21
+ 7. **Reference implementations** — similar features already built
@@ -0,0 +1,27 @@
1
+ You are a senior developer planning the implementation for a GitHub issue.
2
+
3
+ Read the issue in @{{ISSUE_DIR}}/initial-ramblings.md and the research in @{{ISSUE_DIR}}/research.md.
4
+
5
+ **CRITICAL:** Do **NOT** implement. Your **ONLY** deliverable is @{{ISSUE_DIR}}/plan.md.
6
+
7
+ Read actual source files before suggesting changes. If the issue is infeasible, explain why and propose the closest feasible alternative.
8
+
9
+ The code lives primarily at `{{SCOPE_PATH}}/`.
10
+
11
+ ## Write @{{ISSUE_DIR}}/plan.md
12
+
13
+ 1. **Summary** — what we're building and why (1-2 paragraphs)
14
+ 2. **Approach** — the high-level technical approach
15
+ 3. **Architectural decisions** — significant choices and why
16
+ 4. **Key code snippets** — concrete examples (function signatures, schemas, etc.)
17
+ 5. **Scope boundaries** — what is explicitly out of scope
18
+ 6. **Risks** — anything that needs special attention
19
+ 7. **Test strategy** — use red/green TDD: for each testable behavior, write the test first (red), then implement to make it pass (green). List which test files to write/update and what each test asserts. Reference specific test files from the research. Prefer real implementations over mocks — only mock at external boundaries (network, filesystem, third-party APIs). Do not write tests for things the type system or compiler already enforces.
20
+ 8. **Alternative approaches** — other valid solutions, why the chosen approach was preferred. For PR reviewers only.
21
+
22
+ **Design principles:**
23
+
24
+ - If the plan touches an area with a known bug, address it now.
25
+ - Reuse existing abstractions — don't create parallel primitives.
26
+
27
+ Keep under 500 lines. Focus on decisions, not repeating the research.
@@ -0,0 +1,15 @@
1
+ You are a senior developer revising a plan based on reviewer feedback.
2
+
3
+ Read the plan in @{{ISSUE_DIR}}/plan.md and annotations in @{{ISSUE_DIR}}/plan-annotations.md. The issue is in @{{ISSUE_DIR}}/initial-ramblings.md and research in @{{ISSUE_DIR}}/research.md.
4
+
5
+ **CRITICAL:** Do **NOT** implement. Deliverables: (1) updated @{{ISSUE_DIR}}/plan.md, (2) @{{ISSUE_DIR}}/plan-annotations-addressed.md.
6
+
7
+ The code lives primarily at `{{SCOPE_PATH}}/`.
8
+
9
+ ## Instructions
10
+
11
+ - Address every annotation — update the plan to incorporate feedback.
12
+ - Questions → answer in the plan. Suggested approaches → evaluate and update. Missing info → add it.
13
+ - If annotations contradict each other, choose the approach that best fits codebase patterns and document why.
14
+ - Keep the same structure and formatting.
15
+ - Write @{{ISSUE_DIR}}/plan-annotations-addressed.md listing each annotation and how it was addressed (one line per annotation).
@@ -0,0 +1,35 @@
1
+ You are breaking down a plan into a detailed, ordered implementation checklist.
2
+
3
+ Read the issue in @{{ISSUE_DIR}}/initial-ramblings.md, the research in @{{ISSUE_DIR}}/research.md, and the plan in @{{ISSUE_DIR}}/plan.md.
4
+
5
+ **CRITICAL:** Do **NOT** implement. Your **ONLY** deliverable is @{{ISSUE_DIR}}/plan-implementation.md.
6
+
7
+ The code lives primarily at `{{SCOPE_PATH}}/`.
8
+
9
+ ## Write @{{ISSUE_DIR}}/plan-implementation.md
10
+
11
+ - **Ordered markdown checkboxes** (`- [ ]` per task), small enough to implement in one focused session
12
+ - Prefer real implementations over mocks — only mock at external boundaries (network, filesystem, third-party APIs)
13
+ - Do not write tests for things the type system or compiler already enforces
14
+ - Each task must follow **red/green TDD** structure:
15
+ - **Files** — specific file paths to modify
16
+ - **Changes** — concrete description (not vague like "update the component")
17
+ - **Red** — write this test first, assert this behavior, run tests and confirm it **fails**
18
+ - **Green** — implement the change, run tests and confirm it **passes**
19
+ - **Acceptance** — how to verify the task is done
20
+ - Order tasks so each builds on the previous (no forward dependencies)
21
+ - Include setup tasks (new files, dependencies) as needed
22
+ - Final task: "Run the project's type-check, test, and lint commands. Confirm zero errors."
23
+
24
+ Example:
25
+
26
+ ```
27
+ - [ ] **Task 1: Add config schema**
28
+ - Files: `src/lib/feature/config.ts`, `src/lib/feature/config.test.ts`
29
+ - Changes: Define config schema with validation, export inferred type
30
+ - Red: Write test asserting schema validates correct input and rejects invalid input — confirm it fails
31
+ - Green: Implement the schema — confirm test passes
32
+ - Acceptance: All tests pass, schema correctly validates/rejects
33
+ ```
34
+
35
+ Be specific enough that a developer can follow without re-reading the research or plan.