agileflow 2.80.0 → 2.82.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 +3 -1
- package/scripts/agileflow-welcome.js +65 -0
- package/scripts/damage-control-bash.js +22 -115
- package/scripts/damage-control-edit.js +19 -156
- package/scripts/damage-control-write.js +19 -156
- package/scripts/lib/damage-control-utils.js +251 -0
- package/scripts/obtain-context.js +57 -2
- package/scripts/ralph-loop.js +516 -32
- package/scripts/session-manager.js +434 -20
- package/src/core/agents/configuration-visual-e2e.md +300 -0
- package/src/core/agents/orchestrator.md +301 -6
- package/src/core/commands/babysit.md +193 -15
- package/src/core/commands/batch.md +362 -0
- package/src/core/commands/choose.md +337 -0
- package/src/core/commands/configure.md +372 -100
- package/src/core/commands/session/end.md +332 -103
- package/src/core/commands/workflow.md +344 -0
- package/src/core/commands/setup/visual-e2e.md +0 -462
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,285 @@ 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
|
+
// ===== DISCRETION MARKERS =====
|
|
133
|
+
// Semantic conditions wrapped in **...**
|
|
134
|
+
// These are evaluated by the loop to determine completion
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Built-in discretion conditions that can be evaluated programmatically
|
|
138
|
+
* Format: condition key -> evaluation function
|
|
139
|
+
*/
|
|
140
|
+
const DISCRETION_CONDITIONS = {
|
|
141
|
+
// Test-related conditions
|
|
142
|
+
'all tests passing': (rootDir, _ctx) => {
|
|
143
|
+
const testCommand = getTestCommand(rootDir);
|
|
144
|
+
const result = runTests(rootDir, testCommand);
|
|
145
|
+
return {
|
|
146
|
+
passed: result.passed,
|
|
147
|
+
message: result.passed
|
|
148
|
+
? 'All tests passing'
|
|
149
|
+
: `Tests failing: ${result.output.split('\n').slice(-3).join(' ').substring(0, 100)}`,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
'tests pass': (rootDir, _ctx) => {
|
|
154
|
+
const testCommand = getTestCommand(rootDir);
|
|
155
|
+
const result = runTests(rootDir, testCommand);
|
|
156
|
+
return {
|
|
157
|
+
passed: result.passed,
|
|
158
|
+
message: result.passed ? 'Tests pass' : 'Tests failing',
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
// Coverage conditions (requires threshold in context)
|
|
163
|
+
'coverage above threshold': (rootDir, ctx) => {
|
|
164
|
+
const threshold = ctx.coverageThreshold || 80;
|
|
165
|
+
const result = verifyCoverage(rootDir, threshold);
|
|
166
|
+
return {
|
|
167
|
+
passed: result.passed,
|
|
168
|
+
message: `Coverage: ${result.coverage?.toFixed(1) || 0}% (threshold: ${threshold}%)`,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Lint conditions
|
|
173
|
+
'no linting errors': (rootDir, _ctx) => {
|
|
174
|
+
try {
|
|
175
|
+
execSync('npm run lint', {
|
|
176
|
+
cwd: rootDir,
|
|
177
|
+
encoding: 'utf8',
|
|
178
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
179
|
+
timeout: 120000,
|
|
180
|
+
});
|
|
181
|
+
return { passed: true, message: 'No linting errors' };
|
|
182
|
+
} catch (e) {
|
|
183
|
+
return { passed: false, message: 'Linting errors found' };
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Type checking conditions
|
|
188
|
+
'no type errors': (rootDir, _ctx) => {
|
|
189
|
+
try {
|
|
190
|
+
execSync('npx tsc --noEmit', {
|
|
191
|
+
cwd: rootDir,
|
|
192
|
+
encoding: 'utf8',
|
|
193
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
194
|
+
timeout: 120000,
|
|
195
|
+
});
|
|
196
|
+
return { passed: true, message: 'No type errors' };
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return { passed: false, message: 'Type errors found' };
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Build conditions
|
|
203
|
+
'build succeeds': (rootDir, _ctx) => {
|
|
204
|
+
try {
|
|
205
|
+
execSync('npm run build', {
|
|
206
|
+
cwd: rootDir,
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
209
|
+
timeout: 300000,
|
|
210
|
+
});
|
|
211
|
+
return { passed: true, message: 'Build succeeds' };
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return { passed: false, message: 'Build failed' };
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Screenshot/visual conditions
|
|
218
|
+
'all screenshots verified': (rootDir, _ctx) => {
|
|
219
|
+
const result = verifyScreenshots(rootDir);
|
|
220
|
+
return {
|
|
221
|
+
passed: result.passed,
|
|
222
|
+
message: result.passed
|
|
223
|
+
? 'All screenshots verified'
|
|
224
|
+
: `${result.unverified?.length || 0} unverified screenshots`,
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// AC conditions (checks story acceptance criteria in status.json)
|
|
229
|
+
'all acceptance criteria verified': (rootDir, ctx) => {
|
|
230
|
+
const storyId = ctx.currentStoryId;
|
|
231
|
+
if (!storyId) {
|
|
232
|
+
return { passed: false, message: 'No story ID in context' };
|
|
233
|
+
}
|
|
234
|
+
const status = getStatus(rootDir);
|
|
235
|
+
const story = status.stories?.[storyId];
|
|
236
|
+
if (!story) {
|
|
237
|
+
return { passed: false, message: `Story ${storyId} not found` };
|
|
238
|
+
}
|
|
239
|
+
// Check if story has AC and if they're marked complete
|
|
240
|
+
const ac = story.acceptance_criteria || story.ac || [];
|
|
241
|
+
if (!Array.isArray(ac) || ac.length === 0) {
|
|
242
|
+
return { passed: true, message: 'No AC defined (assuming complete)' };
|
|
243
|
+
}
|
|
244
|
+
// Check for ac_status field or assume AC are verified if tests pass
|
|
245
|
+
const acStatus = story.ac_status || {};
|
|
246
|
+
const allVerified = ac.every((_, i) => acStatus[i] === 'verified' || acStatus[i] === true);
|
|
247
|
+
return {
|
|
248
|
+
passed: allVerified,
|
|
249
|
+
message: allVerified
|
|
250
|
+
? 'All AC verified'
|
|
251
|
+
: `${Object.values(acStatus).filter(v => v === 'verified' || v === true).length}/${ac.length} AC verified`,
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Parse discretion condition from string
|
|
258
|
+
* @param {string} condition - e.g., "**all tests passing**" or "**coverage above 80%**"
|
|
259
|
+
* @returns {object} { key, threshold? }
|
|
260
|
+
*/
|
|
261
|
+
function parseDiscretionCondition(condition) {
|
|
262
|
+
// Remove ** markers
|
|
263
|
+
const cleaned = condition.replace(/\*\*/g, '').trim().toLowerCase();
|
|
264
|
+
|
|
265
|
+
// Check for threshold patterns like "coverage above 80%"
|
|
266
|
+
const coverageMatch = cleaned.match(/coverage (?:above|>=?) (\d+)%?/);
|
|
267
|
+
if (coverageMatch) {
|
|
268
|
+
return { key: 'coverage above threshold', threshold: parseInt(coverageMatch[1]) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { key: cleaned };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Evaluate a discretion condition
|
|
276
|
+
* @param {string} condition - The condition string (with or without ** markers)
|
|
277
|
+
* @param {string} rootDir - Project root
|
|
278
|
+
* @param {object} ctx - Context (currentStoryId, coverageThreshold, etc.)
|
|
279
|
+
* @returns {object} { passed: boolean, message: string }
|
|
280
|
+
*/
|
|
281
|
+
function evaluateDiscretionCondition(condition, rootDir, ctx = {}) {
|
|
282
|
+
const parsed = parseDiscretionCondition(condition);
|
|
283
|
+
|
|
284
|
+
// Set threshold in context if parsed from condition
|
|
285
|
+
if (parsed.threshold) {
|
|
286
|
+
ctx.coverageThreshold = parsed.threshold;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const evaluator = DISCRETION_CONDITIONS[parsed.key];
|
|
290
|
+
if (!evaluator) {
|
|
291
|
+
return {
|
|
292
|
+
passed: false,
|
|
293
|
+
message: `Unknown condition: "${parsed.key}". Available: ${Object.keys(DISCRETION_CONDITIONS).join(', ')}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return evaluator(rootDir, ctx);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get discretion conditions from metadata
|
|
302
|
+
* @param {string} rootDir
|
|
303
|
+
* @returns {string[]} Array of condition strings
|
|
304
|
+
*/
|
|
305
|
+
function getDiscretionConditions(rootDir) {
|
|
306
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
307
|
+
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
308
|
+
|
|
309
|
+
if (result.ok && result.data?.ralph_loop?.conditions) {
|
|
310
|
+
return result.data.ralph_loop.conditions;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Parse coverage report (Jest/NYC format)
|
|
317
|
+
function parseCoverageReport(rootDir) {
|
|
318
|
+
const reportPath = getCoverageReportPath(rootDir);
|
|
319
|
+
const fullPath = path.join(rootDir, reportPath);
|
|
320
|
+
const report = safeReadJSON(fullPath, { defaultValue: null });
|
|
321
|
+
|
|
322
|
+
if (!report.ok || !report.data) {
|
|
323
|
+
return { passed: false, coverage: 0, error: 'Coverage report not found at ' + reportPath };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Jest/NYC format: { total: { lines: { pct: 80 }, statements: { pct: 80 } } }
|
|
327
|
+
const total = report.data.total;
|
|
328
|
+
if (!total) {
|
|
329
|
+
return { passed: false, coverage: 0, error: 'Invalid coverage report format' };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const coverage = total.lines?.pct || total.statements?.pct || 0;
|
|
333
|
+
|
|
334
|
+
return { passed: true, coverage, raw: report.data };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Verify coverage meets threshold
|
|
338
|
+
function verifyCoverage(rootDir, threshold) {
|
|
339
|
+
const result = parseCoverageReport(rootDir);
|
|
340
|
+
|
|
341
|
+
if (!result.passed) {
|
|
342
|
+
return {
|
|
343
|
+
passed: false,
|
|
344
|
+
coverage: 0,
|
|
345
|
+
message: `${c.red}✗ ${result.error}${c.reset}`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const met = result.coverage >= threshold;
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
passed: met,
|
|
353
|
+
coverage: result.coverage,
|
|
354
|
+
threshold: threshold,
|
|
355
|
+
message: met
|
|
356
|
+
? `${c.green}✓ Coverage ${result.coverage.toFixed(1)}% ≥ ${threshold}%${c.reset}`
|
|
357
|
+
: `${c.yellow}⏳ Coverage ${result.coverage.toFixed(1)}% < ${threshold}% (need ${(threshold - result.coverage).toFixed(1)}% more)${c.reset}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Run coverage command
|
|
362
|
+
function runCoverage(rootDir) {
|
|
363
|
+
const coverageCmd = getCoverageCommand(rootDir);
|
|
364
|
+
const result = { passed: false, output: '', duration: 0 };
|
|
365
|
+
const startTime = Date.now();
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const output = execSync(coverageCmd, {
|
|
369
|
+
cwd: rootDir,
|
|
370
|
+
encoding: 'utf8',
|
|
371
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
372
|
+
timeout: 300000, // 5 minute timeout
|
|
373
|
+
});
|
|
374
|
+
result.passed = true;
|
|
375
|
+
result.output = output;
|
|
376
|
+
} catch (e) {
|
|
377
|
+
// Coverage command might fail but still generate report
|
|
378
|
+
result.passed = true; // We'll check the report
|
|
379
|
+
result.output = e.stdout || '' + '\n' + (e.stderr || '');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
result.duration = Date.now() - startTime;
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
106
386
|
// Get screenshots directory from metadata or default
|
|
107
387
|
function getScreenshotsDir(rootDir) {
|
|
108
388
|
try {
|
|
@@ -253,13 +533,21 @@ function handleLoop(rootDir) {
|
|
|
253
533
|
const iteration = (loop.iteration || 0) + 1;
|
|
254
534
|
const maxIterations = loop.max_iterations || 20;
|
|
255
535
|
const visualMode = loop.visual_mode || false;
|
|
256
|
-
const
|
|
536
|
+
const coverageMode = loop.coverage_mode || false;
|
|
537
|
+
const coverageThreshold = loop.coverage_threshold || 80;
|
|
538
|
+
const discretionConditions = loop.conditions || getDiscretionConditions(rootDir);
|
|
539
|
+
// Visual, Coverage, and Discretion modes require at least 2 iterations for confirmation
|
|
540
|
+
const hasDiscretionConditions = discretionConditions.length > 0;
|
|
541
|
+
const minIterations = visualMode || coverageMode || hasDiscretionConditions ? 2 : 1;
|
|
257
542
|
|
|
258
543
|
console.log('');
|
|
259
544
|
console.log(
|
|
260
545
|
`${c.brand}${c.bold}══════════════════════════════════════════════════════════${c.reset}`
|
|
261
546
|
);
|
|
262
|
-
|
|
547
|
+
let modeLabel = '';
|
|
548
|
+
if (visualMode) modeLabel += ' [VISUAL]';
|
|
549
|
+
if (coverageMode) modeLabel += ` [COVERAGE ≥${coverageThreshold}%]`;
|
|
550
|
+
if (hasDiscretionConditions) modeLabel += ` [${discretionConditions.length} CONDITIONS]`;
|
|
263
551
|
console.log(
|
|
264
552
|
`${c.brand}${c.bold} RALPH LOOP - Iteration ${iteration}/${maxIterations}${modeLabel}${c.reset}`
|
|
265
553
|
);
|
|
@@ -267,6 +555,8 @@ function handleLoop(rootDir) {
|
|
|
267
555
|
`${c.brand}${c.bold}══════════════════════════════════════════════════════════${c.reset}`
|
|
268
556
|
);
|
|
269
557
|
console.log('');
|
|
558
|
+
// State Narration: Loop iteration marker
|
|
559
|
+
console.log(`🔄 Iteration ${iteration}/${maxIterations}`);
|
|
270
560
|
|
|
271
561
|
// Check iteration limit
|
|
272
562
|
if (iteration > maxIterations) {
|
|
@@ -291,9 +581,8 @@ function handleLoop(rootDir) {
|
|
|
291
581
|
return;
|
|
292
582
|
}
|
|
293
583
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
);
|
|
584
|
+
// State Narration: Current position marker
|
|
585
|
+
console.log(`📍 Working on: ${currentStoryId} - ${currentStory.title || 'Untitled'}`);
|
|
297
586
|
console.log('');
|
|
298
587
|
|
|
299
588
|
// Run tests
|
|
@@ -333,11 +622,34 @@ function handleLoop(rootDir) {
|
|
|
333
622
|
}
|
|
334
623
|
}
|
|
335
624
|
|
|
336
|
-
//
|
|
337
|
-
|
|
625
|
+
// Coverage Mode: Run coverage and verify threshold
|
|
626
|
+
let coverageResult = { passed: true };
|
|
627
|
+
if (coverageMode) {
|
|
628
|
+
console.log('');
|
|
629
|
+
console.log(`${c.blue}Running coverage check...${c.reset}`);
|
|
630
|
+
runCoverage(rootDir);
|
|
631
|
+
coverageResult = verifyCoverage(rootDir, coverageThreshold);
|
|
632
|
+
console.log(coverageResult.message);
|
|
633
|
+
|
|
634
|
+
// Update state with current coverage
|
|
635
|
+
state.ralph_loop.coverage_current = coverageResult.coverage;
|
|
636
|
+
|
|
637
|
+
if (coverageResult.passed) {
|
|
638
|
+
state.ralph_loop.coverage_verified = true;
|
|
639
|
+
} else {
|
|
640
|
+
state.ralph_loop.coverage_verified = false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Enforce minimum iterations for Visual and Coverage modes
|
|
645
|
+
if ((visualMode || coverageMode) && iteration < minIterations) {
|
|
646
|
+
const modeNames = [];
|
|
647
|
+
if (visualMode) modeNames.push('Visual');
|
|
648
|
+
if (coverageMode) modeNames.push('Coverage');
|
|
649
|
+
|
|
338
650
|
console.log('');
|
|
339
651
|
console.log(
|
|
340
|
-
`${c.yellow}⚠
|
|
652
|
+
`${c.yellow}⚠ ${modeNames.join(' + ')} Mode requires ${minIterations}+ iterations for confirmation${c.reset}`
|
|
341
653
|
);
|
|
342
654
|
console.log(
|
|
343
655
|
`${c.dim}Current: iteration ${iteration}. Let loop run once more to confirm.${c.reset}`
|
|
@@ -347,29 +659,76 @@ function handleLoop(rootDir) {
|
|
|
347
659
|
saveSessionState(rootDir, state);
|
|
348
660
|
|
|
349
661
|
console.log('');
|
|
350
|
-
console.log(`${c.brand}▶ Continue
|
|
662
|
+
console.log(`${c.brand}▶ Continue working. Loop will verify again.${c.reset}`);
|
|
351
663
|
return;
|
|
352
664
|
}
|
|
353
665
|
|
|
354
|
-
//
|
|
355
|
-
|
|
666
|
+
// Evaluate discretion conditions
|
|
667
|
+
let discretionResults = [];
|
|
668
|
+
if (hasDiscretionConditions) {
|
|
669
|
+
console.log('');
|
|
670
|
+
console.log(`${c.blue}Evaluating discretion conditions...${c.reset}`);
|
|
671
|
+
const ctx = { currentStoryId, coverageThreshold };
|
|
672
|
+
|
|
673
|
+
for (const condition of discretionConditions) {
|
|
674
|
+
const result = evaluateDiscretionCondition(condition, rootDir, ctx);
|
|
675
|
+
discretionResults.push({ condition, ...result });
|
|
676
|
+
const marker = result.passed ? `${c.green}✓` : `${c.yellow}⏳`;
|
|
677
|
+
console.log(` ${marker} **${condition.replace(/\*\*/g, '')}**: ${result.message}${c.reset}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Track which conditions have been verified
|
|
681
|
+
const allConditionsPassed = discretionResults.every(r => r.passed);
|
|
682
|
+
state.ralph_loop.conditions_verified = allConditionsPassed;
|
|
683
|
+
state.ralph_loop.condition_results = discretionResults.map(r => ({
|
|
684
|
+
condition: r.condition,
|
|
685
|
+
passed: r.passed,
|
|
686
|
+
message: r.message,
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Check if all verification modes passed
|
|
691
|
+
const allDiscretionPassed =
|
|
692
|
+
!hasDiscretionConditions || discretionResults.every(r => r.passed);
|
|
693
|
+
const canComplete =
|
|
694
|
+
testResult.passed &&
|
|
695
|
+
(!visualMode || screenshotResult.passed) &&
|
|
696
|
+
(!coverageMode || coverageResult.passed) &&
|
|
697
|
+
allDiscretionPassed;
|
|
356
698
|
|
|
357
699
|
if (!canComplete) {
|
|
358
|
-
//
|
|
700
|
+
// Something not verified yet
|
|
359
701
|
state.ralph_loop.iteration = iteration;
|
|
360
702
|
saveSessionState(rootDir, state);
|
|
361
703
|
|
|
362
704
|
console.log('');
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
705
|
+
if (visualMode && !screenshotResult.passed) {
|
|
706
|
+
console.log(`${c.cyan}▶ Review unverified screenshots:${c.reset}`);
|
|
707
|
+
console.log(`${c.dim} 1. View each screenshot in screenshots/ directory${c.reset}`);
|
|
708
|
+
console.log(`${c.dim} 2. Rename verified files with 'verified-' prefix${c.reset}`);
|
|
709
|
+
console.log(`${c.dim} 3. Loop will re-verify when you stop${c.reset}`);
|
|
710
|
+
}
|
|
711
|
+
if (coverageMode && !coverageResult.passed) {
|
|
712
|
+
console.log(`${c.cyan}▶ Increase test coverage:${c.reset}`);
|
|
713
|
+
console.log(`${c.dim} Current: ${coverageResult.coverage?.toFixed(1) || 0}%${c.reset}`);
|
|
714
|
+
console.log(`${c.dim} Target: ${coverageThreshold}%${c.reset}`);
|
|
715
|
+
console.log(`${c.dim} Write more tests to cover uncovered code paths.${c.reset}`);
|
|
716
|
+
}
|
|
717
|
+
if (hasDiscretionConditions && !allDiscretionPassed) {
|
|
718
|
+
const failedConditions = discretionResults.filter(r => !r.passed);
|
|
719
|
+
console.log(`${c.cyan}▶ Fix failing conditions:${c.reset}`);
|
|
720
|
+
for (const fc of failedConditions) {
|
|
721
|
+
console.log(`${c.dim} - ${fc.condition.replace(/\*\*/g, '')}: ${fc.message}${c.reset}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
367
724
|
return;
|
|
368
725
|
}
|
|
369
726
|
console.log('');
|
|
370
727
|
|
|
371
728
|
// Mark story complete
|
|
372
729
|
markStoryComplete(rootDir, currentStoryId);
|
|
730
|
+
// State Narration: Completion marker
|
|
731
|
+
console.log(`✅ Story complete: ${currentStoryId}`);
|
|
373
732
|
console.log(`${c.green}✓ Marked ${currentStoryId} as completed${c.reset}`);
|
|
374
733
|
|
|
375
734
|
// Get next story
|
|
@@ -423,6 +782,8 @@ function handleLoop(rootDir) {
|
|
|
423
782
|
}
|
|
424
783
|
} else {
|
|
425
784
|
// Tests failed - feed back to Claude
|
|
785
|
+
// State Narration: Error marker
|
|
786
|
+
console.log(`⚠️ Error: Test failure - ${(testResult.duration / 1000).toFixed(1)}s`);
|
|
426
787
|
console.log(`${c.red}✗ Tests failed${c.reset} (${(testResult.duration / 1000).toFixed(1)}s)`);
|
|
427
788
|
console.log('');
|
|
428
789
|
|
|
@@ -458,7 +819,12 @@ function handleCLI() {
|
|
|
458
819
|
if (!loop || !loop.enabled) {
|
|
459
820
|
console.log(`${c.dim}Ralph Loop: not active${c.reset}`);
|
|
460
821
|
} else {
|
|
461
|
-
|
|
822
|
+
let modeLabel = '';
|
|
823
|
+
if (loop.visual_mode) modeLabel += ` ${c.cyan}[VISUAL]${c.reset}`;
|
|
824
|
+
if (loop.coverage_mode)
|
|
825
|
+
modeLabel += ` ${c.magenta}[COVERAGE ≥${loop.coverage_threshold}%]${c.reset}`;
|
|
826
|
+
if (loop.conditions?.length > 0)
|
|
827
|
+
modeLabel += ` ${c.blue}[${loop.conditions.length} CONDITIONS]${c.reset}`;
|
|
462
828
|
console.log(`${c.green}Ralph Loop: active${c.reset}${modeLabel}`);
|
|
463
829
|
console.log(` Epic: ${loop.epic}`);
|
|
464
830
|
console.log(` Current Story: ${loop.current_story}`);
|
|
@@ -469,6 +835,25 @@ function handleCLI() {
|
|
|
469
835
|
: `${c.yellow}no${c.reset}`;
|
|
470
836
|
console.log(` Screenshots Verified: ${verified}`);
|
|
471
837
|
}
|
|
838
|
+
if (loop.coverage_mode) {
|
|
839
|
+
const verified = loop.coverage_verified
|
|
840
|
+
? `${c.green}yes${c.reset}`
|
|
841
|
+
: `${c.yellow}no${c.reset}`;
|
|
842
|
+
console.log(
|
|
843
|
+
` Coverage: ${(loop.coverage_current || 0).toFixed(1)}% / ${loop.coverage_threshold}% (Verified: ${verified})`
|
|
844
|
+
);
|
|
845
|
+
console.log(` Baseline: ${(loop.coverage_baseline || 0).toFixed(1)}%`);
|
|
846
|
+
}
|
|
847
|
+
if (loop.conditions?.length > 0) {
|
|
848
|
+
const verified = loop.conditions_verified
|
|
849
|
+
? `${c.green}yes${c.reset}`
|
|
850
|
+
: `${c.yellow}no${c.reset}`;
|
|
851
|
+
console.log(` Discretion Conditions: ${loop.conditions.length} (All Verified: ${verified})`);
|
|
852
|
+
for (const result of loop.condition_results || []) {
|
|
853
|
+
const mark = result.passed ? `${c.green}✓${c.reset}` : `${c.yellow}⏳${c.reset}`;
|
|
854
|
+
console.log(` ${mark} ${result.condition.replace(/\*\*/g, '')}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
472
857
|
}
|
|
473
858
|
return true;
|
|
474
859
|
}
|
|
@@ -499,6 +884,10 @@ function handleCLI() {
|
|
|
499
884
|
const epicArg = args.find(a => a.startsWith('--epic='));
|
|
500
885
|
const maxArg = args.find(a => a.startsWith('--max='));
|
|
501
886
|
const visualArg = args.includes('--visual') || args.includes('-v');
|
|
887
|
+
const coverageArg = args.find(a => a.startsWith('--coverage='));
|
|
888
|
+
// Parse conditions (--condition="**all tests passing**" or -c "...")
|
|
889
|
+
const conditionArgs = args.filter(a => a.startsWith('--condition=') || a.startsWith('-c='));
|
|
890
|
+
const conditions = conditionArgs.map(a => a.split('=').slice(1).join('=').replace(/"/g, ''));
|
|
502
891
|
|
|
503
892
|
if (!epicArg) {
|
|
504
893
|
console.log(`${c.red}Error: --epic=EP-XXXX is required${c.reset}`);
|
|
@@ -506,9 +895,28 @@ function handleCLI() {
|
|
|
506
895
|
}
|
|
507
896
|
|
|
508
897
|
const epicId = epicArg.split('=')[1];
|
|
509
|
-
|
|
898
|
+
|
|
899
|
+
// Validate epic ID format
|
|
900
|
+
if (!isValidEpicId(epicId)) {
|
|
901
|
+
console.log(`${c.red}Error: Invalid epic ID "${epicId}". Expected format: EP-XXXX${c.reset}`);
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Validate and bound max iterations (1-100)
|
|
906
|
+
const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 20, 1, 100);
|
|
510
907
|
const visualMode = visualArg;
|
|
511
908
|
|
|
909
|
+
// Parse coverage threshold (0-100)
|
|
910
|
+
let coverageMode = false;
|
|
911
|
+
let coverageThreshold = 80;
|
|
912
|
+
if (coverageArg) {
|
|
913
|
+
coverageMode = true;
|
|
914
|
+
const threshold = parseFloat(coverageArg.split('=')[1]);
|
|
915
|
+
if (!isNaN(threshold)) {
|
|
916
|
+
coverageThreshold = Math.max(0, Math.min(100, threshold));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
512
920
|
// Find first ready story in epic
|
|
513
921
|
const status = getStatus(rootDir);
|
|
514
922
|
const stories = status.stories || {};
|
|
@@ -532,6 +940,22 @@ function handleCLI() {
|
|
|
532
940
|
// Mark first story as in_progress
|
|
533
941
|
markStoryInProgress(rootDir, storyId);
|
|
534
942
|
|
|
943
|
+
// Get baseline coverage if coverage mode is enabled
|
|
944
|
+
let coverageBaseline = 0;
|
|
945
|
+
if (coverageMode) {
|
|
946
|
+
console.log(`${c.dim}Running baseline coverage check...${c.reset}`);
|
|
947
|
+
runCoverage(rootDir);
|
|
948
|
+
const baseline = parseCoverageReport(rootDir);
|
|
949
|
+
if (baseline.passed) {
|
|
950
|
+
coverageBaseline = baseline.coverage;
|
|
951
|
+
console.log(`${c.dim}Baseline coverage: ${coverageBaseline.toFixed(1)}%${c.reset}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Get conditions from metadata if not provided via CLI
|
|
956
|
+
const allConditions =
|
|
957
|
+
conditions.length > 0 ? conditions : getDiscretionConditions(rootDir);
|
|
958
|
+
|
|
535
959
|
// Initialize loop state
|
|
536
960
|
const state = getSessionState(rootDir);
|
|
537
961
|
state.ralph_loop = {
|
|
@@ -542,6 +966,14 @@ function handleCLI() {
|
|
|
542
966
|
max_iterations: maxIterations,
|
|
543
967
|
visual_mode: visualMode,
|
|
544
968
|
screenshots_verified: false,
|
|
969
|
+
coverage_mode: coverageMode,
|
|
970
|
+
coverage_threshold: coverageThreshold,
|
|
971
|
+
coverage_baseline: coverageBaseline,
|
|
972
|
+
coverage_current: coverageBaseline,
|
|
973
|
+
coverage_verified: false,
|
|
974
|
+
conditions: allConditions,
|
|
975
|
+
conditions_verified: false,
|
|
976
|
+
condition_results: [],
|
|
545
977
|
started_at: new Date().toISOString(),
|
|
546
978
|
};
|
|
547
979
|
saveSessionState(rootDir, state);
|
|
@@ -549,7 +981,9 @@ function handleCLI() {
|
|
|
549
981
|
const progress = getEpicProgress(status, epicId);
|
|
550
982
|
|
|
551
983
|
console.log('');
|
|
552
|
-
|
|
984
|
+
let modeLabel = '';
|
|
985
|
+
if (visualMode) modeLabel += ` ${c.cyan}[VISUAL]${c.reset}`;
|
|
986
|
+
if (coverageMode) modeLabel += ` ${c.magenta}[COVERAGE ≥${coverageThreshold}%]${c.reset}`;
|
|
553
987
|
console.log(`${c.green}${c.bold}Ralph Loop Initialized${c.reset}${modeLabel}`);
|
|
554
988
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
555
989
|
console.log(` Epic: ${c.cyan}${epicId}${c.reset}`);
|
|
@@ -557,6 +991,20 @@ function handleCLI() {
|
|
|
557
991
|
console.log(` Max Iterations: ${maxIterations}`);
|
|
558
992
|
if (visualMode) {
|
|
559
993
|
console.log(` Visual Mode: ${c.cyan}enabled${c.reset} (screenshot verification)`);
|
|
994
|
+
}
|
|
995
|
+
if (coverageMode) {
|
|
996
|
+
console.log(
|
|
997
|
+
` Coverage Mode: ${c.magenta}enabled${c.reset} (threshold: ${coverageThreshold}%)`
|
|
998
|
+
);
|
|
999
|
+
console.log(` Baseline: ${coverageBaseline.toFixed(1)}%`);
|
|
1000
|
+
}
|
|
1001
|
+
if (allConditions.length > 0) {
|
|
1002
|
+
console.log(` Conditions: ${c.blue}${allConditions.length} discretion conditions${c.reset}`);
|
|
1003
|
+
for (const cond of allConditions) {
|
|
1004
|
+
console.log(` - **${cond.replace(/\*\*/g, '')}**`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (visualMode || coverageMode || allConditions.length > 0) {
|
|
560
1008
|
console.log(` Min Iterations: 2 (for confirmation)`);
|
|
561
1009
|
}
|
|
562
1010
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
@@ -585,17 +1033,21 @@ function handleCLI() {
|
|
|
585
1033
|
${c.brand}${c.bold}ralph-loop.js${c.reset} - Autonomous Story Processing
|
|
586
1034
|
|
|
587
1035
|
${c.bold}Usage:${c.reset}
|
|
588
|
-
node scripts/ralph-loop.js
|
|
589
|
-
node scripts/ralph-loop.js --init --epic=EP-XXX
|
|
590
|
-
node scripts/ralph-loop.js --init --epic=EP-XXX --visual
|
|
591
|
-
node scripts/ralph-loop.js --
|
|
592
|
-
node scripts/ralph-loop.js --
|
|
593
|
-
node scripts/ralph-loop.js --
|
|
1036
|
+
node scripts/ralph-loop.js Run loop check (Stop hook)
|
|
1037
|
+
node scripts/ralph-loop.js --init --epic=EP-XXX Initialize loop for epic
|
|
1038
|
+
node scripts/ralph-loop.js --init --epic=EP-XXX --visual Initialize with Visual Mode
|
|
1039
|
+
node scripts/ralph-loop.js --init --epic=EP-XXX --coverage=80 Initialize with Coverage Mode
|
|
1040
|
+
node scripts/ralph-loop.js --init --epic=EP-XXX --condition="**all tests passing**"
|
|
1041
|
+
node scripts/ralph-loop.js --status Check loop status
|
|
1042
|
+
node scripts/ralph-loop.js --stop Stop the loop
|
|
1043
|
+
node scripts/ralph-loop.js --reset Reset loop state
|
|
594
1044
|
|
|
595
1045
|
${c.bold}Options:${c.reset}
|
|
596
|
-
--epic=EP-XXXX
|
|
597
|
-
--max=N
|
|
598
|
-
--visual, -v
|
|
1046
|
+
--epic=EP-XXXX Epic ID to process (required for --init)
|
|
1047
|
+
--max=N Max iterations (default: 20)
|
|
1048
|
+
--visual, -v Enable Visual Mode (screenshot verification)
|
|
1049
|
+
--coverage=N Enable Coverage Mode (iterate until N% coverage)
|
|
1050
|
+
--condition="..." Add discretion condition (can use multiple times)
|
|
599
1051
|
|
|
600
1052
|
${c.bold}Visual Mode:${c.reset}
|
|
601
1053
|
When --visual is enabled, the loop also verifies that all screenshots
|
|
@@ -604,16 +1056,48 @@ ${c.bold}Visual Mode:${c.reset}
|
|
|
604
1056
|
This ensures Claude actually looks at UI screenshots before declaring
|
|
605
1057
|
completion. Requires minimum 2 iterations for confirmation.
|
|
606
1058
|
|
|
1059
|
+
${c.bold}Coverage Mode:${c.reset}
|
|
1060
|
+
When --coverage=N is enabled, the loop verifies test coverage meets
|
|
1061
|
+
the threshold N% before completing stories.
|
|
1062
|
+
|
|
1063
|
+
Coverage is read from coverage/coverage-summary.json (Jest/NYC format).
|
|
1064
|
+
Configure in docs/00-meta/agileflow-metadata.json:
|
|
1065
|
+
{ "ralph_loop": { "coverage_command": "npm run test:coverage" } }
|
|
1066
|
+
|
|
607
1067
|
Workflow:
|
|
608
1068
|
1. Tests run → must pass
|
|
609
|
-
2.
|
|
610
|
-
3. Minimum 2 iterations →
|
|
1069
|
+
2. Coverage checked → must meet threshold
|
|
1070
|
+
3. Minimum 2 iterations → confirms coverage is stable
|
|
611
1071
|
4. Only then → story marked complete
|
|
612
1072
|
|
|
1073
|
+
${c.bold}Discretion Conditions:${c.reset}
|
|
1074
|
+
Semantic conditions that must pass before story completion.
|
|
1075
|
+
Use --condition multiple times for multiple conditions.
|
|
1076
|
+
|
|
1077
|
+
Built-in conditions:
|
|
1078
|
+
**all tests passing** Tests must pass
|
|
1079
|
+
**tests pass** Tests must pass (alias)
|
|
1080
|
+
**coverage above 80%** Coverage must meet threshold
|
|
1081
|
+
**no linting errors** npm run lint must pass
|
|
1082
|
+
**no type errors** npx tsc --noEmit must pass
|
|
1083
|
+
**build succeeds** npm run build must pass
|
|
1084
|
+
**all screenshots verified** Screenshots need verified- prefix
|
|
1085
|
+
**all acceptance criteria verified** AC marked complete in status.json
|
|
1086
|
+
|
|
1087
|
+
Configure in docs/00-meta/agileflow-metadata.json:
|
|
1088
|
+
{
|
|
1089
|
+
"ralph_loop": {
|
|
1090
|
+
"conditions": [
|
|
1091
|
+
"**all tests passing**",
|
|
1092
|
+
"**no linting errors**"
|
|
1093
|
+
]
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
613
1097
|
${c.bold}How it works:${c.reset}
|
|
614
|
-
1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop
|
|
1098
|
+
1. Start loop with /agileflow:babysit EPIC=EP-XXX MODE=loop COVERAGE=80
|
|
615
1099
|
2. Work on the current story
|
|
616
|
-
3. When you stop, this hook runs tests
|
|
1100
|
+
3. When you stop, this hook runs tests and verifications
|
|
617
1101
|
4. If all pass → story marked complete, next story loaded
|
|
618
1102
|
5. If any fail → failures shown, you continue fixing
|
|
619
1103
|
6. Loop repeats until epic done or max iterations
|