ralph-prd 1.1.0 → 3.0.0
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 +93 -55
- package/bin/cli.mjs +72 -0
- package/bin/install.mjs +49 -15
- package/package.json +7 -2
- package/ralph/index.mjs +10 -0
- package/ralph/lib/committer.mjs +29 -7
- package/ralph/lib/config.mjs +26 -5
- package/ralph/lib/phase-executor.mjs +16 -2
- package/ralph/lib/plan-parser.mjs +104 -1
- package/ralph/lib/prompts/commit.md +12 -0
- package/ralph/lib/prompts/implementation.md +17 -11
- package/ralph/lib/prompts/implementation_closing_commit.md +18 -1
- package/ralph/lib/prompts/repair.md +1 -1
- package/ralph/lib/prompts/verification.md +1 -1
- package/ralph/lib/state.mjs +6 -2
- package/ralph/lib/transport.mjs +3 -3
- package/ralph/lib/verifier.mjs +17 -5
- package/ralph/ralph-claude.mjs +237 -122
- package/ralph/test/committer.test.mjs +19 -12
- package/ralph/test/config.test.mjs +4 -2
- package/ralph/test/e2e.test.mjs +18 -18
- package/ralph/test/git-coordinator.test.mjs +8 -4
- package/ralph/test/phase-executor.test.mjs +13 -9
package/README.md
CHANGED
|
@@ -2,44 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
AI-powered phased implementation runner for [Claude Code](https://claude.ai/claude-code). Go from PRD to shipped code — automatically.
|
|
4
4
|
|
|
5
|
-
Ralph takes a markdown plan file, breaks it into phases, and executes each one through the Claude CLI with built-in verification, repair loops, and auto-commits.
|
|
5
|
+
Ralph takes a markdown plan file, breaks it into phases and tasks, and executes each one through the Claude CLI with built-in verification, repair loops, and auto-commits.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
One command installs Ralph and all skills into your project's `.claude/` directory:
|
|
10
|
-
|
|
11
9
|
```bash
|
|
12
|
-
|
|
13
|
-
npx ralph-prd
|
|
14
|
-
|
|
15
|
-
# Or using curl
|
|
16
|
-
curl -fsSL https://raw.githubusercontent.com/tahaJemmali/ralph-prd/main/install.sh | bash
|
|
17
|
-
|
|
18
|
-
# Or clone and run locally
|
|
19
|
-
git clone https://github.com/tahaJemmali/ralph-prd.git
|
|
20
|
-
cd your-project && ../ralph-prd/install.sh
|
|
10
|
+
npx ralph-prd init
|
|
21
11
|
```
|
|
22
12
|
|
|
23
|
-
This installs
|
|
24
|
-
- `.claude/ralph/` — the phased runner
|
|
25
|
-
- `.claude/skills/` — 7 Claude Code skills fetched from [`tahaJemmali/skills`](https://github.com/tahaJemmali/skills) via `npx skills add`
|
|
26
|
-
- On first install, adds `.claude/ralph/` and `.claude/skills/` to `.gitignore` (skipped if `.claude/` already exists, so shared setups are respected)
|
|
13
|
+
This installs skills to `.claude/skills/` and scaffolds a config file. Ralph itself runs directly from the npm package — no files are copied to your project.
|
|
27
14
|
|
|
28
|
-
|
|
15
|
+
Skills are fetched from [`tahaJemmali/skills`](https://github.com/tahaJemmali/skills). Installation retries automatically (3 attempts) and falls back to cached skills on network failure.
|
|
29
16
|
|
|
30
|
-
|
|
17
|
+
### Updating
|
|
31
18
|
|
|
32
19
|
```bash
|
|
33
|
-
npx ralph-prd
|
|
20
|
+
npx ralph-prd init
|
|
34
21
|
```
|
|
35
22
|
|
|
36
|
-
|
|
23
|
+
Re-running init re-fetches skills. Your `ralph.config.yaml` is preserved.
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
node .claude/ralph/ralph-claude.mjs --update-skills
|
|
42
|
-
```
|
|
25
|
+
Ralph also checks for updates automatically on every run.
|
|
43
26
|
|
|
44
27
|
## Requirements
|
|
45
28
|
|
|
@@ -68,15 +51,21 @@ Run `/prd-to-plan`. Claude reads the PRD, identifies durable architectural decis
|
|
|
68
51
|
### 4. Execute with Ralph
|
|
69
52
|
|
|
70
53
|
```bash
|
|
71
|
-
|
|
54
|
+
npx ralph-prd run docs/<feature>/plan.md
|
|
72
55
|
```
|
|
73
56
|
|
|
74
57
|
Ralph runs each phase through Claude:
|
|
75
|
-
1. **
|
|
76
|
-
2. **
|
|
77
|
-
3. **
|
|
78
|
-
4. **
|
|
79
|
-
5. **
|
|
58
|
+
1. **Task-level implementation** — each phase is broken into individual tasks (user stories) and implemented one at a time for focused, atomic progress
|
|
59
|
+
2. **Per-task commit** — every task gets its own commit with enriched messages (decisions, blockers, notes for next task)
|
|
60
|
+
3. **Verification** — checks acceptance criteria pass after all tasks in the phase complete
|
|
61
|
+
4. **Repair loop** — if verification fails, auto-repairs (up to N attempts)
|
|
62
|
+
5. **Ship-check** — post-commit quality gate
|
|
63
|
+
6. **Checkpoint** — saves progress per-task for crash recovery
|
|
64
|
+
|
|
65
|
+
Each implementation session receives:
|
|
66
|
+
- The **PRD** for business context (resolved from the plan's `> Source PRD:` line)
|
|
67
|
+
- The **last 5 commits** per repo so tasks build on each other's work
|
|
68
|
+
- The **full plan** for cross-phase awareness
|
|
80
69
|
|
|
81
70
|
### 5. Ship check
|
|
82
71
|
|
|
@@ -85,38 +74,67 @@ Run `/ship-check` to validate the final result against your project's documented
|
|
|
85
74
|
## CLI Reference
|
|
86
75
|
|
|
87
76
|
```bash
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
--
|
|
94
|
-
--
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--skip-
|
|
100
|
-
--
|
|
101
|
-
--
|
|
102
|
-
--
|
|
77
|
+
npx ralph-prd init Install skills and scaffold config
|
|
78
|
+
npx ralph-prd run <plan.md> [OPTIONS] Execute a plan
|
|
79
|
+
npx ralph-prd --version Print version
|
|
80
|
+
|
|
81
|
+
Run options:
|
|
82
|
+
--dry-run Preview all phases without executing
|
|
83
|
+
--reset Delete state and restart from Phase 1
|
|
84
|
+
--only-phase N Force re-run phase N (1-based)
|
|
85
|
+
--i-did-this Skip Claude self-commit; run separate commit step
|
|
86
|
+
--send-it Push branch + open PR when all phases complete
|
|
87
|
+
--wait-for-it Pause before each commit for review
|
|
88
|
+
--skip-ship-check Skip the post-commit ship-check step entirely
|
|
89
|
+
--ship-check-retries=N Retry ship-check up to N times (default 1)
|
|
90
|
+
--skip-on-ship-check-fail Log and continue when ship-check fails
|
|
91
|
+
--skip-on-verify-fail Skip verification when all repair attempts fail
|
|
92
|
+
--log-level=LEVEL none | necessary | dump (default: necessary)
|
|
93
|
+
--update-skills Re-fetch skills and exit
|
|
103
94
|
```
|
|
104
95
|
|
|
105
96
|
### Examples
|
|
106
97
|
|
|
107
98
|
```bash
|
|
108
99
|
# Resume from last incomplete phase
|
|
109
|
-
|
|
100
|
+
npx ralph-prd run docs/auth-rework/plan.md
|
|
110
101
|
|
|
111
102
|
# Preview the plan
|
|
112
|
-
|
|
103
|
+
npx ralph-prd run docs/auth-rework/plan.md --dry-run
|
|
113
104
|
|
|
114
105
|
# Re-run only Phase 3
|
|
115
|
-
|
|
106
|
+
npx ralph-prd run docs/auth-rework/plan.md --only-phase 3
|
|
116
107
|
|
|
117
108
|
# Ship it: run all phases, push, open PR
|
|
118
|
-
|
|
109
|
+
npx ralph-prd run docs/auth-rework/plan.md --send-it
|
|
110
|
+
|
|
111
|
+
# Legacy invocation (still works)
|
|
112
|
+
npx ralph-prd docs/auth-rework/plan.md
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Commit Messages
|
|
116
|
+
|
|
117
|
+
Ralph produces enriched commit messages with context that carries forward between tasks:
|
|
118
|
+
|
|
119
119
|
```
|
|
120
|
+
ralph: add user authentication endpoint
|
|
121
|
+
|
|
122
|
+
- Added POST /auth/login route with JWT response
|
|
123
|
+
- Created JWT token generation utility
|
|
124
|
+
|
|
125
|
+
Decisions:
|
|
126
|
+
- Chose bcrypt over argon2 for password hashing (broader ecosystem support)
|
|
127
|
+
- Used 15-minute JWT expiry with refresh token pattern per PRD requirement
|
|
128
|
+
|
|
129
|
+
Blockers:
|
|
130
|
+
- Redis session store not available yet (Phase 3) — using in-memory Map
|
|
131
|
+
|
|
132
|
+
Next:
|
|
133
|
+
- Token refresh endpoint needs the auth middleware created here
|
|
134
|
+
- Rate limiting should be added before login goes to production
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The **Decisions**, **Blockers**, and **Next** sections are optional — included only when relevant.
|
|
120
138
|
|
|
121
139
|
## Included Skills
|
|
122
140
|
|
|
@@ -132,9 +150,9 @@ node .claude/ralph/ralph-claude.mjs docs/auth-rework/plan.md --send-it
|
|
|
132
150
|
|
|
133
151
|
Skills are fetched from [`tahaJemmali/skills`](https://github.com/tahaJemmali/skills) during install. You can also install or update them independently with `npx skills add tahaJemmali/skills`.
|
|
134
152
|
|
|
135
|
-
##
|
|
153
|
+
## Configuration
|
|
136
154
|
|
|
137
|
-
Create
|
|
155
|
+
Create `.claude/ralph.config.yaml` for multi-repo setups or to customize flags:
|
|
138
156
|
|
|
139
157
|
```yaml
|
|
140
158
|
repos:
|
|
@@ -158,6 +176,8 @@ hooks:
|
|
|
158
176
|
afterCommit: npm test
|
|
159
177
|
```
|
|
160
178
|
|
|
179
|
+
Config lookup order: `.claude/ralph.config.yaml` (canonical) → `.claude/ralph/ralph.config.yaml` (legacy). If no config exists, Ralph uses the current directory as the single repo.
|
|
180
|
+
|
|
161
181
|
## Plan File Format
|
|
162
182
|
|
|
163
183
|
Plans must follow the template produced by `/prd-to-plan`:
|
|
@@ -180,7 +200,9 @@ Plans must follow the template produced by `/prd-to-plan`:
|
|
|
180
200
|
|
|
181
201
|
### What to build
|
|
182
202
|
|
|
183
|
-
|
|
203
|
+
1. First task — implement the login endpoint
|
|
204
|
+
2. Second task — add JWT token generation
|
|
205
|
+
3. Third task — wire up password hashing
|
|
184
206
|
|
|
185
207
|
### Acceptance criteria
|
|
186
208
|
|
|
@@ -190,15 +212,31 @@ Description of the vertical slice.
|
|
|
190
212
|
|
|
191
213
|
Ralph validates the plan structure before execution and checks off criteria as phases complete.
|
|
192
214
|
|
|
215
|
+
The "What to build" section is parsed into individual tasks. Numbered items (`1.`, `2.`) or bullet items (`-`, `*`) each become a separate implementation session with its own commit. If the section is a single paragraph, the entire phase runs as one task.
|
|
216
|
+
|
|
193
217
|
## How It Works
|
|
194
218
|
|
|
195
219
|
- **Zero dependencies** — pure Node.js, no npm packages
|
|
196
|
-
- **
|
|
220
|
+
- **Runs from package** — no files copied to your project (only skills + config)
|
|
221
|
+
- **Task-level execution** — phases are broken into focused tasks, each with its own implementation + commit cycle
|
|
222
|
+
- **PRD context** — every session gets the source PRD and recent git history
|
|
223
|
+
- **Enriched commits** — decisions, blockers, and notes carry knowledge between tasks
|
|
224
|
+
- **Crash recovery** — checkpoints after each task; resume where you left off
|
|
225
|
+
- **Atomic state writes** — write-then-rename prevents corruption on crash
|
|
197
226
|
- **Live streaming** — see Claude's tool calls, thinking, and output in real-time
|
|
198
227
|
- **Full logging** — JSONL logs of every Claude CLI event per phase
|
|
199
|
-
- **
|
|
228
|
+
- **Cross-platform notifications** — macOS, Linux (notify-send), and terminal bell fallback
|
|
229
|
+
- **Resilient installs** — retries skill installation, falls back to cached skills on network failure
|
|
200
230
|
- **Safety** — optional `blocked-commands.txt` and `blocked-paths.txt` restrict what Claude can do
|
|
201
231
|
|
|
232
|
+
## Public API
|
|
233
|
+
|
|
234
|
+
ralph-prd exports a stable API for use by orchestrators like [ralph-prd-afk](https://github.com/tahaJemmali/ralph-prd-afk):
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
import { send, preflight, getCumulativeCost } from 'ralph-prd/transport';
|
|
238
|
+
```
|
|
239
|
+
|
|
202
240
|
## Acknowledgments
|
|
203
241
|
|
|
204
242
|
Three of the skills in this repo — [`/grill-me`](https://skills.sh/mattpocock/skills/grill-me), [`/write-a-prd`](https://skills.sh/mattpocock/skills/write-a-prd), and [`/prd-to-plan`](https://skills.sh/mattpocock/skills/prd-to-plan) — are based on [Matt Pocock](https://github.com/mattpocock)'s work. Matt generously gave his blessing to include them here. If you find these useful, go check out his cohort and give him a star.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/cli.mjs — ralph-prd CLI router
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx ralph-prd init Install skills and scaffold config
|
|
7
|
+
* npx ralph-prd run <plan.md> Execute a plan
|
|
8
|
+
* npx ralph-prd --version Print version
|
|
9
|
+
*
|
|
10
|
+
* The `init` subcommand installs skills to .claude/skills/ and scaffolds
|
|
11
|
+
* ralph.config.yaml. It does NOT copy the runner — ralph-prd executes
|
|
12
|
+
* directly from the npm package.
|
|
13
|
+
*
|
|
14
|
+
* For backward compatibility, running `npx ralph-prd` without a subcommand
|
|
15
|
+
* still triggers init (legacy behavior).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync } from 'fs';
|
|
19
|
+
import { resolve, dirname } from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const PKG_ROOT = resolve(__dirname, '..');
|
|
24
|
+
|
|
25
|
+
// ─── Version ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, 'package.json'), 'utf8'));
|
|
28
|
+
const VERSION = pkg.version;
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
|
|
32
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
33
|
+
console.log(`ralph-prd v${VERSION}`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Subcommand routing ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const subcommand = args.find(a => !a.startsWith('--'));
|
|
40
|
+
|
|
41
|
+
if (subcommand === 'run') {
|
|
42
|
+
// npx ralph-prd run <plan.md> [flags]
|
|
43
|
+
const runArgs = args.filter(a => a !== 'run');
|
|
44
|
+
const planArg = runArgs.find(a => !a.startsWith('--'));
|
|
45
|
+
|
|
46
|
+
if (!planArg) {
|
|
47
|
+
console.error('Usage: npx ralph-prd run <plan-file.md> [OPTIONS]');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Import and run ralph-claude.mjs directly from the package
|
|
52
|
+
// Override process.argv so ralph-claude.mjs sees the correct args
|
|
53
|
+
process.argv = [process.argv[0], resolve(PKG_ROOT, 'ralph', 'ralph-claude.mjs'), ...runArgs];
|
|
54
|
+
|
|
55
|
+
await import(resolve(PKG_ROOT, 'ralph', 'ralph-claude.mjs'));
|
|
56
|
+
|
|
57
|
+
} else if (subcommand === 'init' || !subcommand) {
|
|
58
|
+
// npx ralph-prd init OR npx ralph-prd (legacy: bare invocation = init)
|
|
59
|
+
const { install } = await import('./install.mjs');
|
|
60
|
+
await install();
|
|
61
|
+
|
|
62
|
+
} else if (subcommand === '--update-skills') {
|
|
63
|
+
// npx ralph-prd --update-skills
|
|
64
|
+
process.argv = [process.argv[0], 'ralph-claude.mjs', '--update-skills'];
|
|
65
|
+
await import(resolve(PKG_ROOT, 'ralph', 'ralph-claude.mjs'));
|
|
66
|
+
|
|
67
|
+
} else {
|
|
68
|
+
// Assume it's a plan file path (legacy: npx ralph-prd docs/feature/plan.md)
|
|
69
|
+
// Treat bare arguments as plan files for backward compat
|
|
70
|
+
process.argv = [process.argv[0], resolve(PKG_ROOT, 'ralph', 'ralph-claude.mjs'), ...args];
|
|
71
|
+
await import(resolve(PKG_ROOT, 'ralph', 'ralph-claude.mjs'));
|
|
72
|
+
}
|
package/bin/install.mjs
CHANGED
|
@@ -48,6 +48,13 @@ mkdirSync(claudeDir, { recursive: true });
|
|
|
48
48
|
// Copy ralph runner
|
|
49
49
|
const ralphSrc = resolve(PKG_ROOT, 'ralph');
|
|
50
50
|
const ralphDst = resolve(claudeDir, 'ralph');
|
|
51
|
+
// Preserve user config across updates
|
|
52
|
+
const configDst = resolve(ralphDst, 'ralph.config.yaml');
|
|
53
|
+
let savedConfig = null;
|
|
54
|
+
if (existsSync(configDst)) {
|
|
55
|
+
savedConfig = readFileSync(configDst, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
if (existsSync(ralphDst)) {
|
|
52
59
|
info('Updating existing .claude/ralph/');
|
|
53
60
|
rmSync(ralphDst, { recursive: true });
|
|
@@ -62,16 +69,19 @@ const pkg = JSON.parse(readFileSync(resolve(PKG_ROOT, 'package.json'), 'utf8'));
|
|
|
62
69
|
writeFileSync(resolve(ralphDst, '.ralph-version'), pkg.version + '\n', 'utf8');
|
|
63
70
|
ok(`Installed ralph runner v${pkg.version} -> .claude/ralph/`);
|
|
64
71
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// Restore saved config or create default from sample
|
|
73
|
+
if (savedConfig !== null) {
|
|
74
|
+
writeFileSync(configDst, savedConfig, 'utf8');
|
|
75
|
+
ok('Restored existing ralph.config.yaml');
|
|
76
|
+
} else {
|
|
77
|
+
const configSrc = resolve(ralphDst, 'ralph.config.sample.yaml');
|
|
78
|
+
if (existsSync(configSrc)) {
|
|
79
|
+
copyFileSync(configSrc, configDst);
|
|
80
|
+
ok('Created default ralph.config.yaml -> .claude/ralph/ralph.config.yaml');
|
|
81
|
+
}
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
// Install skills via skills.sh
|
|
74
|
-
info('Installing skills from tahaJemmali/skills…');
|
|
75
85
|
const REQUIRED_SKILLS = [
|
|
76
86
|
'grill-me',
|
|
77
87
|
'write-a-prd',
|
|
@@ -81,15 +91,39 @@ const REQUIRED_SKILLS = [
|
|
|
81
91
|
'review-changes',
|
|
82
92
|
'repo-doc-maintainer',
|
|
83
93
|
];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
|
|
95
|
+
info('Installing skills from tahaJemmali/skills…');
|
|
96
|
+
let skillsOk = false;
|
|
97
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
98
|
+
const skillsResult = spawnSync(
|
|
99
|
+
'npx',
|
|
100
|
+
['skills', 'add', 'tahaJemmali/skills', ...REQUIRED_SKILLS.flatMap(s => ['--skill', s]), '-a', 'claude-code', '-y'],
|
|
101
|
+
{ cwd: projectRoot, stdio: 'inherit', encoding: 'utf8' },
|
|
102
|
+
);
|
|
103
|
+
if (skillsResult.status === 0) {
|
|
104
|
+
skillsOk = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (attempt < 3) {
|
|
108
|
+
info(`Skills install attempt ${attempt} failed, retrying in 2s…`);
|
|
109
|
+
spawnSync('sleep', ['2']);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (skillsOk) {
|
|
114
|
+
ok('Installed skills -> .claude/skills/');
|
|
115
|
+
} else {
|
|
116
|
+
// Check if skills already exist from a previous install
|
|
117
|
+
const skillsDir = resolve(projectRoot, '.claude', 'skills');
|
|
118
|
+
const allPresent = REQUIRED_SKILLS.every(s =>
|
|
119
|
+
existsSync(resolve(skillsDir, s, 'SKILL.md'))
|
|
120
|
+
);
|
|
121
|
+
if (allPresent) {
|
|
122
|
+
info('Skills install failed but existing skills found — continuing with cached skills.');
|
|
123
|
+
} else {
|
|
124
|
+
fail('Skills installation failed after 3 attempts — aborting.\n Retry: npx ralph-prd');
|
|
125
|
+
}
|
|
91
126
|
}
|
|
92
|
-
ok('Installed skills -> .claude/skills/');
|
|
93
127
|
|
|
94
128
|
const gitignorePath = resolve(projectRoot, '.gitignore');
|
|
95
129
|
let gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-prd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "AI-powered phased implementation runner for Claude Code — from PRD to shipped code",
|
|
5
6
|
"bin": {
|
|
6
|
-
"ralph-prd": "./bin/
|
|
7
|
+
"ralph-prd": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./ralph/index.mjs",
|
|
11
|
+
"./transport": "./ralph/lib/transport.mjs"
|
|
7
12
|
},
|
|
8
13
|
"keywords": [
|
|
9
14
|
"claude",
|
package/ralph/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ralph-prd public API
|
|
3
|
+
*
|
|
4
|
+
* Stable exports for use by ralph-prd-afk and other consumers.
|
|
5
|
+
* Import via: import { send, preflight } from 'ralph-prd/transport'
|
|
6
|
+
* Or: import { send, preflight } from 'ralph-prd'
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { send, preflight, getCumulativeCost, TransportError } from './lib/transport.mjs';
|
|
10
|
+
export { resolveRepos } from './lib/config.mjs';
|
package/ralph/lib/committer.mjs
CHANGED
|
@@ -166,14 +166,36 @@ function parseCommitPlan(text) {
|
|
|
166
166
|
const commitLine = lines.find(l => l.startsWith('COMMIT:'));
|
|
167
167
|
const commitSubject = commitLine ? commitLine.slice('COMMIT:'.length).trim() : '';
|
|
168
168
|
|
|
169
|
-
// DESCRIPTION
|
|
170
|
-
const
|
|
171
|
-
let
|
|
169
|
+
// Parse sectioned body: DESCRIPTION, DECISIONS, BLOCKERS, NEXT
|
|
170
|
+
const sections = { DESCRIPTION: [], DECISIONS: [], BLOCKERS: [], NEXT: [] };
|
|
171
|
+
let currentSection = null;
|
|
172
172
|
for (const line of lines) {
|
|
173
|
-
if (line === 'DESCRIPTION:') {
|
|
174
|
-
if (
|
|
173
|
+
if (line === 'DESCRIPTION:') { currentSection = 'DESCRIPTION'; continue; }
|
|
174
|
+
if (line === 'DECISIONS:') { currentSection = 'DECISIONS'; continue; }
|
|
175
|
+
if (line === 'BLOCKERS:') { currentSection = 'BLOCKERS'; continue; }
|
|
176
|
+
if (line === 'NEXT:') { currentSection = 'NEXT'; continue; }
|
|
177
|
+
// Stop collecting on known non-body headers
|
|
178
|
+
if (line === 'FILES:' || line.startsWith('COMMIT:') || line.startsWith('REPO:')) {
|
|
179
|
+
currentSection = null; continue;
|
|
180
|
+
}
|
|
181
|
+
if (currentSection) sections[currentSection].push(line);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Build commit body: description bullets, then optional enrichment sections
|
|
185
|
+
const bodyParts = [];
|
|
186
|
+
if (sections.DESCRIPTION.length > 0) {
|
|
187
|
+
bodyParts.push(sections.DESCRIPTION.join('\n'));
|
|
188
|
+
}
|
|
189
|
+
if (sections.DECISIONS.length > 0) {
|
|
190
|
+
bodyParts.push('Decisions:\n' + sections.DECISIONS.join('\n'));
|
|
191
|
+
}
|
|
192
|
+
if (sections.BLOCKERS.length > 0) {
|
|
193
|
+
bodyParts.push('Blockers:\n' + sections.BLOCKERS.join('\n'));
|
|
194
|
+
}
|
|
195
|
+
if (sections.NEXT.length > 0) {
|
|
196
|
+
bodyParts.push('Next:\n' + sections.NEXT.join('\n'));
|
|
175
197
|
}
|
|
176
|
-
const commitBody =
|
|
198
|
+
const commitBody = bodyParts.join('\n\n').trim();
|
|
177
199
|
|
|
178
200
|
plans.push({ name, files, commitSubject, commitBody, skip: false });
|
|
179
201
|
}
|
|
@@ -325,7 +347,7 @@ export async function runCommitStep({
|
|
|
325
347
|
const changedRepos = await scanChangedRepos(repos);
|
|
326
348
|
|
|
327
349
|
if (changedRepos.length === 0) {
|
|
328
|
-
return {
|
|
350
|
+
return { nextTaskNum: si, anyCommitted: false };
|
|
329
351
|
}
|
|
330
352
|
|
|
331
353
|
// Build and run the commit session
|
package/ralph/lib/config.mjs
CHANGED
|
@@ -158,16 +158,28 @@ function parseConfigYaml(content) {
|
|
|
158
158
|
* Throws an Error with a human-readable message on any validation failure so
|
|
159
159
|
* the caller can print it and exit before any phase begins.
|
|
160
160
|
*
|
|
161
|
-
* @param {string} runnerDir - Absolute path of the directory containing ralph-claude.mjs
|
|
161
|
+
* @param {string} runnerDir - Absolute path of the directory containing ralph-claude.mjs.
|
|
162
|
+
* Also searches process.cwd()/.claude/ for config (new canonical location).
|
|
162
163
|
* @returns {{ repos: Repo[], flags: RalphFlags, hooks: RalphHooks }}
|
|
163
164
|
*/
|
|
164
165
|
export function resolveRepos(runnerDir) {
|
|
165
|
-
|
|
166
|
+
// Config lookup chain: new canonical path → legacy path → no config
|
|
167
|
+
const cwd = process.cwd();
|
|
168
|
+
const canonicalPath = join(cwd, '.claude', CONFIG_FILENAME);
|
|
169
|
+
const legacyPath = join(runnerDir, CONFIG_FILENAME);
|
|
170
|
+
const configPath = existsSync(canonicalPath) ? canonicalPath
|
|
171
|
+
: existsSync(legacyPath) ? legacyPath
|
|
172
|
+
: null;
|
|
173
|
+
|
|
174
|
+
if (configPath === legacyPath && existsSync(legacyPath)) {
|
|
175
|
+
console.log(`[ralph] Config loaded from legacy path: ${legacyPath}`);
|
|
176
|
+
console.log('[ralph] Move to .claude/ralph.config.yaml for the new canonical location.');
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
const defaultFlags = { iDidThis: false, sendIt: false, waitForIt: false, maxRepairs: 3, onlyPhase: null, skipShipCheck: false, shipCheckRetries: 1, skipOnShipCheckFail: true, skipOnVerifyFail: false };
|
|
167
180
|
const defaultHooks = { afterCommit: null };
|
|
168
181
|
|
|
169
|
-
if (!
|
|
170
|
-
const cwd = process.cwd();
|
|
182
|
+
if (!configPath) {
|
|
171
183
|
if (!existsSync(cwd)) {
|
|
172
184
|
throw new Error(`Default repo (cwd) does not exist: ${cwd}`);
|
|
173
185
|
}
|
|
@@ -182,7 +194,16 @@ export function resolveRepos(runnerDir) {
|
|
|
182
194
|
const configDir = dirname(configPath);
|
|
183
195
|
|
|
184
196
|
if (parsed.repos.length === 0) {
|
|
185
|
-
|
|
197
|
+
// No repos defined — fall back to cwd (same as no-config-file behavior),
|
|
198
|
+
// but still respect flags and hooks from the config.
|
|
199
|
+
const cwd = process.cwd();
|
|
200
|
+
if (!existsSync(cwd)) {
|
|
201
|
+
throw new Error(`Default repo (cwd) does not exist: ${cwd}`);
|
|
202
|
+
}
|
|
203
|
+
if (!isGitRepo(cwd)) {
|
|
204
|
+
throw new Error(`Default repo (cwd) is not a git repository: ${cwd}`);
|
|
205
|
+
}
|
|
206
|
+
return { repos: [{ name: basename(cwd), path: cwd }], flags: parsed.flags, hooks: parsed.hooks };
|
|
186
207
|
}
|
|
187
208
|
|
|
188
209
|
const errors = [];
|
|
@@ -59,7 +59,7 @@ export class PhaseExecutorError extends Error {
|
|
|
59
59
|
* @param {string} opts.safetyHeader - May be empty string
|
|
60
60
|
* @returns {string}
|
|
61
61
|
*/
|
|
62
|
-
export function buildImplementationPrompt({ planContent, phase, repos, safetyHeader, selfCommit = true }) {
|
|
62
|
+
export function buildImplementationPrompt({ planContent, phase, repos, safetyHeader, selfCommit = true, prdContent = '', recentCommits = '' }) {
|
|
63
63
|
const primaryRepos = repos.filter(r => !r.writableOnly);
|
|
64
64
|
const writableDirs = repos.filter(r => r.writableOnly);
|
|
65
65
|
|
|
@@ -71,6 +71,16 @@ export function buildImplementationPrompt({ planContent, phase, repos, safetyHea
|
|
|
71
71
|
writableDirs.map(r => ` - ${r.path}`).join('\n')
|
|
72
72
|
: '';
|
|
73
73
|
|
|
74
|
+
// Build recent commits section (shows what prior phases/tasks already built)
|
|
75
|
+
const recentCommitsSection = recentCommits
|
|
76
|
+
? `\n## Recent commits (for context — do NOT redo this work)\n\n${recentCommits}\n\n`
|
|
77
|
+
: '';
|
|
78
|
+
|
|
79
|
+
// Build PRD section (business context for the implementation)
|
|
80
|
+
const prdSection = prdContent
|
|
81
|
+
? `## Source PRD (business context — understand the WHY behind the plan)\n\n${prdContent.trim()}\n\n---\n\n`
|
|
82
|
+
: '';
|
|
83
|
+
|
|
74
84
|
const closingKey = selfCommit ? 'implementation_closing_commit' : 'implementation_closing_no_commit';
|
|
75
85
|
|
|
76
86
|
return (
|
|
@@ -78,6 +88,8 @@ export function buildImplementationPrompt({ planContent, phase, repos, safetyHea
|
|
|
78
88
|
render('implementation', {
|
|
79
89
|
repoLines,
|
|
80
90
|
writableLines,
|
|
91
|
+
recentCommits: recentCommitsSection,
|
|
92
|
+
prdSection,
|
|
81
93
|
planContent: planContent.trim(),
|
|
82
94
|
phaseTitle: phase.title,
|
|
83
95
|
phaseBody: phase.body.trim(),
|
|
@@ -118,8 +130,10 @@ export async function runImplementation({
|
|
|
118
130
|
send,
|
|
119
131
|
isDryRun,
|
|
120
132
|
selfCommit = true,
|
|
133
|
+
prdContent = '',
|
|
134
|
+
recentCommits = '',
|
|
121
135
|
}) {
|
|
122
|
-
const prompt = buildImplementationPrompt({ planContent, phase, repos, safetyHeader, selfCommit });
|
|
136
|
+
const prompt = buildImplementationPrompt({ planContent, phase, repos, safetyHeader, selfCommit, prdContent, recentCommits });
|
|
123
137
|
const step = logWriter.openStep(phaseNum, taskNum, 'implementation', phase.title);
|
|
124
138
|
|
|
125
139
|
step.writeHeader();
|