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
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Task
|
|
5
|
+
* @property {number} index - 0-based index within the phase
|
|
6
|
+
* @property {string} description - The task description text
|
|
7
|
+
* @property {string[]} acceptanceCriteria - Subset of criteria relevant to this task (or all if unsplittable)
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
11
|
* @typedef {Object} Phase
|
|
5
12
|
* @property {number} index - 0-based index among executable phases
|
|
@@ -7,6 +14,7 @@ import { readFileSync } from 'fs';
|
|
|
7
14
|
* @property {string} body - Raw lines of the section body joined with newlines
|
|
8
15
|
* @property {string[]} acceptanceCriteria - Criterion text strings (checkbox prefix stripped)
|
|
9
16
|
* @property {boolean} hasVerification - True when at least one criterion exists
|
|
17
|
+
* @property {Task[]} tasks - Individual tasks extracted from "What to build" (at least 1)
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
20
|
/**
|
|
@@ -76,14 +84,109 @@ export function parsePlanContent(content) {
|
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
const body = section.lines.join('\n').trimEnd();
|
|
88
|
+
const tasks = extractTasks(section.lines, acceptanceCriteria);
|
|
89
|
+
|
|
79
90
|
phases.push({
|
|
80
91
|
index: phases.length,
|
|
81
92
|
title: section.title,
|
|
82
|
-
body
|
|
93
|
+
body,
|
|
83
94
|
acceptanceCriteria,
|
|
84
95
|
hasVerification: acceptanceCriteria.length > 0,
|
|
96
|
+
tasks,
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
return { phases };
|
|
89
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract individual tasks from the "What to build" section of a phase.
|
|
105
|
+
*
|
|
106
|
+
* Looks for numbered items (1. / 2. / etc.) or top-level bullet items (- / * )
|
|
107
|
+
* within the "### What to build" section. If the section is a single block of
|
|
108
|
+
* prose with no list items, the entire phase becomes a single task.
|
|
109
|
+
*
|
|
110
|
+
* @param {string[]} lines - All lines of the phase section
|
|
111
|
+
* @param {string[]} allCriteria - All acceptance criteria for the phase
|
|
112
|
+
* @returns {Task[]}
|
|
113
|
+
*/
|
|
114
|
+
function extractTasks(lines, allCriteria) {
|
|
115
|
+
// Find the "What to build" section boundaries
|
|
116
|
+
let inWhatToBuild = false;
|
|
117
|
+
const wtbLines = [];
|
|
118
|
+
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (/^### What to build\b/i.test(line)) {
|
|
121
|
+
inWhatToBuild = true;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (/^### /.test(line)) {
|
|
125
|
+
if (inWhatToBuild) break;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (inWhatToBuild) {
|
|
129
|
+
wtbLines.push(line);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try to split by numbered items (1. / 2. / etc.)
|
|
134
|
+
const numberedItems = splitByPattern(wtbLines, /^\d+\.\s+/);
|
|
135
|
+
if (numberedItems.length > 1) {
|
|
136
|
+
return numberedItems.map((desc, i) => ({
|
|
137
|
+
index: i,
|
|
138
|
+
description: desc,
|
|
139
|
+
acceptanceCriteria: allCriteria, // all criteria visible to each task
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Try to split by top-level bullet items (- or *)
|
|
144
|
+
const bulletItems = splitByPattern(wtbLines, /^[-*]\s+/);
|
|
145
|
+
if (bulletItems.length > 1) {
|
|
146
|
+
return bulletItems.map((desc, i) => ({
|
|
147
|
+
index: i,
|
|
148
|
+
description: desc,
|
|
149
|
+
acceptanceCriteria: allCriteria,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Single block — the whole phase is one task
|
|
154
|
+
const fullDesc = wtbLines.join('\n').trim();
|
|
155
|
+
return [{
|
|
156
|
+
index: 0,
|
|
157
|
+
description: fullDesc || lines.join('\n').trim(),
|
|
158
|
+
acceptanceCriteria: allCriteria,
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Split lines into groups by a leading pattern (numbered items or bullets).
|
|
164
|
+
* Continuation lines (not matching the pattern) are appended to the current item.
|
|
165
|
+
*
|
|
166
|
+
* @param {string[]} lines
|
|
167
|
+
* @param {RegExp} pattern
|
|
168
|
+
* @returns {string[]} Array of task descriptions (empty lines trimmed)
|
|
169
|
+
*/
|
|
170
|
+
function splitByPattern(lines, pattern) {
|
|
171
|
+
const items = [];
|
|
172
|
+
let current = null;
|
|
173
|
+
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
const trimmed = line.trim();
|
|
176
|
+
if (!trimmed) {
|
|
177
|
+
// Blank line — separator between items, or padding
|
|
178
|
+
if (current !== null) current += '\n';
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (pattern.test(trimmed)) {
|
|
182
|
+
if (current !== null) items.push(current.trim());
|
|
183
|
+
current = trimmed;
|
|
184
|
+
} else if (current !== null) {
|
|
185
|
+
current += '\n' + trimmed;
|
|
186
|
+
}
|
|
187
|
+
// Lines before the first matching item are ignored
|
|
188
|
+
}
|
|
189
|
+
if (current !== null) items.push(current.trim());
|
|
190
|
+
|
|
191
|
+
return items.filter(Boolean);
|
|
192
|
+
}
|
|
@@ -24,6 +24,15 @@ For each repository listed above, output a commit plan using EXACTLY this format
|
|
|
24
24
|
DESCRIPTION:
|
|
25
25
|
- <bullet: what changed and why — focus on intent, not mechanics>
|
|
26
26
|
- <add one bullet per logical group of changes>
|
|
27
|
+
DECISIONS:
|
|
28
|
+
- <bullet: key architectural or library choice you made and WHY>
|
|
29
|
+
- <include trade-offs considered, alternatives rejected>
|
|
30
|
+
BLOCKERS:
|
|
31
|
+
- <bullet: dependency not yet available, workaround applied, TODO left>
|
|
32
|
+
- <omit this section entirely if there are no blockers>
|
|
33
|
+
NEXT:
|
|
34
|
+
- <bullet: what the next task or phase needs to know about this work>
|
|
35
|
+
- <mention any setup, patterns, or utilities created that should be reused>
|
|
27
36
|
END_COMMIT
|
|
28
37
|
|
|
29
38
|
Rules:
|
|
@@ -31,5 +40,8 @@ Rules:
|
|
|
31
40
|
- Use paths exactly as shown in the git status output (relative to repo root).
|
|
32
41
|
- COMMIT line: start with "ralph: " then a short imperative verb phrase (≤72 chars total).
|
|
33
42
|
- DESCRIPTION bullets: explain *what* moved or changed and *why*, not line-by-line mechanics.
|
|
43
|
+
- DECISIONS bullets: document choices that future developers (or the next phase) need to understand. Skip if no notable decisions were made.
|
|
44
|
+
- BLOCKERS bullets: flag incomplete work, missing dependencies, or temporary workarounds. Skip if none.
|
|
45
|
+
- NEXT bullets: leave breadcrumbs for whoever works on this codebase next. Skip if nothing notable.
|
|
34
46
|
- If a repository has no files relevant to this phase, output: REPO: <name>\nSKIP
|
|
35
47
|
- Do not output anything outside the structured REPO / END_COMMIT blocks.
|
|
@@ -4,9 +4,9 @@ Your job is to implement exactly the phase described below and nothing more.
|
|
|
4
4
|
## Repositories in scope
|
|
5
5
|
|
|
6
6
|
{{repoLines}}{{writableLines}}
|
|
7
|
-
|
|
7
|
+
{{recentCommits}}
|
|
8
8
|
---
|
|
9
|
-
|
|
9
|
+
{{prdSection}}
|
|
10
10
|
## Full plan (for context)
|
|
11
11
|
|
|
12
12
|
{{planContent}}
|
|
@@ -23,18 +23,24 @@ Your job is to implement exactly the phase described below and nothing more.
|
|
|
23
23
|
|
|
24
24
|
## Implementation approach
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
### Tracer-bullet principle
|
|
27
|
+
|
|
28
|
+
Build one thin vertical slice end-to-end before writing the next. A tracer bullet pierces all layers — route → handler → service → database — for a single behaviour, proving the path works before you widen it. Do not build all routes first, then all handlers, then all services. Build one complete behaviour at a time.
|
|
29
|
+
|
|
30
|
+
### For backend code (server logic, API routes, business logic, database layers, services, data models, utilities)
|
|
31
|
+
|
|
32
|
+
Apply red → green → refactor within each tracer bullet:
|
|
33
|
+
|
|
34
|
+
1. **RED** — Write one failing test for the behaviour this slice delivers. Run it to confirm it fails before writing any production code.
|
|
35
|
+
2. **GREEN** — Write the minimum production code across all layers to make that one test pass. Nothing more.
|
|
36
|
+
3. Repeat from step 1 for the next slice of behaviour.
|
|
37
|
+
4. **REFACTOR** — Clean up without changing behaviour. Re-run tests to confirm they still pass.
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
2. **GREEN** — Write the minimum production code to make that one test pass. Nothing more.
|
|
31
|
-
3. **REFACTOR** — Clean up without changing behaviour. Re-run tests to confirm they still pass.
|
|
32
|
-
4. Repeat for the next behaviour.
|
|
39
|
+
Do NOT write multiple tests upfront. Do NOT build out a full layer before proving a slice works end-to-end.
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
Do NOT write multiple tests upfront. Do NOT write production code before a failing test exists.
|
|
41
|
+
### Frontend code (UI components, CSS, HTML, browser JS, view templates)
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
Exempt from the TDD cycle — implement it directly. Still follow the tracer-bullet principle: one complete UI slice at a time, not all markup then all styles then all scripts.
|
|
38
44
|
|
|
39
45
|
---
|
|
40
46
|
|
|
@@ -1 +1,18 @@
|
|
|
1
|
-
When you are done with all file changes, commit everything with a clear commit message
|
|
1
|
+
When you are done with all file changes, commit everything with a clear commit message using this format:
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
ralph: <short imperative summary>
|
|
5
|
+
|
|
6
|
+
- <what changed and why — one bullet per logical group>
|
|
7
|
+
|
|
8
|
+
Decisions:
|
|
9
|
+
- <key architectural or library choice and WHY — skip if none>
|
|
10
|
+
|
|
11
|
+
Blockers:
|
|
12
|
+
- <dependency not available, workaround applied, TODO left — skip if none>
|
|
13
|
+
|
|
14
|
+
Next:
|
|
15
|
+
- <what the next task/phase needs to know — skip if nothing notable>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then output a brief summary of what you changed.
|
package/ralph/lib/state.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, renameSync } from 'fs';
|
|
2
2
|
import { resolve, dirname, basename } from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,6 +7,7 @@ import { resolve, dirname, basename } from 'path';
|
|
|
7
7
|
* @property {'implementation'|'verification'|'commit'} step - last completed step
|
|
8
8
|
* @property {string} implementationOutput - result text from the implementation session
|
|
9
9
|
* @property {number} taskNum - next taskNum for the phase
|
|
10
|
+
* @property {number} [completedTaskIndex] - 0-based index of the last fully completed task within the phase (-1 = none)
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -52,7 +53,10 @@ export function loadState(planPath) {
|
|
|
52
53
|
* @param {RalphState} state
|
|
53
54
|
*/
|
|
54
55
|
export function saveState(planPath, state) {
|
|
55
|
-
|
|
56
|
+
const file = stateFilePath(planPath);
|
|
57
|
+
const tmp = file + '.tmp';
|
|
58
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
59
|
+
renameSync(tmp, file);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/**
|
package/ralph/lib/transport.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { spawn, spawnSync } from 'child_process';
|
|
22
|
+
import { relative } from 'path';
|
|
22
23
|
|
|
23
24
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -70,9 +71,8 @@ function resolveCLI() {
|
|
|
70
71
|
/** Strip the absolute repo prefix so paths are readable. */
|
|
71
72
|
function shortPath(p) {
|
|
72
73
|
if (!p) return '';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return idx !== -1 ? p.slice(idx + '/mazadLive-backend/'.length) : p;
|
|
74
|
+
const rel = relative(process.cwd(), p);
|
|
75
|
+
return rel.startsWith('..') ? p : rel;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
package/ralph/lib/verifier.mjs
CHANGED
|
@@ -105,13 +105,17 @@ export function gatherRepoState(repos) {
|
|
|
105
105
|
* @param {import('./config.mjs').Repo[]} opts.repos
|
|
106
106
|
* @param {string} opts.implementationOutput - Full text from the implementation session
|
|
107
107
|
* @param {string} opts.safetyHeader
|
|
108
|
+
* @param {string} [opts.prdContent=''] - Raw PRD markdown for business context
|
|
108
109
|
* @returns {string}
|
|
109
110
|
*/
|
|
110
|
-
function buildVerificationPrompt({ planContent, phase, repos, implementationOutput, safetyHeader }) {
|
|
111
|
+
function buildVerificationPrompt({ planContent, phase, repos, implementationOutput, safetyHeader, prdContent = '' }) {
|
|
111
112
|
const repoState = gatherRepoState(repos);
|
|
112
113
|
const criteriaList = phase.acceptanceCriteria
|
|
113
114
|
.map((c, i) => ` ${i + 1}. ${c}`)
|
|
114
115
|
.join('\n');
|
|
116
|
+
const prdSection = prdContent
|
|
117
|
+
? `## Source PRD (business context)\n\n${prdContent.trim()}\n\n---\n\n`
|
|
118
|
+
: '';
|
|
115
119
|
|
|
116
120
|
return (
|
|
117
121
|
safetyHeader +
|
|
@@ -119,6 +123,7 @@ function buildVerificationPrompt({ planContent, phase, repos, implementationOutp
|
|
|
119
123
|
phaseTitle: phase.title,
|
|
120
124
|
phaseBody: phase.body.trim(),
|
|
121
125
|
criteriaList,
|
|
126
|
+
prdSection,
|
|
122
127
|
planContent: planContent.trim(),
|
|
123
128
|
repoState,
|
|
124
129
|
implementationOutput: implementationOutput.trim(),
|
|
@@ -136,9 +141,10 @@ function buildVerificationPrompt({ planContent, phase, repos, implementationOutp
|
|
|
136
141
|
* @param {import('./config.mjs').Repo[]} opts.repos
|
|
137
142
|
* @param {string} opts.safetyHeader
|
|
138
143
|
* @param {string} opts.failureNotes
|
|
144
|
+
* @param {string} [opts.prdContent=''] - Raw PRD markdown for business context
|
|
139
145
|
* @returns {string}
|
|
140
146
|
*/
|
|
141
|
-
function buildRepairPrompt({ planContent, phase, repos, safetyHeader, failureNotes }) {
|
|
147
|
+
function buildRepairPrompt({ planContent, phase, repos, safetyHeader, failureNotes, prdContent = '' }) {
|
|
142
148
|
const primaryRepos = repos.filter(r => !r.writableOnly);
|
|
143
149
|
const writableDirs = repos.filter(r => r.writableOnly);
|
|
144
150
|
|
|
@@ -149,6 +155,9 @@ function buildRepairPrompt({ planContent, phase, repos, safetyHeader, failureNot
|
|
|
149
155
|
? '\nAdditional writable directories:\n' +
|
|
150
156
|
writableDirs.map(r => ` - ${r.path}`).join('\n')
|
|
151
157
|
: '';
|
|
158
|
+
const prdSection = prdContent
|
|
159
|
+
? `## Source PRD (business context)\n\n${prdContent.trim()}\n\n---\n\n`
|
|
160
|
+
: '';
|
|
152
161
|
|
|
153
162
|
return (
|
|
154
163
|
safetyHeader +
|
|
@@ -156,6 +165,7 @@ function buildRepairPrompt({ planContent, phase, repos, safetyHeader, failureNot
|
|
|
156
165
|
failureNotes: failureNotes.trim(),
|
|
157
166
|
repoLines,
|
|
158
167
|
writableLines,
|
|
168
|
+
prdSection,
|
|
159
169
|
planContent: planContent.trim(),
|
|
160
170
|
phaseTitle: phase.title,
|
|
161
171
|
phaseBody: phase.body.trim(),
|
|
@@ -277,6 +287,7 @@ async function runSession({ stepName, phaseName, prompt, logWriter, phaseNum, ta
|
|
|
277
287
|
* @param {number} opts.stepIndex
|
|
278
288
|
* @param {Function} opts.send
|
|
279
289
|
* @param {number} [opts.maxRepairs=3] - Maximum repair attempts before giving up
|
|
290
|
+
* @param {string} [opts.prdContent=''] - Raw PRD markdown for business context
|
|
280
291
|
* @returns {Promise<{ nextTaskNum: number }>}
|
|
281
292
|
* @throws {VerificationError} when all repair attempts are exhausted
|
|
282
293
|
*/
|
|
@@ -291,6 +302,7 @@ export async function runVerificationLoop({
|
|
|
291
302
|
startTaskNum,
|
|
292
303
|
send,
|
|
293
304
|
maxRepairs = 3,
|
|
305
|
+
prdContent = '',
|
|
294
306
|
}) {
|
|
295
307
|
let taskNum = startTaskNum;
|
|
296
308
|
let lastFailureNotes = '';
|
|
@@ -298,7 +310,7 @@ export async function runVerificationLoop({
|
|
|
298
310
|
// ── Initial verification ───────────────────────────────────────────────────
|
|
299
311
|
|
|
300
312
|
const initialPrompt = buildVerificationPrompt({
|
|
301
|
-
planContent, phase, repos, implementationOutput, safetyHeader,
|
|
313
|
+
planContent, phase, repos, implementationOutput, safetyHeader, prdContent,
|
|
302
314
|
});
|
|
303
315
|
|
|
304
316
|
const initialText = await runSession({
|
|
@@ -332,7 +344,7 @@ export async function runVerificationLoop({
|
|
|
332
344
|
for (let attempt = 1; attempt <= maxRepairs; attempt++) {
|
|
333
345
|
// Repair
|
|
334
346
|
const repairPrompt = buildRepairPrompt({
|
|
335
|
-
planContent, phase, repos, safetyHeader,
|
|
347
|
+
planContent, phase, repos, safetyHeader, prdContent,
|
|
336
348
|
failureNotes: lastFailureNotes || 'The verifier did not provide specific failure notes.',
|
|
337
349
|
});
|
|
338
350
|
|
|
@@ -350,7 +362,7 @@ export async function runVerificationLoop({
|
|
|
350
362
|
const reVerifyPrompt = buildVerificationPrompt({
|
|
351
363
|
planContent, phase, repos,
|
|
352
364
|
implementationOutput: `(repair attempt ${attempt} completed — see repair-${attempt} log)`,
|
|
353
|
-
safetyHeader,
|
|
365
|
+
safetyHeader, prdContent,
|
|
354
366
|
});
|
|
355
367
|
|
|
356
368
|
const reVerifyText = await runSession({
|