agileflow 3.4.2 → 3.4.3
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/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/lib/drivers/claude-driver.ts +1 -1
- package/lib/lazy-require.js +1 -1
- package/package.json +1 -1
- package/scripts/agent-loop.js +290 -230
- package/scripts/check-sessions.js +116 -0
- package/scripts/lib/quality-gates.js +35 -8
- package/scripts/lib/signal-detectors.js +0 -13
- package/scripts/lib/team-events.js +1 -1
- package/scripts/lib/tmux-audit-monitor.js +2 -1
- package/src/core/commands/ads/audit.md +19 -3
- package/src/core/commands/code/accessibility.md +22 -6
- package/src/core/commands/code/api.md +22 -6
- package/src/core/commands/code/architecture.md +22 -6
- package/src/core/commands/code/completeness.md +22 -6
- package/src/core/commands/code/legal.md +22 -6
- package/src/core/commands/code/logic.md +22 -6
- package/src/core/commands/code/performance.md +22 -6
- package/src/core/commands/code/security.md +22 -6
- package/src/core/commands/code/test.md +22 -6
- package/src/core/commands/ideate/features.md +5 -4
- package/src/core/commands/ideate/new.md +8 -7
- package/src/core/commands/seo/audit.md +21 -5
- package/lib/claude-cli-bridge.js +0 -215
- package/lib/dashboard-automations.js +0 -130
- package/lib/dashboard-git.js +0 -254
- package/lib/dashboard-inbox.js +0 -64
- package/lib/dashboard-protocol.js +0 -605
- package/lib/dashboard-server.js +0 -1296
- package/lib/dashboard-session.js +0 -136
- package/lib/dashboard-status.js +0 -72
- package/lib/dashboard-terminal.js +0 -354
- package/lib/dashboard-websocket.js +0 -88
- package/scripts/dashboard-serve.js +0 -336
- package/src/core/commands/serve.md +0 -127
- package/tools/cli/commands/serve.js +0 -492
package/scripts/agent-loop.js
CHANGED
|
@@ -26,17 +26,9 @@ const crypto = require('crypto');
|
|
|
26
26
|
const { c } = require('../lib/colors');
|
|
27
27
|
const { getProjectRoot } = require('../lib/paths');
|
|
28
28
|
const { safeReadJSON, safeWriteJSON, debugLog } = require('../lib/errors');
|
|
29
|
-
const {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
injectCorrelation,
|
|
33
|
-
startSpan,
|
|
34
|
-
getContext,
|
|
35
|
-
} = require('../lib/correlation');
|
|
36
|
-
|
|
37
|
-
const ROOT = getProjectRoot();
|
|
38
|
-
const LOOPS_DIR = path.join(ROOT, '.agileflow', 'sessions', 'agent-loops');
|
|
39
|
-
const BUS_PATH = path.join(ROOT, 'docs', '09-agents', 'bus', 'log.jsonl');
|
|
29
|
+
const { buildSpawnArgs } = require('../lib/validate-commands');
|
|
30
|
+
const { initializeForProject, injectCorrelation } = require('../lib/correlation');
|
|
31
|
+
const qualityGates = require('./lib/quality-gates');
|
|
40
32
|
|
|
41
33
|
// ============================================================================
|
|
42
34
|
// CONSTANTS
|
|
@@ -46,6 +38,7 @@ const MAX_ITERATIONS_HARD_LIMIT = 5;
|
|
|
46
38
|
const MAX_AGENTS_HARD_LIMIT = 3;
|
|
47
39
|
const TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes per loop
|
|
48
40
|
const STALL_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes without progress
|
|
41
|
+
const REPEATED_FAILURE_LIMIT = 3; // Abort after 3 identical failures
|
|
49
42
|
|
|
50
43
|
const GATES = {
|
|
51
44
|
tests: { name: 'Tests', metric: 'pass/fail' },
|
|
@@ -55,39 +48,65 @@ const GATES = {
|
|
|
55
48
|
types: { name: 'TypeScript', metric: 'pass/fail' },
|
|
56
49
|
};
|
|
57
50
|
|
|
51
|
+
// Map agent-loop gate names to quality-gates GATE_TYPES
|
|
52
|
+
const GATE_TYPE_MAP = {
|
|
53
|
+
tests: qualityGates.GATE_TYPES.TESTS,
|
|
54
|
+
coverage: qualityGates.GATE_TYPES.COVERAGE,
|
|
55
|
+
lint: qualityGates.GATE_TYPES.LINT,
|
|
56
|
+
types: qualityGates.GATE_TYPES.TYPES,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// ROOT RESOLUTION (lazy for testability)
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
function _getRoot(rootDir) {
|
|
64
|
+
return rootDir || getProjectRoot();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _loopsDir(rootDir) {
|
|
68
|
+
return path.join(_getRoot(rootDir), '.agileflow', 'sessions', 'agent-loops');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _busPath(rootDir) {
|
|
72
|
+
return path.join(_getRoot(rootDir), 'docs', '09-agents', 'bus', 'log.jsonl');
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
// ============================================================================
|
|
59
76
|
// UTILITY FUNCTIONS
|
|
60
77
|
// ============================================================================
|
|
61
78
|
|
|
62
|
-
function ensureLoopsDir() {
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
function ensureLoopsDir(rootDir) {
|
|
80
|
+
const dir = _loopsDir(rootDir);
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
83
|
}
|
|
66
84
|
}
|
|
67
85
|
|
|
68
|
-
function getLoopPath(loopId) {
|
|
69
|
-
return path.join(
|
|
86
|
+
function getLoopPath(loopId, rootDir) {
|
|
87
|
+
return path.join(_loopsDir(rootDir), `${loopId}.json`);
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
function generateLoopId() {
|
|
73
91
|
return crypto.randomUUID().split('-')[0]; // Short UUID (8 chars)
|
|
74
92
|
}
|
|
75
93
|
|
|
76
|
-
function loadLoop(loopId) {
|
|
77
|
-
const loopPath = getLoopPath(loopId);
|
|
94
|
+
function loadLoop(loopId, rootDir) {
|
|
95
|
+
const loopPath = getLoopPath(loopId, rootDir);
|
|
78
96
|
const result = safeReadJSON(loopPath, { defaultValue: null });
|
|
79
97
|
return result.ok ? result.data : null;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
function saveLoop(loopId, state) {
|
|
83
|
-
ensureLoopsDir();
|
|
84
|
-
const loopPath = getLoopPath(loopId);
|
|
100
|
+
function saveLoop(loopId, state, rootDir) {
|
|
101
|
+
ensureLoopsDir(rootDir);
|
|
102
|
+
const loopPath = getLoopPath(loopId, rootDir);
|
|
85
103
|
state.updated_at = new Date().toISOString();
|
|
86
104
|
safeWriteJSON(loopPath, state, { createDir: true });
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
function emitEvent(event) {
|
|
90
|
-
const
|
|
107
|
+
function emitEvent(event, rootDir) {
|
|
108
|
+
const busPath = _busPath(rootDir);
|
|
109
|
+
const busDir = path.dirname(busPath);
|
|
91
110
|
if (!fs.existsSync(busDir)) {
|
|
92
111
|
fs.mkdirSync(busDir, { recursive: true });
|
|
93
112
|
}
|
|
@@ -100,49 +119,22 @@ function emitEvent(event) {
|
|
|
100
119
|
|
|
101
120
|
const line = JSON.stringify(correlatedEvent) + '\n';
|
|
102
121
|
|
|
103
|
-
fs.appendFileSync(
|
|
122
|
+
fs.appendFileSync(busPath, line);
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
// ============================================================================
|
|
107
|
-
// QUALITY GATE
|
|
126
|
+
// QUALITY GATE INTEGRATION
|
|
108
127
|
// ============================================================================
|
|
109
128
|
|
|
110
|
-
function getTestCommand() {
|
|
111
|
-
const metadataPath = path.join(ROOT, 'docs/00-meta/agileflow-metadata.json');
|
|
112
|
-
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
113
|
-
|
|
114
|
-
if (result.ok && result.data?.ralph_loop?.test_command) {
|
|
115
|
-
return result.data.ralph_loop.test_command;
|
|
116
|
-
}
|
|
117
|
-
return 'npm test';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getCoverageCommand() {
|
|
121
|
-
const metadataPath = path.join(ROOT, 'docs/00-meta/agileflow-metadata.json');
|
|
122
|
-
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
123
|
-
|
|
124
|
-
if (result.ok && result.data?.ralph_loop?.coverage_command) {
|
|
125
|
-
return result.data.ralph_loop.coverage_command;
|
|
126
|
-
}
|
|
127
|
-
return 'npm run test:coverage || npm test -- --coverage';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getCoverageReportPath() {
|
|
131
|
-
const metadataPath = path.join(ROOT, 'docs/00-meta/agileflow-metadata.json');
|
|
132
|
-
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
133
|
-
|
|
134
|
-
if (result.ok && result.data?.ralph_loop?.coverage_report_path) {
|
|
135
|
-
return result.data.ralph_loop.coverage_report_path;
|
|
136
|
-
}
|
|
137
|
-
return 'coverage/coverage-summary.json';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
129
|
/**
|
|
141
130
|
* Run a command safely using spawn with validated arguments
|
|
142
131
|
* @param {string} cmd - Command string to run
|
|
132
|
+
* @param {string} [rootDir] - Project root directory
|
|
143
133
|
* @returns {{ passed: boolean, exitCode: number, error?: string, blocked?: boolean }}
|
|
144
134
|
*/
|
|
145
|
-
function runCommand(cmd) {
|
|
135
|
+
function runCommand(cmd, rootDir) {
|
|
136
|
+
const root = _getRoot(rootDir);
|
|
137
|
+
|
|
146
138
|
// Validate command against allowlist
|
|
147
139
|
const validation = buildSpawnArgs(cmd, { strict: true, logBlocked: true });
|
|
148
140
|
|
|
@@ -165,7 +157,7 @@ function runCommand(cmd) {
|
|
|
165
157
|
try {
|
|
166
158
|
// Use spawnSync with array arguments (no shell injection possible)
|
|
167
159
|
const result = spawnSync(file, args, {
|
|
168
|
-
cwd:
|
|
160
|
+
cwd: root,
|
|
169
161
|
stdio: 'inherit',
|
|
170
162
|
shell: false, // CRITICAL: Do not use shell to prevent injection
|
|
171
163
|
});
|
|
@@ -185,50 +177,33 @@ function runCommand(cmd) {
|
|
|
185
177
|
}
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function checkCoverageGate(threshold) {
|
|
200
|
-
// Run coverage command
|
|
201
|
-
const cmd = getCoverageCommand();
|
|
202
|
-
console.log(`${c.dim}Running: ${cmd}${c.reset}`);
|
|
203
|
-
runCommand(cmd);
|
|
204
|
-
|
|
205
|
-
// Parse coverage report
|
|
206
|
-
const reportPath = path.join(ROOT, getCoverageReportPath());
|
|
207
|
-
const report = safeReadJSON(reportPath, { defaultValue: null });
|
|
208
|
-
|
|
209
|
-
if (!report.ok || !report.data) {
|
|
210
|
-
return {
|
|
211
|
-
passed: false,
|
|
212
|
-
value: 0,
|
|
213
|
-
message: `Coverage report not found at ${getCoverageReportPath()}`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
180
|
+
/**
|
|
181
|
+
* Get custom command from project metadata for a gate
|
|
182
|
+
* @param {string} gateName - Gate name (tests, coverage)
|
|
183
|
+
* @param {string} rootDir - Project root
|
|
184
|
+
* @returns {string|undefined} Custom command or undefined
|
|
185
|
+
*/
|
|
186
|
+
function getCommandFromMetadata(gateName, rootDir) {
|
|
187
|
+
const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
|
|
188
|
+
const result = safeReadJSON(metadataPath, { defaultValue: {} });
|
|
216
189
|
|
|
217
|
-
|
|
218
|
-
const coverage = total?.lines?.pct || total?.statements?.pct || 0;
|
|
219
|
-
const passed = coverage >= threshold;
|
|
190
|
+
if (!result.ok || !result.data?.ralph_loop) return undefined;
|
|
220
191
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
message: passed
|
|
225
|
-
? `Coverage ${coverage.toFixed(1)}% >= ${threshold}%`
|
|
226
|
-
: `Coverage ${coverage.toFixed(1)}% < ${threshold}% (need ${(threshold - coverage).toFixed(1)}% more)`,
|
|
192
|
+
const metaKey = {
|
|
193
|
+
tests: 'test_command',
|
|
194
|
+
coverage: 'coverage_command',
|
|
227
195
|
};
|
|
196
|
+
|
|
197
|
+
return result.data.ralph_loop[metaKey[gateName]] || undefined;
|
|
228
198
|
}
|
|
229
199
|
|
|
230
|
-
|
|
231
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Check visual gate (filesystem-based, not command-based)
|
|
202
|
+
* @param {string} rootDir - Project root
|
|
203
|
+
* @returns {{ passed: boolean, value: number, message: string }}
|
|
204
|
+
*/
|
|
205
|
+
function checkVisualGate(rootDir) {
|
|
206
|
+
const screenshotsDir = path.join(rootDir, 'screenshots');
|
|
232
207
|
|
|
233
208
|
if (!fs.existsSync(screenshotsDir)) {
|
|
234
209
|
return {
|
|
@@ -262,41 +237,41 @@ function checkVisualGate() {
|
|
|
262
237
|
};
|
|
263
238
|
}
|
|
264
239
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const result = runCommand('npx tsc --noEmit');
|
|
278
|
-
return {
|
|
279
|
-
passed: result.passed,
|
|
280
|
-
value: result.passed ? 100 : 0,
|
|
281
|
-
message: result.passed ? 'No type errors' : 'Type errors found',
|
|
282
|
-
};
|
|
283
|
-
}
|
|
240
|
+
/**
|
|
241
|
+
* Run a quality gate check using quality-gates.js integration
|
|
242
|
+
* @param {string} gateName - Gate name from GATES
|
|
243
|
+
* @param {number} threshold - Threshold value (for coverage)
|
|
244
|
+
* @param {string} rootDir - Project root directory
|
|
245
|
+
* @returns {{ passed: boolean, value: number, message: string }}
|
|
246
|
+
*/
|
|
247
|
+
function runGateCheck(gateName, threshold, rootDir) {
|
|
248
|
+
// Visual gate is filesystem-based, not command-based
|
|
249
|
+
if (gateName === 'visual') {
|
|
250
|
+
return checkVisualGate(rootDir);
|
|
251
|
+
}
|
|
284
252
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return checkTestsGate();
|
|
289
|
-
case 'coverage':
|
|
290
|
-
return checkCoverageGate(threshold);
|
|
291
|
-
case 'visual':
|
|
292
|
-
return checkVisualGate();
|
|
293
|
-
case 'lint':
|
|
294
|
-
return checkLintGate();
|
|
295
|
-
case 'types':
|
|
296
|
-
return checkTypesGate();
|
|
297
|
-
default:
|
|
298
|
-
return { passed: false, value: 0, message: `Unknown gate: ${gate}` };
|
|
253
|
+
const gateType = GATE_TYPE_MAP[gateName];
|
|
254
|
+
if (!gateType) {
|
|
255
|
+
return { passed: false, value: 0, message: `Unknown gate: ${gateName}` };
|
|
299
256
|
}
|
|
257
|
+
|
|
258
|
+
// Get custom command from metadata if available
|
|
259
|
+
const command = getCommandFromMetadata(gateName, rootDir);
|
|
260
|
+
|
|
261
|
+
const gate = qualityGates.createGate({
|
|
262
|
+
type: gateType,
|
|
263
|
+
name: GATES[gateName].name,
|
|
264
|
+
command: command,
|
|
265
|
+
threshold: threshold || undefined,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const result = qualityGates.executeGate(gate, { cwd: rootDir });
|
|
269
|
+
|
|
270
|
+
// Map quality-gates result to agent-loop format
|
|
271
|
+
const passed = result.status === qualityGates.GATE_STATUS.PASSED;
|
|
272
|
+
const value = result.value !== undefined ? result.value : passed ? 100 : 0;
|
|
273
|
+
|
|
274
|
+
return { passed, value, message: result.message };
|
|
300
275
|
}
|
|
301
276
|
|
|
302
277
|
// ============================================================================
|
|
@@ -311,10 +286,13 @@ function initLoop(options) {
|
|
|
311
286
|
maxIterations = MAX_ITERATIONS_HARD_LIMIT,
|
|
312
287
|
agentType = 'unknown',
|
|
313
288
|
parentId = null,
|
|
289
|
+
rootDir,
|
|
314
290
|
} = options;
|
|
315
291
|
|
|
292
|
+
const root = _getRoot(rootDir);
|
|
293
|
+
|
|
316
294
|
// Initialize correlation context (trace_id, session_id)
|
|
317
|
-
const { traceId, sessionId } = initializeForProject(
|
|
295
|
+
const { traceId, sessionId } = initializeForProject(root);
|
|
318
296
|
|
|
319
297
|
// Validate gate
|
|
320
298
|
if (!GATES[gate]) {
|
|
@@ -327,12 +305,13 @@ function initLoop(options) {
|
|
|
327
305
|
const maxIter = Math.min(maxIterations, MAX_ITERATIONS_HARD_LIMIT);
|
|
328
306
|
|
|
329
307
|
// Check if we're under the agent limit
|
|
330
|
-
ensureLoopsDir();
|
|
308
|
+
ensureLoopsDir(rootDir);
|
|
309
|
+
const loopsDir = _loopsDir(rootDir);
|
|
331
310
|
const existingLoops = fs
|
|
332
|
-
.readdirSync(
|
|
311
|
+
.readdirSync(loopsDir)
|
|
333
312
|
.filter(f => f.endsWith('.json'))
|
|
334
313
|
.map(f => {
|
|
335
|
-
const loop = safeReadJSON(path.join(
|
|
314
|
+
const loop = safeReadJSON(path.join(loopsDir, f), { defaultValue: null });
|
|
336
315
|
return loop.ok ? loop.data : null;
|
|
337
316
|
})
|
|
338
317
|
.filter(l => l && l.status === 'running');
|
|
@@ -357,22 +336,27 @@ function initLoop(options) {
|
|
|
357
336
|
current_value: 0,
|
|
358
337
|
status: 'running',
|
|
359
338
|
regression_count: 0,
|
|
339
|
+
failure_streak: 0,
|
|
340
|
+
last_failure_message: null,
|
|
360
341
|
started_at: new Date().toISOString(),
|
|
361
342
|
last_progress_at: new Date().toISOString(),
|
|
362
343
|
events: [],
|
|
363
344
|
};
|
|
364
345
|
|
|
365
|
-
saveLoop(loopId, state);
|
|
346
|
+
saveLoop(loopId, state, rootDir);
|
|
366
347
|
|
|
367
|
-
emitEvent(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
348
|
+
emitEvent(
|
|
349
|
+
{
|
|
350
|
+
type: 'agent_loop',
|
|
351
|
+
event: 'init',
|
|
352
|
+
loop_id: loopId,
|
|
353
|
+
agent: agentType,
|
|
354
|
+
gate,
|
|
355
|
+
threshold,
|
|
356
|
+
max_iterations: maxIter,
|
|
357
|
+
},
|
|
358
|
+
rootDir
|
|
359
|
+
);
|
|
376
360
|
|
|
377
361
|
console.log(`${c.green}${c.bold}Agent Loop Initialized${c.reset}`);
|
|
378
362
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
@@ -386,8 +370,10 @@ function initLoop(options) {
|
|
|
386
370
|
return loopId;
|
|
387
371
|
}
|
|
388
372
|
|
|
389
|
-
function checkLoop(loopId) {
|
|
390
|
-
const
|
|
373
|
+
function checkLoop(loopId, options = {}) {
|
|
374
|
+
const { rootDir } = options;
|
|
375
|
+
const root = _getRoot(rootDir);
|
|
376
|
+
const state = loadLoop(loopId, rootDir);
|
|
391
377
|
|
|
392
378
|
if (!state) {
|
|
393
379
|
console.error(`${c.red}Loop not found: ${loopId}${c.reset}`);
|
|
@@ -404,16 +390,19 @@ function checkLoop(loopId) {
|
|
|
404
390
|
if (elapsed > TIMEOUT_MS) {
|
|
405
391
|
state.status = 'aborted';
|
|
406
392
|
state.stopped_reason = 'timeout';
|
|
407
|
-
saveLoop(loopId, state);
|
|
393
|
+
saveLoop(loopId, state, rootDir);
|
|
408
394
|
|
|
409
|
-
emitEvent(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
395
|
+
emitEvent(
|
|
396
|
+
{
|
|
397
|
+
type: 'agent_loop',
|
|
398
|
+
event: 'abort',
|
|
399
|
+
loop_id: loopId,
|
|
400
|
+
agent: state.agent_type,
|
|
401
|
+
reason: 'timeout',
|
|
402
|
+
iteration: state.iteration,
|
|
403
|
+
},
|
|
404
|
+
rootDir
|
|
405
|
+
);
|
|
417
406
|
|
|
418
407
|
console.log(`${c.red}Loop aborted: timeout (${Math.round(elapsed / 1000)}s)${c.reset}`);
|
|
419
408
|
return state;
|
|
@@ -426,16 +415,19 @@ function checkLoop(loopId) {
|
|
|
426
415
|
if (state.iteration > state.max_iterations) {
|
|
427
416
|
state.status = 'failed';
|
|
428
417
|
state.stopped_reason = 'max_iterations';
|
|
429
|
-
saveLoop(loopId, state);
|
|
418
|
+
saveLoop(loopId, state, rootDir);
|
|
430
419
|
|
|
431
|
-
emitEvent(
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
420
|
+
emitEvent(
|
|
421
|
+
{
|
|
422
|
+
type: 'agent_loop',
|
|
423
|
+
event: 'failed',
|
|
424
|
+
loop_id: loopId,
|
|
425
|
+
agent: state.agent_type,
|
|
426
|
+
reason: 'max_iterations',
|
|
427
|
+
final_value: state.current_value,
|
|
428
|
+
},
|
|
429
|
+
rootDir
|
|
430
|
+
);
|
|
439
431
|
|
|
440
432
|
console.log(`${c.red}Loop failed: max iterations (${state.max_iterations}) reached${c.reset}`);
|
|
441
433
|
return state;
|
|
@@ -446,8 +438,8 @@ function checkLoop(loopId) {
|
|
|
446
438
|
);
|
|
447
439
|
console.log(`${c.dim}${'─'.repeat(40)}${c.reset}`);
|
|
448
440
|
|
|
449
|
-
// Run gate check
|
|
450
|
-
const result =
|
|
441
|
+
// Run gate check via quality-gates integration
|
|
442
|
+
const result = runGateCheck(state.quality_gate, state.threshold, root);
|
|
451
443
|
const previousValue = state.current_value;
|
|
452
444
|
state.current_value = result.value;
|
|
453
445
|
|
|
@@ -456,21 +448,63 @@ function checkLoop(loopId) {
|
|
|
456
448
|
iter: state.iteration,
|
|
457
449
|
value: result.value,
|
|
458
450
|
passed: result.passed,
|
|
451
|
+
message: result.message,
|
|
459
452
|
at: new Date().toISOString(),
|
|
460
453
|
});
|
|
461
454
|
|
|
462
455
|
// Emit progress
|
|
463
|
-
emitEvent(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
456
|
+
emitEvent(
|
|
457
|
+
{
|
|
458
|
+
type: 'agent_loop',
|
|
459
|
+
event: 'iteration',
|
|
460
|
+
loop_id: loopId,
|
|
461
|
+
agent: state.agent_type,
|
|
462
|
+
gate: state.quality_gate,
|
|
463
|
+
iter: state.iteration,
|
|
464
|
+
value: result.value,
|
|
465
|
+
threshold: state.threshold,
|
|
466
|
+
passed: result.passed,
|
|
467
|
+
},
|
|
468
|
+
rootDir
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Check for repeated failure (same failure message 3+ times)
|
|
472
|
+
if (!result.passed) {
|
|
473
|
+
if (result.message === state.last_failure_message) {
|
|
474
|
+
state.failure_streak++;
|
|
475
|
+
} else {
|
|
476
|
+
state.failure_streak = 1;
|
|
477
|
+
state.last_failure_message = result.message;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (state.failure_streak >= REPEATED_FAILURE_LIMIT) {
|
|
481
|
+
state.status = 'failed';
|
|
482
|
+
state.stopped_reason = 'repeated_failure';
|
|
483
|
+
saveLoop(loopId, state, rootDir);
|
|
484
|
+
|
|
485
|
+
emitEvent(
|
|
486
|
+
{
|
|
487
|
+
type: 'agent_loop',
|
|
488
|
+
event: 'failed',
|
|
489
|
+
loop_id: loopId,
|
|
490
|
+
agent: state.agent_type,
|
|
491
|
+
reason: 'repeated_failure',
|
|
492
|
+
final_value: result.value,
|
|
493
|
+
failure_message: result.message,
|
|
494
|
+
},
|
|
495
|
+
rootDir
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
console.log(
|
|
499
|
+
`${c.red}Loop failed: same failure repeated ${state.failure_streak} times${c.reset}`
|
|
500
|
+
);
|
|
501
|
+
return state;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
// Reset failure streak on success
|
|
505
|
+
state.failure_streak = 0;
|
|
506
|
+
state.last_failure_message = null;
|
|
507
|
+
}
|
|
474
508
|
|
|
475
509
|
// Check for regression
|
|
476
510
|
if (state.iteration > 1 && result.value < previousValue) {
|
|
@@ -482,16 +516,19 @@ function checkLoop(loopId) {
|
|
|
482
516
|
if (state.regression_count >= 2) {
|
|
483
517
|
state.status = 'failed';
|
|
484
518
|
state.stopped_reason = 'regression_detected';
|
|
485
|
-
saveLoop(loopId, state);
|
|
486
|
-
|
|
487
|
-
emitEvent(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
519
|
+
saveLoop(loopId, state, rootDir);
|
|
520
|
+
|
|
521
|
+
emitEvent(
|
|
522
|
+
{
|
|
523
|
+
type: 'agent_loop',
|
|
524
|
+
event: 'failed',
|
|
525
|
+
loop_id: loopId,
|
|
526
|
+
agent: state.agent_type,
|
|
527
|
+
reason: 'regression_detected',
|
|
528
|
+
final_value: result.value,
|
|
529
|
+
},
|
|
530
|
+
rootDir
|
|
531
|
+
);
|
|
495
532
|
|
|
496
533
|
console.log(`${c.red}Loop failed: regression detected 2+ times${c.reset}`);
|
|
497
534
|
return state;
|
|
@@ -499,6 +536,9 @@ function checkLoop(loopId) {
|
|
|
499
536
|
} else if (result.value > previousValue) {
|
|
500
537
|
state.last_progress_at = new Date().toISOString();
|
|
501
538
|
state.regression_count = 0; // Reset on progress
|
|
539
|
+
} else {
|
|
540
|
+
// Value held steady (no regression, no progress) - reset regression count
|
|
541
|
+
state.regression_count = 0;
|
|
502
542
|
}
|
|
503
543
|
|
|
504
544
|
// Check for stall
|
|
@@ -506,16 +546,19 @@ function checkLoop(loopId) {
|
|
|
506
546
|
if (timeSinceProgress > STALL_THRESHOLD_MS) {
|
|
507
547
|
state.status = 'failed';
|
|
508
548
|
state.stopped_reason = 'stalled';
|
|
509
|
-
saveLoop(loopId, state);
|
|
549
|
+
saveLoop(loopId, state, rootDir);
|
|
510
550
|
|
|
511
|
-
emitEvent(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
551
|
+
emitEvent(
|
|
552
|
+
{
|
|
553
|
+
type: 'agent_loop',
|
|
554
|
+
event: 'failed',
|
|
555
|
+
loop_id: loopId,
|
|
556
|
+
agent: state.agent_type,
|
|
557
|
+
reason: 'stalled',
|
|
558
|
+
final_value: result.value,
|
|
559
|
+
},
|
|
560
|
+
rootDir
|
|
561
|
+
);
|
|
519
562
|
|
|
520
563
|
console.log(`${c.red}Loop failed: stalled (no progress for 5+ minutes)${c.reset}`);
|
|
521
564
|
return state;
|
|
@@ -533,27 +576,30 @@ function checkLoop(loopId) {
|
|
|
533
576
|
// Confirmed pass
|
|
534
577
|
state.status = 'passed';
|
|
535
578
|
state.completed_at = new Date().toISOString();
|
|
536
|
-
saveLoop(loopId, state);
|
|
537
|
-
|
|
538
|
-
emitEvent(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
579
|
+
saveLoop(loopId, state, rootDir);
|
|
580
|
+
|
|
581
|
+
emitEvent(
|
|
582
|
+
{
|
|
583
|
+
type: 'agent_loop',
|
|
584
|
+
event: 'passed',
|
|
585
|
+
loop_id: loopId,
|
|
586
|
+
agent: state.agent_type,
|
|
587
|
+
gate: state.quality_gate,
|
|
588
|
+
final_value: result.value,
|
|
589
|
+
iterations: state.iteration,
|
|
590
|
+
},
|
|
591
|
+
rootDir
|
|
592
|
+
);
|
|
547
593
|
|
|
548
594
|
console.log(`\n${c.green}${c.bold}Loop PASSED${c.reset} after ${state.iteration} iterations`);
|
|
549
595
|
console.log(`Final value: ${result.value}${state.threshold > 0 ? '%' : ''}`);
|
|
550
596
|
} else {
|
|
551
597
|
// Need confirmation iteration
|
|
552
598
|
console.log(`${c.dim}Gate passed - need 1 more iteration to confirm${c.reset}`);
|
|
553
|
-
saveLoop(loopId, state);
|
|
599
|
+
saveLoop(loopId, state, rootDir);
|
|
554
600
|
}
|
|
555
601
|
} else {
|
|
556
|
-
saveLoop(loopId, state);
|
|
602
|
+
saveLoop(loopId, state, rootDir);
|
|
557
603
|
console.log(`${c.dim}Continue iterating...${c.reset}`);
|
|
558
604
|
}
|
|
559
605
|
|
|
@@ -562,8 +608,9 @@ function checkLoop(loopId) {
|
|
|
562
608
|
return state;
|
|
563
609
|
}
|
|
564
610
|
|
|
565
|
-
function getStatus(loopId) {
|
|
566
|
-
const
|
|
611
|
+
function getStatus(loopId, options = {}) {
|
|
612
|
+
const { rootDir } = options;
|
|
613
|
+
const state = loadLoop(loopId, rootDir);
|
|
567
614
|
|
|
568
615
|
if (!state) {
|
|
569
616
|
console.error(`${c.red}Loop not found: ${loopId}${c.reset}`);
|
|
@@ -599,8 +646,9 @@ function getStatus(loopId) {
|
|
|
599
646
|
return state;
|
|
600
647
|
}
|
|
601
648
|
|
|
602
|
-
function abortLoop(loopId, reason = 'manual') {
|
|
603
|
-
const
|
|
649
|
+
function abortLoop(loopId, reason = 'manual', options = {}) {
|
|
650
|
+
const { rootDir } = options;
|
|
651
|
+
const state = loadLoop(loopId, rootDir);
|
|
604
652
|
|
|
605
653
|
if (!state) {
|
|
606
654
|
console.error(`${c.red}Loop not found: ${loopId}${c.reset}`);
|
|
@@ -615,25 +663,30 @@ function abortLoop(loopId, reason = 'manual') {
|
|
|
615
663
|
state.status = 'aborted';
|
|
616
664
|
state.stopped_reason = reason;
|
|
617
665
|
state.completed_at = new Date().toISOString();
|
|
618
|
-
saveLoop(loopId, state);
|
|
666
|
+
saveLoop(loopId, state, rootDir);
|
|
619
667
|
|
|
620
|
-
emitEvent(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
668
|
+
emitEvent(
|
|
669
|
+
{
|
|
670
|
+
type: 'agent_loop',
|
|
671
|
+
event: 'abort',
|
|
672
|
+
loop_id: loopId,
|
|
673
|
+
agent: state.agent_type,
|
|
674
|
+
reason,
|
|
675
|
+
final_value: state.current_value,
|
|
676
|
+
},
|
|
677
|
+
rootDir
|
|
678
|
+
);
|
|
628
679
|
|
|
629
680
|
console.log(`${c.yellow}Loop aborted: ${reason}${c.reset}`);
|
|
630
681
|
return state;
|
|
631
682
|
}
|
|
632
683
|
|
|
633
|
-
function listLoops() {
|
|
634
|
-
|
|
684
|
+
function listLoops(options = {}) {
|
|
685
|
+
const { rootDir } = options;
|
|
686
|
+
const loopsDir = _loopsDir(rootDir);
|
|
687
|
+
ensureLoopsDir(rootDir);
|
|
635
688
|
|
|
636
|
-
const files = fs.readdirSync(
|
|
689
|
+
const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('.json'));
|
|
637
690
|
|
|
638
691
|
if (files.length === 0) {
|
|
639
692
|
console.log(`${c.dim}No agent loops found${c.reset}`);
|
|
@@ -642,7 +695,7 @@ function listLoops() {
|
|
|
642
695
|
|
|
643
696
|
const loops = files
|
|
644
697
|
.map(f => {
|
|
645
|
-
const result = safeReadJSON(path.join(
|
|
698
|
+
const result = safeReadJSON(path.join(loopsDir, f), { defaultValue: null });
|
|
646
699
|
return result.ok ? result.data : null;
|
|
647
700
|
})
|
|
648
701
|
.filter(Boolean);
|
|
@@ -665,16 +718,19 @@ function listLoops() {
|
|
|
665
718
|
return loops;
|
|
666
719
|
}
|
|
667
720
|
|
|
668
|
-
function cleanupLoops() {
|
|
669
|
-
|
|
721
|
+
function cleanupLoops(options = {}) {
|
|
722
|
+
const { rootDir } = options;
|
|
723
|
+
const loopsDir = _loopsDir(rootDir);
|
|
724
|
+
ensureLoopsDir(rootDir);
|
|
670
725
|
|
|
671
|
-
const files = fs.readdirSync(
|
|
726
|
+
const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('.json'));
|
|
672
727
|
let cleaned = 0;
|
|
673
728
|
|
|
674
729
|
files.forEach(f => {
|
|
675
|
-
const
|
|
730
|
+
const filePath = path.join(loopsDir, f);
|
|
731
|
+
const result = safeReadJSON(filePath, { defaultValue: null });
|
|
676
732
|
if (result.ok && result.data && result.data.status !== 'running') {
|
|
677
|
-
fs.unlinkSync(
|
|
733
|
+
fs.unlinkSync(filePath);
|
|
678
734
|
cleaned++;
|
|
679
735
|
}
|
|
680
736
|
});
|
|
@@ -808,9 +864,13 @@ module.exports = {
|
|
|
808
864
|
listLoops,
|
|
809
865
|
cleanupLoops,
|
|
810
866
|
loadLoop,
|
|
867
|
+
runCommand,
|
|
868
|
+
runGateCheck,
|
|
811
869
|
GATES,
|
|
870
|
+
GATE_TYPE_MAP,
|
|
812
871
|
MAX_ITERATIONS_HARD_LIMIT,
|
|
813
872
|
MAX_AGENTS_HARD_LIMIT,
|
|
873
|
+
REPEATED_FAILURE_LIMIT,
|
|
814
874
|
};
|
|
815
875
|
|
|
816
876
|
// Run CLI if executed directly
|