ralph-prd 1.1.2 → 3.0.1

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.
@@ -187,6 +187,29 @@ function ts() {
187
187
  return new Date().toTimeString().slice(0, 8);
188
188
  }
189
189
 
190
+ /**
191
+ * Gather the last N commits (oneline) from each primary repo.
192
+ * Returns a markdown string suitable for injection into prompts.
193
+ *
194
+ * @param {import('./lib/config.mjs').Repo[]} repos
195
+ * @param {number} [count=5]
196
+ * @returns {string}
197
+ */
198
+ function gatherRecentCommits(repos, count = 5) {
199
+ const primaryRepos = repos.filter(r => !r.writableOnly);
200
+ const sections = [];
201
+ for (const repo of primaryRepos) {
202
+ const result = spawnSync('git', ['log', `--oneline`, `-${count}`], {
203
+ cwd: repo.path, encoding: 'utf8',
204
+ });
205
+ const log = (result.stdout ?? '').trim();
206
+ if (log) {
207
+ sections.push(`### ${repo.name}\n\`\`\`\n${log}\n\`\`\``);
208
+ }
209
+ }
210
+ return sections.length > 0 ? sections.join('\n\n') : '';
211
+ }
212
+
190
213
  /** Pause execution until the user presses Enter. */
191
214
  function waitForUser(message = 'Press Enter to continue…') {
192
215
  return new Promise(resolve => {
@@ -202,12 +225,17 @@ function waitForUser(message = 'Press Enter to continue…') {
202
225
  * @param {string} message
203
226
  */
204
227
  function notify(title, message) {
205
- if (process.platform !== 'darwin') return;
206
- const safeTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
207
- const safeMsg = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
208
- spawnSync('osascript', ['-e', `display notification "${safeMsg}" with title "${safeTitle}"`], {
209
- stdio: 'ignore',
210
- });
228
+ if (process.platform === 'darwin') {
229
+ const safeTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
230
+ const safeMsg = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
231
+ spawnSync('osascript', ['-e', `display notification "${safeMsg}" with title "${safeTitle}"`], {
232
+ stdio: 'ignore',
233
+ });
234
+ } else if (process.platform === 'linux') {
235
+ spawnSync('notify-send', [title, message], { stdio: 'ignore' });
236
+ } else {
237
+ process.stderr.write('\x07');
238
+ }
211
239
  }
212
240
 
213
241
  /**
@@ -335,6 +363,23 @@ async function main() {
335
363
  process.exit(1);
336
364
  }
337
365
 
366
+ // Resolve PRD content from the plan's `> Source PRD:` line (if present)
367
+ let prdContent = '';
368
+ const prdMatch = planContent.match(/^>\s*Source PRD:\s*(.+)$/m);
369
+ if (prdMatch) {
370
+ const prdRelPath = prdMatch[1].trim();
371
+ const prdAbsPath = resolve(dirname(planPath), prdRelPath);
372
+ if (existsSync(prdAbsPath)) {
373
+ prdContent = readFileSync(prdAbsPath, 'utf8');
374
+ } else {
375
+ // Try relative to plan's parent directory (docs/<id>/PRD.md next to plan.md)
376
+ const siblingPath = resolve(dirname(planPath), 'PRD.md');
377
+ if (existsSync(siblingPath)) {
378
+ prdContent = readFileSync(siblingPath, 'utf8');
379
+ }
380
+ }
381
+ }
382
+
338
383
  // Resolve repos + config flags (validates config + git repos)
339
384
  let repos;
340
385
  let configFlags;
@@ -504,62 +549,199 @@ async function main() {
504
549
 
505
550
  console.log(`\n[${ts()}] Phase ${phaseNum}/${phases.length}: ${phase.title}`);
506
551
 
552
+ const tasks = phase.tasks ?? [{ index: 0, description: phase.body, acceptanceCriteria: phase.acceptanceCriteria }];
553
+ const isMultiTask = tasks.length > 1;
554
+
507
555
  // ── Check for mid-phase checkpoint (crash recovery) ─────────────────────
508
556
  const cp = state.checkpoint;
509
557
  const resuming = cp && cp.phaseIndex === phase.index;
510
558
  const resumeAfter = resuming ? cp.step : null; // step already completed
559
+ const resumeTaskIndex = resuming ? (cp.completedTaskIndex ?? -1) : -1;
511
560
  if (resuming) {
512
561
  taskNum = cp.taskNum ?? taskNum;
513
562
  console.log(` [${ts()}] resuming after "${resumeAfter}" (checkpoint found)`);
514
563
  }
515
564
 
516
- // ── Implementation session ──────────────────────────────────────────────
565
+ // ── Task-level implementation + commit loop ─────────────────────────────
566
+ // Each task gets a focused implementation session + commit. Verification
567
+ // and ship-check run once per phase after all tasks are done.
517
568
  let implementationOutput = resuming ? (cp.implementationOutput ?? '') : '';
518
- if (resumeAfter === null) {
519
- // No checkpoint — run implementation from scratch
520
- process.stdout.write(` [${ts()}] implementation… `);
521
- try {
522
- implementationOutput = await runImplementation({
523
- planContent,
524
- phase,
525
- repos,
526
- safetyHeader,
527
- logWriter,
528
- phaseNum,
529
- taskNum: taskNum++,
530
- send,
531
- isDryRun: false,
532
- selfCommit: !iDidThis,
533
- });
534
- console.log('ok');
535
- } catch (err) {
536
- console.log('failed');
537
- const msg = err instanceof PhaseExecutorError
538
- ? `Phase "${err.phaseName}" failed at step "${err.step}": ${err.message}`
539
- : `Unexpected error: ${err.message}`;
540
- console.error(`\n${msg}`);
541
- console.error(`Logs: ${logsDir}`);
542
- notify('Ralph — failed', msg);
543
- process.exit(1);
544
- }
569
+ let allTasksDone = resumeAfter === 'verification' || resumeAfter === 'commit';
545
570
 
546
- // Checkpoint: implementation done — save so we can skip it on crash
547
- if (onlyPhase === null) {
548
- saveCheckpoint(planPath, {
549
- phaseIndex: phase.index,
550
- step: 'implementation',
551
- implementationOutput,
552
- taskNum,
553
- });
571
+ if (!allTasksDone) {
572
+ if (isMultiTask) {
573
+ console.log(` [${ts()}] ${tasks.length} tasks in this phase`);
554
574
  }
555
- } else {
556
- console.log(` [${ts()}] implementation… skipped (checkpoint)`);
575
+
576
+ for (let ti = 0; ti < tasks.length; ti++) {
577
+ const task = tasks[ti];
578
+
579
+ // Skip already-completed tasks (crash recovery)
580
+ if (ti <= resumeTaskIndex) {
581
+ if (isMultiTask) console.log(` [${ts()}] task ${ti + 1}/${tasks.length}… skipped (checkpoint)`);
582
+ continue;
583
+ }
584
+ // If resuming on the task after the last completed one and implementation
585
+ // was checkpointed, skip the implementation step for this task
586
+ const isResumeTask = resuming && ti === resumeTaskIndex + 1;
587
+ const skipImplementation = isResumeTask && resumeAfter === 'implementation';
588
+
589
+ if (isMultiTask) {
590
+ const taskPreview = task.description.split('\n')[0].slice(0, 80);
591
+ console.log(` [${ts()}] task ${ti + 1}/${tasks.length}: ${taskPreview}`);
592
+ }
593
+
594
+ // ── Implementation ────────────────────────────────────────────────
595
+ if (!skipImplementation) {
596
+ process.stdout.write(` [${ts()}] implementation… `);
597
+ try {
598
+ const recentCommits = gatherRecentCommits(repos);
599
+ // Build a focused phase-like object for this task
600
+ const taskPhaseBody = isMultiTask
601
+ ? `${phase.body}\n\n---\n\n**Current task (${ti + 1}/${tasks.length}) — implement ONLY this task:**\n\n${task.description}`
602
+ : phase.body;
603
+
604
+ implementationOutput = await runImplementation({
605
+ planContent,
606
+ phase: { ...phase, body: taskPhaseBody },
607
+ repos,
608
+ safetyHeader,
609
+ logWriter,
610
+ phaseNum,
611
+ taskNum: taskNum++,
612
+ send,
613
+ isDryRun: false,
614
+ selfCommit: !iDidThis,
615
+ prdContent,
616
+ recentCommits,
617
+ });
618
+ console.log('ok');
619
+ } catch (err) {
620
+ console.log('failed');
621
+ const msg = err instanceof PhaseExecutorError
622
+ ? `Phase "${err.phaseName}" failed at step "${err.step}": ${err.message}`
623
+ : `Unexpected error: ${err.message}`;
624
+ console.error(`\n${msg}`);
625
+ console.error(`Logs: ${logsDir}`);
626
+ notify('Ralph — failed', msg);
627
+ process.exit(1);
628
+ }
629
+
630
+ // Checkpoint: task implementation done
631
+ if (onlyPhase === null) {
632
+ saveCheckpoint(planPath, {
633
+ phaseIndex: phase.index,
634
+ step: 'implementation',
635
+ implementationOutput,
636
+ taskNum,
637
+ completedTaskIndex: ti - 1,
638
+ });
639
+ }
640
+ } else {
641
+ console.log(` [${ts()}] implementation… skipped (checkpoint)`);
642
+ }
643
+
644
+ // ── Per-task commit ───────────────────────────────────────────────
645
+ if (!iDidThis) {
646
+ const uncommitted = await scanChangedRepos(repos);
647
+ if (uncommitted.length === 0) {
648
+ console.log(` [${ts()}] commit… done by Claude`);
649
+ } else {
650
+ console.log(` [${ts()}] commit… Claude did not commit, falling back to structured commit`);
651
+ try {
652
+ const { nextTaskNum, anyCommitted } = await runCommitStep({
653
+ phase,
654
+ repos,
655
+ safetyHeader,
656
+ logWriter,
657
+ phaseNum,
658
+ taskNum,
659
+ send,
660
+ });
661
+ taskNum = nextTaskNum;
662
+ if (anyCommitted) {
663
+ console.log(` [${ts()}] fallback commit… ok`);
664
+ } else {
665
+ console.log(` [${ts()}] fallback commit… skipped (no changes)`);
666
+ }
667
+ } catch (err) {
668
+ const msg = err instanceof CommitError
669
+ ? `Phase "${err.phaseName}" fallback commit failed: ${err.message}`
670
+ : `Unexpected error during fallback commit: ${err.message}`;
671
+ console.error(`\n${msg}`);
672
+ console.error(`Logs: ${logsDir}`);
673
+ notify('Ralph — failed', msg);
674
+ process.exit(1);
675
+ }
676
+ }
677
+ } else {
678
+ if (waitForIt) {
679
+ await waitForUser(`\n [wait-for-it] Task ${ti + 1}/${tasks.length} ready to commit. Press Enter to proceed… `);
680
+ }
681
+ process.stdout.write(` [${ts()}] commit… `);
682
+ try {
683
+ const { nextTaskNum, anyCommitted } = await runCommitStep({
684
+ phase,
685
+ repos,
686
+ safetyHeader,
687
+ logWriter,
688
+ phaseNum,
689
+ taskNum,
690
+ send,
691
+ });
692
+ taskNum = nextTaskNum;
693
+ if (anyCommitted) {
694
+ console.log('ok');
695
+ } else {
696
+ console.log('skipped (no changes)');
697
+ }
698
+ } catch (err) {
699
+ console.log('failed');
700
+ const msg = err instanceof CommitError
701
+ ? `Phase "${err.phaseName}" commit failed: ${err.message}`
702
+ : `Unexpected error during commit: ${err.message}`;
703
+ console.error(`\n${msg}`);
704
+ console.error(`Logs: ${logsDir}`);
705
+ notify('Ralph — failed', msg);
706
+ process.exit(1);
707
+ }
708
+ }
709
+
710
+ // ── afterCommit hook (per task) ───────────────────────────────────
711
+ if (hooks?.afterCommit) {
712
+ const primaryRepo = repos.find(r => !r.writableOnly);
713
+ process.stdout.write(` [${ts()}] afterCommit hook… `);
714
+ const hookResult = spawnSync(hooks.afterCommit, {
715
+ shell: true,
716
+ cwd: primaryRepo.path,
717
+ encoding: 'utf8',
718
+ stdio: 'inherit',
719
+ });
720
+ if (hookResult.status !== 0) {
721
+ const hookMsg = `afterCommit hook exited with code ${hookResult.status}: ${hooks.afterCommit}`;
722
+ console.error(`\n${hookMsg}`);
723
+ notify('Ralph — failed', hookMsg);
724
+ process.exit(1);
725
+ }
726
+ console.log('ok');
727
+ }
728
+
729
+ // Checkpoint: task fully complete
730
+ if (onlyPhase === null) {
731
+ saveCheckpoint(planPath, {
732
+ phaseIndex: phase.index,
733
+ step: 'implementation',
734
+ implementationOutput,
735
+ taskNum,
736
+ completedTaskIndex: ti,
737
+ });
738
+ }
739
+ } // end task loop
557
740
  }
558
741
 
559
- // ── Verification + repair ───────────────────────────────────────────────
742
+ // ── Phase-level verification + repair (after all tasks) ─────────────────
560
743
  let repairCount = 0;
561
744
  if (resumeAfter === 'verification' || resumeAfter === 'commit') {
562
- // Verification already passed in a previous run
563
745
  console.log(` [${ts()}] verification… skipped (checkpoint)`);
564
746
  } else if (phase.hasVerification) {
565
747
  process.stdout.write(` [${ts()}] verification… `);
@@ -575,6 +757,7 @@ async function main() {
575
757
  startTaskNum: taskNum,
576
758
  send,
577
759
  maxRepairs: configFlags.maxRepairs,
760
+ prdContent,
578
761
  }));
579
762
  console.log('ok');
580
763
  } catch (err) {
@@ -600,7 +783,6 @@ async function main() {
600
783
  }
601
784
  }
602
785
 
603
- // Checkpoint: verification done
604
786
  if (onlyPhase === null) {
605
787
  saveCheckpoint(planPath, {
606
788
  phaseIndex: phase.index,
@@ -613,94 +795,27 @@ async function main() {
613
795
  console.log(` [${ts()}] verification… skipped (no acceptance criteria)`);
614
796
  }
615
797
 
616
- // ── Commit step (only when --i-did-this; otherwise Claude self-committed) ─
617
- if (resumeAfter === 'commit') {
618
- console.log(` [${ts()}] commit… skipped (checkpoint)`);
619
- } else if (!iDidThis) {
620
- // Claude was asked to self-commit during implementation.
621
- // Verify it actually happened — if uncommitted changes remain, fall back
622
- // to the structured commit step so work is never silently lost.
798
+ // ── Phase-level final commit (verification repairs may have changed files) ─
799
+ if (resumeAfter !== 'commit') {
623
800
  const uncommitted = await scanChangedRepos(repos);
624
- if (uncommitted.length === 0) {
625
- console.log(` [${ts()}] commit… done by Claude`);
626
- } else {
627
- console.log(` [${ts()}] commit… Claude did not commit, falling back to structured commit`);
801
+ if (uncommitted.length > 0) {
802
+ process.stdout.write(` [${ts()}] post-verification commit… `);
628
803
  try {
629
804
  const { nextTaskNum, anyCommitted } = await runCommitStep({
630
- phase,
631
- repos,
632
- safetyHeader,
633
- logWriter,
634
- phaseNum,
635
- taskNum,
636
- send,
805
+ phase, repos, safetyHeader, logWriter, phaseNum, taskNum, send,
637
806
  });
638
807
  taskNum = nextTaskNum;
639
- if (anyCommitted) {
640
- console.log(` [${ts()}] fallback commit… ok`);
641
- } else {
642
- console.log(` [${ts()}] fallback commit… skipped (no changes)`);
643
- }
808
+ console.log(anyCommitted ? 'ok' : 'skipped (no changes)');
644
809
  } catch (err) {
645
810
  const msg = err instanceof CommitError
646
- ? `Phase "${err.phaseName}" fallback commit failed: ${err.message}`
647
- : `Unexpected error during fallback commit: ${err.message}`;
811
+ ? `Phase "${err.phaseName}" post-verification commit failed: ${err.message}`
812
+ : `Unexpected error: ${err.message}`;
648
813
  console.error(`\n${msg}`);
649
814
  console.error(`Logs: ${logsDir}`);
650
815
  notify('Ralph — failed', msg);
651
816
  process.exit(1);
652
817
  }
653
818
  }
654
- } else {
655
- if (waitForIt) {
656
- await waitForUser(`\n [wait-for-it] Phase ${phaseNum} ready to commit. Press Enter to proceed… `);
657
- }
658
- process.stdout.write(` [${ts()}] commit… `);
659
- try {
660
- const { nextTaskNum, anyCommitted } = await runCommitStep({
661
- phase,
662
- repos,
663
- safetyHeader,
664
- logWriter,
665
- phaseNum,
666
- taskNum,
667
- send,
668
- });
669
- taskNum = nextTaskNum;
670
- if (anyCommitted) {
671
- console.log('ok');
672
- } else {
673
- console.log('skipped (no changes)');
674
- }
675
- } catch (err) {
676
- console.log('failed');
677
- const msg = err instanceof CommitError
678
- ? `Phase "${err.phaseName}" commit failed: ${err.message}`
679
- : `Unexpected error during commit: ${err.message}`;
680
- console.error(`\n${msg}`);
681
- console.error(`Logs: ${logsDir}`);
682
- notify('Ralph — failed', msg);
683
- process.exit(1);
684
- }
685
- }
686
-
687
- // ── afterCommit hook ──────────────────────────────────────────────────────
688
- if (hooks?.afterCommit) {
689
- const primaryRepo = repos.find(r => !r.writableOnly);
690
- process.stdout.write(` [${ts()}] afterCommit hook… `);
691
- const hookResult = spawnSync(hooks.afterCommit, {
692
- shell: true,
693
- cwd: primaryRepo.path,
694
- encoding: 'utf8',
695
- stdio: 'inherit',
696
- });
697
- if (hookResult.status !== 0) {
698
- const hookMsg = `afterCommit hook exited with code ${hookResult.status}: ${hooks.afterCommit}`;
699
- console.error(`\n${hookMsg}`);
700
- notify('Ralph — failed', hookMsg);
701
- process.exit(1);
702
- }
703
- console.log('ok');
704
819
  }
705
820
 
706
821
  // ── Ship-check ────────────────────────────────────────────────────────────
@@ -24,18 +24,19 @@ describe('runCommitStep', () => {
24
24
  let sendCalled = false;
25
25
  const send = async () => { sendCalled = true; return ''; };
26
26
 
27
- const { anyCommitted, nextStepIndex } = await runCommitStep({
27
+ const { anyCommitted, nextTaskNum } = await runCommitStep({
28
28
  phase: PHASE,
29
29
  repos: [{ name: 'r', path: repoDir }],
30
30
  safetyHeader: '',
31
31
  logWriter: lw,
32
- stepIndex: 3,
32
+ phaseNum: 1,
33
+ taskNum: 3,
33
34
  send,
34
35
  });
35
36
 
36
37
  assert.equal(anyCommitted, false);
37
38
  assert.equal(sendCalled, false);
38
- assert.equal(nextStepIndex, 3); // unchanged when skipped
39
+ assert.equal(nextTaskNum, 3); // unchanged when skipped
39
40
  });
40
41
 
41
42
  test('changed repo → commit session runs, file is committed', async () => {
@@ -53,7 +54,8 @@ describe('runCommitStep', () => {
53
54
  repos: [{ name: 'myrepo', path: repoDir }],
54
55
  safetyHeader: '',
55
56
  logWriter: lw,
56
- stepIndex: 1,
57
+ phaseNum: 1,
58
+ taskNum: 1,
57
59
  send,
58
60
  });
59
61
 
@@ -75,7 +77,8 @@ describe('runCommitStep', () => {
75
77
  repos: [{ name: 'myrepo', path: repoDir }],
76
78
  safetyHeader: '',
77
79
  logWriter: lw,
78
- stepIndex: 1,
80
+ phaseNum: 1,
81
+ taskNum: 1,
79
82
  send,
80
83
  });
81
84
 
@@ -98,7 +101,8 @@ describe('runCommitStep', () => {
98
101
  repos: [{ name: 'r', path: repoDir }],
99
102
  safetyHeader: '',
100
103
  logWriter: lw,
101
- stepIndex: 1,
104
+ phaseNum: 1,
105
+ taskNum: 1,
102
106
  send,
103
107
  });
104
108
 
@@ -125,7 +129,8 @@ describe('runCommitStep', () => {
125
129
  ],
126
130
  safetyHeader: '',
127
131
  logWriter: lw,
128
- stepIndex: 1,
132
+ phaseNum: 1,
133
+ taskNum: 1,
129
134
  send,
130
135
  });
131
136
 
@@ -154,7 +159,8 @@ describe('runCommitStep', () => {
154
159
  repos: [{ name: 'r', path: repoDir }],
155
160
  safetyHeader: '',
156
161
  logWriter: lw,
157
- stepIndex: 1,
162
+ phaseNum: 1,
163
+ taskNum: 1,
158
164
  send,
159
165
  }),
160
166
  (err) => {
@@ -165,7 +171,7 @@ describe('runCommitStep', () => {
165
171
  );
166
172
  });
167
173
 
168
- test('nextStepIndex increments by 1 after commit session', async () => {
174
+ test('nextTaskNum increments by 1 after commit session', async () => {
169
175
  const repoDir = makeTempRepo();
170
176
  writeFileSync(join(repoDir, 'f.txt'), 'f\n');
171
177
 
@@ -175,16 +181,17 @@ describe('runCommitStep', () => {
175
181
  'REPO: r\nFILES:\n- f.txt\nCOMMIT: ralph: commit f',
176
182
  ]);
177
183
 
178
- const { nextStepIndex } = await runCommitStep({
184
+ const { nextTaskNum } = await runCommitStep({
179
185
  phase: PHASE,
180
186
  repos: [{ name: 'r', path: repoDir }],
181
187
  safetyHeader: '',
182
188
  logWriter: lw,
183
- stepIndex: 5,
189
+ phaseNum: 1,
190
+ taskNum: 5,
184
191
  send,
185
192
  });
186
193
 
187
- assert.equal(nextStepIndex, 6);
194
+ assert.equal(nextTaskNum, 6);
188
195
  });
189
196
 
190
197
  });
@@ -76,10 +76,12 @@ describe('config', () => {
76
76
  assert.equal(writableDirs[0].path, docsDir);
77
77
  });
78
78
 
79
- test('config with empty repos section → throws', () => {
79
+ test('config with empty repos section → falls back to cwd', () => {
80
80
  const configDir = makeTempDir();
81
81
  writeFileSync(join(configDir, 'ralph.config.yaml'), 'repos:\n', 'utf8');
82
- assert.throws(() => resolveRepos(configDir));
82
+ const { repos } = resolveRepos(configDir);
83
+ assert.equal(repos.length, 1);
84
+ assert.equal(repos[0].path, process.cwd());
83
85
  });
84
86
 
85
87
  test('no config file → flags all default to false', () => {
@@ -40,28 +40,28 @@ Create a hello.txt file.
40
40
  * Run one complete phase (implementation → verification → commit → mark state).
41
41
  * Returns { success, error } instead of calling process.exit().
42
42
  */
43
- async function runPhase({ phase, planPath, planContent, repos, logWriter, send, stepIndex = 1 }) {
43
+ async function runPhase({ phase, planPath, planContent, repos, logWriter, send, phaseNum = 1, taskNum = 1 }) {
44
44
  const safetyHeader = '';
45
- let si = stepIndex;
45
+ let si = taskNum;
46
46
 
47
47
  // Implementation
48
48
  const implOutput = await runImplementation({
49
49
  planContent, phase, repos, safetyHeader,
50
- logWriter, stepIndex: si++, send, isDryRun: false,
50
+ logWriter, phaseNum, taskNum: si++, send, isDryRun: false,
51
51
  });
52
52
 
53
53
  // Verification (if applicable)
54
54
  if (phase.hasVerification) {
55
- ({ nextStepIndex: si } = await runVerificationLoop({
55
+ ({ nextTaskNum: si } = await runVerificationLoop({
56
56
  planContent, phase, repos, safetyHeader,
57
57
  implementationOutput: implOutput,
58
- logWriter, stepIndex: si, send,
58
+ logWriter, phaseNum, startTaskNum: si, send,
59
59
  }));
60
60
  }
61
61
 
62
62
  // Commit
63
- const { nextStepIndex: nextSi } = await runCommitStep({
64
- phase, repos, safetyHeader, logWriter, stepIndex: si, send,
63
+ const { nextTaskNum: nextSi } = await runCommitStep({
64
+ phase, repos, safetyHeader, logWriter, phaseNum, taskNum: si, send,
65
65
  });
66
66
  si = nextSi;
67
67
 
@@ -69,7 +69,7 @@ async function runPhase({ phase, planPath, planContent, repos, logWriter, send,
69
69
  if (phase.hasVerification) mutateCheckboxes(planPath, phase);
70
70
  markPhaseComplete(planPath, phase.index);
71
71
 
72
- return { nextStepIndex: si };
72
+ return { nextTaskNum: si };
73
73
  }
74
74
 
75
75
  describe('e2e: full single-phase run with fake transport', () => {
@@ -116,9 +116,9 @@ describe('e2e: full single-phase run with fake transport', () => {
116
116
  assert.ok(gitLog.includes('ralph:'), 'git log should contain ralph commit');
117
117
 
118
118
  // Logs: step files exist
119
- assert.ok(existsSync(join(logDir, 'step-1-implementation.log')));
120
- assert.ok(existsSync(join(logDir, 'step-2-verification.log')));
121
- assert.ok(existsSync(join(logDir, 'step-3-commit.log')));
119
+ assert.ok(existsSync(join(logDir, 'phase-1-implementation.log')));
120
+ assert.ok(existsSync(join(logDir, 'phase-1-verification.log')));
121
+ assert.ok(existsSync(join(logDir, 'phase-1-commit.log')));
122
122
  });
123
123
 
124
124
  });
@@ -173,10 +173,10 @@ describe('e2e: repair-loop run (fail → repair → pass)', () => {
173
173
  assert.ok(state.completedPhases.includes(0));
174
174
 
175
175
  // All session logs exist
176
- assert.ok(existsSync(join(logDir, 'step-1-implementation.log')));
177
- assert.ok(existsSync(join(logDir, 'step-2-verification.log')));
178
- assert.ok(existsSync(join(logDir, 'step-3-repair.log')));
179
- assert.ok(existsSync(join(logDir, 'step-4-re-verification.log')));
176
+ assert.ok(existsSync(join(logDir, 'phase-1-implementation.log')));
177
+ assert.ok(existsSync(join(logDir, 'phase-1-verification.log')));
178
+ assert.ok(existsSync(join(logDir, 'phase-1-repair-1.log')));
179
+ assert.ok(existsSync(join(logDir, 'phase-1-re-verification-1.log')));
180
180
  });
181
181
 
182
182
  });
@@ -227,9 +227,9 @@ describe('e2e: double-fail run exits non-zero and preserves logs', () => {
227
227
  assert.ok(!state.completedPhases.includes(0), 'phase should not be marked complete');
228
228
 
229
229
  // Logs must be preserved on disk
230
- assert.ok(existsSync(join(logDir, 'step-1-implementation.log')), 'impl log preserved');
231
- assert.ok(existsSync(join(logDir, 'step-2-verification.log')), 'verify log preserved');
232
- assert.ok(existsSync(join(logDir, 'step-3-repair.log')), 'repair log preserved');
230
+ assert.ok(existsSync(join(logDir, 'phase-1-implementation.log')), 'impl log preserved');
231
+ assert.ok(existsSync(join(logDir, 'phase-1-verification.log')), 'verify log preserved');
232
+ assert.ok(existsSync(join(logDir, 'phase-1-repair-1.log')), 'repair log preserved');
233
233
  });
234
234
 
235
235
  test('resume from first incomplete phase after partial failure', async () => {
@@ -53,13 +53,17 @@ describe('prepareBranch', () => {
53
53
 
54
54
  test('no-op when already on the target branch', async () => {
55
55
  const repoPath = makeTempRepo();
56
+ // Switch to a non-protected branch first (default branch may be main/master)
57
+ execSync('git checkout -b feature-test', { cwd: repoPath, stdio: 'pipe' });
58
+
59
+ const repos = [{ name: 'r', path: repoPath }];
60
+ // Should not throw — already on feature-test
61
+ await prepareBranch(repos, 'feature-test');
62
+
56
63
  const current = execSync('git rev-parse --abbrev-ref HEAD', {
57
64
  cwd: repoPath, encoding: 'utf8',
58
65
  }).trim();
59
-
60
- const repos = [{ name: 'r', path: repoPath }];
61
- // Should not throw
62
- await prepareBranch(repos, current);
66
+ assert.equal(current, 'feature-test');
63
67
  });
64
68
 
65
69
  test('throws GitCoordinatorError for invalid repo path', async () => {