agileflow 2.80.0 → 2.81.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.
@@ -36,6 +36,7 @@ const { execSync, spawnSync } = require('child_process');
36
36
  const { c } = require('../lib/colors');
37
37
  const { getProjectRoot } = require('../lib/paths');
38
38
  const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
39
+ const { isValidEpicId, parseIntBounded } = require('../lib/validate');
39
40
 
40
41
  // Read session state
41
42
  function getSessionState(rootDir) {
@@ -103,6 +104,101 @@ function runTests(rootDir, testCommand) {
103
104
  return result;
104
105
  }
105
106
 
107
+ // Get coverage command from metadata or default
108
+ function getCoverageCommand(rootDir) {
109
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
110
+ const result = safeReadJSON(metadataPath, { defaultValue: {} });
111
+
112
+ if (result.ok && result.data?.ralph_loop?.coverage_command) {
113
+ return result.data.ralph_loop.coverage_command;
114
+ }
115
+
116
+ // Default: try common coverage commands
117
+ return 'npm run test:coverage || npm test -- --coverage';
118
+ }
119
+
120
+ // Get coverage report path from metadata or default
121
+ function getCoverageReportPath(rootDir) {
122
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
123
+ const result = safeReadJSON(metadataPath, { defaultValue: {} });
124
+
125
+ if (result.ok && result.data?.ralph_loop?.coverage_report_path) {
126
+ return result.data.ralph_loop.coverage_report_path;
127
+ }
128
+
129
+ return 'coverage/coverage-summary.json';
130
+ }
131
+
132
+ // Parse coverage report (Jest/NYC format)
133
+ function parseCoverageReport(rootDir) {
134
+ const reportPath = getCoverageReportPath(rootDir);
135
+ const fullPath = path.join(rootDir, reportPath);
136
+ const report = safeReadJSON(fullPath, { defaultValue: null });
137
+
138
+ if (!report.ok || !report.data) {
139
+ return { passed: false, coverage: 0, error: 'Coverage report not found at ' + reportPath };
140
+ }
141
+
142
+ // Jest/NYC format: { total: { lines: { pct: 80 }, statements: { pct: 80 } } }
143
+ const total = report.data.total;
144
+ if (!total) {
145
+ return { passed: false, coverage: 0, error: 'Invalid coverage report format' };
146
+ }
147
+
148
+ const coverage = total.lines?.pct || total.statements?.pct || 0;
149
+
150
+ return { passed: true, coverage, raw: report.data };
151
+ }
152
+
153
+ // Verify coverage meets threshold
154
+ function verifyCoverage(rootDir, threshold) {
155
+ const result = parseCoverageReport(rootDir);
156
+
157
+ if (!result.passed) {
158
+ return {
159
+ passed: false,
160
+ coverage: 0,
161
+ message: `${c.red}✗ ${result.error}${c.reset}`,
162
+ };
163
+ }
164
+
165
+ const met = result.coverage >= threshold;
166
+
167
+ return {
168
+ passed: met,
169
+ coverage: result.coverage,
170
+ threshold: threshold,
171
+ message: met
172
+ ? `${c.green}✓ Coverage ${result.coverage.toFixed(1)}% ≥ ${threshold}%${c.reset}`
173
+ : `${c.yellow}⏳ Coverage ${result.coverage.toFixed(1)}% < ${threshold}% (need ${(threshold - result.coverage).toFixed(1)}% more)${c.reset}`,
174
+ };
175
+ }
176
+
177
+ // Run coverage command
178
+ function runCoverage(rootDir) {
179
+ const coverageCmd = getCoverageCommand(rootDir);
180
+ const result = { passed: false, output: '', duration: 0 };
181
+ const startTime = Date.now();
182
+
183
+ try {
184
+ const output = execSync(coverageCmd, {
185
+ cwd: rootDir,
186
+ encoding: 'utf8',
187
+ stdio: ['pipe', 'pipe', 'pipe'],
188
+ timeout: 300000, // 5 minute timeout
189
+ });
190
+ result.passed = true;
191
+ result.output = output;
192
+ } catch (e) {
193
+ // Coverage command might fail but still generate report
194
+ result.passed = true; // We'll check the report
195
+ result.output = e.stdout || '' + '\n' + (e.stderr || '');
196
+ }
197
+
198
+ result.duration = Date.now() - startTime;
199
+ return result;
200
+ }
201
+
106
202
  // Get screenshots directory from metadata or default
107
203
  function getScreenshotsDir(rootDir) {
108
204
  try {
@@ -253,13 +349,18 @@ function handleLoop(rootDir) {
253
349
  const iteration = (loop.iteration || 0) + 1;
254
350
  const maxIterations = loop.max_iterations || 20;
255
351
  const visualMode = loop.visual_mode || false;
256
- const minIterations = visualMode ? 2 : 1; // Visual mode requires at least 2 iterations
352
+ const coverageMode = loop.coverage_mode || false;
353
+ const coverageThreshold = loop.coverage_threshold || 80;
354
+ // Visual and Coverage modes require at least 2 iterations for confirmation
355
+ const minIterations = visualMode || coverageMode ? 2 : 1;
257
356
 
258
357
  console.log('');
259
358
  console.log(
260
359
  `${c.brand}${c.bold}══════════════════════════════════════════════════════════${c.reset}`
261
360
  );
262
- const modeLabel = visualMode ? ' [VISUAL MODE]' : '';
361
+ let modeLabel = '';
362
+ if (visualMode) modeLabel += ' [VISUAL]';
363
+ if (coverageMode) modeLabel += ` [COVERAGE ≥${coverageThreshold}%]`;
263
364
  console.log(
264
365
  `${c.brand}${c.bold} RALPH LOOP - Iteration ${iteration}/${maxIterations}${modeLabel}${c.reset}`
265
366
  );
@@ -333,11 +434,34 @@ function handleLoop(rootDir) {
333
434
  }
334
435
  }
335
436
 
336
- // Visual Mode: Enforce minimum iterations
337
- if (visualMode && iteration < minIterations) {
437
+ // Coverage Mode: Run coverage and verify threshold
438
+ let coverageResult = { passed: true };
439
+ if (coverageMode) {
440
+ console.log('');
441
+ console.log(`${c.blue}Running coverage check...${c.reset}`);
442
+ runCoverage(rootDir);
443
+ coverageResult = verifyCoverage(rootDir, coverageThreshold);
444
+ console.log(coverageResult.message);
445
+
446
+ // Update state with current coverage
447
+ state.ralph_loop.coverage_current = coverageResult.coverage;
448
+
449
+ if (coverageResult.passed) {
450
+ state.ralph_loop.coverage_verified = true;
451
+ } else {
452
+ state.ralph_loop.coverage_verified = false;
453
+ }
454
+ }
455
+
456
+ // Enforce minimum iterations for Visual and Coverage modes
457
+ if ((visualMode || coverageMode) && iteration < minIterations) {
458
+ const modeNames = [];
459
+ if (visualMode) modeNames.push('Visual');
460
+ if (coverageMode) modeNames.push('Coverage');
461
+
338
462
  console.log('');
339
463
  console.log(
340
- `${c.yellow}⚠ Visual Mode requires ${minIterations}+ iterations for confirmation${c.reset}`
464
+ `${c.yellow}⚠ ${modeNames.join(' + ')} Mode requires ${minIterations}+ iterations for confirmation${c.reset}`
341
465
  );
342
466
  console.log(
343
467
  `${c.dim}Current: iteration ${iteration}. Let loop run once more to confirm.${c.reset}`
@@ -347,23 +471,34 @@ function handleLoop(rootDir) {
347
471
  saveSessionState(rootDir, state);
348
472
 
349
473
  console.log('');
350
- console.log(`${c.brand}▶ Continue reviewing. Loop will verify again.${c.reset}`);
474
+ console.log(`${c.brand}▶ Continue working. Loop will verify again.${c.reset}`);
351
475
  return;
352
476
  }
353
477
 
354
- // Check if both tests AND screenshots (in visual mode) passed
355
- const canComplete = testResult.passed && (!visualMode || screenshotResult.passed);
478
+ // Check if all verification modes passed
479
+ const canComplete =
480
+ testResult.passed &&
481
+ (!visualMode || screenshotResult.passed) &&
482
+ (!coverageMode || coverageResult.passed);
356
483
 
357
484
  if (!canComplete) {
358
- // Screenshots not verified yet
485
+ // Something not verified yet
359
486
  state.ralph_loop.iteration = iteration;
360
487
  saveSessionState(rootDir, state);
361
488
 
362
489
  console.log('');
363
- console.log(`${c.cyan}▶ Review unverified screenshots:${c.reset}`);
364
- console.log(`${c.dim} 1. View each screenshot in screenshots/ directory${c.reset}`);
365
- console.log(`${c.dim} 2. Rename verified files with 'verified-' prefix${c.reset}`);
366
- console.log(`${c.dim} 3. Loop will re-verify when you stop${c.reset}`);
490
+ if (visualMode && !screenshotResult.passed) {
491
+ console.log(`${c.cyan} Review unverified screenshots:${c.reset}`);
492
+ console.log(`${c.dim} 1. View each screenshot in screenshots/ directory${c.reset}`);
493
+ console.log(`${c.dim} 2. Rename verified files with 'verified-' prefix${c.reset}`);
494
+ console.log(`${c.dim} 3. Loop will re-verify when you stop${c.reset}`);
495
+ }
496
+ if (coverageMode && !coverageResult.passed) {
497
+ console.log(`${c.cyan}▶ Increase test coverage:${c.reset}`);
498
+ console.log(`${c.dim} Current: ${coverageResult.coverage?.toFixed(1) || 0}%${c.reset}`);
499
+ console.log(`${c.dim} Target: ${coverageThreshold}%${c.reset}`);
500
+ console.log(`${c.dim} Write more tests to cover uncovered code paths.${c.reset}`);
501
+ }
367
502
  return;
368
503
  }
369
504
  console.log('');
@@ -458,7 +593,10 @@ function handleCLI() {
458
593
  if (!loop || !loop.enabled) {
459
594
  console.log(`${c.dim}Ralph Loop: not active${c.reset}`);
460
595
  } else {
461
- const modeLabel = loop.visual_mode ? ` ${c.cyan}[VISUAL]${c.reset}` : '';
596
+ let modeLabel = '';
597
+ if (loop.visual_mode) modeLabel += ` ${c.cyan}[VISUAL]${c.reset}`;
598
+ if (loop.coverage_mode)
599
+ modeLabel += ` ${c.magenta}[COVERAGE ≥${loop.coverage_threshold}%]${c.reset}`;
462
600
  console.log(`${c.green}Ralph Loop: active${c.reset}${modeLabel}`);
463
601
  console.log(` Epic: ${loop.epic}`);
464
602
  console.log(` Current Story: ${loop.current_story}`);
@@ -469,6 +607,15 @@ function handleCLI() {
469
607
  : `${c.yellow}no${c.reset}`;
470
608
  console.log(` Screenshots Verified: ${verified}`);
471
609
  }
610
+ if (loop.coverage_mode) {
611
+ const verified = loop.coverage_verified
612
+ ? `${c.green}yes${c.reset}`
613
+ : `${c.yellow}no${c.reset}`;
614
+ console.log(
615
+ ` Coverage: ${(loop.coverage_current || 0).toFixed(1)}% / ${loop.coverage_threshold}% (Verified: ${verified})`
616
+ );
617
+ console.log(` Baseline: ${(loop.coverage_baseline || 0).toFixed(1)}%`);
618
+ }
472
619
  }
473
620
  return true;
474
621
  }
@@ -499,6 +646,7 @@ function handleCLI() {
499
646
  const epicArg = args.find(a => a.startsWith('--epic='));
500
647
  const maxArg = args.find(a => a.startsWith('--max='));
501
648
  const visualArg = args.includes('--visual') || args.includes('-v');
649
+ const coverageArg = args.find(a => a.startsWith('--coverage='));
502
650
 
503
651
  if (!epicArg) {
504
652
  console.log(`${c.red}Error: --epic=EP-XXXX is required${c.reset}`);
@@ -506,9 +654,28 @@ function handleCLI() {
506
654
  }
507
655
 
508
656
  const epicId = epicArg.split('=')[1];
509
- const maxIterations = maxArg ? parseInt(maxArg.split('=')[1]) : 20;
657
+
658
+ // Validate epic ID format
659
+ if (!isValidEpicId(epicId)) {
660
+ console.log(`${c.red}Error: Invalid epic ID "${epicId}". Expected format: EP-XXXX${c.reset}`);
661
+ return true;
662
+ }
663
+
664
+ // Validate and bound max iterations (1-100)
665
+ const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 20, 1, 100);
510
666
  const visualMode = visualArg;
511
667
 
668
+ // Parse coverage threshold (0-100)
669
+ let coverageMode = false;
670
+ let coverageThreshold = 80;
671
+ if (coverageArg) {
672
+ coverageMode = true;
673
+ const threshold = parseFloat(coverageArg.split('=')[1]);
674
+ if (!isNaN(threshold)) {
675
+ coverageThreshold = Math.max(0, Math.min(100, threshold));
676
+ }
677
+ }
678
+
512
679
  // Find first ready story in epic
513
680
  const status = getStatus(rootDir);
514
681
  const stories = status.stories || {};
@@ -532,6 +699,18 @@ function handleCLI() {
532
699
  // Mark first story as in_progress
533
700
  markStoryInProgress(rootDir, storyId);
534
701
 
702
+ // Get baseline coverage if coverage mode is enabled
703
+ let coverageBaseline = 0;
704
+ if (coverageMode) {
705
+ console.log(`${c.dim}Running baseline coverage check...${c.reset}`);
706
+ runCoverage(rootDir);
707
+ const baseline = parseCoverageReport(rootDir);
708
+ if (baseline.passed) {
709
+ coverageBaseline = baseline.coverage;
710
+ console.log(`${c.dim}Baseline coverage: ${coverageBaseline.toFixed(1)}%${c.reset}`);
711
+ }
712
+ }
713
+
535
714
  // Initialize loop state
536
715
  const state = getSessionState(rootDir);
537
716
  state.ralph_loop = {
@@ -542,6 +721,11 @@ function handleCLI() {
542
721
  max_iterations: maxIterations,
543
722
  visual_mode: visualMode,
544
723
  screenshots_verified: false,
724
+ coverage_mode: coverageMode,
725
+ coverage_threshold: coverageThreshold,
726
+ coverage_baseline: coverageBaseline,
727
+ coverage_current: coverageBaseline,
728
+ coverage_verified: false,
545
729
  started_at: new Date().toISOString(),
546
730
  };
547
731
  saveSessionState(rootDir, state);
@@ -549,7 +733,9 @@ function handleCLI() {
549
733
  const progress = getEpicProgress(status, epicId);
550
734
 
551
735
  console.log('');
552
- const modeLabel = visualMode ? ` ${c.cyan}[VISUAL MODE]${c.reset}` : '';
736
+ let modeLabel = '';
737
+ if (visualMode) modeLabel += ` ${c.cyan}[VISUAL]${c.reset}`;
738
+ if (coverageMode) modeLabel += ` ${c.magenta}[COVERAGE ≥${coverageThreshold}%]${c.reset}`;
553
739
  console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}${modeLabel}`);
554
740
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
555
741
  console.log(` Epic: ${c.cyan}${epicId}${c.reset}`);
@@ -557,6 +743,14 @@ function handleCLI() {
557
743
  console.log(` Max Iterations: ${maxIterations}`);
558
744
  if (visualMode) {
559
745
  console.log(` Visual Mode: ${c.cyan}enabled${c.reset} (screenshot verification)`);
746
+ }
747
+ if (coverageMode) {
748
+ console.log(
749
+ ` Coverage Mode: ${c.magenta}enabled${c.reset} (threshold: ${coverageThreshold}%)`
750
+ );
751
+ console.log(` Baseline: ${coverageBaseline.toFixed(1)}%`);
752
+ }
753
+ if (visualMode || coverageMode) {
560
754
  console.log(` Min Iterations: 2 (for confirmation)`);
561
755
  }
562
756
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
@@ -585,17 +779,19 @@ function handleCLI() {
585
779
  ${c.brand}${c.bold}ralph-loop.js${c.reset} - Autonomous Story Processing
586
780
 
587
781
  ${c.bold}Usage:${c.reset}
588
- node scripts/ralph-loop.js Run loop check (Stop hook)
589
- node scripts/ralph-loop.js --init --epic=EP-XXX Initialize loop for epic
590
- node scripts/ralph-loop.js --init --epic=EP-XXX --visual Initialize with Visual Mode
591
- node scripts/ralph-loop.js --status Check loop status
592
- node scripts/ralph-loop.js --stop Stop the loop
593
- node scripts/ralph-loop.js --reset Reset loop state
782
+ node scripts/ralph-loop.js Run loop check (Stop hook)
783
+ node scripts/ralph-loop.js --init --epic=EP-XXX Initialize loop for epic
784
+ node scripts/ralph-loop.js --init --epic=EP-XXX --visual Initialize with Visual Mode
785
+ node scripts/ralph-loop.js --init --epic=EP-XXX --coverage=80 Initialize with Coverage Mode
786
+ node scripts/ralph-loop.js --status Check loop status
787
+ node scripts/ralph-loop.js --stop Stop the loop
788
+ node scripts/ralph-loop.js --reset Reset loop state
594
789
 
595
790
  ${c.bold}Options:${c.reset}
596
791
  --epic=EP-XXXX Epic ID to process (required for --init)
597
792
  --max=N Max iterations (default: 20)
598
793
  --visual, -v Enable Visual Mode (screenshot verification)
794
+ --coverage=N Enable Coverage Mode (iterate until N% coverage)
599
795
 
600
796
  ${c.bold}Visual Mode:${c.reset}
601
797
  When --visual is enabled, the loop also verifies that all screenshots
@@ -604,16 +800,24 @@ ${c.bold}Visual Mode:${c.reset}
604
800
  This ensures Claude actually looks at UI screenshots before declaring
605
801
  completion. Requires minimum 2 iterations for confirmation.
606
802
 
803
+ ${c.bold}Coverage Mode:${c.reset}
804
+ When --coverage=N is enabled, the loop verifies test coverage meets
805
+ the threshold N% before completing stories.
806
+
807
+ Coverage is read from coverage/coverage-summary.json (Jest/NYC format).
808
+ Configure in docs/00-meta/agileflow-metadata.json:
809
+ { "ralph_loop": { "coverage_command": "npm run test:coverage" } }
810
+
607
811
  Workflow:
608
812
  1. Tests run → must pass
609
- 2. Screenshots verifiedall must have 'verified-' prefix
610
- 3. Minimum 2 iterations → prevents premature completion
813
+ 2. Coverage checked → must meet threshold
814
+ 3. Minimum 2 iterations → confirms coverage is stable
611
815
  4. Only then → story marked complete
612
816
 
613
817
  ${c.bold}How it works:${c.reset}
614
- 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop VISUAL=true
818
+ 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop COVERAGE=80
615
819
  2. Work on the current story
616
- 3. When you stop, this hook runs tests (and screenshot verification in Visual Mode)
820
+ 3. When you stop, this hook runs tests and verifications
617
821
  4. If all pass → story marked complete, next story loaded
618
822
  5. If any fail → failures shown, you continue fixing
619
823
  6. Loop repeats until epic done or max iterations