@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.
- package/README.md +82 -72
- package/package.json +8 -7
- package/src/commands/auto-claude.ts +219 -0
- package/src/commands/doctor.ts +1 -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 +14 -0
- package/src/lib/auto-claude/pipeline.test.ts +14 -0
- package/src/lib/auto-claude/pipeline.ts +64 -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 +145 -0
- package/src/lib/auto-claude/prompt-templates/index.ts +44 -0
- package/src/lib/auto-claude/steps/create-pr.ts +93 -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 +334 -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,14 +1,6 @@
|
|
|
1
1
|
# Towles Tool
|
|
2
2
|
|
|
3
|
-
Personal CLI toolkit with
|
|
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
|
|
21
|
+
pnpm start
|
|
30
22
|
```
|
|
31
23
|
|
|
32
24
|
## CLI Commands
|
|
33
25
|
|
|
34
|
-
###
|
|
26
|
+
### Auto-Claude (issue-to-PR pipeline)
|
|
35
27
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
### Git
|
|
82
88
|
|
|
83
|
-
| Command
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
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
|
-
|
|
95
|
+
### Journaling
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
###
|
|
103
|
+
### Utilities
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
## Guidelines
|
|
110
121
|
|
|
111
|
-
- [
|
|
112
|
-
- [
|
|
113
|
-
- [
|
|
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)
|
|
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.
|
|
4
|
-
"description": "CLI tool with
|
|
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
|
-
"
|
|
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
|
+
}
|
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";
|
|
@@ -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
|
|
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
|
}
|
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,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
|
+
});
|