agileflow 2.78.0 → 2.80.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.
@@ -15,13 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
-
19
- // Color codes for output
20
- const c = {
21
- red: '\x1b[38;5;203m',
22
- reset: '\x1b[0m',
23
- dim: '\x1b[2m'
24
- };
18
+ const { c } = require('../lib/colors');
25
19
 
26
20
  /**
27
21
  * Find project root by looking for .agileflow directory
@@ -54,7 +48,7 @@ function parseSimpleYAML(content) {
54
48
  const config = {
55
49
  zeroAccessPaths: [],
56
50
  readOnlyPaths: [],
57
- noDeletePaths: []
51
+ noDeletePaths: [],
58
52
  };
59
53
 
60
54
  let currentSection = null;
@@ -92,7 +86,7 @@ function loadPatterns(projectRoot) {
92
86
  const configPaths = [
93
87
  path.join(projectRoot, '.agileflow/config/damage-control-patterns.yaml'),
94
88
  path.join(projectRoot, '.agileflow/config/damage-control-patterns.yml'),
95
- path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml')
89
+ path.join(projectRoot, '.agileflow/templates/damage-control-patterns.yaml'),
96
90
  ];
97
91
 
98
92
  for (const configPath of configPaths) {
@@ -168,7 +162,7 @@ function validatePath(filePath, config) {
168
162
  return {
169
163
  action: 'block',
170
164
  reason: `Zero-access path: ${zeroMatch}`,
171
- detail: 'This file is protected and cannot be accessed'
165
+ detail: 'This file is protected and cannot be accessed',
172
166
  };
173
167
  }
174
168
 
@@ -178,7 +172,7 @@ function validatePath(filePath, config) {
178
172
  return {
179
173
  action: 'block',
180
174
  reason: `Read-only path: ${readOnlyMatch}`,
181
- detail: 'This file is read-only and cannot be written to'
175
+ detail: 'This file is read-only and cannot be written to',
182
176
  };
183
177
  }
184
178
 
@@ -215,7 +209,7 @@ function main() {
215
209
  const result = validatePath(filePath, config);
216
210
 
217
211
  if (result.action === 'block') {
218
- console.error(`${c.red}[BLOCKED]${c.reset} ${result.reason}`);
212
+ console.error(`${c.coral}[BLOCKED]${c.reset} ${result.reason}`);
219
213
  console.error(`${c.dim}${result.detail}${c.reset}`);
220
214
  console.error(`${c.dim}File: ${filePath}${c.reset}`);
221
215
  process.exit(2);
@@ -156,12 +156,12 @@ function formatOutput(info, asJson = false, compact = false) {
156
156
  brand: '\x1b[38;2;232;104;58m', // #e8683a - AgileFlow brand orange
157
157
 
158
158
  // Vibrant 256-color palette (modern, sleek look)
159
- mintGreen: '\x1b[38;5;158m', // Healthy/success states
160
- peach: '\x1b[38;5;215m', // Warning states
161
- coral: '\x1b[38;5;203m', // Critical/error states
162
- lightGreen: '\x1b[38;5;194m', // Session healthy
163
- skyBlue: '\x1b[38;5;117m', // Directories/paths
164
- lavender: '\x1b[38;5;147m', // Model info
159
+ mintGreen: '\x1b[38;5;158m', // Healthy/success states
160
+ peach: '\x1b[38;5;215m', // Warning states
161
+ coral: '\x1b[38;5;203m', // Critical/error states
162
+ lightGreen: '\x1b[38;5;194m', // Session healthy
163
+ skyBlue: '\x1b[38;5;117m', // Directories/paths
164
+ lavender: '\x1b[38;5;147m', // Model info
165
165
  };
166
166
 
167
167
  // Beautiful compact colorful format (using vibrant 256-color palette)
@@ -18,6 +18,7 @@
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
20
  const { execSync } = require('child_process');
21
+ const { c: C, box } = require('../lib/colors');
21
22
 
22
23
  const DISPLAY_LIMIT = 30000; // Claude Code's Bash tool display limit
23
24
 
@@ -28,11 +29,27 @@ if (commandName) {
28
29
  if (fs.existsSync(sessionStatePath)) {
29
30
  try {
30
31
  const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
31
- state.active_command = {
32
+
33
+ // Initialize active_commands array if not present
34
+ if (!Array.isArray(state.active_commands)) {
35
+ state.active_commands = [];
36
+ }
37
+
38
+ // Remove any existing entry for this command (avoid duplicates)
39
+ state.active_commands = state.active_commands.filter(c => c.name !== commandName);
40
+
41
+ // Add the new command
42
+ state.active_commands.push({
32
43
  name: commandName,
33
44
  activated_at: new Date().toISOString(),
34
45
  state: {},
35
- };
46
+ });
47
+
48
+ // Remove legacy active_command field (only use active_commands array now)
49
+ if (state.active_command !== undefined) {
50
+ delete state.active_command;
51
+ }
52
+
36
53
  fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
37
54
  } catch (e) {
38
55
  // Silently continue if session state can't be updated
@@ -40,33 +57,6 @@ if (commandName) {
40
57
  }
41
58
  }
42
59
 
43
- // ANSI colors
44
- const C = {
45
- reset: '\x1b[0m',
46
- dim: '\x1b[2m',
47
- bold: '\x1b[1m',
48
- cyan: '\x1b[36m',
49
- yellow: '\x1b[33m',
50
- green: '\x1b[32m',
51
- red: '\x1b[31m',
52
- magenta: '\x1b[35m',
53
- blue: '\x1b[34m',
54
- brightCyan: '\x1b[96m',
55
- brightYellow: '\x1b[93m',
56
- brightGreen: '\x1b[92m',
57
- brand: '\x1b[38;2;232;104;58m', // AgileFlow brand orange
58
-
59
- // Vibrant 256-color palette (modern, sleek look)
60
- mintGreen: '\x1b[38;5;158m', // Healthy/success states
61
- peach: '\x1b[38;5;215m', // Warning states
62
- coral: '\x1b[38;5;203m', // Critical/error states
63
- lightGreen: '\x1b[38;5;194m', // Session healthy
64
- lightYellow: '\x1b[38;5;228m', // Session warning
65
- skyBlue: '\x1b[38;5;117m', // Directories/paths
66
- lavender: '\x1b[38;5;147m', // Model info
67
- softGold: '\x1b[38;5;222m', // Cost/money
68
- };
69
-
70
60
  function safeRead(filePath) {
71
61
  try {
72
62
  return fs.readFileSync(filePath, 'utf8');
@@ -211,7 +201,8 @@ function generateSummary() {
211
201
 
212
202
  // Header row (full width, no column divider)
213
203
  const title = commandName ? `Context [${commandName}]` : 'Context Summary';
214
- const branchColor = branch === 'main' ? C.mintGreen : branch.startsWith('fix') ? C.coral : C.skyBlue;
204
+ const branchColor =
205
+ branch === 'main' ? C.mintGreen : branch.startsWith('fix') ? C.coral : C.skyBlue;
215
206
  const maxBranchLen = 20;
216
207
  const branchDisplay =
217
208
  branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
@@ -286,7 +277,12 @@ function generateSummary() {
286
277
 
287
278
  // Research
288
279
  const researchText = researchFiles.length > 0 ? `${researchFiles.length} notes` : 'none';
289
- summary += row('Research', researchText, C.lavender, researchFiles.length > 0 ? C.skyBlue : C.dim);
280
+ summary += row(
281
+ 'Research',
282
+ researchText,
283
+ C.lavender,
284
+ researchFiles.length > 0 ? C.skyBlue : C.dim
285
+ );
290
286
 
291
287
  // Epics
292
288
  const epicText = epicFiles.length > 0 ? `${epicFiles.length} epics` : 'none';
@@ -370,14 +366,48 @@ function generateFullContent() {
370
366
  } else {
371
367
  content += `${C.dim}No active session${C.reset}\n`;
372
368
  }
373
- if (sessionState.active_command) {
369
+ // Show all active commands (array)
370
+ if (Array.isArray(sessionState.active_commands) && sessionState.active_commands.length > 0) {
371
+ const cmdNames = sessionState.active_commands.map(c => c.name).join(', ');
372
+ content += `Active commands: ${C.skyBlue}${cmdNames}${C.reset}\n`;
373
+ } else if (sessionState.active_command) {
374
+ // Backwards compatibility for old format
374
375
  content += `Active command: ${C.skyBlue}${sessionState.active_command.name}${C.reset}\n`;
375
376
  }
376
377
  } else {
377
378
  content += `${C.dim}No session-state.json found${C.reset}\n`;
378
379
  }
379
380
 
380
- // 4. DOCS STRUCTURE (using vibrant 256-color palette)
381
+ // 4. INTERACTION MODE (AskUserQuestion guidance)
382
+ const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
383
+ const askUserQuestionConfig = metadata?.features?.askUserQuestion;
384
+
385
+ if (askUserQuestionConfig?.enabled) {
386
+ content += `\n${C.brand}${C.bold}═══ ⚡ INTERACTION MODE: AskUserQuestion ENABLED ═══${C.reset}\n`;
387
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
388
+ content += `${C.bold}CRITICAL RULE:${C.reset} End ${C.skyBlue}EVERY${C.reset} response with the AskUserQuestion tool.\n\n`;
389
+ content += `${C.mintGreen}✓ CORRECT:${C.reset} Call the actual AskUserQuestion tool\n`;
390
+ content += `${C.coral}✗ WRONG:${C.reset} Text like "Want me to continue?" or "What's next?"\n\n`;
391
+ content += `${C.lavender}Required format:${C.reset}\n`;
392
+ content += `${C.dim}\`\`\`xml
393
+ <invoke name="AskUserQuestion">
394
+ <parameter name="questions">[{
395
+ "question": "What would you like to do next?",
396
+ "header": "Next step",
397
+ "multiSelect": false,
398
+ "options": [
399
+ {"label": "Option A (Recommended)", "description": "Why this is best"},
400
+ {"label": "Option B", "description": "Alternative approach"},
401
+ {"label": "Pause", "description": "Stop here for now"}
402
+ ]
403
+ }]</parameter>
404
+ </invoke>
405
+ \`\`\`${C.reset}\n`;
406
+ content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
407
+ content += `${C.dim}Mode: ${askUserQuestionConfig.mode || 'all'} | Configure: /agileflow:configure${C.reset}\n\n`;
408
+ }
409
+
410
+ // 5. DOCS STRUCTURE (using vibrant 256-color palette)
381
411
  content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
382
412
  const docsDir = 'docs';
383
413
  const docFolders = safeLs(docsDir).filter(f => {
@@ -401,7 +431,7 @@ function generateFullContent() {
401
431
  });
402
432
  }
403
433
 
404
- // 5. RESEARCH NOTES - List + Full content of most recent (using vibrant 256-color palette)
434
+ // 6. RESEARCH NOTES - List + Full content of most recent (using vibrant 256-color palette)
405
435
  content += `\n${C.skyBlue}${C.bold}═══ Research Notes ═══${C.reset}\n`;
406
436
  const researchDir = 'docs/10-research';
407
437
  const researchFiles = safeLs(researchDir).filter(f => f.endsWith('.md') && f !== 'README.md');
@@ -424,7 +454,7 @@ function generateFullContent() {
424
454
  content += `${C.dim}No research notes${C.reset}\n`;
425
455
  }
426
456
 
427
- // 6. BUS MESSAGES (using vibrant 256-color palette)
457
+ // 7. BUS MESSAGES (using vibrant 256-color palette)
428
458
  content += `\n${C.skyBlue}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
429
459
  const busPath = 'docs/09-agents/bus/log.jsonl';
430
460
  const busContent = safeRead(busPath);
@@ -448,7 +478,7 @@ function generateFullContent() {
448
478
  content += `${C.dim}No bus log found${C.reset}\n`;
449
479
  }
450
480
 
451
- // 7. KEY FILES - Full content
481
+ // 8. KEY FILES - Full content
452
482
  content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
453
483
 
454
484
  const keyFilesToRead = [
@@ -474,7 +504,7 @@ function generateFullContent() {
474
504
  const settingsExists = fs.existsSync('.claude/settings.json');
475
505
  content += `\n ${settingsExists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`} .claude/settings.json\n`;
476
506
 
477
- // 8. EPICS FOLDER
507
+ // 9. EPICS FOLDER
478
508
  content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
479
509
  const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
480
510
  if (epicFiles.length > 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,67 @@ 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(
328
+ ` ${c.dim}... and ${screenshotResult.unverified.length - 5} more${c.reset}`
329
+ );
330
+ }
331
+ }
332
+ state.ralph_loop.screenshots_verified = false;
333
+ }
334
+ }
335
+
336
+ // Visual Mode: Enforce minimum iterations
337
+ if (visualMode && iteration < minIterations) {
338
+ console.log('');
339
+ console.log(
340
+ `${c.yellow}⚠ Visual Mode requires ${minIterations}+ iterations for confirmation${c.reset}`
341
+ );
342
+ console.log(
343
+ `${c.dim}Current: iteration ${iteration}. Let loop run once more to confirm.${c.reset}`
344
+ );
345
+
346
+ state.ralph_loop.iteration = iteration;
347
+ saveSessionState(rootDir, state);
348
+
349
+ console.log('');
350
+ console.log(`${c.brand}▶ Continue reviewing. Loop will verify again.${c.reset}`);
351
+ return;
352
+ }
353
+
354
+ // Check if both tests AND screenshots (in visual mode) passed
355
+ const canComplete = testResult.passed && (!visualMode || screenshotResult.passed);
356
+
357
+ if (!canComplete) {
358
+ // Screenshots not verified yet
359
+ state.ralph_loop.iteration = iteration;
360
+ saveSessionState(rootDir, state);
361
+
362
+ 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}`);
367
+ return;
368
+ }
264
369
  console.log('');
265
370
 
266
371
  // Mark story complete
@@ -353,10 +458,17 @@ function handleCLI() {
353
458
  if (!loop || !loop.enabled) {
354
459
  console.log(`${c.dim}Ralph Loop: not active${c.reset}`);
355
460
  } else {
356
- console.log(`${c.green}Ralph Loop: active${c.reset}`);
461
+ const modeLabel = loop.visual_mode ? ` ${c.cyan}[VISUAL]${c.reset}` : '';
462
+ console.log(`${c.green}Ralph Loop: active${c.reset}${modeLabel}`);
357
463
  console.log(` Epic: ${loop.epic}`);
358
464
  console.log(` Current Story: ${loop.current_story}`);
359
465
  console.log(` Iteration: ${loop.iteration || 0}/${loop.max_iterations || 20}`);
466
+ if (loop.visual_mode) {
467
+ const verified = loop.screenshots_verified
468
+ ? `${c.green}yes${c.reset}`
469
+ : `${c.yellow}no${c.reset}`;
470
+ console.log(` Screenshots Verified: ${verified}`);
471
+ }
360
472
  }
361
473
  return true;
362
474
  }
@@ -386,6 +498,7 @@ function handleCLI() {
386
498
  if (args.some(a => a.startsWith('--init'))) {
387
499
  const epicArg = args.find(a => a.startsWith('--epic='));
388
500
  const maxArg = args.find(a => a.startsWith('--max='));
501
+ const visualArg = args.includes('--visual') || args.includes('-v');
389
502
 
390
503
  if (!epicArg) {
391
504
  console.log(`${c.red}Error: --epic=EP-XXXX is required${c.reset}`);
@@ -394,6 +507,7 @@ function handleCLI() {
394
507
 
395
508
  const epicId = epicArg.split('=')[1];
396
509
  const maxIterations = maxArg ? parseInt(maxArg.split('=')[1]) : 20;
510
+ const visualMode = visualArg;
397
511
 
398
512
  // Find first ready story in epic
399
513
  const status = getStatus(rootDir);
@@ -426,6 +540,8 @@ function handleCLI() {
426
540
  current_story: storyId,
427
541
  iteration: 0,
428
542
  max_iterations: maxIterations,
543
+ visual_mode: visualMode,
544
+ screenshots_verified: false,
429
545
  started_at: new Date().toISOString(),
430
546
  };
431
547
  saveSessionState(rootDir, state);
@@ -433,11 +549,16 @@ function handleCLI() {
433
549
  const progress = getEpicProgress(status, epicId);
434
550
 
435
551
  console.log('');
436
- console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}`);
552
+ const modeLabel = visualMode ? ` ${c.cyan}[VISUAL MODE]${c.reset}` : '';
553
+ console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}${modeLabel}`);
437
554
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
438
555
  console.log(` Epic: ${c.cyan}${epicId}${c.reset}`);
439
556
  console.log(` Stories: ${progress.ready} ready, ${progress.total} total`);
440
557
  console.log(` Max Iterations: ${maxIterations}`);
558
+ if (visualMode) {
559
+ console.log(` Visual Mode: ${c.cyan}enabled${c.reset} (screenshot verification)`);
560
+ console.log(` Min Iterations: 2 (for confirmation)`);
561
+ }
441
562
  console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
442
563
  console.log('');
443
564
  console.log(`${c.brand}▶ Starting Story:${c.reset} ${storyId}`);
@@ -464,22 +585,37 @@ function handleCLI() {
464
585
  ${c.brand}${c.bold}ralph-loop.js${c.reset} - Autonomous Story Processing
465
586
 
466
587
  ${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
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
472
594
 
473
595
  ${c.bold}Options:${c.reset}
474
596
  --epic=EP-XXXX Epic ID to process (required for --init)
475
597
  --max=N Max iterations (default: 20)
598
+ --visual, -v Enable Visual Mode (screenshot verification)
599
+
600
+ ${c.bold}Visual Mode:${c.reset}
601
+ When --visual is enabled, the loop also verifies that all screenshots
602
+ in the screenshots/ directory have been reviewed (verified- prefix).
603
+
604
+ This ensures Claude actually looks at UI screenshots before declaring
605
+ completion. Requires minimum 2 iterations for confirmation.
606
+
607
+ Workflow:
608
+ 1. Tests run → must pass
609
+ 2. Screenshots verified → all must have 'verified-' prefix
610
+ 3. Minimum 2 iterations → prevents premature completion
611
+ 4. Only then → story marked complete
476
612
 
477
613
  ${c.bold}How it works:${c.reset}
478
- 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop
614
+ 1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop VISUAL=true
479
615
  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
616
+ 3. When you stop, this hook runs tests (and screenshot verification in Visual Mode)
617
+ 4. If all pass → story marked complete, next story loaded
618
+ 5. If any fail → failures shown, you continue fixing
483
619
  6. Loop repeats until epic done or max iterations
484
620
  `);
485
621
  return true;