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.
@@ -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: section.lines.join('\n').trimEnd(),
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
- For **backend code** (server logic, API routes, business logic, database layers, services, data models, utilities):
27
- Follow a strict red → green → refactor cycle, one behaviour at a time:
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
- 1. **RED** Write one failing test for a single behaviour. Run it to confirm it fails before writing any production code.
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
- Work in tracer-bullet style: one thin slice end-to-end before moving to the next.
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
- **Frontend code** (UI components, CSS, HTML, browser JS, view templates) is exempt implement it directly without the TDD cycle.
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 in the format: "ralph: <short imperative summary>" followed by a blank line and a bullet list describing what changed and why. Then output a brief summary of what you changed.
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.
@@ -14,7 +14,7 @@ Your job is to fix exactly the issues listed in the failure notes and nothing mo
14
14
 
15
15
  ---
16
16
 
17
- ## Full plan (for context)
17
+ {{prdSection}}## Full plan (for context)
18
18
 
19
19
  {{planContent}}
20
20
 
@@ -14,7 +14,7 @@ You are Ralph's verification agent. Your only job is to check whether the implem
14
14
 
15
15
  ---
16
16
 
17
- ## Full plan (for context)
17
+ {{prdSection}}## Full plan (for context)
18
18
 
19
19
  {{planContent}}
20
20
 
@@ -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
- writeFileSync(stateFilePath(planPath), JSON.stringify(state, null, 2) + '\n', 'utf8');
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
  /**
@@ -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
- // Try to trim everything up to and including the repo root segment
74
- const idx = p.indexOf('/mazadLive-backend/');
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
  /**
@@ -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({