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.
Files changed (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/lib/drivers/claude-driver.ts +1 -1
  4. package/lib/lazy-require.js +1 -1
  5. package/package.json +1 -1
  6. package/scripts/agent-loop.js +290 -230
  7. package/scripts/check-sessions.js +116 -0
  8. package/scripts/lib/quality-gates.js +35 -8
  9. package/scripts/lib/signal-detectors.js +0 -13
  10. package/scripts/lib/team-events.js +1 -1
  11. package/scripts/lib/tmux-audit-monitor.js +2 -1
  12. package/src/core/commands/ads/audit.md +19 -3
  13. package/src/core/commands/code/accessibility.md +22 -6
  14. package/src/core/commands/code/api.md +22 -6
  15. package/src/core/commands/code/architecture.md +22 -6
  16. package/src/core/commands/code/completeness.md +22 -6
  17. package/src/core/commands/code/legal.md +22 -6
  18. package/src/core/commands/code/logic.md +22 -6
  19. package/src/core/commands/code/performance.md +22 -6
  20. package/src/core/commands/code/security.md +22 -6
  21. package/src/core/commands/code/test.md +22 -6
  22. package/src/core/commands/ideate/features.md +5 -4
  23. package/src/core/commands/ideate/new.md +8 -7
  24. package/src/core/commands/seo/audit.md +21 -5
  25. package/lib/claude-cli-bridge.js +0 -215
  26. package/lib/dashboard-automations.js +0 -130
  27. package/lib/dashboard-git.js +0 -254
  28. package/lib/dashboard-inbox.js +0 -64
  29. package/lib/dashboard-protocol.js +0 -605
  30. package/lib/dashboard-server.js +0 -1296
  31. package/lib/dashboard-session.js +0 -136
  32. package/lib/dashboard-status.js +0 -72
  33. package/lib/dashboard-terminal.js +0 -354
  34. package/lib/dashboard-websocket.js +0 -88
  35. package/scripts/dashboard-serve.js +0 -336
  36. package/src/core/commands/serve.md +0 -127
  37. package/tools/cli/commands/serve.js +0 -492
@@ -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 { validateCommand, buildSpawnArgs } = require('../lib/validate-commands');
30
- const {
31
- initializeForProject,
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
- if (!fs.existsSync(LOOPS_DIR)) {
64
- fs.mkdirSync(LOOPS_DIR, { recursive: true });
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(LOOPS_DIR, `${loopId}.json`);
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 busDir = path.dirname(BUS_PATH);
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(BUS_PATH, line);
122
+ fs.appendFileSync(busPath, line);
104
123
  }
105
124
 
106
125
  // ============================================================================
107
- // QUALITY GATE CHECKS
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: ROOT,
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
- function checkTestsGate() {
189
- const cmd = getTestCommand();
190
- console.log(`${c.dim}Running: ${cmd}${c.reset}`);
191
- const result = runCommand(cmd);
192
- return {
193
- passed: result.passed,
194
- value: result.passed ? 100 : 0,
195
- message: result.passed ? 'All tests passing' : 'Tests failing',
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
- const total = report.data.total;
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
- return {
222
- passed,
223
- value: coverage,
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
- function checkVisualGate() {
231
- const screenshotsDir = path.join(ROOT, 'screenshots');
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
- function checkLintGate() {
266
- console.log(`${c.dim}Running: npm run lint${c.reset}`);
267
- const result = runCommand('npm run lint');
268
- return {
269
- passed: result.passed,
270
- value: result.passed ? 100 : 0,
271
- message: result.passed ? 'Lint passing' : 'Lint errors found',
272
- };
273
- }
274
-
275
- function checkTypesGate() {
276
- console.log(`${c.dim}Running: npx tsc --noEmit${c.reset}`);
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
- function checkGate(gate, threshold) {
286
- switch (gate) {
287
- case 'tests':
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(ROOT);
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(LOOPS_DIR)
311
+ .readdirSync(loopsDir)
333
312
  .filter(f => f.endsWith('.json'))
334
313
  .map(f => {
335
- const loop = safeReadJSON(path.join(LOOPS_DIR, f), { defaultValue: null });
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
- type: 'agent_loop',
369
- event: 'init',
370
- loop_id: loopId,
371
- agent: agentType,
372
- gate,
373
- threshold,
374
- max_iterations: maxIter,
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 state = loadLoop(loopId);
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
- type: 'agent_loop',
411
- event: 'abort',
412
- loop_id: loopId,
413
- agent: state.agent_type,
414
- reason: 'timeout',
415
- iteration: state.iteration,
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
- type: 'agent_loop',
433
- event: 'failed',
434
- loop_id: loopId,
435
- agent: state.agent_type,
436
- reason: 'max_iterations',
437
- final_value: state.current_value,
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 = checkGate(state.quality_gate, state.threshold);
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
- type: 'agent_loop',
465
- event: 'iteration',
466
- loop_id: loopId,
467
- agent: state.agent_type,
468
- gate: state.quality_gate,
469
- iter: state.iteration,
470
- value: result.value,
471
- threshold: state.threshold,
472
- passed: result.passed,
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
- type: 'agent_loop',
489
- event: 'failed',
490
- loop_id: loopId,
491
- agent: state.agent_type,
492
- reason: 'regression_detected',
493
- final_value: result.value,
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
- type: 'agent_loop',
513
- event: 'failed',
514
- loop_id: loopId,
515
- agent: state.agent_type,
516
- reason: 'stalled',
517
- final_value: result.value,
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
- type: 'agent_loop',
540
- event: 'passed',
541
- loop_id: loopId,
542
- agent: state.agent_type,
543
- gate: state.quality_gate,
544
- final_value: result.value,
545
- iterations: state.iteration,
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 state = loadLoop(loopId);
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 state = loadLoop(loopId);
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
- type: 'agent_loop',
622
- event: 'abort',
623
- loop_id: loopId,
624
- agent: state.agent_type,
625
- reason,
626
- final_value: state.current_value,
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
- ensureLoopsDir();
684
+ function listLoops(options = {}) {
685
+ const { rootDir } = options;
686
+ const loopsDir = _loopsDir(rootDir);
687
+ ensureLoopsDir(rootDir);
635
688
 
636
- const files = fs.readdirSync(LOOPS_DIR).filter(f => f.endsWith('.json'));
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(LOOPS_DIR, f), { defaultValue: null });
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
- ensureLoopsDir();
721
+ function cleanupLoops(options = {}) {
722
+ const { rootDir } = options;
723
+ const loopsDir = _loopsDir(rootDir);
724
+ ensureLoopsDir(rootDir);
670
725
 
671
- const files = fs.readdirSync(LOOPS_DIR).filter(f => f.endsWith('.json'));
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 result = safeReadJSON(path.join(LOOPS_DIR, f), { defaultValue: null });
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(path.join(LOOPS_DIR, f));
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