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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +126 -17
- package/scripts/agileflow-welcome.js +77 -98
- package/scripts/auto-self-improve.js +23 -45
- package/scripts/check-update.js +35 -42
- package/scripts/damage-control/bash-tool-damage-control.js +258 -0
- package/scripts/damage-control/edit-tool-damage-control.js +259 -0
- package/scripts/damage-control/patterns.yaml +227 -0
- package/scripts/damage-control/write-tool-damage-control.js +254 -0
- package/scripts/damage-control-bash.js +28 -22
- package/scripts/damage-control-edit.js +6 -12
- package/scripts/damage-control-write.js +6 -12
- package/scripts/get-env.js +6 -6
- package/scripts/obtain-context.js +67 -37
- package/scripts/ralph-loop.js +199 -63
- package/scripts/screenshot-verifier.js +215 -0
- package/scripts/session-manager.js +12 -33
- package/src/core/agents/configuration-damage-control.md +248 -0
- package/src/core/commands/babysit.md +30 -2
- package/src/core/commands/configure.md +46 -9
- package/src/core/commands/setup/visual-e2e.md +462 -0
- package/src/core/experts/documentation/expertise.yaml +25 -0
- package/src/core/skills/_learnings/code-review.yaml +118 -0
- package/src/core/skills/_learnings/story-writer.yaml +71 -0
- package/tools/cli/commands/start.js +19 -21
- package/tools/cli/installers/ide/claude-code.js +140 -0
- package/tools/cli/tui/Dashboard.js +3 -4
- package/tools/postinstall.js +1 -9
|
@@ -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.
|
|
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);
|
package/scripts/get-env.js
CHANGED
|
@@ -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',
|
|
160
|
-
peach: '\x1b[38;5;215m',
|
|
161
|
-
coral: '\x1b[38;5;203m',
|
|
162
|
-
lightGreen: '\x1b[38;5;194m',
|
|
163
|
-
skyBlue: '\x1b[38;5;117m',
|
|
164
|
-
lavender: '\x1b[38;5;147m',
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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) {
|
package/scripts/ralph-loop.js
CHANGED
|
@@ -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.
|
|
11
|
-
* 4.
|
|
12
|
-
* 5.
|
|
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
|
-
//
|
|
30
|
-
const c =
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
468
|
-
node scripts/ralph-loop.js --init --epic=EP-XXX
|
|
469
|
-
node scripts/ralph-loop.js --
|
|
470
|
-
node scripts/ralph-loop.js --
|
|
471
|
-
node scripts/ralph-loop.js --
|
|
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
|
|
482
|
-
5. If
|
|
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;
|