mindforge-cc 10.7.0 → 11.2.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/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +18 -4
- package/CHANGELOG.md +165 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +81 -1
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +105 -70
- package/bin/autonomous/auto-runner.js +377 -34
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +86 -26
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +68 -5
- package/bin/dashboard/sse-bridge.js +10 -13
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/temporal-hub.js +131 -34
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/approve.js +41 -5
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +95 -28
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +79 -9
- package/bin/hindsight-injector.js +8 -9
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +52 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +111 -6
- package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +58 -4
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +57 -2
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +59 -26
- package/bin/models/openai-provider.js +50 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/security/trust-boundaries.js +102 -0
- package/bin/security/trust-gate-hook.js +39 -0
- package/bin/skill-registry.js +3 -2
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/utils/append-queue.js +55 -0
- package/bin/utils/file-io.js +90 -38
- package/bin/utils/index.js +58 -0
- package/bin/utils/version-check.js +59 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +2 -2
- package/bin/dashboard/team-tracker.js +0 -0
|
@@ -19,9 +19,9 @@ const progressStream = require('./progress-stream');
|
|
|
19
19
|
const ContextRefactorer = require('./context-refactorer');
|
|
20
20
|
|
|
21
21
|
// Extracted modules (lightweight, always needed)
|
|
22
|
-
const {
|
|
22
|
+
const { appendAuditEntrySync } = require('./audit-writer');
|
|
23
23
|
const { createStateManager } = require('./state-manager');
|
|
24
|
-
const { createWaveExecutor } = require('./wave-executor');
|
|
24
|
+
const { createWaveExecutor, Semaphore } = require('./wave-executor');
|
|
25
25
|
|
|
26
26
|
// ── Lazy-loaded heavy modules ────────────────────────────────────────────────
|
|
27
27
|
// These are only required at the point of first use to reduce startup cost.
|
|
@@ -44,6 +44,68 @@ function lazyRequire(cached, modulePath) {
|
|
|
44
44
|
return cached;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* UC-14: Pure timeout predicate evaluated at wave boundaries.
|
|
49
|
+
*
|
|
50
|
+
* Three cases:
|
|
51
|
+
* 1. falsy `timeoutAt` (null / undefined / '') → NO timeout is configured →
|
|
52
|
+
* false. Autonomous mode never times out in that case.
|
|
53
|
+
* 2. truthy but UNPARSEABLE deadline (`Date.parse` → NaN, e.g. 'garbage') → a
|
|
54
|
+
* MALFORMED deadline. We FAIL CLOSED: warn to stderr AND return true (treat
|
|
55
|
+
* as expired). For a stability/bounding feature, a broken deadline silently
|
|
56
|
+
* never firing would let a run proceed UNBOUNDED — the wrong direction. A
|
|
57
|
+
* malformed deadline must be VISIBLE and must HALT, not silently fail open.
|
|
58
|
+
* 3. valid date → `now > parsed` (timed out once `now` strictly exceeds it).
|
|
59
|
+
*
|
|
60
|
+
* `Date.now()` is the sane default for a RUNTIME comparison (unlike the council
|
|
61
|
+
* UC, which avoided `Date.now()` only for deterministic/resumable FILENAMES — a
|
|
62
|
+
* wall-clock read here is exactly what a timeout check wants). Callers inside the
|
|
63
|
+
* run-loop should still pass an explicit `now` for testability/consistency within
|
|
64
|
+
* a single iteration.
|
|
65
|
+
*
|
|
66
|
+
* NOTE: this is otherwise a PURE function (used directly in tests). The only
|
|
67
|
+
* side-effect is the diagnostic stderr write on the malformed branch — never on
|
|
68
|
+
* the falsy or valid-date paths — so existing pure assertions are unaffected.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|null|undefined} timeoutAt — ISO-8601 deadline, or falsy for "no timeout"
|
|
71
|
+
* @param {number} [now=Date.now()] — Epoch ms to compare against
|
|
72
|
+
* @returns {boolean} true iff a deadline is set AND it has passed (or is malformed)
|
|
73
|
+
*/
|
|
74
|
+
function isTimedOut(timeoutAt, now = Date.now()) {
|
|
75
|
+
if (!timeoutAt) return false;
|
|
76
|
+
const parsed = Date.parse(timeoutAt);
|
|
77
|
+
if (Number.isNaN(parsed)) {
|
|
78
|
+
// Malformed deadline → fail CLOSED (visible + halt), never silently fail open.
|
|
79
|
+
process.stderr.write(`[auto-runner] malformed timeout_at "${timeoutAt}" — treating as expired to fail closed\n`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return now > parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* UC-14: Pure decision for the opt-in terminal-ESCALATE rollback hook.
|
|
87
|
+
*
|
|
88
|
+
* SAFE BY DEFAULT: rollback-on-escalate is opt-in via
|
|
89
|
+
* `wave_execution.rollback_on_escalate` (default false). Even when enabled, an
|
|
90
|
+
* actual destructive `git reset` is gated on the runner tracking per-wave commit
|
|
91
|
+
* SHAs — which it does NOT today. So `gitReset` is always false until commit
|
|
92
|
+
* tracking exists; we only ever RECORD the rollback point.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} config — Parsed .mindforge/config.json (or {})
|
|
95
|
+
* @param {boolean} [hasCommitTracking=false] — Whether per-wave commit SHAs are tracked
|
|
96
|
+
* @returns {{ record: boolean, gitReset: boolean, reason: string }}
|
|
97
|
+
*/
|
|
98
|
+
function decideRollback(config, hasCommitTracking = false) {
|
|
99
|
+
const enabled = config?.wave_execution?.rollback_on_escalate === true;
|
|
100
|
+
if (!enabled) {
|
|
101
|
+
return { record: true, gitReset: false, reason: 'rollback_on_escalate disabled (default) — recording intent only' };
|
|
102
|
+
}
|
|
103
|
+
if (!hasCommitTracking) {
|
|
104
|
+
return { record: true, gitReset: false, reason: 'rollback_on_escalate enabled but per-wave commit tracking unavailable — recording intent only, actual git reset deferred' };
|
|
105
|
+
}
|
|
106
|
+
return { record: true, gitReset: true, reason: 'rollback_on_escalate enabled and commit tracking available' };
|
|
107
|
+
}
|
|
108
|
+
|
|
47
109
|
class AutoRunner {
|
|
48
110
|
constructor(options = {}) {
|
|
49
111
|
if (options.phase != null && !/^[a-zA-Z0-9_-]+$/.test(String(options.phase))) {
|
|
@@ -61,7 +123,6 @@ class AutoRunner {
|
|
|
61
123
|
|
|
62
124
|
// Extracted module instances
|
|
63
125
|
this.stateManager = createStateManager(planningDir);
|
|
64
|
-
this.auditWriter = createAuditWriter(this.auditPath);
|
|
65
126
|
this.waveExecutor = createWaveExecutor({
|
|
66
127
|
onTaskStart: ({ task, wave }) => this.writeAudit({ event: 'task_started', phase: this.phase, wave: wave + 1, task_id: task.id, task_name: task.name }),
|
|
67
128
|
onTaskComplete: ({ task, wave, duration_ms }) => this.writeAudit({ event: 'task_completed', phase: this.phase, wave: wave + 1, task_id: task.id, task_name: task.name, duration_ms }),
|
|
@@ -173,6 +234,10 @@ class AutoRunner {
|
|
|
173
234
|
|
|
174
235
|
while (await this.hasNextWave()) {
|
|
175
236
|
if (this.isPaused) break;
|
|
237
|
+
// UC-14: enforce the wave-boundary timeout BEFORE dispatching this wave.
|
|
238
|
+
// Re-read auto-state each iteration so an externally-updated timeout_at is
|
|
239
|
+
// honored; stop cleanly (status 'timeout' + resumable state) when passed.
|
|
240
|
+
if (this._enforceWaveTimeout()) return;
|
|
176
241
|
const permit = await this.evaluateWavePolicy();
|
|
177
242
|
if (!permit) { this.writeAudit({ event: 'auto_mode_denied', reason: 'Policy violation detected' }); break; }
|
|
178
243
|
const isReliable = await this.checkArbitrage();
|
|
@@ -188,10 +253,77 @@ class AutoRunner {
|
|
|
188
253
|
await this.complete();
|
|
189
254
|
}
|
|
190
255
|
|
|
256
|
+
/**
|
|
257
|
+
* UC-14: Wave-boundary timeout enforcement.
|
|
258
|
+
*
|
|
259
|
+
* Re-reads the current `timeout_at` from auto-state.json (V9 design field) and,
|
|
260
|
+
* if the deadline has passed, stops the run CLEANLY:
|
|
261
|
+
* 1. persists resumable state (currentWaveIndex + completedTasks) so a later
|
|
262
|
+
* `/mindforge:auto` resumes exactly where it left off,
|
|
263
|
+
* 2. sets status to 'timeout' (a VALID_STATUS), keeping the resumable fields,
|
|
264
|
+
* 3. writes an `auto_mode_timeout` audit event,
|
|
265
|
+
* 4. logs the resume command and returns true so the caller stops the loop.
|
|
266
|
+
*
|
|
267
|
+
* Returns false (no-op) when no timeout is set or the deadline hasn't passed.
|
|
268
|
+
* @returns {boolean} true iff the run timed out and the loop should stop
|
|
269
|
+
*/
|
|
270
|
+
_enforceWaveTimeout() {
|
|
271
|
+
const autoState = this.stateManager.getState();
|
|
272
|
+
if (!isTimedOut(autoState.timeout_at)) return false;
|
|
273
|
+
|
|
274
|
+
const waveNum = this.currentWaveIndex + 1;
|
|
275
|
+
|
|
276
|
+
// 1 + 2: persist resumable progress AND flip status to 'timeout' in one write.
|
|
277
|
+
this.updateState({
|
|
278
|
+
status: 'timeout',
|
|
279
|
+
currentWaveIndex: this.currentWaveIndex,
|
|
280
|
+
completedTasks: Array.from(this.completedTasks),
|
|
281
|
+
timedOutAt: new Date().toISOString(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 3: audit the clean stop.
|
|
285
|
+
this.writeAudit({
|
|
286
|
+
event: 'auto_mode_timeout',
|
|
287
|
+
phase: this.phase,
|
|
288
|
+
wave: waveNum,
|
|
289
|
+
timeout_at: autoState.timeout_at,
|
|
290
|
+
completed_tasks: this.completedTasks.size,
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// 4: clear operator-facing message including the resume command.
|
|
295
|
+
console.warn(
|
|
296
|
+
`\n⏲️ TIMEOUT at wave boundary ${waveNum}/${this.waves.length} ` +
|
|
297
|
+
`(deadline ${autoState.timeout_at}). Stopped cleanly with ${this.completedTasks.size} task(s) completed.\n` +
|
|
298
|
+
` Resume with: /mindforge:auto --phase ${this.phase}`
|
|
299
|
+
);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
191
303
|
runPreFlight() {
|
|
192
304
|
console.log('🔍 Running pre-flight checks...');
|
|
305
|
+
|
|
306
|
+
// UC-01: fail closed on version drift before any wave executes
|
|
307
|
+
try {
|
|
308
|
+
const { assertVersionConsistency } = require('../utils/version-check');
|
|
309
|
+
assertVersionConsistency(process.cwd());
|
|
310
|
+
} catch (e) {
|
|
311
|
+
throw new Error(`[pre-flight] ${e.message}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
193
314
|
const handoff = this.stateManager.readHandoff();
|
|
194
|
-
|
|
315
|
+
|
|
316
|
+
// FIX 3.2: read config ONCE for the whole pre-flight path and thread the
|
|
317
|
+
// resolved useDag boolean into both _assertNoCycles and _buildWaves, instead
|
|
318
|
+
// of each method re-reading + re-parsing .mindforge/config.json.
|
|
319
|
+
const useDag = this._useDagMode();
|
|
320
|
+
|
|
321
|
+
// UC-03: when DAG ordering is enabled (opt-in via config), detect cycles
|
|
322
|
+
// BEFORE any wave executes and HALT LOUD. Default OFF — legacy behavior
|
|
323
|
+
// (single-wave / explicit .wave grouping) is untouched.
|
|
324
|
+
this._assertNoCycles(handoff.handoffs, useDag);
|
|
325
|
+
|
|
326
|
+
this.waves = this._buildWaves(handoff.handoffs, useDag);
|
|
195
327
|
this.currentWaveIndex = 0;
|
|
196
328
|
|
|
197
329
|
const savedState = this.stateManager.getState();
|
|
@@ -218,24 +350,47 @@ class AutoRunner {
|
|
|
218
350
|
const wave = this.waves[this.currentWaveIndex];
|
|
219
351
|
const waveNum = this.currentWaveIndex + 1;
|
|
220
352
|
const pending = wave.tasks.filter(t => !this.completedTasks.has(t.id));
|
|
353
|
+
const maxConcurrency = this._getWaveConcurrency();
|
|
221
354
|
|
|
222
|
-
console.log(`\n⚡ Wave ${waveNum}/${this.waves.length}: ${pending.length} tasks`);
|
|
355
|
+
console.log(`\n⚡ Wave ${waveNum}/${this.waves.length}: ${pending.length} tasks (concurrency: ${maxConcurrency})`);
|
|
223
356
|
if (idcStatus.action === 'UPGRADE_MIR') console.log(` [IDC-ACTIVE] MIR Override: ${idcStatus.new_mir}`);
|
|
224
357
|
this.writeAudit({ event: 'wave_started', phase: this.phase, wave: waveNum, task_count: pending.length });
|
|
225
358
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
359
|
+
const semaphore = new Semaphore(maxConcurrency);
|
|
360
|
+
|
|
361
|
+
const settled = await Promise.allSettled(
|
|
362
|
+
pending.map(async (task) => {
|
|
363
|
+
await semaphore.acquire();
|
|
364
|
+
const taskStart = Date.now();
|
|
365
|
+
console.log(` → Task: ${task.name || task.id}`);
|
|
366
|
+
try {
|
|
367
|
+
this.writeAudit({ event: 'task_started', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id });
|
|
368
|
+
this.writeAudit({ event: 'task_completed', phase: this.phase, wave: waveNum, task_id: task.id, task_name: task.name || task.id, duration_ms: Date.now() - taskStart });
|
|
369
|
+
this.completedTasks.add(task.id);
|
|
370
|
+
return { taskId: task.id, status: 'fulfilled' };
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.error(` Task failed: ${task.id} — ${err.message}`);
|
|
373
|
+
this.writeAudit({ event: 'task_failed', phase: this.phase, wave: waveNum, task_id: task.id, error: err.message, duration_ms: Date.now() - taskStart });
|
|
374
|
+
throw { taskId: task.id, error: err, task };
|
|
375
|
+
} finally {
|
|
376
|
+
semaphore.release();
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const failures = settled.filter(r => r.status === 'rejected');
|
|
382
|
+
if (failures.length > 0) {
|
|
383
|
+
for (const failure of failures) {
|
|
384
|
+
const { taskId, error, task } = failure.reason;
|
|
385
|
+
const strategy = repairOperator.determineRepairStrategy({ planId: task.plan || taskId, phase: this.phase, attemptNumber: 1, errorOutput: error.message, isTier3Change: false, isOnCriticalPath: (task.depends_on || []).length > 0 });
|
|
386
|
+
if (strategy === 'ESCALATE') {
|
|
387
|
+
this.writeAudit({ event: 'auto_mode_escalated', reason: `Task ${taskId} unrecoverable` });
|
|
388
|
+
// UC-14: opt-in rollback hook on terminal ESCALATE. SAFE DEFAULT —
|
|
389
|
+
// records the rollback point only; never mutates git (see decideRollback).
|
|
390
|
+
this._recordRollbackPoint(waveNum, taskId);
|
|
391
|
+
this.isPaused = true;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
239
394
|
}
|
|
240
395
|
}
|
|
241
396
|
|
|
@@ -244,12 +399,171 @@ class AutoRunner {
|
|
|
244
399
|
this.currentWaveIndex++;
|
|
245
400
|
}
|
|
246
401
|
|
|
402
|
+
_getWaveConcurrency() {
|
|
403
|
+
try {
|
|
404
|
+
const configPath = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
405
|
+
if (fs.existsSync(configPath)) {
|
|
406
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
407
|
+
if (typeof config.wave_concurrency === 'number' && config.wave_concurrency > 0) {
|
|
408
|
+
return config.wave_concurrency;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch (e) { /* Fall through to default */ }
|
|
412
|
+
return 3;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* UC-14: Rollback-on-escalate is OPT-IN. Reads
|
|
417
|
+
* `wave_execution.rollback_on_escalate` from config; defaults to false so the
|
|
418
|
+
* SAFE behavior (record rollback intent only — NO git mutation) is the default.
|
|
419
|
+
* Even when true, an actual git reset is further gated on per-wave commit
|
|
420
|
+
* tracking, which the runner does not yet have (see decideRollback).
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
*/
|
|
423
|
+
_rollbackOnEscalate() {
|
|
424
|
+
try {
|
|
425
|
+
const configPath = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
426
|
+
if (fs.existsSync(configPath)) {
|
|
427
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
428
|
+
return config.wave_execution?.rollback_on_escalate === true;
|
|
429
|
+
}
|
|
430
|
+
} catch (e) { /* Fall through to default */ }
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* UC-14: Records the rollback point on terminal ESCALATE WITHOUT mutating git.
|
|
436
|
+
*
|
|
437
|
+
* The runner does NOT track per-wave commit SHAs, so even with
|
|
438
|
+
* `rollback_on_escalate` enabled the actual `git reset --hard` is deferred —
|
|
439
|
+
* we never run a destructive reset against untracked commit boundaries. We
|
|
440
|
+
* emit a `rollback_point_recorded` audit event (and a clear log) recording the
|
|
441
|
+
* intended rollback wave so a human can act on it.
|
|
442
|
+
*
|
|
443
|
+
* @param {number} waveNum — 1-based wave number that hit terminal escalation
|
|
444
|
+
* @param {string} taskId — The task id whose repair was exhausted
|
|
445
|
+
*/
|
|
446
|
+
_recordRollbackPoint(waveNum, taskId) {
|
|
447
|
+
const config = (() => {
|
|
448
|
+
try {
|
|
449
|
+
const configPath = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
450
|
+
if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
451
|
+
} catch (e) { /* ignore — treat as no config */ }
|
|
452
|
+
return {};
|
|
453
|
+
})();
|
|
454
|
+
|
|
455
|
+
// hasCommitTracking is hard-false: no per-wave SHA tracking exists yet.
|
|
456
|
+
const decision = decideRollback(config, /* hasCommitTracking */ false);
|
|
457
|
+
|
|
458
|
+
// DRY: derive the flag from the `config` we ALREADY read above instead of
|
|
459
|
+
// calling _rollbackOnEscalate(), which would re-read + re-parse the SAME
|
|
460
|
+
// config.json a second time within this single escalation path.
|
|
461
|
+
const rollbackEnabled = config?.wave_execution?.rollback_on_escalate === true;
|
|
462
|
+
|
|
463
|
+
this.writeAudit({
|
|
464
|
+
event: 'rollback_point_recorded',
|
|
465
|
+
phase: this.phase,
|
|
466
|
+
wave: waveNum,
|
|
467
|
+
task_id: taskId,
|
|
468
|
+
commits: [], // none tracked yet — rollback target is the prior wave boundary
|
|
469
|
+
git_reset_performed: decision.gitReset,
|
|
470
|
+
reason: decision.reason,
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
});
|
|
473
|
+
console.warn(`↩️ ROLLBACK POINT recorded at wave ${waveNum} (task ${taskId}): ${decision.reason}`);
|
|
474
|
+
if (rollbackEnabled && !decision.gitReset) {
|
|
475
|
+
console.warn(' (rollback_on_escalate is ON, but actual git reset is deferred until per-wave commit tracking exists.)');
|
|
476
|
+
}
|
|
477
|
+
return decision;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* UC-03: DAG wave ordering is OPT-IN. Reads `wave_execution.use_dag` from
|
|
482
|
+
* config; defaults to false so legacy single-wave / explicit-.wave behavior
|
|
483
|
+
* is untouched. Enabling this reorders tasks by `depends_on` topology.
|
|
484
|
+
* @returns {boolean}
|
|
485
|
+
*/
|
|
486
|
+
_useDagMode() {
|
|
487
|
+
try {
|
|
488
|
+
const configPath = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
489
|
+
if (fs.existsSync(configPath)) {
|
|
490
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
491
|
+
return config.wave_execution?.use_dag === true;
|
|
492
|
+
}
|
|
493
|
+
} catch (e) { /* Fall through to default */ }
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* UC-03: Pre-flight cycle detection. ONLY active when DAG mode is enabled
|
|
499
|
+
* (opt-in) AND no explicit numeric `.wave` field is present (DAG would be the
|
|
500
|
+
* active strategy). Throws `[pre-flight] Circular dependency...` to HALT LOUD
|
|
501
|
+
* before any wave executes — never warn-and-continue. No-op when DAG is off.
|
|
502
|
+
*
|
|
503
|
+
* FIX 1: the pre-flight graph must NOT differ from the graph planWaves
|
|
504
|
+
* actually executes. planWaves builds its graph from normalizeTask, which
|
|
505
|
+
* SYNTHESIZES a random id (`task_<rand>`) for any handoff lacking both id and
|
|
506
|
+
* task_id. A random id can't be matched between two separate calls, AND a task
|
|
507
|
+
* with no stable id can never be a `depends_on` TARGET (nothing can reference
|
|
508
|
+
* an id that doesn't exist until it's randomly minted). So we cycle-check the
|
|
509
|
+
* SUBSET of handoffs that carry a real, stable id (id || task_id) — a subset
|
|
510
|
+
* guaranteed consistent with execution — and log a warning for any id-less
|
|
511
|
+
* handoff excluded from the check.
|
|
512
|
+
*
|
|
513
|
+
* @param {Array} handoffs — Raw handoffs array from HANDOFF.json
|
|
514
|
+
* @param {boolean} [useDag] — Resolved DAG mode (threaded from runPreFlight to
|
|
515
|
+
* avoid re-reading config). Falls back to _useDagMode() for direct callers.
|
|
516
|
+
*/
|
|
517
|
+
_assertNoCycles(handoffs, useDag = this._useDagMode()) {
|
|
518
|
+
if (!Array.isArray(handoffs) || handoffs.length === 0) return;
|
|
519
|
+
if (!useDag) return;
|
|
520
|
+
|
|
521
|
+
// Explicit .wave field wins over DAG, so cycle check is irrelevant there.
|
|
522
|
+
const hasWaveField = handoffs.some(h => typeof h.wave === 'number');
|
|
523
|
+
if (hasWaveField) return;
|
|
524
|
+
|
|
525
|
+
// Only handoffs with a STABLE id participate in cycle-checking (see above).
|
|
526
|
+
const stable = handoffs.filter(h => h.id || h.task_id);
|
|
527
|
+
const idless = handoffs.length - stable.length;
|
|
528
|
+
if (idless > 0) {
|
|
529
|
+
console.warn(
|
|
530
|
+
`[pre-flight] ${idless} handoff(s) lack a stable id (id/task_id) and are ` +
|
|
531
|
+
'excluded from cycle-checking; an id-less task cannot be a dependency target.'
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
if (stable.length === 0) return;
|
|
535
|
+
|
|
536
|
+
const { buildGraph, groupIntoWaves } = require('./dependency-dag');
|
|
537
|
+
let graph;
|
|
538
|
+
try {
|
|
539
|
+
graph = buildGraph(stable.map(h => ({
|
|
540
|
+
id: h.id || h.task_id,
|
|
541
|
+
depends_on: Array.isArray(h.depends_on) ? h.depends_on : [],
|
|
542
|
+
})));
|
|
543
|
+
} catch (e) {
|
|
544
|
+
// Unknown-dependency reference — also a planning error; fail loud.
|
|
545
|
+
throw new Error(`[pre-flight] ${e.message}`);
|
|
546
|
+
}
|
|
547
|
+
// FIX 3.1: hasCircularDependency() is itself `try{groupIntoWaves}catch`, so
|
|
548
|
+
// running it then groupIntoWaves again ran Kahn 2-3 times and left an
|
|
549
|
+
// unreachable defensive throw. Run Kahn ONCE here; on throw, rethrow with the
|
|
550
|
+
// descriptive (circular OR unknown-dep) message prefixed for pre-flight.
|
|
551
|
+
try {
|
|
552
|
+
groupIntoWaves(graph);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
throw new Error(`[pre-flight] ${e.message}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
247
558
|
/**
|
|
248
559
|
* Build wave groups from HANDOFF handoffs array.
|
|
249
560
|
* Kept as instance method for backward compatibility with tests.
|
|
561
|
+
* @param {Array} handoffs — Raw handoffs array from HANDOFF.json
|
|
562
|
+
* @param {boolean} [useDag] — Resolved DAG mode (threaded from runPreFlight).
|
|
563
|
+
* Falls back to _useDagMode() for direct callers (e.g. tests).
|
|
250
564
|
*/
|
|
251
|
-
_buildWaves(handoffs) {
|
|
252
|
-
return this.waveExecutor.planWaves(handoffs);
|
|
565
|
+
_buildWaves(handoffs, useDag = this._useDagMode()) {
|
|
566
|
+
return this.waveExecutor.planWaves(handoffs, { useDag });
|
|
253
567
|
}
|
|
254
568
|
|
|
255
569
|
async checkIntelligenceDrift() {
|
|
@@ -316,28 +630,33 @@ class AutoRunner {
|
|
|
316
630
|
if (captured.length + stability.length > 0) console.log(`🧠 Knowledge Graph: Captured ${captured.length + stability.length} insights.`);
|
|
317
631
|
} catch (err) { console.error('⚠️ Knowledge Capture failed:', err.message); }
|
|
318
632
|
|
|
633
|
+
try {
|
|
634
|
+
_TemporalHub = lazyRequire(_TemporalHub, '../engine/temporal-hub');
|
|
635
|
+
const gcConfig = this._loadTemporalGcConfig();
|
|
636
|
+
const gcResult = await _TemporalHub.gc(gcConfig);
|
|
637
|
+
if (gcResult.deleted > 0) {
|
|
638
|
+
this.writeAudit({ event: 'temporal_gc_completed', deleted: gcResult.deleted, remaining: gcResult.remaining });
|
|
639
|
+
}
|
|
640
|
+
} catch (e) { /* GC failure is non-critical */ }
|
|
641
|
+
|
|
319
642
|
this.writeAudit({ event: 'auto_mode_completed', timestamp: new Date().toISOString() });
|
|
320
|
-
await this.auditWriter.close();
|
|
321
643
|
}
|
|
322
644
|
|
|
323
645
|
writeAudit(event) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// Also buffer in async writer for future async consumers
|
|
332
|
-
this.auditWriter.write(event);
|
|
646
|
+
// UC-04b: single unified, hash-chained, synchronous-DURABLE append. The sync
|
|
647
|
+
// fsync'd write means in-process consumers (StuckMonitor reads the event
|
|
648
|
+
// object directly below; any file re-readers see it immediately) get durable
|
|
649
|
+
// data at once — replacing the old dual raw-appendFileSync + async-buffer write
|
|
650
|
+
// that left the live AUDIT.jsonl chain unverifiable.
|
|
651
|
+
const stamped = appendAuditEntrySync(this.auditPath, event);
|
|
333
652
|
|
|
334
653
|
const STATE_CHANGING_EVENTS = ['auto_mode_started', 'phase_planned', 'phase_execution_started', 'task_completed', 'hindsight_injected', 'auto_mode_completed'];
|
|
335
|
-
if (STATE_CHANGING_EVENTS.includes(
|
|
654
|
+
if (STATE_CHANGING_EVENTS.includes(stamped.event)) {
|
|
336
655
|
_TemporalHub = lazyRequire(_TemporalHub, '../engine/temporal-hub');
|
|
337
|
-
_TemporalHub.captureState(
|
|
656
|
+
_TemporalHub.captureState(stamped.id, { agent: stamped.agent || 'auto-runner', event: stamped.event, phase: this.phase }).catch(() => {});
|
|
338
657
|
}
|
|
339
658
|
|
|
340
|
-
const result = this.monitor.analyze(
|
|
659
|
+
const result = this.monitor.analyze(stamped);
|
|
341
660
|
if (result) this.handleStuck(result);
|
|
342
661
|
}
|
|
343
662
|
|
|
@@ -370,7 +689,7 @@ class AutoRunner {
|
|
|
370
689
|
|
|
371
690
|
async evaluateWavePolicy() {
|
|
372
691
|
_ZTAIManager = lazyRequire(_ZTAIManager, '../governance/ztai-manager');
|
|
373
|
-
const manager =
|
|
692
|
+
const manager = _ZTAIManager;
|
|
374
693
|
const identity = await manager.getIdentity();
|
|
375
694
|
const intent = { did: identity.did, action: 'process_phase_wave', resource: `projects/${process.env.MF_PROJECT_ID || 'MF-ALPHA'}/phases/${this.phase}/*`, tier: identity.tier || 1, metadata: { engine: 'Nimbus-S4', mode: 'autonomous', wave_timestamp: new Date().toISOString() } };
|
|
376
695
|
const result = this.policyEngine.evaluate(intent);
|
|
@@ -444,6 +763,30 @@ class AutoRunner {
|
|
|
444
763
|
const lines = buf.toString('utf8').trim().split('\n');
|
|
445
764
|
return lines.slice(-count).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
446
765
|
}
|
|
766
|
+
|
|
767
|
+
_loadTemporalGcConfig() {
|
|
768
|
+
try {
|
|
769
|
+
const configPath = path.join(process.cwd(), '.mindforge', 'config.json');
|
|
770
|
+
if (fs.existsSync(configPath)) {
|
|
771
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
772
|
+
if (config.temporal) {
|
|
773
|
+
return {
|
|
774
|
+
maxSnapshots: config.temporal.max_snapshots || 50,
|
|
775
|
+
maxAgeDays: config.temporal.max_age_days || 7
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
} catch (e) { /* Fall through to defaults */ }
|
|
780
|
+
return { maxSnapshots: 50, maxAgeDays: 7 };
|
|
781
|
+
}
|
|
447
782
|
}
|
|
448
783
|
|
|
784
|
+
// Primary export remains the AutoRunner class (back-compat with all callers/tests
|
|
785
|
+
// that do `const AutoRunner = require('./auto-runner')`). UC-14 pure helpers are
|
|
786
|
+
// attached as named properties so `require('./auto-runner').isTimedOut` works too.
|
|
787
|
+
AutoRunner.isTimedOut = isTimedOut;
|
|
788
|
+
AutoRunner.decideRollback = decideRollback;
|
|
789
|
+
|
|
449
790
|
module.exports = AutoRunner;
|
|
791
|
+
module.exports.isTimedOut = isTimedOut;
|
|
792
|
+
module.exports.decideRollback = decideRollback;
|
|
@@ -14,18 +14,15 @@ class ContextRefactorer {
|
|
|
14
14
|
this.history = [];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Analyze the current context density.
|
|
19
|
-
* Density = (Implementation Events) / (Total Events)
|
|
20
|
-
*/
|
|
21
17
|
analyzeDensity(events) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (this.history.length < 5) return { density: 1.0, shouldRefactor: false };
|
|
18
|
+
const windowSize = this._getAdaptiveWindow(events);
|
|
19
|
+
this.history = events.slice(-windowSize);
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
if (this.history.length < 5) return { density: 1.0, shouldRefactor: false, windowSize };
|
|
22
|
+
|
|
23
|
+
const implementationEvents = this.history.filter(h =>
|
|
24
|
+
h.tool === 'run_command' ||
|
|
25
|
+
h.tool === 'replace_file_content' ||
|
|
29
26
|
h.tool === 'multi_replace_file_content' ||
|
|
30
27
|
h.event === 'task_completed'
|
|
31
28
|
);
|
|
@@ -35,10 +32,28 @@ class ContextRefactorer {
|
|
|
35
32
|
|
|
36
33
|
return {
|
|
37
34
|
density: parseFloat(density.toFixed(2)),
|
|
38
|
-
shouldRefactor: density < this.threshold
|
|
35
|
+
shouldRefactor: density < this.threshold,
|
|
36
|
+
windowSize
|
|
39
37
|
};
|
|
40
38
|
}
|
|
41
39
|
|
|
40
|
+
_getAdaptiveWindow(events) {
|
|
41
|
+
const recent = events.slice(-10);
|
|
42
|
+
if (recent.length === 0) return 20;
|
|
43
|
+
|
|
44
|
+
const implEvents = recent.filter(e =>
|
|
45
|
+
e.event === 'run_command' ||
|
|
46
|
+
e.event?.includes('replace_file') ||
|
|
47
|
+
e.event === 'task_completed'
|
|
48
|
+
).length;
|
|
49
|
+
|
|
50
|
+
const velocity = implEvents / recent.length;
|
|
51
|
+
|
|
52
|
+
if (velocity > 0.6) return 10;
|
|
53
|
+
if (velocity < 0.3) return 30;
|
|
54
|
+
return 20;
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* Generates a "Context Refactor" recommendation.
|
|
44
59
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — Dependency DAG (UC-03).
|
|
4
|
+
* Kahn topological sort + cycle detection.
|
|
5
|
+
* Ported from the previously test-only implementation into the real engine.
|
|
6
|
+
*
|
|
7
|
+
* TODO(UC-xx): same-wave file-conflict detection once tasks carry file lists.
|
|
8
|
+
* Handoff tasks (see normalizeTask in wave-executor.js and validateHandoff in
|
|
9
|
+
* state-manager.js) currently expose only id/name/plan/depends_on — there is
|
|
10
|
+
* no `files` field to compare. A `findFileConflicts(plans)` check (two tasks
|
|
11
|
+
* in the SAME wave writing the same file -> warn) cannot be wired meaningfully
|
|
12
|
+
* until the handoff schema captures per-task file lists, so it is intentionally
|
|
13
|
+
* not implemented here rather than left as dead exported surface.
|
|
14
|
+
*/
|
|
15
|
+
function groupIntoWaves(graph) {
|
|
16
|
+
// Self-defense: every dependency target must be a known graph node. Throw a
|
|
17
|
+
// DISTINCT "unknown dependency" error (not the misleading "Circular" message)
|
|
18
|
+
// so callers that invoke groupIntoWaves standalone still fail descriptively.
|
|
19
|
+
for (const id of Object.keys(graph)) {
|
|
20
|
+
const deps = graph[id].dependsOn || [];
|
|
21
|
+
for (const d of deps) {
|
|
22
|
+
if (!(d in graph)) {
|
|
23
|
+
throw new Error(`Unknown dependency "${d}" referenced by "${id}"`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const remaining = new Set(Object.keys(graph));
|
|
28
|
+
const completed = new Set();
|
|
29
|
+
const waves = [];
|
|
30
|
+
while (remaining.size > 0) {
|
|
31
|
+
const wave = [];
|
|
32
|
+
for (const id of remaining) {
|
|
33
|
+
const deps = graph[id].dependsOn || [];
|
|
34
|
+
if (deps.every(d => completed.has(d))) wave.push(id);
|
|
35
|
+
}
|
|
36
|
+
if (wave.length === 0 && remaining.size > 0) {
|
|
37
|
+
throw new Error(`Circular dependency detected among: ${[...remaining].join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
waves.push(wave.sort());
|
|
40
|
+
wave.forEach(id => { completed.add(id); remaining.delete(id); });
|
|
41
|
+
}
|
|
42
|
+
return waves;
|
|
43
|
+
}
|
|
44
|
+
function hasCircularDependency(graph) {
|
|
45
|
+
try { groupIntoWaves(graph); return false; } catch { return true; }
|
|
46
|
+
}
|
|
47
|
+
function buildGraph(tasks) {
|
|
48
|
+
const ids = new Set(tasks.map(t => t.id));
|
|
49
|
+
const graph = {};
|
|
50
|
+
for (const t of tasks) {
|
|
51
|
+
const deps = Array.isArray(t.depends_on) ? t.depends_on : [];
|
|
52
|
+
for (const d of deps) {
|
|
53
|
+
if (!ids.has(d)) throw new Error(`Task "${t.id}" depends on unknown task "${d}"`);
|
|
54
|
+
}
|
|
55
|
+
graph[t.id] = { dependsOn: deps };
|
|
56
|
+
}
|
|
57
|
+
return graph;
|
|
58
|
+
}
|
|
59
|
+
module.exports = { groupIntoWaves, hasCircularDependency, buildGraph };
|