@towles/tool 0.0.59 → 0.0.61
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.ts +5 -1
- package/src/commands/gh/branch.test.ts +107 -108
- package/src/commands/gh/branch.ts +42 -38
- package/src/lib/auto-claude/pipeline-execution.test.ts +191 -0
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +21 -0
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +27 -0
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +15 -0
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +35 -0
- package/src/lib/auto-claude/prompt-templates/05_implement.prompt.md +36 -0
- package/src/lib/auto-claude/prompt-templates/06_review.prompt.md +32 -0
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +30 -0
- package/src/lib/auto-claude/prompt-templates/CLAUDE.md +12 -0
- package/src/lib/auto-claude/prompt-templates/index.test.ts +2 -2
- package/src/lib/auto-claude/prompt-templates/index.ts +7 -7
- package/src/lib/auto-claude/run-claude.test.ts +155 -0
- package/src/lib/auto-claude/spawn-claude.ts +14 -0
- package/src/lib/auto-claude/steps/steps.test.ts +304 -0
- package/src/lib/auto-claude/test-helpers.ts +139 -0
- package/src/lib/auto-claude/utils-execution.test.ts +152 -0
- package/src/lib/auto-claude/utils.test.ts +7 -7
- package/src/lib/auto-claude/utils.ts +99 -21
- package/src/utils/git/branch-name.test.ts +83 -0
- package/src/utils/git/branch-name.ts +10 -0
- package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +0 -28
- package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +0 -28
- package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +0 -21
- package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +0 -33
- package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +0 -31
- package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +0 -30
- package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +0 -39
- package/src/utils/git/git-wrapper.test.ts +0 -26
- package/src/utils/git/git-wrapper.ts +0 -15
package/package.json
CHANGED
|
@@ -202,7 +202,11 @@ async function syncWithRemote(): Promise<void> {
|
|
|
202
202
|
}
|
|
203
203
|
const status = await git(["status", "--porcelain"]);
|
|
204
204
|
if (status.length > 0) {
|
|
205
|
-
|
|
205
|
+
const files = status.trim().split("\n");
|
|
206
|
+
consola.warn(`Working tree has ${files.length} uncommitted change(s):`);
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
consola.warn(` ${file.trim()}`);
|
|
209
|
+
}
|
|
206
210
|
}
|
|
207
211
|
await git(["pull", cfg.remote, cfg.mainBranch]);
|
|
208
212
|
}
|
|
@@ -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
|
|
4
|
+
import { buildIssueChoices, computeColumnLayout } from "./branch";
|
|
4
5
|
|
|
5
|
-
const
|
|
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("
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
});
|
|
@@ -5,12 +5,48 @@ import { colors } from "consola/utils";
|
|
|
5
5
|
import { Fzf } from "fzf";
|
|
6
6
|
import consola from "consola";
|
|
7
7
|
|
|
8
|
+
import { exec } from "tinyexec";
|
|
9
|
+
|
|
8
10
|
import { BaseCommand } from "../base.js";
|
|
9
|
-
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
10
11
|
import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
|
|
11
|
-
import {
|
|
12
|
+
import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
13
|
+
import { createBranchNameFromIssue } from "../../utils/git/branch-name.js";
|
|
12
14
|
import { getTerminalColumns, limitText, printWithHexColor } from "../../utils/render.js";
|
|
13
15
|
|
|
16
|
+
export interface ColumnLayout {
|
|
17
|
+
longestNumber: number;
|
|
18
|
+
longestLabels: number;
|
|
19
|
+
descriptionLength: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function computeColumnLayout(issues: Issue[], terminalColumns: number): ColumnLayout {
|
|
23
|
+
const longestNumber = Math.max(...issues.map((i) => i.number.toString().length));
|
|
24
|
+
const longestLabels = Math.max(
|
|
25
|
+
...issues.map((i) => i.labels.map((x) => x.name).join(", ").length),
|
|
26
|
+
);
|
|
27
|
+
const lineMaxLength = Math.min(terminalColumns, 130);
|
|
28
|
+
const descriptionLength = lineMaxLength - longestNumber - longestLabels - 15;
|
|
29
|
+
|
|
30
|
+
return { longestNumber, longestLabels, descriptionLength };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildIssueChoices(issues: Issue[], layout: ColumnLayout): Choice[] {
|
|
34
|
+
const choices: Choice[] = issues.map((i) => {
|
|
35
|
+
const labelText = i.labels
|
|
36
|
+
.map((l) => printWithHexColor({ msg: l.name, hex: l.color }))
|
|
37
|
+
.join(", ");
|
|
38
|
+
const labelTextNoColor = i.labels.map((l) => l.name).join(", ");
|
|
39
|
+
const labelStartpad = layout.longestLabels - labelTextNoColor.length;
|
|
40
|
+
return {
|
|
41
|
+
title: i.number.toString(),
|
|
42
|
+
value: i.number,
|
|
43
|
+
description: `${limitText(i.title, layout.descriptionLength).padEnd(layout.descriptionLength)} ${"".padStart(labelStartpad)}${labelText}`,
|
|
44
|
+
} as Choice;
|
|
45
|
+
});
|
|
46
|
+
choices.push({ title: "Cancel", value: "cancel" });
|
|
47
|
+
return choices;
|
|
48
|
+
}
|
|
49
|
+
|
|
14
50
|
/**
|
|
15
51
|
* Create a git branch from a GitHub issue
|
|
16
52
|
*/
|
|
@@ -55,29 +91,8 @@ export default class GhBranch extends BaseCommand {
|
|
|
55
91
|
consola.log(colors.green(`${currentIssues.length} Issues found assigned to you`));
|
|
56
92
|
}
|
|
57
93
|
|
|
58
|
-
|
|
59
|
-
|
|
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" });
|
|
94
|
+
const layout = computeColumnLayout(currentIssues, getTerminalColumns());
|
|
95
|
+
const choices = buildIssueChoices(currentIssues, layout);
|
|
81
96
|
|
|
82
97
|
const fzf = new Fzf(choices, {
|
|
83
98
|
selector: (item) => `${item.value} ${item.description}`,
|
|
@@ -115,21 +130,10 @@ export default class GhBranch extends BaseCommand {
|
|
|
115
130
|
`Selected issue ${colors.green(selectedIssue.number)} - ${colors.green(selectedIssue.title)}`,
|
|
116
131
|
);
|
|
117
132
|
|
|
118
|
-
const branchName =
|
|
119
|
-
|
|
133
|
+
const branchName = createBranchNameFromIssue(selectedIssue);
|
|
134
|
+
await exec("git", ["checkout", "-b", branchName]);
|
|
120
135
|
} catch {
|
|
121
136
|
this.exit(1);
|
|
122
137
|
}
|
|
123
138
|
}
|
|
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
139
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
createSpawnClaudeMock,
|
|
13
|
+
createTestRepoWithRemote,
|
|
14
|
+
errorClaudeJson,
|
|
15
|
+
successClaudeJson,
|
|
16
|
+
} from "./test-helpers";
|
|
17
|
+
import type { MockClaudeImpl, TestRepo } from "./test-helpers";
|
|
18
|
+
import type { IssueContext } from "./utils";
|
|
19
|
+
|
|
20
|
+
consola.level = -999;
|
|
21
|
+
|
|
22
|
+
let mockClaudeImpl: MockClaudeImpl = null;
|
|
23
|
+
vi.mock("./spawn-claude", () => createSpawnClaudeMock(() => mockClaudeImpl));
|
|
24
|
+
|
|
25
|
+
// ── Mock tinyexec: intercept "gh" calls, pass through git ──
|
|
26
|
+
|
|
27
|
+
let mockGhImpl: ((args: string[]) => Promise<{ stdout: string; exitCode: number }>) | null = null;
|
|
28
|
+
|
|
29
|
+
vi.mock("tinyexec", async (importOriginal) => {
|
|
30
|
+
const original = await importOriginal<typeof import("tinyexec")>();
|
|
31
|
+
return {
|
|
32
|
+
...original,
|
|
33
|
+
x: vi.fn(
|
|
34
|
+
async (
|
|
35
|
+
cmd: string,
|
|
36
|
+
args: string[],
|
|
37
|
+
opts?: Record<string, unknown>,
|
|
38
|
+
): Promise<{ stdout: string; exitCode: number }> => {
|
|
39
|
+
if (cmd === "gh" && mockGhImpl) {
|
|
40
|
+
return mockGhImpl(args);
|
|
41
|
+
}
|
|
42
|
+
if (cmd === "gh") {
|
|
43
|
+
throw new Error("Unexpected gh call -- set mockGhImpl");
|
|
44
|
+
}
|
|
45
|
+
return original.x(cmd, args, opts as never) as unknown as Promise<{
|
|
46
|
+
stdout: string;
|
|
47
|
+
exitCode: number;
|
|
48
|
+
}>;
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("runPipeline", () => {
|
|
55
|
+
let originalCwd: string;
|
|
56
|
+
let repo: TestRepo;
|
|
57
|
+
let ctx: IssueContext;
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
originalCwd = process.cwd();
|
|
61
|
+
repo = createTestRepoWithRemote();
|
|
62
|
+
process.chdir(repo.dir);
|
|
63
|
+
await initConfig({
|
|
64
|
+
repo: "test/repo",
|
|
65
|
+
mainBranch: "main",
|
|
66
|
+
maxImplementIterations: 2,
|
|
67
|
+
});
|
|
68
|
+
ctx = buildTestContext(repo.dir);
|
|
69
|
+
mockClaudeImpl = null;
|
|
70
|
+
mockGhImpl = null;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
process.chdir(originalCwd);
|
|
75
|
+
repo.cleanup();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("writes initial-ramblings.md on first run", async () => {
|
|
79
|
+
const { runPipeline } = await import("./pipeline");
|
|
80
|
+
|
|
81
|
+
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
82
|
+
|
|
83
|
+
await runPipeline(ctx);
|
|
84
|
+
|
|
85
|
+
const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
|
|
86
|
+
expect(existsSync(ramblingsPath)).toBe(true);
|
|
87
|
+
|
|
88
|
+
const content = readFileSync(ramblingsPath, "utf-8");
|
|
89
|
+
expect(content).toContain(ctx.title);
|
|
90
|
+
expect(content).toContain(`${ctx.repo}#${ctx.number}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("skips writing initial-ramblings.md if already present", async () => {
|
|
94
|
+
const { runPipeline } = await import("./pipeline");
|
|
95
|
+
|
|
96
|
+
mkdirSync(ctx.issueDir, { recursive: true });
|
|
97
|
+
const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
|
|
98
|
+
writeFileSync(ramblingsPath, "# Existing ramblings");
|
|
99
|
+
|
|
100
|
+
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
101
|
+
|
|
102
|
+
await runPipeline(ctx);
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(ramblingsPath, "utf-8");
|
|
105
|
+
expect(content).toBe("# Existing ramblings");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("stops after --until step", async () => {
|
|
109
|
+
const { runPipeline } = await import("./pipeline");
|
|
110
|
+
|
|
111
|
+
let claudeCallCount = 0;
|
|
112
|
+
const researchPath = join(ctx.issueDir, ARTIFACTS.research);
|
|
113
|
+
|
|
114
|
+
mockClaudeImpl = () => {
|
|
115
|
+
claudeCallCount++;
|
|
116
|
+
mkdirSync(ctx.issueDir, { recursive: true });
|
|
117
|
+
writeFileSync(researchPath, "x".repeat(250));
|
|
118
|
+
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await runPipeline(ctx, "research");
|
|
122
|
+
|
|
123
|
+
expect(claudeCallCount).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("stops and checks out main on step failure", async () => {
|
|
127
|
+
const { runPipeline } = await import("./pipeline");
|
|
128
|
+
|
|
129
|
+
mockClaudeImpl = () => ({ stdout: errorClaudeJson(), exitCode: 0 });
|
|
130
|
+
|
|
131
|
+
await runPipeline(ctx);
|
|
132
|
+
|
|
133
|
+
const currentBranch = execSync("git branch --show-current", {
|
|
134
|
+
cwd: repo.dir,
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
}).trim();
|
|
137
|
+
expect(currentBranch).toBe("main");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("runs all steps in order when all succeed", async () => {
|
|
141
|
+
const { runPipeline } = await import("./pipeline");
|
|
142
|
+
|
|
143
|
+
let claudeCallCount = 0;
|
|
144
|
+
mockClaudeImpl = () => {
|
|
145
|
+
claudeCallCount++;
|
|
146
|
+
mkdirSync(ctx.issueDir, { recursive: true });
|
|
147
|
+
|
|
148
|
+
switch (claudeCallCount) {
|
|
149
|
+
case 1:
|
|
150
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.research), "x".repeat(250));
|
|
151
|
+
break;
|
|
152
|
+
case 2:
|
|
153
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.plan), "# Plan");
|
|
154
|
+
break;
|
|
155
|
+
case 3:
|
|
156
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.planImplementation), "# Impl Plan");
|
|
157
|
+
break;
|
|
158
|
+
case 4:
|
|
159
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.completedSummary), "# Done");
|
|
160
|
+
break;
|
|
161
|
+
case 5:
|
|
162
|
+
writeFileSync(join(ctx.issueDir, ARTIFACTS.review), "# Review\nLooks good.");
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
return { stdout: successClaudeJson(), exitCode: 0 };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
let ghCallCount = 0;
|
|
169
|
+
mockGhImpl = async (args: string[]) => {
|
|
170
|
+
ghCallCount++;
|
|
171
|
+
if (args[0] === "pr" && args[1] === "list") {
|
|
172
|
+
return { stdout: "[]", exitCode: 0 };
|
|
173
|
+
}
|
|
174
|
+
if (args[0] === "pr" && args[1] === "create") {
|
|
175
|
+
return { stdout: "https://github.com/test/repo/pull/1", exitCode: 0 };
|
|
176
|
+
}
|
|
177
|
+
return { stdout: "", exitCode: 0 };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
await runPipeline(ctx);
|
|
181
|
+
|
|
182
|
+
const prUrlPath = join(ctx.issueDir, ARTIFACTS.prUrl);
|
|
183
|
+
if (existsSync(prUrlPath)) {
|
|
184
|
+
const prUrl = readFileSync(prUrlPath, "utf-8");
|
|
185
|
+
expect(prUrl).toContain("github.com");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
expect(claudeCallCount).toBe(5);
|
|
189
|
+
expect(ghCallCount).toBeGreaterThanOrEqual(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -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.
|