@towles/tool 0.0.54 → 0.0.55

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 (43) hide show
  1. package/README.md +52 -12
  2. package/package.json +4 -4
  3. package/src/commands/auto-claude.ts +219 -0
  4. package/src/commands/doctor.ts +1 -34
  5. package/src/config/settings.ts +0 -10
  6. package/src/lib/auto-claude/config.test.ts +53 -0
  7. package/src/lib/auto-claude/config.ts +68 -0
  8. package/src/lib/auto-claude/index.ts +14 -0
  9. package/src/lib/auto-claude/pipeline.test.ts +14 -0
  10. package/src/lib/auto-claude/pipeline.ts +64 -0
  11. package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
  12. package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
  13. package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
  14. package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
  15. package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
  16. package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
  17. package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
  18. package/src/lib/auto-claude/prompt-templates/index.test.ts +145 -0
  19. package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
  20. package/src/lib/auto-claude/steps/create-pr.ts +93 -0
  21. package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
  22. package/src/lib/auto-claude/steps/implement.ts +63 -0
  23. package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
  24. package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
  25. package/src/lib/auto-claude/steps/plan.ts +14 -0
  26. package/src/lib/auto-claude/steps/refresh.ts +114 -0
  27. package/src/lib/auto-claude/steps/remove-label.ts +22 -0
  28. package/src/lib/auto-claude/steps/research.ts +21 -0
  29. package/src/lib/auto-claude/steps/review.ts +14 -0
  30. package/src/lib/auto-claude/utils.test.ts +136 -0
  31. package/src/lib/auto-claude/utils.ts +334 -0
  32. package/src/commands/ralph/plan/add.ts +0 -69
  33. package/src/commands/ralph/plan/done.ts +0 -82
  34. package/src/commands/ralph/plan/list.test.ts +0 -48
  35. package/src/commands/ralph/plan/list.ts +0 -100
  36. package/src/commands/ralph/plan/remove.ts +0 -71
  37. package/src/commands/ralph/run.test.ts +0 -607
  38. package/src/commands/ralph/run.ts +0 -362
  39. package/src/commands/ralph/show.ts +0 -88
  40. package/src/lib/ralph/execution.ts +0 -292
  41. package/src/lib/ralph/formatter.ts +0 -240
  42. package/src/lib/ralph/index.ts +0 -4
  43. package/src/lib/ralph/state.ts +0 -201
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Towles Tool
2
2
 
3
- Personal CLI toolkit with autonomous task runner and developer utilities.
3
+ Personal CLI toolkit with auto-claude pipeline and developer utilities.
4
4
 
5
5
  ## Installation
6
6
 
@@ -23,17 +23,58 @@ pnpm start
23
23
 
24
24
  ## CLI Commands
25
25
 
26
- ### Ralph (autonomous runner)
26
+ ### Auto-Claude (issue-to-PR pipeline)
27
27
 
28
- | Command | Description |
29
- | ----------------------------- | ---------------------- |
30
- | `tt ralph plan add "path.md"` | Add plan from file |
31
- | `tt ralph plan list` | View plans |
32
- | `tt ralph plan done <id>` | Mark complete |
33
- | `tt ralph plan remove <id>` | Remove plan |
34
- | `tt ralph show` | Show plan with mermaid |
35
- | `tt ralph run` | Run (auto-commits) |
36
- | `tt ralph run --planId 5` | Run specific plan |
28
+ A fully autonomous issue-to-PR pipeline — what a more productizable version of the ralph planning/execution loop looks like. Cloud-based agents (GitHub Copilot, Anthropic agents) can create PRs but can't run your full stack — Docker, Postgres, Playwright, Chrome DevTools MCP, etc. Running locally gives Claude access to the complete environment to run, test, and iterate.
29
+
30
+ Label issues with `auto-claude`, start the loop, and walk away. Queue up multiple issues during the day and let them run overnight, or tag an issue from your phone or the Claude mobile app and have it waiting as a PR by morning.
31
+
32
+ Inspired by [Boris Tane's workflow](https://boristane.com/blog/how-i-use-claude-code/) and [Francisco Hermida's auto-pr](https://github.com/franciscohermida/auto-pr).
33
+
34
+ ```bash
35
+ tt auto-claude --issue 42 # Process specific issue
36
+ tt auto-claude --issue 42 --until plan # Stop after planning step
37
+ tt auto-claude --refresh --issue 42 # Rebase stale PR branch
38
+ tt auto-claude --reset 42 # Reset state for an issue
39
+ tt auto-claude --loop # Start polling loop
40
+ ```
41
+
42
+ **Slot-based workflow:** Run auto-claude in a dedicated clone of the repo — not the one you're actively editing. Keep 3-5 clones (e.g. `slot-1`, `slot-2`, `slot-primary`) so each issue gets its own isolated environment. `slot-primary` is typically the one open in VS Code for manual work; the numbered slots run auto-claude independently. Each slot has its own `.env` so services and ports don't collide between slots. Claude Code's worktree feature may replace this approach in the future, but full repo clones have been more reliable in practice.
43
+
44
+ #### Pipeline Steps
45
+
46
+ | Step | What it does | Artifact produced |
47
+ | ----------------------- | ------------------------------------------------------------------------------------------------ | ------------------------ |
48
+ | **research** | Deep-reads the codebase for context relevant to the issue | `research.md` |
49
+ | **plan** | High-level technical plan with architectural decisions and alternatives | `plan.md` |
50
+ | **plan-annotations** | _(optional)_ Addresses reviewer feedback if `plan-annotations.md` exists | updates `plan.md` |
51
+ | **plan-implementation** | Breaks plan into an ordered checkbox task list | `plan-implementation.md` |
52
+ | **implement** | Executes tasks one-by-one, checking boxes and committing as it goes (loops up to 100 iterations) | `completed-summary.md` |
53
+ | **review** | Self-reviews the diff, fixes issues, rates confidence | `review.md` |
54
+ | **create-pr** | Pushes branch and opens a PR with artifact links and review summary | GitHub PR |
55
+ | **remove-label** | Removes the `auto-claude` label so the issue isn't picked up again | — |
56
+
57
+ All artifacts are written to `.auto-claude/issue-{N}/`. Use `--until <step>` to pause after any step (e.g. `--until plan` to review before implementation). The plan-annotations step lets you drop feedback into `plan-annotations.md` and re-run — the pipeline will revise the plan before continuing.
58
+
59
+ #### How it works under the hood
60
+
61
+ 1. **Auto-detects** repo (`gh repo view`) and main branch (`git symbolic-ref`) from cwd — no config file needed
62
+ 2. **Creates a branch** `auto-claude/issue-{N}` from main
63
+ 3. **Runs Claude Code CLI** (`claude -p`) in print mode with JSON output for each step, using prompt templates with token replacement (`{{ISSUE_DIR}}`, `{{SCOPE_PATH}}`, `{{MAIN_BRANCH}}`)
64
+ 4. **Artifacts drive state** — each step checks if its output file exists before running (idempotent). Resume after a crash by re-running the same command
65
+ 5. **Returns to main** after each issue completes or fails
66
+
67
+ #### Code layout
68
+
69
+ ```
70
+ src/commands/auto-claude.ts # oclif command (alias: ac)
71
+ src/lib/auto-claude/
72
+ config.ts # Zod schema, initConfig(), getConfig()
73
+ utils.ts # exec helpers, runClaude, templates, IssueContext
74
+ pipeline.ts # step orchestration
75
+ steps/ # one file per pipeline step
76
+ prompt-templates/ # 7 .md prompt files with {{TOKEN}} placeholders
77
+ ```
37
78
 
38
79
  ### Observability
39
80
 
@@ -79,7 +120,6 @@ pnpm start
79
120
  ## Guidelines
80
121
 
81
122
  - [Architecture](docs/architecture.md) - CLI structure, plugin system, tech stack
82
- - [Claude Code Planning and Running Usage](docs/ralph-tools-for-claude-code.md) - "Claude Code" autonomous runner
83
123
  - [CICD via GitHub Actions](docs/github-actions.md) - Automated release workflow
84
124
  - [Testing](docs/testings.md) - Info about Tests
85
125
 
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.54",
4
- "description": "CLI tool with autonomous task runner (ralph), observability, and quality-of-life commands for daily development.",
3
+ "version": "0.0.55",
4
+ "description": "CLI tool with auto-claude pipeline, developer tools, and journaling via markdown.",
5
5
  "keywords": [
6
+ "auto-claude",
6
7
  "autonomic",
7
8
  "claude",
8
9
  "cli",
9
10
  "git",
10
11
  "journal",
11
- "oclif",
12
- "ralph"
12
+ "oclif"
13
13
  ],
14
14
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
15
15
  "bugs": {
@@ -0,0 +1,219 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { Flags } from "@oclif/core";
5
+ import consola from "consola";
6
+
7
+ import { BaseCommand } from "./base.js";
8
+ import {
9
+ STEP_NAMES,
10
+ buildIssueContext,
11
+ fetchIssue,
12
+ fetchIssues,
13
+ getConfig,
14
+ git,
15
+ initConfig,
16
+ log,
17
+ runPipeline,
18
+ sleep,
19
+ stepRefresh,
20
+ } from "../lib/auto-claude/index.js";
21
+ import type { IssueContext, StepName } from "../lib/auto-claude/index.js";
22
+
23
+ export default class AutoClaude extends BaseCommand {
24
+ static override aliases = ["ac"];
25
+
26
+ static override description = "Automated issue-to-PR pipeline using Claude Code";
27
+
28
+ static override examples = [
29
+ {
30
+ description: "Process a specific issue",
31
+ command: "<%= config.bin %> auto-claude --issue 42",
32
+ },
33
+ {
34
+ description: "Run until plan step",
35
+ command: "<%= config.bin %> auto-claude --issue 42 --until plan",
36
+ },
37
+ {
38
+ description: "Reset local state for an issue",
39
+ command: "<%= config.bin %> auto-claude --reset 42",
40
+ },
41
+ {
42
+ description: "Refresh a stale PR branch",
43
+ command: "<%= config.bin %> auto-claude --refresh --issue 42",
44
+ },
45
+ {
46
+ description: "Loop mode: poll for labeled issues",
47
+ command: "<%= config.bin %> auto-claude --loop",
48
+ },
49
+ {
50
+ description: "Loop with custom interval",
51
+ command: "<%= config.bin %> auto-claude --loop --interval 45",
52
+ },
53
+ ];
54
+
55
+ static override flags = {
56
+ ...BaseCommand.baseFlags,
57
+ issue: Flags.integer({
58
+ char: "i",
59
+ description: "Process a specific issue number",
60
+ }),
61
+ until: Flags.string({
62
+ char: "u",
63
+ description: `Stop after this step (${STEP_NAMES.join(", ")})`,
64
+ options: [...STEP_NAMES],
65
+ }),
66
+ reset: Flags.integer({
67
+ description: "Delete local state for an issue (force restart)",
68
+ }),
69
+ refresh: Flags.boolean({
70
+ description: "Rebase a stale PR branch onto current main",
71
+ default: false,
72
+ }),
73
+ loop: Flags.boolean({
74
+ description: "Poll for labeled issues continuously",
75
+ default: false,
76
+ }),
77
+ interval: Flags.integer({
78
+ description: "Poll interval in minutes (default: 30)",
79
+ }),
80
+ limit: Flags.integer({
81
+ description: "Max issues per iteration (default: 1)",
82
+ default: 1,
83
+ }),
84
+ label: Flags.string({
85
+ description: "Trigger label (default: auto-claude)",
86
+ }),
87
+ "main-branch": Flags.string({
88
+ description: "Override main branch detection",
89
+ }),
90
+ "scope-path": Flags.string({
91
+ description: "Path within repo to scope work (default: .)",
92
+ }),
93
+ };
94
+
95
+ async run(): Promise<void> {
96
+ const { flags } = await this.parse(AutoClaude);
97
+
98
+ const cfg = await initConfig({
99
+ triggerLabel: flags.label,
100
+ mainBranch: flags["main-branch"],
101
+ scopePath: flags["scope-path"],
102
+ loopRetryEnabled: flags.loop || undefined,
103
+ });
104
+
105
+ if (flags.reset) {
106
+ const issueDir = join(process.cwd(), `.auto-claude/issue-${flags.reset}`);
107
+ log(`Resetting state for issue-${flags.reset}...`);
108
+ rmSync(issueDir, { recursive: true, force: true });
109
+ log(`Cleaned ${issueDir}`);
110
+ return;
111
+ }
112
+
113
+ if (flags.refresh) {
114
+ if (!flags.issue) {
115
+ this.error("--refresh requires --issue <number>");
116
+ }
117
+ const ctx = buildIssueContext(
118
+ { number: flags.issue, title: `Issue #${flags.issue}`, body: "" },
119
+ cfg.repo,
120
+ cfg.scopePath,
121
+ );
122
+ await stepRefresh(ctx);
123
+ return;
124
+ }
125
+
126
+ const untilStep = flags.until as StepName | undefined;
127
+ const loopMode = flags.loop;
128
+ const intervalMs = (flags.interval ?? cfg.loopIntervalMinutes) * 60_000;
129
+ const limit = flags.limit ?? 1;
130
+
131
+ if (loopMode) {
132
+ registerShutdownHandlers();
133
+ log(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
134
+ }
135
+
136
+ let iteration = 0;
137
+
138
+ do {
139
+ const iterationStart = Date.now();
140
+ iteration++;
141
+
142
+ if (loopMode) {
143
+ consola.box({ title: `Iteration #${iteration}`, message: new Date().toISOString() });
144
+ }
145
+
146
+ try {
147
+ await syncWithRemote();
148
+ } catch (e) {
149
+ log(`Sync failed: ${e instanceof Error ? e.message : String(e)}`);
150
+ if (loopMode) {
151
+ log(`Will retry in ${Math.round(intervalMs / 1000)}s...`);
152
+ await sleep(intervalMs);
153
+ continue;
154
+ }
155
+ throw e;
156
+ }
157
+
158
+ let contexts: IssueContext[];
159
+ if (flags.issue) {
160
+ const ctx = await fetchIssue(flags.issue);
161
+ contexts = ctx ? [ctx] : [];
162
+ } else {
163
+ contexts = await fetchIssues(limit);
164
+ }
165
+
166
+ if (contexts.length === 0) {
167
+ log("No issues to process.");
168
+ } else {
169
+ log(`Processing ${contexts.length} issue(s)...\n`);
170
+
171
+ for (const ctx of contexts) {
172
+ try {
173
+ await runPipeline(ctx, untilStep);
174
+ } catch (e) {
175
+ consola.error(`Pipeline error for ${ctx.repo}#${ctx.number}:`, e);
176
+ }
177
+ }
178
+ }
179
+
180
+ if (loopMode) {
181
+ const waitMs = Math.max(0, intervalMs - (Date.now() - iterationStart));
182
+ if (waitMs > 0) {
183
+ log(`Waiting ${Math.round(waitMs / 1000)}s until next iteration...`);
184
+ await sleep(waitMs);
185
+ }
186
+ }
187
+ } while (loopMode);
188
+
189
+ log("Done.");
190
+ }
191
+ }
192
+
193
+ async function syncWithRemote(): Promise<void> {
194
+ const cfg = getConfig();
195
+ log("Syncing with remote...");
196
+ await git(["fetch", "--all", "--prune"]);
197
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
198
+ if (branch !== cfg.mainBranch) {
199
+ log(`Warning: on branch "${branch}", switching to ${cfg.mainBranch}...`);
200
+ await git(["checkout", cfg.mainBranch]).catch(() => {});
201
+ }
202
+ const status = await git(["status", "--porcelain"]);
203
+ if (status.length > 0) {
204
+ throw new Error("Working tree has uncommitted changes. Commit or stash them first.");
205
+ }
206
+ await git(["pull", cfg.remote, cfg.mainBranch]);
207
+ }
208
+
209
+ function registerShutdownHandlers(): void {
210
+ for (const signal of ["SIGINT", "SIGTERM"] as const) {
211
+ process.on(signal, () => {
212
+ log(`Received ${signal}, shutting down...`);
213
+ setTimeout(() => process.exit(1), 5_000).unref();
214
+ git(["checkout", getConfig().mainBranch])
215
+ .catch(() => {})
216
+ .then(() => process.exit(0));
217
+ });
218
+ }
219
+ }
@@ -1,5 +1,3 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
1
  import { x } from "tinyexec";
4
2
  import pc from "picocolors";
5
3
  import { BaseCommand } from "./base.js";
@@ -63,19 +61,8 @@ export default class Doctor extends BaseCommand {
63
61
  }
64
62
  }
65
63
 
66
- // Check ralph files in .gitignore
67
- this.log("");
68
- const gitignoreCheck = this.checkRalphGitignore();
69
- const gitignoreIcon = gitignoreCheck.ok ? pc.green("✓") : pc.yellow("⚠");
70
- this.log(
71
- `${gitignoreIcon} .gitignore: ${gitignoreCheck.ok ? "ralph-* excluded" : "ralph-* NOT excluded"}`,
72
- );
73
- if (!gitignoreCheck.ok) {
74
- this.log(` ${pc.dim('Add "ralph-*" to .gitignore to exclude local ralph state files')}`);
75
- }
76
-
77
64
  // Summary
78
- const allOk = checks.every((c) => c.ok) && ghAuth.ok && gitignoreCheck.ok;
65
+ const allOk = checks.every((c) => c.ok) && ghAuth.ok;
79
66
  this.log("");
80
67
  if (allOk) {
81
68
  this.log(pc.green("All checks passed!"));
@@ -113,24 +100,4 @@ export default class Doctor extends BaseCommand {
113
100
  return { ok: false };
114
101
  }
115
102
  }
116
-
117
- private checkRalphGitignore(): { ok: boolean } {
118
- const gitignorePath = path.join(process.cwd(), ".gitignore");
119
- try {
120
- if (!fs.existsSync(gitignorePath)) {
121
- return { ok: false };
122
- }
123
- const content = fs.readFileSync(gitignorePath, "utf-8");
124
- // Check for ralph-* pattern or specific ralph files
125
- const hasRalphPattern = content.split("\n").some((line) => {
126
- const trimmed = line.trim();
127
- return (
128
- trimmed === "ralph-*" || trimmed === "ralph-*.json" || trimmed === "ralph-state.json"
129
- );
130
- });
131
- return { ok: hasRalphPattern };
132
- } catch {
133
- return { ok: false };
134
- }
135
- }
136
103
  }
@@ -13,13 +13,6 @@ export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", TOOL_NAME);
13
13
  /** User settings file path */
14
14
  export const USER_SETTINGS_PATH = path.join(DEFAULT_CONFIG_DIR, `${TOOL_NAME}.settings.json`);
15
15
 
16
- export const RalphSettingsSchema = z.object({
17
- // Base directory for ralph files (relative to cwd or absolute)
18
- stateDir: z.string().default("./.claude/.ralph"),
19
- });
20
-
21
- export type RalphSettings = z.infer<typeof RalphSettingsSchema>;
22
-
23
16
  export const JournalSettingsSchema = z.object({
24
17
  // Base folder where all journal files are stored
25
18
  baseFolder: z.string().default(path.join(homedir())),
@@ -48,9 +41,6 @@ export const UserSettingsSchema = z.object({
48
41
  journalSettings: JournalSettingsSchema.optional().transform(
49
42
  (v) => v ?? JournalSettingsSchema.parse({}),
50
43
  ),
51
- ralphSettings: RalphSettingsSchema.optional().transform(
52
- (v) => v ?? RalphSettingsSchema.parse({}),
53
- ),
54
44
  });
55
45
 
56
46
  type UserSettings = z.infer<typeof UserSettingsSchema>;
@@ -0,0 +1,53 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+
3
+ import { AutoClaudeConfigSchema, getConfig, initConfig } from "./config";
4
+
5
+ describe("AutoClaudeConfigSchema", () => {
6
+ it("should apply all defaults when only repo is provided", () => {
7
+ const cfg = AutoClaudeConfigSchema.parse({ repo: "owner/repo" });
8
+
9
+ expect(cfg.triggerLabel).toBe("auto-claude");
10
+ expect(cfg.scopePath).toBe(".");
11
+ expect(cfg.mainBranch).toBe("main");
12
+ expect(cfg.remote).toBe("origin");
13
+ expect(cfg.maxImplementIterations).toBe(5);
14
+ expect(cfg.maxTurns).toBeUndefined();
15
+ expect(cfg.loopIntervalMinutes).toBe(30);
16
+ expect(cfg.loopRetryEnabled).toBe(false);
17
+ expect(cfg.maxRetries).toBe(5);
18
+ expect(cfg.retryDelayMs).toBe(30_000);
19
+ expect(cfg.maxRetryDelayMs).toBe(300_000);
20
+ });
21
+
22
+ it("should allow overriding defaults", () => {
23
+ const cfg = AutoClaudeConfigSchema.parse({
24
+ repo: "owner/repo",
25
+ triggerLabel: "bot",
26
+ maxImplementIterations: 10,
27
+ maxRetries: 3,
28
+ loopRetryEnabled: true,
29
+ });
30
+
31
+ expect(cfg.triggerLabel).toBe("bot");
32
+ expect(cfg.maxImplementIterations).toBe(10);
33
+ expect(cfg.maxRetries).toBe(3);
34
+ expect(cfg.loopRetryEnabled).toBe(true);
35
+ });
36
+
37
+ it("should require repo field", () => {
38
+ expect(() => AutoClaudeConfigSchema.parse({})).toThrow();
39
+ });
40
+ });
41
+
42
+ describe("getConfig", () => {
43
+ afterEach(() => {
44
+ // Reset internal config state by re-initializing
45
+ });
46
+
47
+ it("should return config after initConfig with explicit repo and mainBranch", async () => {
48
+ await initConfig({ repo: "test/repo", mainBranch: "main" });
49
+ const cfg = getConfig();
50
+ expect(cfg.repo).toBe("test/repo");
51
+ expect(cfg.mainBranch).toBe("main");
52
+ });
53
+ });
@@ -0,0 +1,68 @@
1
+ import consola from "consola";
2
+ import { x } from "tinyexec";
3
+ import { z } from "zod/v4";
4
+
5
+ export const AutoClaudeConfigSchema = z.object({
6
+ triggerLabel: z.string().default("auto-claude"),
7
+ repo: z.string(),
8
+ scopePath: z.string().default("."),
9
+ mainBranch: z.string().default("main"),
10
+ remote: z.string().default("origin"),
11
+ maxImplementIterations: z.number().default(5),
12
+ maxTurns: z.number().optional(),
13
+ loopIntervalMinutes: z.number().default(30),
14
+ loopRetryEnabled: z.boolean().default(false),
15
+ maxRetries: z.number().default(5),
16
+ retryDelayMs: z.number().default(30_000),
17
+ maxRetryDelayMs: z.number().default(300_000),
18
+ });
19
+
20
+ export type AutoClaudeConfig = z.infer<typeof AutoClaudeConfigSchema>;
21
+
22
+ let _config: AutoClaudeConfig | undefined;
23
+
24
+ export async function initConfig(
25
+ overrides: Partial<AutoClaudeConfig> = {},
26
+ ): Promise<AutoClaudeConfig> {
27
+ // Auto-detect repo
28
+ let repo = overrides.repo;
29
+ if (!repo) {
30
+ const result = await x(
31
+ "gh",
32
+ ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
33
+ {
34
+ nodeOptions: { cwd: process.cwd() },
35
+ throwOnError: true,
36
+ },
37
+ );
38
+ repo = result.stdout.trim();
39
+ }
40
+ consola.info(`Detected repo: ${repo}`);
41
+
42
+ // Auto-detect main branch
43
+ let mainBranch = overrides.mainBranch;
44
+ if (!mainBranch) {
45
+ try {
46
+ const result = await x("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
47
+ nodeOptions: { cwd: process.cwd() },
48
+ throwOnError: true,
49
+ });
50
+ mainBranch = result.stdout.trim().replace("refs/remotes/origin/", "");
51
+ } catch {
52
+ mainBranch = "main";
53
+ }
54
+ }
55
+
56
+ _config = AutoClaudeConfigSchema.parse({
57
+ ...overrides,
58
+ repo,
59
+ mainBranch,
60
+ });
61
+
62
+ return _config;
63
+ }
64
+
65
+ export function getConfig(): AutoClaudeConfig {
66
+ if (!_config) throw new Error("Config not initialized. Call initConfig() first.");
67
+ return _config;
68
+ }
@@ -0,0 +1,14 @@
1
+ export { type AutoClaudeConfig, AutoClaudeConfigSchema, getConfig, initConfig } from "./config.js";
2
+ export { STEP_NAMES, runPipeline } from "./pipeline.js";
3
+ export type { StepName } from "./prompt-templates/index.js";
4
+ export { fetchIssue, fetchIssues } from "./steps/fetch-issues.js";
5
+ export { stepRefresh } from "./steps/refresh.js";
6
+ export {
7
+ type IssueContext,
8
+ buildContextFromArtifacts,
9
+ buildIssueContext,
10
+ ensureBranch,
11
+ git,
12
+ log,
13
+ sleep,
14
+ } from "./utils.js";
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { STEP_NAMES } from "./pipeline";
4
+ import { PIPELINE_STEPS } from "./prompt-templates/index";
5
+
6
+ describe("STEP_NAMES", () => {
7
+ it("should be derived from PIPELINE_STEPS", () => {
8
+ expect(STEP_NAMES).toEqual(PIPELINE_STEPS.map((s) => s.name));
9
+ });
10
+
11
+ it("should have 8 steps", () => {
12
+ expect(STEP_NAMES).toHaveLength(8);
13
+ });
14
+ });
@@ -0,0 +1,64 @@
1
+ import { join } from "node:path";
2
+
3
+ import { getConfig } from "./config.js";
4
+ import { ARTIFACTS, PIPELINE_STEPS } from "./prompt-templates/index.js";
5
+ import type { StepName } from "./prompt-templates/index.js";
6
+ import { stepCreatePR } from "./steps/create-pr.js";
7
+ import { stepImplement } from "./steps/implement.js";
8
+ import { stepPlanAnnotations } from "./steps/plan-annotations.js";
9
+ import { stepPlanImplementation } from "./steps/plan-implementation.js";
10
+ import { stepPlan } from "./steps/plan.js";
11
+ import { stepRemoveLabel } from "./steps/remove-label.js";
12
+ import { stepResearch } from "./steps/research.js";
13
+ import { stepReview } from "./steps/review.js";
14
+ import { ensureDir, fileExists, git, log, writeFile } from "./utils.js";
15
+ import type { IssueContext } from "./utils.js";
16
+
17
+ const STEP_RUNNERS: Record<StepName, (ctx: IssueContext) => Promise<boolean>> = {
18
+ research: stepResearch,
19
+ plan: stepPlan,
20
+ "plan-annotations": stepPlanAnnotations,
21
+ "plan-implementation": stepPlanImplementation,
22
+ implement: stepImplement,
23
+ review: stepReview,
24
+ "create-pr": stepCreatePR,
25
+ "remove-label": stepRemoveLabel,
26
+ };
27
+
28
+ export { type StepName, STEP_NAMES } from "./prompt-templates/index.js";
29
+
30
+ export async function runPipeline(ctx: IssueContext, untilStep?: StepName): Promise<void> {
31
+ log(`Pipeline starting for ${ctx.repo}#${ctx.number}: ${ctx.title}`);
32
+
33
+ ensureDir(ctx.issueDir);
34
+ const ramblingsPath = join(ctx.issueDir, ARTIFACTS.initialRamblings);
35
+ if (!fileExists(ramblingsPath)) {
36
+ const content = `# ${ctx.title}\n\n> ${ctx.repo}#${ctx.number}\n\n${ctx.body ?? ""}`;
37
+ writeFile(ramblingsPath, content);
38
+ log("Saved initial-ramblings.md");
39
+ }
40
+
41
+ for (const step of PIPELINE_STEPS) {
42
+ const runner = STEP_RUNNERS[step.name];
43
+ const success = await runner(ctx);
44
+
45
+ if (!success) {
46
+ log(`Pipeline stopped at "${step.name}" for ${ctx.repo}#${ctx.number}`);
47
+ await checkoutMain();
48
+ return;
49
+ }
50
+
51
+ if (untilStep && step.name === untilStep) {
52
+ log(`Pipeline paused after "${step.name}" (--until ${untilStep})`);
53
+ await checkoutMain();
54
+ return;
55
+ }
56
+ }
57
+
58
+ log(`Pipeline complete for ${ctx.repo}#${ctx.number}`);
59
+ await checkoutMain();
60
+ }
61
+
62
+ async function checkoutMain(): Promise<void> {
63
+ await git(["checkout", getConfig().mainBranch]).catch(() => {});
64
+ }
@@ -0,0 +1,28 @@
1
+ You are a senior developer researching a codebase to prepare for implementing a GitHub issue.
2
+
3
+ ## Your task
4
+
5
+ Read the issue description in @{{ISSUE_DIR}}/initial-ramblings.md and then **research the codebase in depth** to understand what would be involved in implementing it.
6
+
7
+ **CRITICAL RULES:**
8
+
9
+ - Do **NOT** implement the issue. Do not create, modify, or delete any project source files.
10
+ - Your **ONLY** deliverable is writing the file @{{ISSUE_DIR}}/research.md.
11
+ - If the issue seems trivial, research it anyway — document the relevant files, patterns, and context.
12
+
13
+ ## Where to look
14
+
15
+ The code for this project lives primarily at `{{SCOPE_PATH}}/`. Start your investigation there but explore any related files across the monorepo.
16
+
17
+ Read every relevant file in full. Understand how the system works deeply — its architecture, data flow, and all its specificities. Do not skim. Do not stop researching until you have a thorough understanding of every part of the codebase that this issue touches.
18
+
19
+ ## What to write in @{{ISSUE_DIR}}/research.md
20
+
21
+ 1. **Relevant files** — every file that would need to be read or modified, with brief descriptions of what each does
22
+ 2. **Existing patterns** — how similar features are currently implemented in this codebase (naming conventions, folder structure, component patterns, API patterns)
23
+ 3. **Dependencies** — libraries, utilities, shared code, and services that are relevant
24
+ 4. **Potential impact areas** — what else might break or need updating (tests, types, imports, configs)
25
+ 5. **Edge cases and constraints** — anything tricky that the implementation should watch out for
26
+ 6. **Reference implementations** — if there's a similar feature already built, document it as a reference
27
+
28
+ Be thorough. Keep researching until you have complete understanding — missing information here means a worse plan later.
@@ -0,0 +1,28 @@
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 RULES:**
6
+
7
+ - Do **NOT** implement the issue. Do not create, modify, or delete any project source files.
8
+ - Your **ONLY** deliverable is writing the file @{{ISSUE_DIR}}/plan.md.
9
+ - Read the actual source files before suggesting changes. Base the plan on what the code actually does, not assumptions.
10
+
11
+ The code for this project lives primarily at `{{SCOPE_PATH}}/`.
12
+
13
+ Write @{{ISSUE_DIR}}/plan.md containing:
14
+
15
+ 1. **Summary** — what we're building and why (1-2 paragraphs)
16
+ 2. **Approach** — the high-level technical approach chosen
17
+ 3. **Architectural decisions** — any significant choices made and why (e.g., which component pattern, state management approach, API structure)
18
+ 4. **Key code snippets** — include concrete code examples showing the important parts of the implementation (function signatures, component structure, schema changes, etc.)
19
+ 5. **Scope boundaries** — what is explicitly out of scope to keep the change focused
20
+ 6. **Risks** — anything that could go wrong or needs special attention during implementation
21
+ 7. **Alternative approaches** — a brief section listing other valid ways to solve this problem. For each alternative, include: the approach name, a one-liner on how it works, and why the chosen approach was preferred. Consider industry best practices, common patterns, and obvious alternatives. This section is for PR reviewers only — it will NOT be used in the implementation plan.
22
+
23
+ **Design principles to follow:**
24
+
25
+ - Fixing a known issue later instead of now is not simplicity — if the plan touches an area with a known bug, address it.
26
+ - Adding a second primitive for something we already have a primitive for is not simplicity — reuse existing abstractions.
27
+
28
+ Keep it concise and focused on decisions, not on repeating the research.