@towles/tool 0.0.54 → 0.0.56
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/README.md +52 -12
- package/package.json +4 -4
- package/src/commands/auto-claude.ts +220 -0
- package/src/commands/doctor.ts +2 -34
- package/src/config/settings.ts +0 -10
- package/src/lib/auto-claude/config.test.ts +53 -0
- package/src/lib/auto-claude/config.ts +68 -0
- package/src/lib/auto-claude/index.ts +15 -0
- package/src/lib/auto-claude/pipeline.test.ts +14 -0
- package/src/lib/auto-claude/pipeline.ts +67 -0
- package/src/lib/auto-claude/prompt-templates/01-prompt-research.md +28 -0
- package/src/lib/auto-claude/prompt-templates/02-prompt-plan.md +28 -0
- package/src/lib/auto-claude/prompt-templates/03-prompt-plan-annotations.md +21 -0
- package/src/lib/auto-claude/prompt-templates/04-prompt-plan-implementation.md +33 -0
- package/src/lib/auto-claude/prompt-templates/05-prompt-implement.md +31 -0
- package/src/lib/auto-claude/prompt-templates/06-prompt-review.md +30 -0
- package/src/lib/auto-claude/prompt-templates/07-prompt-refresh.md +39 -0
- package/src/lib/auto-claude/prompt-templates/index.test.ts +146 -0
- package/src/lib/auto-claude/prompt-templates/index.ts +45 -0
- package/src/lib/auto-claude/steps/create-pr.ts +95 -0
- package/src/lib/auto-claude/steps/fetch-issues.ts +64 -0
- package/src/lib/auto-claude/steps/implement.ts +63 -0
- package/src/lib/auto-claude/steps/plan-annotations.ts +54 -0
- package/src/lib/auto-claude/steps/plan-implementation.ts +14 -0
- package/src/lib/auto-claude/steps/plan.ts +14 -0
- package/src/lib/auto-claude/steps/refresh.ts +114 -0
- package/src/lib/auto-claude/steps/remove-label.ts +22 -0
- package/src/lib/auto-claude/steps/research.ts +21 -0
- package/src/lib/auto-claude/steps/review.ts +14 -0
- package/src/lib/auto-claude/utils.test.ts +136 -0
- package/src/lib/auto-claude/utils.ts +347 -0
- package/src/commands/ralph/plan/add.ts +0 -69
- package/src/commands/ralph/plan/done.ts +0 -82
- package/src/commands/ralph/plan/list.test.ts +0 -48
- package/src/commands/ralph/plan/list.ts +0 -100
- package/src/commands/ralph/plan/remove.ts +0 -71
- package/src/commands/ralph/run.test.ts +0 -607
- package/src/commands/ralph/run.ts +0 -362
- package/src/commands/ralph/show.ts +0 -88
- package/src/lib/ralph/execution.ts +0 -292
- package/src/lib/ralph/formatter.ts +0 -240
- package/src/lib/ralph/index.ts +0 -4
- 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
|
|
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
|
-
###
|
|
26
|
+
### Auto-Claude (issue-to-PR pipeline)
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
4
|
-
"description": "CLI tool with
|
|
3
|
+
"version": "0.0.56",
|
|
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,220 @@
|
|
|
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
|
+
logBanner,
|
|
18
|
+
runPipeline,
|
|
19
|
+
sleep,
|
|
20
|
+
stepRefresh,
|
|
21
|
+
} from "../lib/auto-claude/index.js";
|
|
22
|
+
import type { IssueContext, StepName } from "../lib/auto-claude/index.js";
|
|
23
|
+
|
|
24
|
+
export default class AutoClaude extends BaseCommand {
|
|
25
|
+
static override aliases = ["ac"];
|
|
26
|
+
|
|
27
|
+
static override description = "Automated issue-to-PR pipeline using Claude Code";
|
|
28
|
+
|
|
29
|
+
static override examples = [
|
|
30
|
+
{
|
|
31
|
+
description: "Process a specific issue",
|
|
32
|
+
command: "<%= config.bin %> auto-claude --issue 42",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
description: "Run until plan step",
|
|
36
|
+
command: "<%= config.bin %> auto-claude --issue 42 --until plan",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
description: "Reset local state for an issue",
|
|
40
|
+
command: "<%= config.bin %> auto-claude --reset 42",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: "Refresh a stale PR branch",
|
|
44
|
+
command: "<%= config.bin %> auto-claude --refresh --issue 42",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: "Loop mode: poll for labeled issues",
|
|
48
|
+
command: "<%= config.bin %> auto-claude --loop",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description: "Loop with custom interval",
|
|
52
|
+
command: "<%= config.bin %> auto-claude --loop --interval 45",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
static override flags = {
|
|
57
|
+
...BaseCommand.baseFlags,
|
|
58
|
+
issue: Flags.integer({
|
|
59
|
+
char: "i",
|
|
60
|
+
description: "Process a specific issue number",
|
|
61
|
+
}),
|
|
62
|
+
until: Flags.string({
|
|
63
|
+
char: "u",
|
|
64
|
+
description: `Stop after this step (${STEP_NAMES.join(", ")})`,
|
|
65
|
+
options: [...STEP_NAMES],
|
|
66
|
+
}),
|
|
67
|
+
reset: Flags.integer({
|
|
68
|
+
description: "Delete local state for an issue (force restart)",
|
|
69
|
+
}),
|
|
70
|
+
refresh: Flags.boolean({
|
|
71
|
+
description: "Rebase a stale PR branch onto current main",
|
|
72
|
+
default: false,
|
|
73
|
+
}),
|
|
74
|
+
loop: Flags.boolean({
|
|
75
|
+
description: "Poll for labeled issues continuously",
|
|
76
|
+
default: false,
|
|
77
|
+
}),
|
|
78
|
+
interval: Flags.integer({
|
|
79
|
+
description: "Poll interval in minutes (default: 30)",
|
|
80
|
+
}),
|
|
81
|
+
limit: Flags.integer({
|
|
82
|
+
description: "Max issues per iteration (default: 1)",
|
|
83
|
+
default: 1,
|
|
84
|
+
}),
|
|
85
|
+
label: Flags.string({
|
|
86
|
+
description: "Trigger label (default: auto-claude)",
|
|
87
|
+
}),
|
|
88
|
+
"main-branch": Flags.string({
|
|
89
|
+
description: "Override main branch detection",
|
|
90
|
+
}),
|
|
91
|
+
"scope-path": Flags.string({
|
|
92
|
+
description: "Path within repo to scope work (default: .)",
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
async run(): Promise<void> {
|
|
97
|
+
const { flags } = await this.parse(AutoClaude);
|
|
98
|
+
|
|
99
|
+
const cfg = await initConfig({
|
|
100
|
+
triggerLabel: flags.label,
|
|
101
|
+
mainBranch: flags["main-branch"],
|
|
102
|
+
scopePath: flags["scope-path"],
|
|
103
|
+
loopRetryEnabled: flags.loop || undefined,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (flags.reset) {
|
|
107
|
+
const issueDir = join(process.cwd(), `.auto-claude/issue-${flags.reset}`);
|
|
108
|
+
log(`Resetting state for issue-${flags.reset}...`);
|
|
109
|
+
rmSync(issueDir, { recursive: true, force: true });
|
|
110
|
+
log(`Cleaned ${issueDir}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (flags.refresh) {
|
|
115
|
+
if (!flags.issue) {
|
|
116
|
+
this.error("--refresh requires --issue <number>");
|
|
117
|
+
}
|
|
118
|
+
const ctx = buildIssueContext(
|
|
119
|
+
{ number: flags.issue, title: `Issue #${flags.issue}`, body: "" },
|
|
120
|
+
cfg.repo,
|
|
121
|
+
cfg.scopePath,
|
|
122
|
+
);
|
|
123
|
+
await stepRefresh(ctx);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const untilStep = flags.until as StepName | undefined;
|
|
128
|
+
const loopMode = flags.loop;
|
|
129
|
+
const intervalMs = (flags.interval ?? cfg.loopIntervalMinutes) * 60_000;
|
|
130
|
+
const limit = flags.limit ?? 1;
|
|
131
|
+
|
|
132
|
+
if (loopMode) {
|
|
133
|
+
registerShutdownHandlers();
|
|
134
|
+
log(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let iteration = 0;
|
|
138
|
+
|
|
139
|
+
do {
|
|
140
|
+
const iterationStart = Date.now();
|
|
141
|
+
iteration++;
|
|
142
|
+
|
|
143
|
+
if (loopMode) {
|
|
144
|
+
logBanner(`Iteration #${iteration} — ${new Date().toISOString()}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await syncWithRemote();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
log(`Sync failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
151
|
+
if (loopMode) {
|
|
152
|
+
log(`Will retry in ${Math.round(intervalMs / 1000)}s...`);
|
|
153
|
+
await sleep(intervalMs);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let contexts: IssueContext[];
|
|
160
|
+
if (flags.issue) {
|
|
161
|
+
const ctx = await fetchIssue(flags.issue);
|
|
162
|
+
contexts = ctx ? [ctx] : [];
|
|
163
|
+
} else {
|
|
164
|
+
contexts = await fetchIssues(limit);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (contexts.length === 0) {
|
|
168
|
+
log("No issues to process.");
|
|
169
|
+
} else {
|
|
170
|
+
log(`Processing ${contexts.length} issue(s)...\n`);
|
|
171
|
+
|
|
172
|
+
for (const ctx of contexts) {
|
|
173
|
+
try {
|
|
174
|
+
await runPipeline(ctx, untilStep);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
consola.error(`Pipeline error for ${ctx.repo}#${ctx.number}:`, e);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (loopMode) {
|
|
182
|
+
const waitMs = Math.max(0, intervalMs - (Date.now() - iterationStart));
|
|
183
|
+
if (waitMs > 0) {
|
|
184
|
+
log(`Waiting ${Math.round(waitMs / 1000)}s until next iteration...`);
|
|
185
|
+
await sleep(waitMs);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} while (loopMode);
|
|
189
|
+
|
|
190
|
+
log("Done.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function syncWithRemote(): Promise<void> {
|
|
195
|
+
const cfg = getConfig();
|
|
196
|
+
log("Syncing with remote...");
|
|
197
|
+
await git(["fetch", "--all", "--prune"]);
|
|
198
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
199
|
+
if (branch !== cfg.mainBranch) {
|
|
200
|
+
log(`Warning: on branch "${branch}", switching to ${cfg.mainBranch}...`);
|
|
201
|
+
await git(["checkout", cfg.mainBranch]).catch(() => {});
|
|
202
|
+
}
|
|
203
|
+
const status = await git(["status", "--porcelain"]);
|
|
204
|
+
if (status.length > 0) {
|
|
205
|
+
throw new Error("Working tree has uncommitted changes. Commit or stash them first.");
|
|
206
|
+
}
|
|
207
|
+
await git(["pull", cfg.remote, cfg.mainBranch]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function registerShutdownHandlers(): void {
|
|
211
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
212
|
+
process.on(signal, () => {
|
|
213
|
+
log(`Received ${signal}, shutting down...`);
|
|
214
|
+
setTimeout(() => process.exit(1), 5_000).unref();
|
|
215
|
+
git(["checkout", getConfig().mainBranch])
|
|
216
|
+
.catch(() => {})
|
|
217
|
+
.then(() => process.exit(0));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -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";
|
|
@@ -32,6 +30,7 @@ export default class Doctor extends BaseCommand {
|
|
|
32
30
|
this.checkCommand("gh", ["--version"], /gh version ([\d.]+)/),
|
|
33
31
|
this.checkCommand("node", ["--version"], /v?([\d.]+)/),
|
|
34
32
|
this.checkCommand("bun", ["--version"], /([\d.]+)/),
|
|
33
|
+
this.checkCommand("pnpm", ["--version"], /([\d.]+)/),
|
|
35
34
|
]);
|
|
36
35
|
|
|
37
36
|
// Display results
|
|
@@ -63,19 +62,8 @@ export default class Doctor extends BaseCommand {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
|
|
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
65
|
// Summary
|
|
78
|
-
const allOk = checks.every((c) => c.ok) && ghAuth.ok
|
|
66
|
+
const allOk = checks.every((c) => c.ok) && ghAuth.ok;
|
|
79
67
|
this.log("");
|
|
80
68
|
if (allOk) {
|
|
81
69
|
this.log(pc.green("All checks passed!"));
|
|
@@ -113,24 +101,4 @@ export default class Doctor extends BaseCommand {
|
|
|
113
101
|
return { ok: false };
|
|
114
102
|
}
|
|
115
103
|
}
|
|
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
104
|
}
|
package/src/config/settings.ts
CHANGED
|
@@ -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,15 @@
|
|
|
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
|
+
logBanner,
|
|
14
|
+
sleep,
|
|
15
|
+
} 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,67 @@
|
|
|
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, readFile, 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
|
+
const prUrlPath = join(ctx.issueDir, ARTIFACTS.prUrl);
|
|
59
|
+
const prUrl = fileExists(prUrlPath) ? readFile(prUrlPath).trim() : "";
|
|
60
|
+
const prSuffix = prUrl ? ` — ${prUrl}` : "";
|
|
61
|
+
log(`Pipeline complete for ${ctx.repo}#${ctx.number}${prSuffix}`);
|
|
62
|
+
await checkoutMain();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function checkoutMain(): Promise<void> {
|
|
66
|
+
await git(["checkout", getConfig().mainBranch]).catch(() => {});
|
|
67
|
+
}
|
|
@@ -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.
|