@towles/tool 0.0.61 → 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
@@ -0,0 +1,52 @@
1
+ You are a code simplification agent. Review the changes on this branch and simplify the implementation.
2
+
3
+ The issue is in @{{ISSUE_DIR}}/initial-ramblings.md — for context only.
4
+ The completed summary is in @{{ISSUE_DIR}}/completed-summary.md.
5
+ The code lives primarily at `{{SCOPE_PATH}}/`.
6
+ The base branch is `{{MAIN_BRANCH}}`.
7
+
8
+ ## How to work
9
+
10
+ 1. Run `git diff {{MAIN_BRANCH}}...HEAD --stat` to see all changed files
11
+ 2. Run `git diff {{MAIN_BRANCH}}...HEAD` to review the full diff
12
+ 3. For each changed file, read the complete file (not just the diff) to understand the full context
13
+
14
+ ## What to simplify
15
+
16
+ - **Dead code** — remove unused variables, imports, functions, or parameters added by the implementation
17
+ - **Over-abstraction** — inline single-use helpers, remove unnecessary wrapper functions, flatten needless indirection
18
+ - **Complexity** — simplify conditionals, reduce nesting, prefer early returns
19
+ - **Duplication** — extract repeated patterns into shared helpers (only when genuinely duplicated, not merely similar)
20
+ - **Naming** — rename unclear variables or functions for better readability
21
+ - **Type safety** — remove unnecessary type assertions, tighten `any` types
22
+
23
+ ## What NOT to change
24
+
25
+ - Do not alter behavior — every simplification must be behavior-preserving
26
+ - Do not refactor code outside the scope of this branch's changes
27
+ - Do not add new features or fix unrelated bugs
28
+ - Do not remove code that is used elsewhere but appears unused in the diff
29
+
30
+ ## Verification
31
+
32
+ After each round of simplifications:
33
+
34
+ 1. Run the project's type-check command — fix any errors
35
+ 2. Run the project's test command — fix any regressions
36
+ 3. Run the project's lint command — fix any violations
37
+
38
+ Commit simplifications as `refactor(scope): code-simplify review for issue #N` (use the actual issue number from the issue directory name).
39
+
40
+ ## Output
41
+
42
+ Write @{{ISSUE_DIR}}/simplify-summary.md with:
43
+
44
+ - List of simplifications made (file path + what changed)
45
+ - If no simplifications were needed, state that explicitly
46
+ - Verification results (all checks pass / any issues encountered)
47
+
48
+ ## Guidelines
49
+
50
+ - Prefer fewer, well-justified changes over many trivial ones
51
+ - If the implementation is already clean, say so — don't force changes
52
+ - Follow the project's coding conventions from CLAUDE.md
@@ -1,4 +1,4 @@
1
- You are reviewing code changes for the issue in @{{ISSUE_DIR}}/initial-ramblings.md. The plan is in @{{ISSUE_DIR}}/plan.md and checklist in @{{ISSUE_DIR}}/plan-implementation.md.
1
+ You are reviewing code changes for the issue in @{{ISSUE_DIR}}/initial-ramblings.md. The plan is in @{{ISSUE_DIR}}/plan.md.
2
2
 
3
3
  ## Automated checks
4
4
 
@@ -18,15 +18,38 @@ Run `git diff {{MAIN_BRANCH}}...HEAD` and check:
18
18
  8. **Incomplete work** — TODOs, placeholders, unfinished implementations?
19
19
  9. **Test coverage** — new behaviors covered by tests? Existing tests updated?
20
20
 
21
- After review, run the code-simplify skill on changed files. Apply simplifications that improve clarity without changing behavior — commit separately.
22
-
23
21
  Fix issues directly, commit as `fix(scope): review fixes for issue #N`.
24
22
 
25
23
  ## Write @{{ISSUE_DIR}}/review.md
26
24
 
27
- - **Status**: PASS, PASS WITH FIXES, or FAIL
28
- - FAIL = fundamentally broken (wrong approach, missing core functionality, unfixable regressions). Explain what's wrong.
29
- - **Issues found** — what was wrong and what you fixed
25
+ **CRITICAL**: The first line of review.md must be exactly `PASS` or `FAIL` (no markdown, no prefix, just the word). This is machine-parsed.
26
+
27
+ Format:
28
+
29
+ ```
30
+ PASS
31
+ ```
32
+
33
+ or
34
+
35
+ ```
36
+ FAIL
37
+ ```
38
+
39
+ Followed by:
40
+
41
+ - **Issues found** — what was wrong and what you fixed (if any)
30
42
  - **Confidence level** — high/medium/low
31
43
  - **Notes** — anything the PR reviewer should check
32
44
  - **Recommended follow-ups** — only if genuinely valuable. Omit if nothing worth flagging.
45
+
46
+ ### When to PASS vs FAIL
47
+
48
+ - **PASS** — implementation is correct, tests pass, code quality is acceptable. Minor fixes you made during review are fine.
49
+ - **FAIL** — fundamentally broken: wrong approach, missing core functionality, unfixable regressions, or critical bugs you cannot fix in review. Explain what's wrong and what the implementer should change. Your feedback will be passed back to the implement step for a retry.
50
+
51
+ ## Guidelines
52
+
53
+ - Follow the project's coding conventions from CLAUDE.md
54
+ - Be thorough but pragmatic — don't fail for style nits
55
+ - If you fix issues during review, that's a PASS (with fixes noted), not a FAIL
@@ -10,15 +10,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
 
11
11
  describe("TEMPLATES", () => {
12
12
  it("should have all expected keys", () => {
13
- expect(Object.keys(TEMPLATES)).toEqual([
14
- "research",
15
- "plan",
16
- "planAnnotations",
17
- "planImplementation",
18
- "implement",
19
- "review",
20
- "refresh",
21
- ]);
13
+ expect(Object.keys(TEMPLATES)).toEqual(["plan", "implement", "simplify", "review"]);
22
14
  });
23
15
 
24
16
  it("every template file should exist on disk", () => {
@@ -43,8 +35,8 @@ describe("TEMPLATES", () => {
43
35
  });
44
36
 
45
37
  describe("PIPELINE_STEPS", () => {
46
- it("should have 8 steps", () => {
47
- expect(PIPELINE_STEPS).toHaveLength(8);
38
+ it("should have 4 steps", () => {
39
+ expect(PIPELINE_STEPS).toHaveLength(4);
48
40
  });
49
41
 
50
42
  it("each step should have order, name, and label", () => {
@@ -65,16 +57,7 @@ describe("PIPELINE_STEPS", () => {
65
57
  });
66
58
 
67
59
  it("names should match expected pipeline order", () => {
68
- expect(PIPELINE_STEPS.map((s) => s.name)).toEqual([
69
- "research",
70
- "plan",
71
- "plan-annotations",
72
- "plan-implementation",
73
- "implement",
74
- "review",
75
- "create-pr",
76
- "remove-label",
77
- ]);
60
+ expect(PIPELINE_STEPS.map((s) => s.name)).toEqual(["plan", "implement", "simplify", "review"]);
78
61
  });
79
62
 
80
63
  it("labels should include the order number", () => {
@@ -92,25 +75,12 @@ describe("STEP_NAMES", () => {
92
75
  });
93
76
 
94
77
  describe("STEP_LABELS", () => {
95
- it("should have camelCase keys for all pipeline steps plus refresh", () => {
96
- expect(Object.keys(STEP_LABELS)).toEqual([
97
- "research",
98
- "plan",
99
- "planAnnotations",
100
- "planImplementation",
101
- "implement",
102
- "review",
103
- "createPr",
104
- "removeLabel",
105
- "refresh",
106
- ]);
78
+ it("should have keys matching step names", () => {
79
+ expect(Object.keys(STEP_LABELS)).toEqual(["plan", "implement", "simplify", "review"]);
107
80
  });
108
81
 
109
- it("pipeline labels should have numeric prefixes", () => {
110
- const pipelineLabels = { ...STEP_LABELS };
111
- delete pipelineLabels.refresh;
112
-
113
- for (const label of Object.values(pipelineLabels)) {
82
+ it("labels should have numeric prefixes", () => {
83
+ for (const label of Object.values(STEP_LABELS)) {
114
84
  expect(label).toMatch(/^\d{2}-/);
115
85
  }
116
86
  });
@@ -127,12 +97,9 @@ describe("ARTIFACTS", () => {
127
97
  it("should have all expected keys", () => {
128
98
  expect(Object.keys(ARTIFACTS)).toEqual([
129
99
  "initialRamblings",
130
- "research",
131
100
  "plan",
132
- "planAnnotations",
133
- "planAnnotationsAddressed",
134
- "planImplementation",
135
101
  "completedSummary",
102
+ "simplifySummary",
136
103
  "review",
137
104
  "prUrl",
138
105
  ]);
@@ -1,45 +1,30 @@
1
1
  export const TEMPLATES = {
2
- research: "01_research.prompt.md",
3
- plan: "02_plan.prompt.md",
4
- planAnnotations: "03_plan-annotations.prompt.md",
5
- planImplementation: "04_plan-implementation.prompt.md",
6
- implement: "05_implement.prompt.md",
7
- review: "06_review.prompt.md",
8
- refresh: "07_refresh.prompt.md",
2
+ plan: "01_plan.prompt.md",
3
+ implement: "02_implement.prompt.md",
4
+ simplify: "03_simplify.prompt.md",
5
+ review: "04_review.prompt.md",
9
6
  } as const;
10
7
 
11
8
  export const PIPELINE_STEPS = [
12
- { order: 1, name: "research", label: "01-Research" },
13
- { order: 2, name: "plan", label: "02-Plan" },
14
- { order: 3, name: "plan-annotations", label: "03-Plan-Annotations" },
15
- { order: 4, name: "plan-implementation", label: "04-Plan-Implementation" },
16
- { order: 5, name: "implement", label: "05-Implement" },
17
- { order: 6, name: "review", label: "06-Review" },
18
- { order: 7, name: "create-pr", label: "07-Create PR" },
19
- { order: 8, name: "remove-label", label: "08-Remove Label" },
9
+ { order: 1, name: "plan", label: "01-Plan" },
10
+ { order: 2, name: "implement", label: "02-Implement" },
11
+ { order: 3, name: "simplify", label: "03-Simplify" },
12
+ { order: 4, name: "review", label: "04-Review" },
20
13
  ] as const;
21
14
 
22
15
  export type StepName = (typeof PIPELINE_STEPS)[number]["name"];
23
16
  export const STEP_NAMES = PIPELINE_STEPS.map((s) => s.name);
24
17
 
25
- function toCamelCase(s: string): string {
26
- return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
27
- }
28
-
29
- /** Keyed lookup for step labels (includes standalone "refresh" step) */
30
- export const STEP_LABELS = {
31
- ...Object.fromEntries(PIPELINE_STEPS.map((s) => [toCamelCase(s.name), s.label])),
32
- refresh: "Refresh",
33
- } as Record<string, string>;
18
+ /** Keyed lookup for step labels */
19
+ export const STEP_LABELS = Object.fromEntries(
20
+ PIPELINE_STEPS.map((s) => [s.name, s.label]),
21
+ ) as Record<string, string>;
34
22
 
35
23
  export const ARTIFACTS = {
36
24
  initialRamblings: "initial-ramblings.md",
37
- research: "research.md",
38
25
  plan: "plan.md",
39
- planAnnotations: "plan-annotations.md",
40
- planAnnotationsAddressed: "plan-annotations-addressed.md",
41
- planImplementation: "plan-implementation.md",
42
26
  completedSummary: "completed-summary.md",
27
+ simplifySummary: "simplify-summary.md",
43
28
  review: "review.md",
44
29
  prUrl: "pr-url.txt",
45
30
  } as const;
@@ -43,11 +43,10 @@ describe("runClaude (mocked spawn-claude)", () => {
43
43
 
44
44
  await initConfig({ repo: "test/repo", mainBranch: "main" });
45
45
 
46
- const { runClaude } = await import("./utils");
46
+ const { runClaude } = await import("./claude-cli");
47
47
 
48
48
  const result = await runClaude({
49
49
  promptFile: "test-prompt.md",
50
- permissionMode: "plan",
51
50
  maxTurns: 10,
52
51
  });
53
52
 
@@ -62,8 +61,8 @@ describe("runClaude (mocked spawn-claude)", () => {
62
61
  "--output-format",
63
62
  "stream-json",
64
63
  "--verbose",
65
- "--permission-mode",
66
- "plan",
64
+ "--include-partial-messages",
65
+ "--dangerously-skip-permissions",
67
66
  "--max-turns",
68
67
  "10",
69
68
  "@test-prompt.md",
@@ -71,85 +70,66 @@ describe("runClaude (mocked spawn-claude)", () => {
71
70
  );
72
71
  });
73
72
 
74
- it("returns fallback when no result event in stream", async () => {
73
+ it("logs tool names from stream_event and shows thinking indicator", async () => {
74
+ const infoSpy = vi.spyOn(consola, "info");
75
+
76
+ const thinkingEvent = {
77
+ type: "stream_event",
78
+ event: {
79
+ type: "content_block_start",
80
+ content_block: { type: "thinking" },
81
+ },
82
+ };
83
+ const toolEvent = {
84
+ type: "stream_event",
85
+ event: {
86
+ type: "content_block_start",
87
+ content_block: { type: "tool_use", name: "Edit" },
88
+ },
89
+ };
90
+ const resultEvent = {
91
+ result: "Done",
92
+ is_error: false,
93
+ total_cost_usd: 0.01,
94
+ num_turns: 1,
95
+ };
96
+
75
97
  mockSpawnImpl = () => ({
76
- stdout: '{"type":"system","message":"starting"}',
98
+ stdout: [thinkingEvent, toolEvent, resultEvent].map((e) => JSON.stringify(e)).join("\n"),
77
99
  exitCode: 0,
78
100
  });
79
101
 
80
102
  await initConfig({ repo: "test/repo", mainBranch: "main" });
81
103
 
82
- const { runClaude } = await import("./utils");
104
+ const { runClaude } = await import("./claude-cli");
105
+ const result = await runClaude({ promptFile: "test.md" });
83
106
 
84
- const result = await runClaude({
85
- promptFile: "test.md",
86
- permissionMode: "acceptEdits",
87
- });
107
+ expect(result.result).toBe("Done");
108
+ expect(result.num_turns).toBe(1);
88
109
 
89
- expect(result.result).toBe("");
90
- expect(result.is_error).toBe(false);
91
- expect(result.total_cost_usd).toBe(0);
92
- });
110
+ const infoCalls = infoSpy.mock.calls.map((c) => String(c[0]));
111
+ expect(infoCalls.some((msg) => msg.includes("thinking"))).toBe(true);
112
+ expect(infoCalls.some((msg) => msg.includes("Edit"))).toBe(true);
93
113
 
94
- it("retries on failure when retry is enabled", async () => {
95
- let callCount = 0;
96
- mockSpawnImpl = () => {
97
- callCount++;
98
- if (callCount < 3) {
99
- return { stdout: "", exitCode: 1 };
100
- }
101
- return {
102
- stdout: JSON.stringify({
103
- result: "ok",
104
- is_error: false,
105
- total_cost_usd: 0,
106
- num_turns: 1,
107
- }),
108
- exitCode: 0,
109
- };
110
- };
114
+ infoSpy.mockRestore();
115
+ });
111
116
 
112
- await initConfig({
113
- repo: "test/repo",
114
- mainBranch: "main",
115
- loopRetryEnabled: true,
116
- maxRetries: 5,
117
- retryDelayMs: 1,
118
- maxRetryDelayMs: 5,
117
+ it("returns fallback when no result event in stream", async () => {
118
+ mockSpawnImpl = () => ({
119
+ stdout: '{"type":"system","message":"starting"}',
120
+ exitCode: 0,
119
121
  });
120
122
 
121
- const { runClaude } = await import("./utils");
123
+ await initConfig({ repo: "test/repo", mainBranch: "main" });
124
+
125
+ const { runClaude } = await import("./claude-cli");
122
126
 
123
127
  const result = await runClaude({
124
128
  promptFile: "test.md",
125
- permissionMode: "plan",
126
- retry: true,
127
- });
128
-
129
- expect(result.result).toBe("ok");
130
- expect(callCount).toBe(3);
131
- }, 10_000);
132
-
133
- it("throws after max retries exhausted", async () => {
134
- mockSpawnImpl = () => ({ stdout: "", exitCode: 1 });
135
-
136
- await initConfig({
137
- repo: "test/repo",
138
- mainBranch: "main",
139
- loopRetryEnabled: true,
140
- maxRetries: 2,
141
- retryDelayMs: 1,
142
- maxRetryDelayMs: 5,
143
129
  });
144
130
 
145
- const { runClaude } = await import("./utils");
146
-
147
- await expect(
148
- runClaude({
149
- promptFile: "test.md",
150
- permissionMode: "plan",
151
- retry: true,
152
- }),
153
- ).rejects.toThrow("Claude failed after 2 retries");
154
- }, 10_000);
131
+ expect(result.result).toBe("");
132
+ expect(result.is_error).toBe(false);
133
+ expect(result.total_cost_usd).toBe(0);
134
+ });
155
135
  });
@@ -0,0 +1,6 @@
1
+ export { execSafe, git } from "../../utils/git/exec.js";
2
+ export { gh, ghRaw } from "../../utils/git/gh-cli-wrapper.js";
3
+
4
+ export function sleep(ms: number): Promise<void> {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
@@ -2,21 +2,23 @@ import { join } from "node:path";
2
2
 
3
3
  import consola from "consola";
4
4
 
5
+ import { fileExists, readFile, writeFile } from "../../../utils/fs.js";
6
+ import { execSafe, git } from "../../../utils/git/exec.js";
7
+ import { ghRaw } from "../../../utils/git/gh-cli-wrapper.js";
5
8
  import { getConfig } from "../config.js";
6
- import { ARTIFACTS, STEP_LABELS } from "../prompt-templates/index.js";
7
- import { fileExists, ghRaw, git, log, logStep, readFile, writeFile } from "../utils.js";
9
+ import { ARTIFACTS } from "../prompt-templates/index.js";
10
+ import { log } from "../utils.js";
8
11
  import type { IssueContext } from "../utils.js";
9
12
 
10
- export async function stepCreatePR(ctx: IssueContext): Promise<boolean> {
13
+ export async function createPr(ctx: IssueContext): Promise<string> {
11
14
  const cfg = getConfig();
12
15
 
13
- if (await hasOpenPR(ctx.branch)) {
14
- logStep(STEP_LABELS.createPr, ctx, true);
15
- return true;
16
+ const existingUrl = await getExistingPrUrl(ctx.branch);
17
+ if (existingUrl) {
18
+ log(`PR already exists: ${existingUrl}`);
19
+ return existingUrl;
16
20
  }
17
21
 
18
- logStep(STEP_LABELS.createPr, ctx);
19
-
20
22
  await git(["push", "-u", cfg.remote, ctx.branch]);
21
23
 
22
24
  const reviewPath = join(ctx.issueDir, ARTIFACTS.review);
@@ -33,9 +35,9 @@ export async function stepCreatePR(ctx: IssueContext): Promise<boolean> {
33
35
  "",
34
36
  "## Pipeline Artifacts",
35
37
  "",
36
- `- Research: \`.auto-claude/issue-${ctx.number}/research.md\``,
37
38
  `- Plan: \`.auto-claude/issue-${ctx.number}/plan.md\``,
38
- `- Implementation Plan: \`.auto-claude/issue-${ctx.number}/plan-implementation.md\``,
39
+ `- Implementation: \`.auto-claude/issue-${ctx.number}/completed-summary.md\``,
40
+ `- Simplify: \`.auto-claude/issue-${ctx.number}/simplify-summary.md\``,
39
41
  `- Review: \`.auto-claude/issue-${ctx.number}/review.md\``,
40
42
  "",
41
43
  "## Review Summary",
@@ -57,23 +59,84 @@ export async function stepCreatePR(ctx: IssueContext): Promise<boolean> {
57
59
  ctx.title,
58
60
  "--body",
59
61
  body,
60
- "--label",
61
- cfg.triggerLabel,
62
62
  ]);
63
63
 
64
- if (prUrl) {
65
- const url = prUrl.trim();
66
- writeFile(join(ctx.issueDir, ARTIFACTS.prUrl), url);
67
- log(`PR created: ${url}`);
68
- } else {
69
- consola.error("Failed to create PR");
70
- return false;
64
+ if (!prUrl) {
65
+ throw new Error("Failed to create PR — gh returned empty output");
66
+ }
67
+
68
+ writeFile(join(ctx.issueDir, ARTIFACTS.prUrl), prUrl);
69
+ log(`PR created: ${prUrl}`);
70
+
71
+ try {
72
+ await attachArtifacts(ctx, prUrl);
73
+ } catch (e) {
74
+ consola.warn(`Artifact upload failed (non-blocking): ${e}`);
75
+ }
76
+
77
+ return prUrl;
78
+ }
79
+
80
+ async function attachArtifacts(ctx: IssueContext, prUrl: string): Promise<void> {
81
+ const archivePath = join(ctx.issueDir, "artifacts.tar.gz");
82
+ const tag = `ac-issue-${ctx.number}`;
83
+ const cfg = getConfig();
84
+
85
+ log("Archiving pipeline artifacts…");
86
+ await execSafe("tar", [
87
+ "czf",
88
+ archivePath,
89
+ "-C",
90
+ ctx.issueDir,
91
+ "--exclude=*.prompt.md",
92
+ "--exclude=artifacts.tar.gz",
93
+ ".",
94
+ ]);
95
+
96
+ log("Uploading artifacts to GitHub release…");
97
+ await ghRaw(["release", "delete", tag, "--yes", "--repo", cfg.repo]);
98
+
99
+ await ghRaw([
100
+ "release",
101
+ "create",
102
+ tag,
103
+ "--prerelease",
104
+ "--title",
105
+ `Artifacts: #${ctx.number}`,
106
+ "--repo",
107
+ cfg.repo,
108
+ archivePath,
109
+ ]);
110
+
111
+ const assetUrl = await ghRaw([
112
+ "release",
113
+ "view",
114
+ tag,
115
+ "--json",
116
+ "assets",
117
+ "--jq",
118
+ ".assets[0].url",
119
+ "--repo",
120
+ cfg.repo,
121
+ ]);
122
+
123
+ if (!assetUrl) {
124
+ consola.warn("Could not get artifact download URL — skipping PR comment");
125
+ return;
71
126
  }
72
127
 
73
- return true;
128
+ const comment = [
129
+ "## Pipeline Artifacts",
130
+ "",
131
+ `[Download artifacts (tar.gz)](${assetUrl})`,
132
+ "",
133
+ "Contains: plan.md, completed-summary.md, simplify-summary.md, review.md",
134
+ ].join("\n");
135
+
136
+ await ghRaw(["pr", "comment", prUrl, "--body", comment]);
74
137
  }
75
138
 
76
- async function hasOpenPR(branch: string): Promise<boolean> {
139
+ async function getExistingPrUrl(branch: string): Promise<string | null> {
77
140
  const out = await ghRaw([
78
141
  "pr",
79
142
  "list",
@@ -84,12 +147,13 @@ async function hasOpenPR(branch: string): Promise<boolean> {
84
147
  "--state",
85
148
  "open",
86
149
  "--json",
87
- "number",
150
+ "url",
88
151
  ]);
89
152
  try {
90
- const prs = JSON.parse(out);
91
- return Array.isArray(prs) && prs.length > 0;
153
+ const prs = JSON.parse(out) as Array<{ url: string }>;
154
+ return prs.length > 0 ? prs[0].url : null;
92
155
  } catch {
93
- return false;
156
+ consola.debug(`Failed to parse PR list JSON for branch "${branch}"`);
157
+ return null;
94
158
  }
95
159
  }
@@ -1,5 +1,7 @@
1
+ import consola from "consola";
1
2
  import { getConfig } from "../config.js";
2
- import { buildIssueContext, gh, log } from "../utils.js";
3
+ import { gh } from "../../../utils/git/gh-cli-wrapper.js";
4
+ import { buildIssueContext, log } from "../utils.js";
3
5
  import type { IssueContext } from "../utils.js";
4
6
 
5
7
  interface GhIssue {
@@ -59,6 +61,7 @@ export async function fetchIssue(issueNumber: number): Promise<IssueContext | un
59
61
  ]);
60
62
  return buildIssueContext(issue, cfg.repo, cfg.scopePath);
61
63
  } catch {
64
+ consola.debug(`Could not fetch issue #${issueNumber} from ${cfg.repo}`);
62
65
  return undefined;
63
66
  }
64
67
  }
@@ -4,16 +4,11 @@ import consola from "consola";
4
4
 
5
5
  import { getConfig } from "../config.js";
6
6
  import { ARTIFACTS, STEP_LABELS, TEMPLATES } from "../prompt-templates/index.js";
7
- import {
8
- buildTokens,
9
- commitArtifacts,
10
- fileExists,
11
- git,
12
- log,
13
- logStep,
14
- resolveTemplate,
15
- runClaude,
16
- } from "../utils.js";
7
+ import { fileExists, readFile } from "../../../utils/fs.js";
8
+ import { git } from "../../../utils/git/exec.js";
9
+ import { runClaude } from "../claude-cli.js";
10
+ import { resolveTemplate } from "../templates.js";
11
+ import { buildTokens, log, logStep } from "../utils.js";
17
12
  import type { IssueContext } from "../utils.js";
18
13
 
19
14
  export async function stepImplement(ctx: IssueContext): Promise<boolean> {
@@ -29,15 +24,17 @@ export async function stepImplement(ctx: IssueContext): Promise<boolean> {
29
24
 
30
25
  await git(["checkout", ctx.branch]);
31
26
 
27
+ const reviewPath = join(ctx.issueDir, ARTIFACTS.review);
28
+ const reviewFeedback = fileExists(reviewPath) ? readFile(reviewPath) : "";
29
+
32
30
  for (let i = 1; i <= maxIterations; i++) {
33
31
  log(`Implementation iteration ${i}/${maxIterations}`);
34
32
 
35
- const tokens = buildTokens(ctx);
33
+ const tokens = buildTokens(ctx, { REVIEW_FEEDBACK: reviewFeedback });
36
34
  const promptFile = resolveTemplate(TEMPLATES.implement, tokens, ctx.issueDir);
37
35
 
38
36
  const result = await runClaude({
39
37
  promptFile,
40
- permissionMode: "acceptEdits",
41
38
  maxTurns: getConfig().maxTurns,
42
39
  });
43
40
 
@@ -48,10 +45,6 @@ export async function stepImplement(ctx: IssueContext): Promise<boolean> {
48
45
 
49
46
  if (fileExists(completedPath)) {
50
47
  log(`Implementation complete after ${i} iteration(s)`);
51
- await commitArtifacts(
52
- ctx,
53
- `chore(auto-claude): implementation complete for ${ctx.repo}#${ctx.number}`,
54
- );
55
48
  return true;
56
49
  }
57
50
 
@@ -0,0 +1,34 @@
1
+ import { join } from "node:path";
2
+
3
+ import { ARTIFACTS, STEP_LABELS, TEMPLATES } from "../prompt-templates/index.js";
4
+ import { ensureBranch, runStepWithArtifact } from "../utils.js";
5
+ import type { IssueContext } from "../utils.js";
6
+
7
+ export async function stepPlan(ctx: IssueContext): Promise<boolean> {
8
+ await ensureBranch(ctx.branch);
9
+
10
+ return runStepWithArtifact({
11
+ stepName: STEP_LABELS.plan,
12
+ ctx,
13
+ artifactPath: join(ctx.issueDir, ARTIFACTS.plan),
14
+ templateName: TEMPLATES.plan,
15
+ });
16
+ }
17
+
18
+ export async function stepSimplify(ctx: IssueContext): Promise<boolean> {
19
+ return runStepWithArtifact({
20
+ stepName: STEP_LABELS.simplify,
21
+ ctx,
22
+ artifactPath: join(ctx.issueDir, ARTIFACTS.simplifySummary),
23
+ templateName: TEMPLATES.simplify,
24
+ });
25
+ }
26
+
27
+ export async function stepReview(ctx: IssueContext): Promise<boolean> {
28
+ return runStepWithArtifact({
29
+ stepName: STEP_LABELS.review,
30
+ ctx,
31
+ artifactPath: join(ctx.issueDir, ARTIFACTS.review),
32
+ templateName: TEMPLATES.review,
33
+ });
34
+ }