agileflow 2.79.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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/scripts/agent-loop.js +765 -0
- package/scripts/agileflow-configure.js +129 -18
- package/scripts/agileflow-welcome.js +113 -16
- package/scripts/damage-control/bash-tool-damage-control.js +7 -6
- package/scripts/damage-control/edit-tool-damage-control.js +4 -24
- package/scripts/damage-control/patterns.yaml +32 -32
- package/scripts/damage-control/write-tool-damage-control.js +4 -24
- package/scripts/damage-control-bash.js +38 -125
- package/scripts/damage-control-edit.js +22 -165
- package/scripts/damage-control-write.js +22 -165
- package/scripts/get-env.js +6 -6
- package/scripts/lib/damage-control-utils.js +251 -0
- package/scripts/obtain-context.js +103 -37
- package/scripts/ralph-loop.js +243 -31
- package/scripts/screenshot-verifier.js +4 -2
- package/scripts/session-manager.js +434 -20
- package/src/core/agents/configuration-visual-e2e.md +300 -0
- package/src/core/agents/orchestrator.md +166 -0
- package/src/core/commands/babysit.md +61 -15
- package/src/core/commands/configure.md +408 -99
- package/src/core/commands/session/end.md +332 -103
- package/src/core/experts/documentation/expertise.yaml +25 -0
- package/tools/cli/commands/start.js +19 -21
- package/tools/cli/installers/ide/claude-code.js +32 -19
- package/tools/cli/tui/Dashboard.js +3 -4
- package/tools/postinstall.js +1 -9
- package/src/core/commands/setup/visual-e2e.md +0 -462
|
@@ -18,12 +18,14 @@
|
|
|
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');
|
|
22
|
+
const { isValidCommandName } = require('../lib/validate');
|
|
21
23
|
|
|
22
24
|
const DISPLAY_LIMIT = 30000; // Claude Code's Bash tool display limit
|
|
23
25
|
|
|
24
26
|
// Optional: Register command for PreCompact context preservation
|
|
25
27
|
const commandName = process.argv[2];
|
|
26
|
-
if (commandName) {
|
|
28
|
+
if (commandName && isValidCommandName(commandName)) {
|
|
27
29
|
const sessionStatePath = 'docs/09-agents/session-state.json';
|
|
28
30
|
if (fs.existsSync(sessionStatePath)) {
|
|
29
31
|
try {
|
|
@@ -44,8 +46,10 @@ if (commandName) {
|
|
|
44
46
|
state: {},
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
//
|
|
48
|
-
state.active_command
|
|
49
|
+
// Remove legacy active_command field (only use active_commands array now)
|
|
50
|
+
if (state.active_command !== undefined) {
|
|
51
|
+
delete state.active_command;
|
|
52
|
+
}
|
|
49
53
|
|
|
50
54
|
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
51
55
|
} catch (e) {
|
|
@@ -54,33 +58,6 @@ if (commandName) {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// ANSI colors
|
|
58
|
-
const C = {
|
|
59
|
-
reset: '\x1b[0m',
|
|
60
|
-
dim: '\x1b[2m',
|
|
61
|
-
bold: '\x1b[1m',
|
|
62
|
-
cyan: '\x1b[36m',
|
|
63
|
-
yellow: '\x1b[33m',
|
|
64
|
-
green: '\x1b[32m',
|
|
65
|
-
red: '\x1b[31m',
|
|
66
|
-
magenta: '\x1b[35m',
|
|
67
|
-
blue: '\x1b[34m',
|
|
68
|
-
brightCyan: '\x1b[96m',
|
|
69
|
-
brightYellow: '\x1b[93m',
|
|
70
|
-
brightGreen: '\x1b[92m',
|
|
71
|
-
brand: '\x1b[38;2;232;104;58m', // AgileFlow brand orange
|
|
72
|
-
|
|
73
|
-
// Vibrant 256-color palette (modern, sleek look)
|
|
74
|
-
mintGreen: '\x1b[38;5;158m', // Healthy/success states
|
|
75
|
-
peach: '\x1b[38;5;215m', // Warning states
|
|
76
|
-
coral: '\x1b[38;5;203m', // Critical/error states
|
|
77
|
-
lightGreen: '\x1b[38;5;194m', // Session healthy
|
|
78
|
-
lightYellow: '\x1b[38;5;228m', // Session warning
|
|
79
|
-
skyBlue: '\x1b[38;5;117m', // Directories/paths
|
|
80
|
-
lavender: '\x1b[38;5;147m', // Model info
|
|
81
|
-
softGold: '\x1b[38;5;222m', // Cost/money
|
|
82
|
-
};
|
|
83
|
-
|
|
84
61
|
function safeRead(filePath) {
|
|
85
62
|
try {
|
|
86
63
|
return fs.readFileSync(filePath, 'utf8');
|
|
@@ -225,7 +202,8 @@ function generateSummary() {
|
|
|
225
202
|
|
|
226
203
|
// Header row (full width, no column divider)
|
|
227
204
|
const title = commandName ? `Context [${commandName}]` : 'Context Summary';
|
|
228
|
-
const branchColor =
|
|
205
|
+
const branchColor =
|
|
206
|
+
branch === 'main' ? C.mintGreen : branch.startsWith('fix') ? C.coral : C.skyBlue;
|
|
229
207
|
const maxBranchLen = 20;
|
|
230
208
|
const branchDisplay =
|
|
231
209
|
branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
|
|
@@ -300,7 +278,12 @@ function generateSummary() {
|
|
|
300
278
|
|
|
301
279
|
// Research
|
|
302
280
|
const researchText = researchFiles.length > 0 ? `${researchFiles.length} notes` : 'none';
|
|
303
|
-
summary += row(
|
|
281
|
+
summary += row(
|
|
282
|
+
'Research',
|
|
283
|
+
researchText,
|
|
284
|
+
C.lavender,
|
|
285
|
+
researchFiles.length > 0 ? C.skyBlue : C.dim
|
|
286
|
+
);
|
|
304
287
|
|
|
305
288
|
// Epics
|
|
306
289
|
const epicText = epicFiles.length > 0 ? `${epicFiles.length} epics` : 'none';
|
|
@@ -396,7 +379,90 @@ function generateFullContent() {
|
|
|
396
379
|
content += `${C.dim}No session-state.json found${C.reset}\n`;
|
|
397
380
|
}
|
|
398
381
|
|
|
399
|
-
// 4.
|
|
382
|
+
// 4. SESSION CONTEXT (multi-session awareness)
|
|
383
|
+
content += `\n${C.skyBlue}${C.bold}═══ Session Context ═══${C.reset}\n`;
|
|
384
|
+
const sessionManagerPath = path.join(__dirname, 'session-manager.js');
|
|
385
|
+
const altSessionManagerPath = '.agileflow/scripts/session-manager.js';
|
|
386
|
+
|
|
387
|
+
if (fs.existsSync(sessionManagerPath) || fs.existsSync(altSessionManagerPath)) {
|
|
388
|
+
const managerPath = fs.existsSync(sessionManagerPath)
|
|
389
|
+
? sessionManagerPath
|
|
390
|
+
: altSessionManagerPath;
|
|
391
|
+
const sessionStatus = safeExec(`node "${managerPath}" status`);
|
|
392
|
+
|
|
393
|
+
if (sessionStatus) {
|
|
394
|
+
try {
|
|
395
|
+
const statusData = JSON.parse(sessionStatus);
|
|
396
|
+
if (statusData.current) {
|
|
397
|
+
const session = statusData.current;
|
|
398
|
+
const isMain = session.is_main === true;
|
|
399
|
+
|
|
400
|
+
if (isMain) {
|
|
401
|
+
content += `Session: ${C.mintGreen}Main project${C.reset} (Session ${session.id || 1})\n`;
|
|
402
|
+
} else {
|
|
403
|
+
// NON-MAIN SESSION - Show prominent banner
|
|
404
|
+
const sessionName = session.nickname
|
|
405
|
+
? `${session.id} "${session.nickname}"`
|
|
406
|
+
: `${session.id}`;
|
|
407
|
+
content += `${C.teal}${C.bold}🔀 SESSION ${sessionName} (worktree)${C.reset}\n`;
|
|
408
|
+
content += `Branch: ${C.skyBlue}${session.branch || 'unknown'}${C.reset}\n`;
|
|
409
|
+
content += `Path: ${C.dim}${session.path || process.cwd()}${C.reset}\n`;
|
|
410
|
+
|
|
411
|
+
// Calculate relative path to main
|
|
412
|
+
const mainPath = process.cwd().replace(/-[^/]+$/, ''); // Heuristic: strip session suffix
|
|
413
|
+
content += `Main project: ${C.dim}${mainPath}${C.reset}\n`;
|
|
414
|
+
|
|
415
|
+
// Remind about merge flow
|
|
416
|
+
content += `${C.lavender}💡 When done: /agileflow:session:end → merge to main${C.reset}\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Show other active sessions
|
|
420
|
+
if (statusData.otherActive > 0) {
|
|
421
|
+
content += `${C.peach}⚠️ ${statusData.otherActive} other session(s) active${C.reset}\n`;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
content += `${C.dim}No session registered${C.reset}\n`;
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
content += `${C.dim}Session manager available but status parse failed${C.reset}\n`;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
content += `${C.dim}Session manager available${C.reset}\n`;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
content += `${C.dim}Multi-session not configured${C.reset}\n`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 5. INTERACTION MODE (AskUserQuestion guidance)
|
|
437
|
+
const metadata = safeReadJSON('docs/00-meta/agileflow-metadata.json');
|
|
438
|
+
const askUserQuestionConfig = metadata?.features?.askUserQuestion;
|
|
439
|
+
|
|
440
|
+
if (askUserQuestionConfig?.enabled) {
|
|
441
|
+
content += `\n${C.brand}${C.bold}═══ ⚡ INTERACTION MODE: AskUserQuestion ENABLED ═══${C.reset}\n`;
|
|
442
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
443
|
+
content += `${C.bold}CRITICAL RULE:${C.reset} End ${C.skyBlue}EVERY${C.reset} response with the AskUserQuestion tool.\n\n`;
|
|
444
|
+
content += `${C.mintGreen}✓ CORRECT:${C.reset} Call the actual AskUserQuestion tool\n`;
|
|
445
|
+
content += `${C.coral}✗ WRONG:${C.reset} Text like "Want me to continue?" or "What's next?"\n\n`;
|
|
446
|
+
content += `${C.lavender}Required format:${C.reset}\n`;
|
|
447
|
+
content += `${C.dim}\`\`\`xml
|
|
448
|
+
<invoke name="AskUserQuestion">
|
|
449
|
+
<parameter name="questions">[{
|
|
450
|
+
"question": "What would you like to do next?",
|
|
451
|
+
"header": "Next step",
|
|
452
|
+
"multiSelect": false,
|
|
453
|
+
"options": [
|
|
454
|
+
{"label": "Option A (Recommended)", "description": "Why this is best"},
|
|
455
|
+
{"label": "Option B", "description": "Alternative approach"},
|
|
456
|
+
{"label": "Pause", "description": "Stop here for now"}
|
|
457
|
+
]
|
|
458
|
+
}]</parameter>
|
|
459
|
+
</invoke>
|
|
460
|
+
\`\`\`${C.reset}\n`;
|
|
461
|
+
content += `${C.dim}${'─'.repeat(60)}${C.reset}\n`;
|
|
462
|
+
content += `${C.dim}Mode: ${askUserQuestionConfig.mode || 'all'} | Configure: /agileflow:configure${C.reset}\n\n`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 5. DOCS STRUCTURE (using vibrant 256-color palette)
|
|
400
466
|
content += `\n${C.skyBlue}${C.bold}═══ Documentation ═══${C.reset}\n`;
|
|
401
467
|
const docsDir = 'docs';
|
|
402
468
|
const docFolders = safeLs(docsDir).filter(f => {
|
|
@@ -420,7 +486,7 @@ function generateFullContent() {
|
|
|
420
486
|
});
|
|
421
487
|
}
|
|
422
488
|
|
|
423
|
-
//
|
|
489
|
+
// 6. RESEARCH NOTES - List + Full content of most recent (using vibrant 256-color palette)
|
|
424
490
|
content += `\n${C.skyBlue}${C.bold}═══ Research Notes ═══${C.reset}\n`;
|
|
425
491
|
const researchDir = 'docs/10-research';
|
|
426
492
|
const researchFiles = safeLs(researchDir).filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
@@ -443,7 +509,7 @@ function generateFullContent() {
|
|
|
443
509
|
content += `${C.dim}No research notes${C.reset}\n`;
|
|
444
510
|
}
|
|
445
511
|
|
|
446
|
-
//
|
|
512
|
+
// 7. BUS MESSAGES (using vibrant 256-color palette)
|
|
447
513
|
content += `\n${C.skyBlue}${C.bold}═══ Recent Agent Messages ═══${C.reset}\n`;
|
|
448
514
|
const busPath = 'docs/09-agents/bus/log.jsonl';
|
|
449
515
|
const busContent = safeRead(busPath);
|
|
@@ -467,7 +533,7 @@ function generateFullContent() {
|
|
|
467
533
|
content += `${C.dim}No bus log found${C.reset}\n`;
|
|
468
534
|
}
|
|
469
535
|
|
|
470
|
-
//
|
|
536
|
+
// 8. KEY FILES - Full content
|
|
471
537
|
content += `\n${C.cyan}${C.bold}═══ Key Context Files (Full Content) ═══${C.reset}\n`;
|
|
472
538
|
|
|
473
539
|
const keyFilesToRead = [
|
|
@@ -493,7 +559,7 @@ function generateFullContent() {
|
|
|
493
559
|
const settingsExists = fs.existsSync('.claude/settings.json');
|
|
494
560
|
content += `\n ${settingsExists ? `${C.green}✓${C.reset}` : `${C.dim}○${C.reset}`} .claude/settings.json\n`;
|
|
495
561
|
|
|
496
|
-
//
|
|
562
|
+
// 9. EPICS FOLDER
|
|
497
563
|
content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
|
|
498
564
|
const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
499
565
|
if (epicFiles.length > 0) {
|
package/scripts/ralph-loop.js
CHANGED
|
@@ -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 {
|
|
@@ -134,7 +230,7 @@ function verifyScreenshots(rootDir) {
|
|
|
134
230
|
const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
135
231
|
let files;
|
|
136
232
|
try {
|
|
137
|
-
files = fs.readdirSync(fullPath).filter(
|
|
233
|
+
files = fs.readdirSync(fullPath).filter(file => {
|
|
138
234
|
const ext = path.extname(file).toLowerCase();
|
|
139
235
|
return imageExtensions.includes(ext);
|
|
140
236
|
});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
);
|
|
@@ -320,44 +421,84 @@ function handleLoop(rootDir) {
|
|
|
320
421
|
console.log(`${c.yellow}⚠ ${screenshotResult.output}${c.reset}`);
|
|
321
422
|
if (screenshotResult.unverified.length > 0) {
|
|
322
423
|
console.log(`${c.dim}Unverified screenshots:${c.reset}`);
|
|
323
|
-
screenshotResult.unverified.slice(0, 5).forEach(
|
|
424
|
+
screenshotResult.unverified.slice(0, 5).forEach(file => {
|
|
324
425
|
console.log(` ${c.yellow}- ${file}${c.reset}`);
|
|
325
426
|
});
|
|
326
427
|
if (screenshotResult.unverified.length > 5) {
|
|
327
|
-
console.log(
|
|
428
|
+
console.log(
|
|
429
|
+
` ${c.dim}... and ${screenshotResult.unverified.length - 5} more${c.reset}`
|
|
430
|
+
);
|
|
328
431
|
}
|
|
329
432
|
}
|
|
330
433
|
state.ralph_loop.screenshots_verified = false;
|
|
331
434
|
}
|
|
332
435
|
}
|
|
333
436
|
|
|
334
|
-
//
|
|
335
|
-
|
|
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
|
+
|
|
336
462
|
console.log('');
|
|
337
|
-
console.log(
|
|
338
|
-
|
|
463
|
+
console.log(
|
|
464
|
+
`${c.yellow}⚠ ${modeNames.join(' + ')} Mode requires ${minIterations}+ iterations for confirmation${c.reset}`
|
|
465
|
+
);
|
|
466
|
+
console.log(
|
|
467
|
+
`${c.dim}Current: iteration ${iteration}. Let loop run once more to confirm.${c.reset}`
|
|
468
|
+
);
|
|
339
469
|
|
|
340
470
|
state.ralph_loop.iteration = iteration;
|
|
341
471
|
saveSessionState(rootDir, state);
|
|
342
472
|
|
|
343
473
|
console.log('');
|
|
344
|
-
console.log(`${c.brand}▶ Continue
|
|
474
|
+
console.log(`${c.brand}▶ Continue working. Loop will verify again.${c.reset}`);
|
|
345
475
|
return;
|
|
346
476
|
}
|
|
347
477
|
|
|
348
|
-
// Check if
|
|
349
|
-
const canComplete =
|
|
478
|
+
// Check if all verification modes passed
|
|
479
|
+
const canComplete =
|
|
480
|
+
testResult.passed &&
|
|
481
|
+
(!visualMode || screenshotResult.passed) &&
|
|
482
|
+
(!coverageMode || coverageResult.passed);
|
|
350
483
|
|
|
351
484
|
if (!canComplete) {
|
|
352
|
-
//
|
|
485
|
+
// Something not verified yet
|
|
353
486
|
state.ralph_loop.iteration = iteration;
|
|
354
487
|
saveSessionState(rootDir, state);
|
|
355
488
|
|
|
356
489
|
console.log('');
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
502
|
return;
|
|
362
503
|
}
|
|
363
504
|
console.log('');
|
|
@@ -452,15 +593,29 @@ function handleCLI() {
|
|
|
452
593
|
if (!loop || !loop.enabled) {
|
|
453
594
|
console.log(`${c.dim}Ralph Loop: not active${c.reset}`);
|
|
454
595
|
} else {
|
|
455
|
-
|
|
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}`;
|
|
456
600
|
console.log(`${c.green}Ralph Loop: active${c.reset}${modeLabel}`);
|
|
457
601
|
console.log(` Epic: ${loop.epic}`);
|
|
458
602
|
console.log(` Current Story: ${loop.current_story}`);
|
|
459
603
|
console.log(` Iteration: ${loop.iteration || 0}/${loop.max_iterations || 20}`);
|
|
460
604
|
if (loop.visual_mode) {
|
|
461
|
-
const verified = loop.screenshots_verified
|
|
605
|
+
const verified = loop.screenshots_verified
|
|
606
|
+
? `${c.green}yes${c.reset}`
|
|
607
|
+
: `${c.yellow}no${c.reset}`;
|
|
462
608
|
console.log(` Screenshots Verified: ${verified}`);
|
|
463
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
|
+
}
|
|
464
619
|
}
|
|
465
620
|
return true;
|
|
466
621
|
}
|
|
@@ -491,6 +646,7 @@ function handleCLI() {
|
|
|
491
646
|
const epicArg = args.find(a => a.startsWith('--epic='));
|
|
492
647
|
const maxArg = args.find(a => a.startsWith('--max='));
|
|
493
648
|
const visualArg = args.includes('--visual') || args.includes('-v');
|
|
649
|
+
const coverageArg = args.find(a => a.startsWith('--coverage='));
|
|
494
650
|
|
|
495
651
|
if (!epicArg) {
|
|
496
652
|
console.log(`${c.red}Error: --epic=EP-XXXX is required${c.reset}`);
|
|
@@ -498,9 +654,28 @@ function handleCLI() {
|
|
|
498
654
|
}
|
|
499
655
|
|
|
500
656
|
const epicId = epicArg.split('=')[1];
|
|
501
|
-
|
|
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);
|
|
502
666
|
const visualMode = visualArg;
|
|
503
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
|
+
|
|
504
679
|
// Find first ready story in epic
|
|
505
680
|
const status = getStatus(rootDir);
|
|
506
681
|
const stories = status.stories || {};
|
|
@@ -524,6 +699,18 @@ function handleCLI() {
|
|
|
524
699
|
// Mark first story as in_progress
|
|
525
700
|
markStoryInProgress(rootDir, storyId);
|
|
526
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
|
+
|
|
527
714
|
// Initialize loop state
|
|
528
715
|
const state = getSessionState(rootDir);
|
|
529
716
|
state.ralph_loop = {
|
|
@@ -534,6 +721,11 @@ function handleCLI() {
|
|
|
534
721
|
max_iterations: maxIterations,
|
|
535
722
|
visual_mode: visualMode,
|
|
536
723
|
screenshots_verified: false,
|
|
724
|
+
coverage_mode: coverageMode,
|
|
725
|
+
coverage_threshold: coverageThreshold,
|
|
726
|
+
coverage_baseline: coverageBaseline,
|
|
727
|
+
coverage_current: coverageBaseline,
|
|
728
|
+
coverage_verified: false,
|
|
537
729
|
started_at: new Date().toISOString(),
|
|
538
730
|
};
|
|
539
731
|
saveSessionState(rootDir, state);
|
|
@@ -541,7 +733,9 @@ function handleCLI() {
|
|
|
541
733
|
const progress = getEpicProgress(status, epicId);
|
|
542
734
|
|
|
543
735
|
console.log('');
|
|
544
|
-
|
|
736
|
+
let modeLabel = '';
|
|
737
|
+
if (visualMode) modeLabel += ` ${c.cyan}[VISUAL]${c.reset}`;
|
|
738
|
+
if (coverageMode) modeLabel += ` ${c.magenta}[COVERAGE ≥${coverageThreshold}%]${c.reset}`;
|
|
545
739
|
console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}${modeLabel}`);
|
|
546
740
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
547
741
|
console.log(` Epic: ${c.cyan}${epicId}${c.reset}`);
|
|
@@ -549,6 +743,14 @@ function handleCLI() {
|
|
|
549
743
|
console.log(` Max Iterations: ${maxIterations}`);
|
|
550
744
|
if (visualMode) {
|
|
551
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) {
|
|
552
754
|
console.log(` Min Iterations: 2 (for confirmation)`);
|
|
553
755
|
}
|
|
554
756
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
@@ -577,17 +779,19 @@ function handleCLI() {
|
|
|
577
779
|
${c.brand}${c.bold}ralph-loop.js${c.reset} - Autonomous Story Processing
|
|
578
780
|
|
|
579
781
|
${c.bold}Usage:${c.reset}
|
|
580
|
-
node scripts/ralph-loop.js
|
|
581
|
-
node scripts/ralph-loop.js --init --epic=EP-XXX
|
|
582
|
-
node scripts/ralph-loop.js --init --epic=EP-XXX --visual
|
|
583
|
-
node scripts/ralph-loop.js --
|
|
584
|
-
node scripts/ralph-loop.js --
|
|
585
|
-
node scripts/ralph-loop.js --
|
|
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
|
|
586
789
|
|
|
587
790
|
${c.bold}Options:${c.reset}
|
|
588
791
|
--epic=EP-XXXX Epic ID to process (required for --init)
|
|
589
792
|
--max=N Max iterations (default: 20)
|
|
590
793
|
--visual, -v Enable Visual Mode (screenshot verification)
|
|
794
|
+
--coverage=N Enable Coverage Mode (iterate until N% coverage)
|
|
591
795
|
|
|
592
796
|
${c.bold}Visual Mode:${c.reset}
|
|
593
797
|
When --visual is enabled, the loop also verifies that all screenshots
|
|
@@ -596,16 +800,24 @@ ${c.bold}Visual Mode:${c.reset}
|
|
|
596
800
|
This ensures Claude actually looks at UI screenshots before declaring
|
|
597
801
|
completion. Requires minimum 2 iterations for confirmation.
|
|
598
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
|
+
|
|
599
811
|
Workflow:
|
|
600
812
|
1. Tests run → must pass
|
|
601
|
-
2.
|
|
602
|
-
3. Minimum 2 iterations →
|
|
813
|
+
2. Coverage checked → must meet threshold
|
|
814
|
+
3. Minimum 2 iterations → confirms coverage is stable
|
|
603
815
|
4. Only then → story marked complete
|
|
604
816
|
|
|
605
817
|
${c.bold}How it works:${c.reset}
|
|
606
|
-
1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop
|
|
818
|
+
1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop COVERAGE=80
|
|
607
819
|
2. Work on the current story
|
|
608
|
-
3. When you stop, this hook runs tests
|
|
820
|
+
3. When you stop, this hook runs tests and verifications
|
|
609
821
|
4. If all pass → story marked complete, next story loaded
|
|
610
822
|
5. If any fail → failures shown, you continue fixing
|
|
611
823
|
6. Loop repeats until epic done or max iterations
|
|
@@ -97,7 +97,7 @@ function getImageFiles(dir) {
|
|
|
97
97
|
|
|
98
98
|
try {
|
|
99
99
|
const files = fs.readdirSync(dir);
|
|
100
|
-
return files.filter(
|
|
100
|
+
return files.filter(file => {
|
|
101
101
|
const ext = path.extname(file).toLowerCase();
|
|
102
102
|
return imageExtensions.includes(ext);
|
|
103
103
|
});
|
|
@@ -172,7 +172,9 @@ function formatResult(result, options) {
|
|
|
172
172
|
|
|
173
173
|
if (result.success) {
|
|
174
174
|
console.log(`${c.green}${c.bold}All screenshots verified${c.reset}`);
|
|
175
|
-
console.log(
|
|
175
|
+
console.log(
|
|
176
|
+
`${c.dim}${result.verified}/${result.total} screenshots have 'verified-' prefix${c.reset}`
|
|
177
|
+
);
|
|
176
178
|
} else {
|
|
177
179
|
console.log(`${c.red}${c.bold}Unverified screenshots found${c.reset}`);
|
|
178
180
|
console.log(`${c.dim}${result.verified}/${result.total} verified${c.reset}`);
|