agileflow 2.77.0 → 2.79.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.
Files changed (128) hide show
  1. package/README.md +6 -6
  2. package/package.json +6 -1
  3. package/scripts/agileflow-configure.js +174 -2
  4. package/scripts/agileflow-statusline.sh +171 -78
  5. package/scripts/agileflow-welcome.js +88 -64
  6. package/scripts/auto-self-improve.js +23 -45
  7. package/scripts/check-update.js +35 -42
  8. package/scripts/damage-control/bash-tool-damage-control.js +257 -0
  9. package/scripts/damage-control/edit-tool-damage-control.js +279 -0
  10. package/scripts/damage-control/patterns.yaml +227 -0
  11. package/scripts/damage-control/write-tool-damage-control.js +274 -0
  12. package/scripts/damage-control-bash.js +232 -0
  13. package/scripts/damage-control-edit.js +243 -0
  14. package/scripts/damage-control-write.js +243 -0
  15. package/scripts/obtain-context.js +22 -3
  16. package/scripts/ralph-loop.js +191 -63
  17. package/scripts/screenshot-verifier.js +213 -0
  18. package/scripts/session-manager.js +12 -33
  19. package/src/core/agents/accessibility.md +124 -53
  20. package/src/core/agents/adr-writer.md +192 -52
  21. package/src/core/agents/analytics.md +139 -60
  22. package/src/core/agents/api.md +173 -63
  23. package/src/core/agents/ci.md +139 -57
  24. package/src/core/agents/compliance.md +159 -68
  25. package/src/core/agents/configuration/damage-control.md +356 -0
  26. package/src/core/agents/configuration-damage-control.md +248 -0
  27. package/src/core/agents/database.md +162 -61
  28. package/src/core/agents/datamigration.md +179 -66
  29. package/src/core/agents/design.md +179 -57
  30. package/src/core/agents/devops.md +160 -3
  31. package/src/core/agents/documentation.md +204 -60
  32. package/src/core/agents/epic-planner.md +147 -55
  33. package/src/core/agents/integrations.md +197 -69
  34. package/src/core/agents/mentor.md +158 -57
  35. package/src/core/agents/mobile.md +159 -67
  36. package/src/core/agents/monitoring.md +154 -65
  37. package/src/core/agents/multi-expert.md +115 -43
  38. package/src/core/agents/orchestrator.md +77 -24
  39. package/src/core/agents/performance.md +130 -75
  40. package/src/core/agents/product.md +151 -55
  41. package/src/core/agents/qa.md +162 -74
  42. package/src/core/agents/readme-updater.md +178 -76
  43. package/src/core/agents/refactor.md +148 -95
  44. package/src/core/agents/research.md +143 -72
  45. package/src/core/agents/security.md +154 -65
  46. package/src/core/agents/testing.md +176 -97
  47. package/src/core/agents/ui.md +170 -79
  48. package/src/core/commands/adr/list.md +171 -0
  49. package/src/core/commands/adr/update.md +235 -0
  50. package/src/core/commands/adr/view.md +252 -0
  51. package/src/core/commands/adr.md +207 -50
  52. package/src/core/commands/agent.md +16 -0
  53. package/src/core/commands/assign.md +148 -44
  54. package/src/core/commands/auto.md +18 -1
  55. package/src/core/commands/babysit.md +391 -38
  56. package/src/core/commands/baseline.md +14 -0
  57. package/src/core/commands/blockers.md +170 -51
  58. package/src/core/commands/board.md +144 -66
  59. package/src/core/commands/changelog.md +15 -0
  60. package/src/core/commands/ci.md +179 -69
  61. package/src/core/commands/compress.md +18 -0
  62. package/src/core/commands/configure.md +16 -0
  63. package/src/core/commands/context/export.md +193 -4
  64. package/src/core/commands/context/full.md +191 -18
  65. package/src/core/commands/context/note.md +248 -4
  66. package/src/core/commands/debt.md +17 -0
  67. package/src/core/commands/deploy.md +208 -65
  68. package/src/core/commands/deps.md +15 -0
  69. package/src/core/commands/diagnose.md +16 -0
  70. package/src/core/commands/docs.md +196 -64
  71. package/src/core/commands/epic/list.md +170 -0
  72. package/src/core/commands/epic/view.md +242 -0
  73. package/src/core/commands/epic.md +192 -69
  74. package/src/core/commands/feedback.md +191 -71
  75. package/src/core/commands/handoff.md +162 -48
  76. package/src/core/commands/help.md +9 -0
  77. package/src/core/commands/ideate.md +446 -0
  78. package/src/core/commands/impact.md +16 -0
  79. package/src/core/commands/metrics.md +141 -37
  80. package/src/core/commands/multi-expert.md +77 -0
  81. package/src/core/commands/packages.md +16 -0
  82. package/src/core/commands/pr.md +161 -67
  83. package/src/core/commands/readme-sync.md +16 -0
  84. package/src/core/commands/research/analyze.md +568 -0
  85. package/src/core/commands/research/ask.md +345 -20
  86. package/src/core/commands/research/import.md +562 -19
  87. package/src/core/commands/research/list.md +173 -5
  88. package/src/core/commands/research/view.md +181 -8
  89. package/src/core/commands/retro.md +135 -48
  90. package/src/core/commands/review.md +219 -47
  91. package/src/core/commands/session/end.md +209 -0
  92. package/src/core/commands/session/history.md +210 -0
  93. package/src/core/commands/session/init.md +116 -0
  94. package/src/core/commands/session/new.md +296 -0
  95. package/src/core/commands/session/resume.md +166 -0
  96. package/src/core/commands/session/status.md +166 -0
  97. package/src/core/commands/setup/visual-e2e.md +462 -0
  98. package/src/core/commands/skill/create.md +115 -17
  99. package/src/core/commands/skill/delete.md +117 -0
  100. package/src/core/commands/skill/edit.md +104 -0
  101. package/src/core/commands/skill/list.md +128 -0
  102. package/src/core/commands/skill/test.md +135 -0
  103. package/src/core/commands/skill/upgrade.md +542 -0
  104. package/src/core/commands/sprint.md +17 -1
  105. package/src/core/commands/status.md +133 -21
  106. package/src/core/commands/story/list.md +176 -0
  107. package/src/core/commands/story/view.md +265 -0
  108. package/src/core/commands/story-validate.md +101 -1
  109. package/src/core/commands/story.md +204 -51
  110. package/src/core/commands/template.md +16 -1
  111. package/src/core/commands/tests.md +226 -64
  112. package/src/core/commands/update.md +17 -1
  113. package/src/core/commands/validate-expertise.md +16 -0
  114. package/src/core/commands/velocity.md +140 -36
  115. package/src/core/commands/verify.md +14 -0
  116. package/src/core/commands/whats-new.md +30 -0
  117. package/src/core/skills/_learnings/README.md +91 -0
  118. package/src/core/skills/_learnings/_template.yaml +106 -0
  119. package/src/core/skills/_learnings/code-review.yaml +118 -0
  120. package/src/core/skills/_learnings/commit.yaml +69 -0
  121. package/src/core/skills/_learnings/story-writer.yaml +71 -0
  122. package/src/core/templates/damage-control-patterns.yaml +234 -0
  123. package/src/core/templates/skill-template.md +53 -11
  124. package/tools/cli/commands/start.js +180 -0
  125. package/tools/cli/installers/ide/claude-code.js +127 -0
  126. package/tools/cli/tui/Dashboard.js +66 -0
  127. package/tools/cli/tui/StoryList.js +69 -0
  128. package/tools/cli/tui/index.js +16 -0
@@ -7,12 +7,18 @@
7
7
  * It runs as a Stop hook and handles:
8
8
  * 1. Checking if loop mode is enabled
9
9
  * 2. Running test validation
10
- * 3. Updating story status on success
11
- * 4. Feeding context back for next iteration
12
- * 5. Tracking iterations and enforcing limits
10
+ * 3. Running screenshot verification (Visual Mode)
11
+ * 4. Updating story status on success
12
+ * 5. Feeding context back for next iteration
13
+ * 6. Tracking iterations and enforcing limits
13
14
  *
14
15
  * Named after the "Ralph Wiggum" pattern from Anthropic.
15
16
  *
17
+ * Visual Mode:
18
+ * When visual_mode is enabled, the loop also verifies that all
19
+ * screenshots have been reviewed (verified- prefix). This ensures
20
+ * Claude actually looks at UI screenshots before declaring completion.
21
+ *
16
22
  * Usage (as Stop hook):
17
23
  * node scripts/ralph-loop.js
18
24
  *
@@ -26,78 +32,46 @@ const fs = require('fs');
26
32
  const path = require('path');
27
33
  const { execSync, spawnSync } = require('child_process');
28
34
 
29
- // ANSI colors
30
- const c = {
31
- reset: '\x1b[0m',
32
- bold: '\x1b[1m',
33
- dim: '\x1b[2m',
34
- red: '\x1b[31m',
35
- green: '\x1b[32m',
36
- yellow: '\x1b[33m',
37
- blue: '\x1b[34m',
38
- cyan: '\x1b[36m',
39
- brand: '\x1b[38;2;232;104;58m',
40
- };
41
-
42
- // Find project root
43
- function getProjectRoot() {
44
- let dir = process.cwd();
45
- while (!fs.existsSync(path.join(dir, '.agileflow')) && dir !== '/') {
46
- dir = path.dirname(dir);
47
- }
48
- return dir !== '/' ? dir : process.cwd();
49
- }
35
+ // Shared utilities
36
+ const { c } = require('../lib/colors');
37
+ const { getProjectRoot } = require('../lib/paths');
38
+ const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
50
39
 
51
40
  // Read session state
52
41
  function getSessionState(rootDir) {
53
42
  const statePath = path.join(rootDir, 'docs/09-agents/session-state.json');
54
- try {
55
- if (fs.existsSync(statePath)) {
56
- return JSON.parse(fs.readFileSync(statePath, 'utf8'));
57
- }
58
- } catch (e) {}
59
- return {};
43
+ const result = safeReadJSON(statePath, { defaultValue: {} });
44
+ return result.ok ? result.data : {};
60
45
  }
61
46
 
62
47
  // Write session state
63
48
  function saveSessionState(rootDir, state) {
64
49
  const statePath = path.join(rootDir, 'docs/09-agents/session-state.json');
65
- const dir = path.dirname(statePath);
66
- if (!fs.existsSync(dir)) {
67
- fs.mkdirSync(dir, { recursive: true });
68
- }
69
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
50
+ safeWriteJSON(statePath, state, { createDir: true });
70
51
  }
71
52
 
72
53
  // Read status.json for stories
73
54
  function getStatus(rootDir) {
74
55
  const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
75
- try {
76
- if (fs.existsSync(statusPath)) {
77
- return JSON.parse(fs.readFileSync(statusPath, 'utf8'));
78
- }
79
- } catch (e) {}
80
- return { stories: {}, epics: {} };
56
+ const result = safeReadJSON(statusPath, { defaultValue: { stories: {}, epics: {} } });
57
+ return result.ok ? result.data : { stories: {}, epics: {} };
81
58
  }
82
59
 
83
60
  // Save status.json
84
61
  function saveStatus(rootDir, status) {
85
62
  const statusPath = path.join(rootDir, 'docs/09-agents/status.json');
86
- fs.writeFileSync(statusPath, JSON.stringify(status, null, 2) + '\n');
63
+ safeWriteJSON(statusPath, status);
87
64
  }
88
65
 
89
66
  // Get test command from package.json or metadata
90
67
  function getTestCommand(rootDir) {
91
68
  // Check agileflow metadata first
92
- try {
93
- const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
94
- if (fs.existsSync(metadataPath)) {
95
- const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
96
- if (metadata.ralph_loop?.test_command) {
97
- return metadata.ralph_loop.test_command;
98
- }
99
- }
100
- } catch (e) {}
69
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
70
+ const result = safeReadJSON(metadataPath, { defaultValue: {} });
71
+
72
+ if (result.ok && result.data?.ralph_loop?.test_command) {
73
+ return result.data.ralph_loop.test_command;
74
+ }
101
75
 
102
76
  // Default to npm test
103
77
  return 'npm test';
@@ -129,6 +103,73 @@ function runTests(rootDir, testCommand) {
129
103
  return result;
130
104
  }
131
105
 
106
+ // Get screenshots directory from metadata or default
107
+ function getScreenshotsDir(rootDir) {
108
+ try {
109
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
110
+ if (fs.existsSync(metadataPath)) {
111
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
112
+ if (metadata.ralph_loop?.screenshots_dir) {
113
+ return metadata.ralph_loop.screenshots_dir;
114
+ }
115
+ }
116
+ } catch (e) {}
117
+ return './screenshots';
118
+ }
119
+
120
+ // Run screenshot verification (Visual Mode)
121
+ function verifyScreenshots(rootDir) {
122
+ const result = { passed: false, output: '', total: 0, verified: 0, unverified: [] };
123
+ const screenshotsDir = getScreenshotsDir(rootDir);
124
+ const fullPath = path.resolve(rootDir, screenshotsDir);
125
+
126
+ // Check if directory exists
127
+ if (!fs.existsSync(fullPath)) {
128
+ result.passed = true; // No screenshots = nothing to verify
129
+ result.output = 'No screenshots directory found';
130
+ return result;
131
+ }
132
+
133
+ // Get all image files
134
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
135
+ let files;
136
+ try {
137
+ files = fs.readdirSync(fullPath).filter((file) => {
138
+ const ext = path.extname(file).toLowerCase();
139
+ return imageExtensions.includes(ext);
140
+ });
141
+ } catch (e) {
142
+ result.output = `Error reading screenshots directory: ${e.message}`;
143
+ return result;
144
+ }
145
+
146
+ if (files.length === 0) {
147
+ result.passed = true;
148
+ result.output = 'No screenshots found in directory';
149
+ return result;
150
+ }
151
+
152
+ // Check each file for verified- prefix
153
+ for (const file of files) {
154
+ if (file.startsWith('verified-')) {
155
+ result.verified++;
156
+ } else {
157
+ result.unverified.push(file);
158
+ }
159
+ }
160
+
161
+ result.total = files.length;
162
+ result.passed = result.unverified.length === 0;
163
+
164
+ if (result.passed) {
165
+ result.output = `All ${result.total} screenshots verified`;
166
+ } else {
167
+ result.output = `${result.unverified.length} screenshots missing 'verified-' prefix`;
168
+ }
169
+
170
+ return result;
171
+ }
172
+
132
173
  // Get next ready story in epic
133
174
  function getNextStory(status, epicId, currentStoryId) {
134
175
  const stories = status.stories || {};
@@ -211,13 +252,16 @@ function handleLoop(rootDir) {
211
252
  const status = getStatus(rootDir);
212
253
  const iteration = (loop.iteration || 0) + 1;
213
254
  const maxIterations = loop.max_iterations || 20;
255
+ const visualMode = loop.visual_mode || false;
256
+ const minIterations = visualMode ? 2 : 1; // Visual mode requires at least 2 iterations
214
257
 
215
258
  console.log('');
216
259
  console.log(
217
260
  `${c.brand}${c.bold}══════════════════════════════════════════════════════════${c.reset}`
218
261
  );
262
+ const modeLabel = visualMode ? ' [VISUAL MODE]' : '';
219
263
  console.log(
220
- `${c.brand}${c.bold} RALPH LOOP - Iteration ${iteration}/${maxIterations}${c.reset}`
264
+ `${c.brand}${c.bold} RALPH LOOP - Iteration ${iteration}/${maxIterations}${modeLabel}${c.reset}`
221
265
  );
222
266
  console.log(
223
267
  `${c.brand}${c.bold}══════════════════════════════════════════════════════════${c.reset}`
@@ -261,6 +305,61 @@ function handleLoop(rootDir) {
261
305
 
262
306
  if (testResult.passed) {
263
307
  console.log(`${c.green}✓ Tests passed${c.reset} (${(testResult.duration / 1000).toFixed(1)}s)`);
308
+
309
+ // Visual Mode: Also verify screenshots
310
+ let screenshotResult = { passed: true };
311
+ if (visualMode) {
312
+ console.log('');
313
+ console.log(`${c.blue}Verifying screenshots...${c.reset}`);
314
+ screenshotResult = verifyScreenshots(rootDir);
315
+
316
+ if (screenshotResult.passed) {
317
+ console.log(`${c.green}✓ ${screenshotResult.output}${c.reset}`);
318
+ state.ralph_loop.screenshots_verified = true;
319
+ } else {
320
+ console.log(`${c.yellow}⚠ ${screenshotResult.output}${c.reset}`);
321
+ if (screenshotResult.unverified.length > 0) {
322
+ console.log(`${c.dim}Unverified screenshots:${c.reset}`);
323
+ screenshotResult.unverified.slice(0, 5).forEach((file) => {
324
+ console.log(` ${c.yellow}- ${file}${c.reset}`);
325
+ });
326
+ if (screenshotResult.unverified.length > 5) {
327
+ console.log(` ${c.dim}... and ${screenshotResult.unverified.length - 5} more${c.reset}`);
328
+ }
329
+ }
330
+ state.ralph_loop.screenshots_verified = false;
331
+ }
332
+ }
333
+
334
+ // Visual Mode: Enforce minimum iterations
335
+ if (visualMode && iteration < minIterations) {
336
+ console.log('');
337
+ console.log(`${c.yellow}⚠ Visual Mode requires ${minIterations}+ iterations for confirmation${c.reset}`);
338
+ console.log(`${c.dim}Current: iteration ${iteration}. Let loop run once more to confirm.${c.reset}`);
339
+
340
+ state.ralph_loop.iteration = iteration;
341
+ saveSessionState(rootDir, state);
342
+
343
+ console.log('');
344
+ console.log(`${c.brand}▶ Continue reviewing. Loop will verify again.${c.reset}`);
345
+ return;
346
+ }
347
+
348
+ // Check if both tests AND screenshots (in visual mode) passed
349
+ const canComplete = testResult.passed && (!visualMode || screenshotResult.passed);
350
+
351
+ if (!canComplete) {
352
+ // Screenshots not verified yet
353
+ state.ralph_loop.iteration = iteration;
354
+ saveSessionState(rootDir, state);
355
+
356
+ console.log('');
357
+ console.log(`${c.cyan}▶ Review unverified screenshots:${c.reset}`);
358
+ console.log(`${c.dim} 1. View each screenshot in screenshots/ directory${c.reset}`);
359
+ console.log(`${c.dim} 2. Rename verified files with 'verified-' prefix${c.reset}`);
360
+ console.log(`${c.dim} 3. Loop will re-verify when you stop${c.reset}`);
361
+ return;
362
+ }
264
363
  console.log('');
265
364
 
266
365
  // Mark story complete
@@ -353,10 +452,15 @@ function handleCLI() {
353
452
  if (!loop || !loop.enabled) {
354
453
  console.log(`${c.dim}Ralph Loop: not active${c.reset}`);
355
454
  } else {
356
- console.log(`${c.green}Ralph Loop: active${c.reset}`);
455
+ const modeLabel = loop.visual_mode ? ` ${c.cyan}[VISUAL]${c.reset}` : '';
456
+ console.log(`${c.green}Ralph Loop: active${c.reset}${modeLabel}`);
357
457
  console.log(` Epic: ${loop.epic}`);
358
458
  console.log(` Current Story: ${loop.current_story}`);
359
459
  console.log(` Iteration: ${loop.iteration || 0}/${loop.max_iterations || 20}`);
460
+ if (loop.visual_mode) {
461
+ const verified = loop.screenshots_verified ? `${c.green}yes${c.reset}` : `${c.yellow}no${c.reset}`;
462
+ console.log(` Screenshots Verified: ${verified}`);
463
+ }
360
464
  }
361
465
  return true;
362
466
  }
@@ -386,6 +490,7 @@ function handleCLI() {
386
490
  if (args.some(a => a.startsWith('--init'))) {
387
491
  const epicArg = args.find(a => a.startsWith('--epic='));
388
492
  const maxArg = args.find(a => a.startsWith('--max='));
493
+ const visualArg = args.includes('--visual') || args.includes('-v');
389
494
 
390
495
  if (!epicArg) {
391
496
  console.log(`${c.red}Error: --epic=EP-XXXX is required${c.reset}`);
@@ -394,6 +499,7 @@ function handleCLI() {
394
499
 
395
500
  const epicId = epicArg.split('=')[1];
396
501
  const maxIterations = maxArg ? parseInt(maxArg.split('=')[1]) : 20;
502
+ const visualMode = visualArg;
397
503
 
398
504
  // Find first ready story in epic
399
505
  const status = getStatus(rootDir);
@@ -426,6 +532,8 @@ function handleCLI() {
426
532
  current_story: storyId,
427
533
  iteration: 0,
428
534
  max_iterations: maxIterations,
535
+ visual_mode: visualMode,
536
+ screenshots_verified: false,
429
537
  started_at: new Date().toISOString(),
430
538
  };
431
539
  saveSessionState(rootDir, state);
@@ -433,11 +541,16 @@ function handleCLI() {
433
541
  const progress = getEpicProgress(status, epicId);
434
542
 
435
543
  console.log('');
436
- console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}`);
544
+ const modeLabel = visualMode ? ` ${c.cyan}[VISUAL MODE]${c.reset}` : '';
545
+ console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}${modeLabel}`);
437
546
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
438
547
  console.log(` Epic: ${c.cyan}${epicId}${c.reset}`);
439
548
  console.log(` Stories: ${progress.ready} ready, ${progress.total} total`);
440
549
  console.log(` Max Iterations: ${maxIterations}`);
550
+ if (visualMode) {
551
+ console.log(` Visual Mode: ${c.cyan}enabled${c.reset} (screenshot verification)`);
552
+ console.log(` Min Iterations: 2 (for confirmation)`);
553
+ }
441
554
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
442
555
  console.log('');
443
556
  console.log(`${c.brand}▶ Starting Story:${c.reset} ${storyId}`);
@@ -464,22 +577,37 @@ function handleCLI() {
464
577
  ${c.brand}${c.bold}ralph-loop.js${c.reset} - Autonomous Story Processing
465
578
 
466
579
  ${c.bold}Usage:${c.reset}
467
- node scripts/ralph-loop.js Run loop check (Stop hook)
468
- node scripts/ralph-loop.js --init --epic=EP-XXX Initialize loop for epic
469
- node scripts/ralph-loop.js --status Check loop status
470
- node scripts/ralph-loop.js --stop Stop the loop
471
- node scripts/ralph-loop.js --reset Reset loop state
580
+ node scripts/ralph-loop.js Run loop check (Stop hook)
581
+ node scripts/ralph-loop.js --init --epic=EP-XXX Initialize loop for epic
582
+ node scripts/ralph-loop.js --init --epic=EP-XXX --visual Initialize with Visual Mode
583
+ node scripts/ralph-loop.js --status Check loop status
584
+ node scripts/ralph-loop.js --stop Stop the loop
585
+ node scripts/ralph-loop.js --reset Reset loop state
472
586
 
473
587
  ${c.bold}Options:${c.reset}
474
588
  --epic=EP-XXXX Epic ID to process (required for --init)
475
589
  --max=N Max iterations (default: 20)
590
+ --visual, -v Enable Visual Mode (screenshot verification)
591
+
592
+ ${c.bold}Visual Mode:${c.reset}
593
+ When --visual is enabled, the loop also verifies that all screenshots
594
+ in the screenshots/ directory have been reviewed (verified- prefix).
595
+
596
+ This ensures Claude actually looks at UI screenshots before declaring
597
+ completion. Requires minimum 2 iterations for confirmation.
598
+
599
+ Workflow:
600
+ 1. Tests run → must pass
601
+ 2. Screenshots verified → all must have 'verified-' prefix
602
+ 3. Minimum 2 iterations → prevents premature completion
603
+ 4. Only then → story marked complete
476
604
 
477
605
  ${c.bold}How it works:${c.reset}
478
- 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop
606
+ 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop VISUAL=true
479
607
  2. Work on the current story
480
- 3. When you stop, this hook runs tests
481
- 4. If tests pass → story marked complete, next story loaded
482
- 5. If tests fail → failures shown, you continue fixing
608
+ 3. When you stop, this hook runs tests (and screenshot verification in Visual Mode)
609
+ 4. If all pass → story marked complete, next story loaded
610
+ 5. If any fail → failures shown, you continue fixing
483
611
  6. Loop repeats until epic done or max iterations
484
612
  `);
485
613
  return true;
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * screenshot-verifier.js - Verify all screenshots have been reviewed
5
+ *
6
+ * Part of Visual Mode for UI development. This script checks that all
7
+ * screenshots in a directory have been prefixed with "verified-" to
8
+ * confirm Claude has visually reviewed each one.
9
+ *
10
+ * Usage:
11
+ * node scripts/screenshot-verifier.js # Default: ./screenshots
12
+ * node scripts/screenshot-verifier.js --path ./e2e/shots # Custom path
13
+ * node scripts/screenshot-verifier.js --help # Show help
14
+ *
15
+ * Exit codes:
16
+ * 0 - All screenshots verified (or no screenshots found)
17
+ * 1 - Some screenshots missing verified- prefix
18
+ * 2 - Error (directory not found, etc.)
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // Shared utilities
25
+ const { c } = require('../lib/colors');
26
+
27
+ // Parse command line arguments
28
+ function parseArgs() {
29
+ const args = process.argv.slice(2);
30
+ const options = {
31
+ path: './screenshots',
32
+ help: false,
33
+ quiet: false,
34
+ };
35
+
36
+ for (let i = 0; i < args.length; i++) {
37
+ const arg = args[i];
38
+
39
+ if (arg === '--help' || arg === '-h') {
40
+ options.help = true;
41
+ } else if (arg === '--quiet' || arg === '-q') {
42
+ options.quiet = true;
43
+ } else if (arg === '--path' || arg === '-p') {
44
+ if (args[i + 1]) {
45
+ options.path = args[i + 1];
46
+ i++;
47
+ }
48
+ } else if (!arg.startsWith('-')) {
49
+ // Positional argument - treat as path
50
+ options.path = arg;
51
+ }
52
+ }
53
+
54
+ return options;
55
+ }
56
+
57
+ // Show help message
58
+ function showHelp() {
59
+ console.log(`
60
+ ${c.brand}${c.bold}screenshot-verifier${c.reset} - Verify all screenshots have been reviewed
61
+
62
+ ${c.bold}USAGE${c.reset}
63
+ node screenshot-verifier.js [OPTIONS] [PATH]
64
+
65
+ ${c.bold}OPTIONS${c.reset}
66
+ --path, -p <dir> Directory containing screenshots (default: ./screenshots)
67
+ --quiet, -q Only output on failure
68
+ --help, -h Show this help message
69
+
70
+ ${c.bold}EXAMPLES${c.reset}
71
+ node screenshot-verifier.js # Check ./screenshots
72
+ node screenshot-verifier.js ./tests/e2e/shots # Check custom path
73
+ node screenshot-verifier.js --path ./shots -q # Quiet mode
74
+
75
+ ${c.bold}EXIT CODES${c.reset}
76
+ 0 - All screenshots verified (or no screenshots found)
77
+ 1 - Some screenshots missing 'verified-' prefix
78
+ 2 - Error (directory not found, etc.)
79
+
80
+ ${c.bold}VISUAL MODE${c.reset}
81
+ This script is part of AgileFlow's Visual Mode for UI development.
82
+
83
+ Workflow:
84
+ 1. Playwright tests create screenshots in the target directory
85
+ 2. Claude reviews each screenshot visually
86
+ 3. Claude renames verified screenshots with 'verified-' prefix
87
+ 4. This script confirms all screenshots have been reviewed
88
+
89
+ The 'verified-' prefix ensures Claude actually looked at each image
90
+ rather than declaring completion prematurely.
91
+ `);
92
+ }
93
+
94
+ // Get all image files in directory
95
+ function getImageFiles(dir) {
96
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
97
+
98
+ try {
99
+ const files = fs.readdirSync(dir);
100
+ return files.filter((file) => {
101
+ const ext = path.extname(file).toLowerCase();
102
+ return imageExtensions.includes(ext);
103
+ });
104
+ } catch (err) {
105
+ if (err.code === 'ENOENT') {
106
+ return null; // Directory doesn't exist
107
+ }
108
+ throw err;
109
+ }
110
+ }
111
+
112
+ // Main verification logic
113
+ function verifyScreenshots(options) {
114
+ const dir = path.resolve(options.path);
115
+
116
+ // Check if directory exists
117
+ if (!fs.existsSync(dir)) {
118
+ if (!options.quiet) {
119
+ console.log(`${c.yellow}No screenshots directory found at: ${dir}${c.reset}`);
120
+ console.log(`${c.dim}Create the directory or run tests to generate screenshots.${c.reset}`);
121
+ }
122
+ return { success: true, total: 0, verified: 0, unverified: [] };
123
+ }
124
+
125
+ // Get all image files
126
+ const files = getImageFiles(dir);
127
+
128
+ if (files === null) {
129
+ console.error(`${c.red}Error: Could not read directory: ${dir}${c.reset}`);
130
+ process.exit(2);
131
+ }
132
+
133
+ if (files.length === 0) {
134
+ if (!options.quiet) {
135
+ console.log(`${c.yellow}No screenshots found in: ${dir}${c.reset}`);
136
+ }
137
+ return { success: true, total: 0, verified: 0, unverified: [] };
138
+ }
139
+
140
+ // Check each file for verified- prefix
141
+ const verified = [];
142
+ const unverified = [];
143
+
144
+ for (const file of files) {
145
+ if (file.startsWith('verified-')) {
146
+ verified.push(file);
147
+ } else {
148
+ unverified.push(file);
149
+ }
150
+ }
151
+
152
+ return {
153
+ success: unverified.length === 0,
154
+ total: files.length,
155
+ verified: verified.length,
156
+ unverified,
157
+ };
158
+ }
159
+
160
+ // Format result output
161
+ function formatResult(result, options) {
162
+ if (options.quiet && result.success) {
163
+ return;
164
+ }
165
+
166
+ console.log('');
167
+
168
+ if (result.total === 0) {
169
+ console.log(`${c.yellow}No screenshots to verify${c.reset}`);
170
+ return;
171
+ }
172
+
173
+ if (result.success) {
174
+ console.log(`${c.green}${c.bold}All screenshots verified${c.reset}`);
175
+ console.log(`${c.dim}${result.verified}/${result.total} screenshots have 'verified-' prefix${c.reset}`);
176
+ } else {
177
+ console.log(`${c.red}${c.bold}Unverified screenshots found${c.reset}`);
178
+ console.log(`${c.dim}${result.verified}/${result.total} verified${c.reset}`);
179
+ console.log('');
180
+ console.log(`${c.yellow}Missing 'verified-' prefix:${c.reset}`);
181
+ for (const file of result.unverified) {
182
+ console.log(` ${c.red}- ${file}${c.reset}`);
183
+ }
184
+ console.log('');
185
+ console.log(`${c.cyan}To verify: Review each screenshot visually, then rename:${c.reset}`);
186
+ console.log(`${c.dim} mv screenshots/example.png screenshots/verified-example.png${c.reset}`);
187
+ }
188
+
189
+ console.log('');
190
+ }
191
+
192
+ // Main entry point
193
+ function main() {
194
+ const options = parseArgs();
195
+
196
+ if (options.help) {
197
+ showHelp();
198
+ process.exit(0);
199
+ }
200
+
201
+ const result = verifyScreenshots(options);
202
+ formatResult(result, options);
203
+
204
+ process.exit(result.success ? 0 : 1);
205
+ }
206
+
207
+ // Run if called directly
208
+ if (require.main === module) {
209
+ main();
210
+ }
211
+
212
+ // Export for testing
213
+ module.exports = { verifyScreenshots, getImageFiles };
@@ -13,30 +13,10 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { execSync, spawnSync } = require('child_process');
15
15
 
16
- // ANSI colors
17
- const c = {
18
- reset: '\x1b[0m',
19
- bold: '\x1b[1m',
20
- dim: '\x1b[2m',
21
- red: '\x1b[31m',
22
- green: '\x1b[32m',
23
- yellow: '\x1b[33m',
24
- blue: '\x1b[34m',
25
- cyan: '\x1b[36m',
26
- brand: '\x1b[38;2;232;104;58m',
27
- };
28
-
29
- // Find project root (has .agileflow or .git)
30
- function getProjectRoot() {
31
- let dir = process.cwd();
32
- while (dir !== '/') {
33
- if (fs.existsSync(path.join(dir, '.agileflow')) || fs.existsSync(path.join(dir, '.git'))) {
34
- return dir;
35
- }
36
- dir = path.dirname(dir);
37
- }
38
- return process.cwd();
39
- }
16
+ // Shared utilities
17
+ const { c } = require('../lib/colors');
18
+ const { getProjectRoot } = require('../lib/paths');
19
+ const { safeReadJSON } = require('../lib/errors');
40
20
 
41
21
  const ROOT = getProjectRoot();
42
22
  const SESSIONS_DIR = path.join(ROOT, '.agileflow', 'sessions');
@@ -162,17 +142,16 @@ function getCurrentBranch() {
162
142
 
163
143
  // Get current story from status.json
164
144
  function getCurrentStory() {
165
- try {
166
- const statusPath = path.join(ROOT, 'docs', '09-agents', 'status.json');
167
- if (!fs.existsSync(statusPath)) return null;
145
+ const statusPath = path.join(ROOT, 'docs', '09-agents', 'status.json');
146
+ const result = safeReadJSON(statusPath, { defaultValue: null });
168
147
 
169
- const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
170
- for (const [id, story] of Object.entries(status.stories || {})) {
171
- if (story.status === 'in_progress') {
172
- return { id, title: story.title };
173
- }
148
+ if (!result.ok || !result.data) return null;
149
+
150
+ for (const [id, story] of Object.entries(result.data.stories || {})) {
151
+ if (story.status === 'in_progress') {
152
+ return { id, title: story.title };
174
153
  }
175
- } catch (e) {}
154
+ }
176
155
  return null;
177
156
  }
178
157