@towles/tool 0.0.53 → 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 +82 -72
  2. package/package.json +8 -7
  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,14 +1,6 @@
1
1
  # Towles Tool
2
2
 
3
- Personal CLI toolkit with autonomous task runner and quality-of-life commands for daily development.
4
-
5
- ## Features
6
-
7
- - **Ralph** - Autonomous task runner with session forking and context reuse
8
- - **Observability** - Token usage visualization with interactive treemaps
9
- - **Git workflows** - Branch creation, PR generation, and cleanup
10
- - **Journaling** - Daily notes, meeting notes, and general notes
11
- - **Claude Code plugins** - Personal plugin marketplace for Claude Code integration
3
+ Personal CLI toolkit with auto-claude pipeline and developer utilities.
12
4
 
13
5
  ## Installation
14
6
 
@@ -26,93 +18,111 @@ claude plugin update tt@towles-tool
26
18
  git clone https://github.com/ChrisTowles/towles-tool.git
27
19
  cd towles-tool
28
20
  pnpm install
29
- pnpm start # Run directly with tsx
21
+ pnpm start
30
22
  ```
31
23
 
32
24
  ## CLI Commands
33
25
 
34
- ### Ralph (autonomous runner)
26
+ ### Auto-Claude (issue-to-PR pipeline)
35
27
 
36
- | Command | Description |
37
- | --------------------------- | --------------------------------------------- |
38
- | `tt ralph plan add <desc>` | Add task to plan |
39
- | `tt ralph plan list` | View tasks |
40
- | `tt ralph plan done <id>` | Mark task complete |
41
- | `tt ralph plan remove <id>` | Remove task |
42
- | `tt ralph run` | Run autonomous loop (auto-commits by default) |
43
- | `tt ralph show` | Show plan with mermaid graph |
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.
44
29
 
45
- ### Observability
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.
46
31
 
47
- | Command | Description |
48
- | ------------------------- | ------------------------------------- |
49
- | `tt graph` | Generate HTML treemap of all sessions |
50
- | `tt graph --session <id>` | Single session treemap |
51
- | `tt graph --open` | Auto-open in browser |
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).
52
33
 
53
- Treemap colors indicate input/output token ratio: green <2:1, yellow 2-5:1, red >5:1.
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
+ ```
54
41
 
55
- ### Git
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.
56
43
 
57
- | Command | Alias | Description |
58
- | -------------------- | ------- | ------------------------------- |
59
- | `tt gh branch` | | Create branch from GitHub issue |
60
- | `tt gh pr` | `tt pr` | Create pull request |
61
- | `tt gh branch-clean` | | Delete merged branches |
44
+ #### Pipeline Steps
62
45
 
63
- ### Journaling
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 | — |
64
56
 
65
- | Command | Alias | Description |
66
- | ------------------------ | ---------- | -------------------------------- |
67
- | `tt journal daily-notes` | `tt today` | Weekly files with daily sections |
68
- | `tt journal meeting` | `tt m` | Meeting notes |
69
- | `tt journal note` | `tt n` | General notes |
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.
70
58
 
71
- ### Utilities
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
+ ```
72
78
 
73
- | Command | Alias | Description |
74
- | ------------ | -------- | ------------------------------ |
75
- | `tt config` | `tt cfg` | Show configuration |
76
- | `tt doctor` | | Check dependencies |
77
- | `tt install` | | Configure Claude Code settings |
79
+ ### Observability
78
80
 
79
- ## Claude Code Plugin Skills
81
+ | Command | Description |
82
+ | ------------------------- | ------------------------ |
83
+ | `tt graph` | Token Usage (auto-opens) |
84
+ | `tt graph --session <id>` | Single session |
85
+ | `tt graph --days 14` | Filter to last N days |
80
86
 
81
- Available via `/tt:<command>`:
87
+ ### Git
82
88
 
83
- | Command | Description |
84
- | ------------- | --------------------------------------------- |
85
- | `/tt:commit` | AI-powered conventional commit messages |
86
- | `/tt:plan` | Interview user and create implementation plan |
87
- | `/tt:improve` | Explore codebase and suggest improvements |
88
- | `/tt:refine` | Fix grammar/spelling in files |
89
+ | Command | Description |
90
+ | -------------------- | ------------------------------- |
91
+ | `tt gh branch` | Create branch from GitHub issue |
92
+ | `tt gh pr` | Create pull request |
93
+ | `tt gh branch-clean` | Delete merged branches |
89
94
 
90
- ## Development
95
+ ### Journaling
91
96
 
92
- ```bash
93
- pnpm start # Run CLI with tsx
94
- pnpm test # Run tests
95
- pnpm lint # Run oxlint
96
- pnpm format # Format with oxfmt
97
- pnpm typecheck # Type check
98
- ```
97
+ | Command | Alias | Description |
98
+ | ------------------------ | ---------- | ------------- |
99
+ | `tt journal daily-notes` | `tt today` | Weekly/daily |
100
+ | `tt journal meeting` | `tt m` | Meeting notes |
101
+ | `tt journal note` | `tt n` | General notes |
99
102
 
100
- ### Releasing
103
+ ### Utilities
101
104
 
102
- ```bash
103
- gh workflow run release.yml -f bump_type=patch # or minor/major
104
- gh run watch
105
- ```
105
+ | Command | Description |
106
+ | ------------ | ------------------------------ |
107
+ | `tt config` | Show configuration |
108
+ | `tt doctor` | Check dependencies |
109
+ | `tt install` | Configure Claude Code settings |
110
+
111
+ ## Claude Code Skills
106
112
 
107
- ## Resources
113
+ | Skill | Description |
114
+ | ------------------------ | ----------------------------- |
115
+ | `/tt:plan` | Create implementation plan |
116
+ | `/tt:improve` | Suggest codebase improvements |
117
+ | `/tt:refactor-claude-md` | Fix grammar/spelling |
118
+ | `/tt:refine` | Fix grammar/spelling |
108
119
 
109
- ### Claude Code Plugin Development
120
+ ## Guidelines
110
121
 
111
- - [Claude Code Plugins Announcement](https://www.anthropic.com/news/claude-code-plugins)
112
- - [Official Claude Code Plugins](https://github.com/anthropics/claude-code/tree/main/plugins)
113
- - [Skills Guide](https://docs.claude.com/en/api/skills-guide)
114
- - [Best Practices](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/best-practices)
122
+ - [Architecture](docs/architecture.md) - CLI structure, plugin system, tech stack
123
+ - [CICD via GitHub Actions](docs/github-actions.md) - Automated release workflow
124
+ - [Testing](docs/testings.md) - Info about Tests
115
125
 
116
126
  ## License
117
127
 
118
- [MIT](./LICENSE) License © [Chris Towles](https://github.com/ChrisTowles)
128
+ [MIT](./LICENSE) © [Chris Towles](https://github.com/ChrisTowles)
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.53",
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": {
@@ -79,15 +79,16 @@
79
79
  "vitest": "^3.1.3"
80
80
  },
81
81
  "simple-git-hooks": {
82
- "pre-commit": "pnpm lint-staged && pnpm typecheck"
82
+ "pre-commit": "pnpm lint-staged && pnpm format && pnpm typecheck"
83
83
  },
84
84
  "lint-staged": {
85
85
  "package.json": "oxfmt --write",
86
86
  "*.{ts,tsx,mts,cts,js,cjs,mjs}": [
87
- "oxfmt --write",
88
87
  "oxlint --fix"
89
88
  ],
90
- "*.{json,md,yaml,yml}": "oxfmt --write"
89
+ "*.*": [
90
+ "oxfmt --write"
91
+ ]
91
92
  },
92
93
  "oclif": {
93
94
  "bin": "tt",
@@ -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
+ });