mindforge-cc 10.7.0 → 11.0.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.
Files changed (50) hide show
  1. package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
  2. package/.mindforge/config.json +6 -1
  3. package/CHANGELOG.md +64 -0
  4. package/MINDFORGE.md +3 -3
  5. package/README.md +49 -4
  6. package/RELEASENOTES.md +80 -0
  7. package/SECURITY.md +20 -8
  8. package/bin/autonomous/audit-writer.js +13 -0
  9. package/bin/autonomous/auto-runner.js +74 -16
  10. package/bin/autonomous/context-refactorer.js +26 -11
  11. package/bin/autonomous/state-manager.js +62 -6
  12. package/bin/autonomous/stuck-monitor.js +46 -7
  13. package/bin/autonomous/wave-executor.js +66 -25
  14. package/bin/dashboard/api-router.js +43 -0
  15. package/bin/dashboard/metrics-aggregator.js +28 -1
  16. package/bin/dashboard/server.js +67 -4
  17. package/bin/dashboard/sse-bridge.js +4 -4
  18. package/bin/engine/feedback-loop.js +8 -0
  19. package/bin/engine/intelligence-interlock.js +32 -15
  20. package/bin/engine/logic-drift-detector.js +2 -1
  21. package/bin/engine/nexus-tracer.js +3 -2
  22. package/bin/engine/remediation-engine.js +155 -32
  23. package/bin/engine/self-corrective-synthesizer.js +84 -10
  24. package/bin/engine/sre-manager.js +12 -4
  25. package/bin/engine/temporal-hub.js +131 -34
  26. package/bin/governance/approve.js +41 -5
  27. package/bin/governance/impact-analyzer.js +28 -0
  28. package/bin/governance/policy-engine.js +10 -3
  29. package/bin/governance/quantum-crypto.js +32 -19
  30. package/bin/governance/rbac-manager.js +74 -2
  31. package/bin/governance/ztai-manager.js +49 -7
  32. package/bin/hindsight-injector.js +3 -3
  33. package/bin/memory/eis-client.js +71 -34
  34. package/bin/memory/embedding-engine.js +61 -0
  35. package/bin/memory/knowledge-graph.js +58 -5
  36. package/bin/memory/knowledge-indexer.js +53 -6
  37. package/bin/memory/knowledge-store.js +22 -0
  38. package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
  39. package/bin/migrations/schema-versions.js +13 -0
  40. package/bin/models/anthropic-provider.js +45 -0
  41. package/bin/models/cloud-broker.js +68 -20
  42. package/bin/models/gemini-provider.js +51 -0
  43. package/bin/models/model-client.js +20 -0
  44. package/bin/models/model-router.js +28 -8
  45. package/bin/models/openai-provider.js +44 -0
  46. package/bin/utils/file-io.js +63 -1
  47. package/bin/utils/index.js +58 -0
  48. package/docs/getting-started.md +1 -1
  49. package/docs/user-guide.md +2 -2
  50. package/package.json +2 -2
@@ -7,8 +7,59 @@
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
+ const { atomicWriteJSON } = require('../utils/file-io');
10
11
 
11
12
  const VALID_PHASES = ['idle', 'planning', 'executing', 'verifying', 'complete', 'running', 'paused', 'completed'];
13
+ const VALID_STATUSES = ['idle', 'running', 'paused', 'completed', 'escalated', 'timeout'];
14
+
15
+ /**
16
+ * Validates HANDOFF.json structure without blocking on failure (fail-open).
17
+ * Warns on malformed fields but always returns the data for processing.
18
+ * @param {*} data — Parsed HANDOFF.json content
19
+ * @returns {{ valid: boolean, warnings: string[] }}
20
+ */
21
+ function validateHandoff(data) {
22
+ const warnings = [];
23
+
24
+ if (!data || typeof data !== 'object') {
25
+ warnings.push('HANDOFF.json is not a valid object');
26
+ return { valid: false, warnings };
27
+ }
28
+
29
+ if (!data.schema_version) {
30
+ warnings.push('Missing schema_version field');
31
+ }
32
+
33
+ if (data.handoffs && !Array.isArray(data.handoffs)) {
34
+ warnings.push('handoffs field must be an array');
35
+ }
36
+
37
+ if (data.handoffs && Array.isArray(data.handoffs)) {
38
+ for (let i = 0; i < data.handoffs.length; i++) {
39
+ const task = data.handoffs[i];
40
+ if (!task.id) warnings.push(`handoffs[${i}] missing required field: id`);
41
+ if (!task.name) warnings.push(`handoffs[${i}] missing required field: name`);
42
+ }
43
+ }
44
+
45
+ if (data.status && !VALID_STATUSES.includes(data.status)) {
46
+ warnings.push(`Invalid status: "${data.status}". Expected one of: ${VALID_STATUSES.join(', ')}`);
47
+ }
48
+
49
+ if (data.wave_current !== undefined && typeof data.wave_current !== 'number') {
50
+ warnings.push('wave_current must be a number');
51
+ }
52
+
53
+ if (data.tasks_completed !== undefined && typeof data.tasks_completed !== 'number') {
54
+ warnings.push('tasks_completed must be a number');
55
+ }
56
+
57
+ if (data.timestamps && typeof data.timestamps !== 'object') {
58
+ warnings.push('timestamps must be an object');
59
+ }
60
+
61
+ return { valid: warnings.length === 0, warnings };
62
+ }
12
63
 
13
64
  /**
14
65
  * Creates a state manager for the given planning directory.
@@ -45,7 +96,7 @@ function createStateManager(planningDir) {
45
96
  function updateState(patch) {
46
97
  const current = getState();
47
98
  const merged = Object.assign(Object.create(null), current, patch);
48
- fs.writeFileSync(statePath, JSON.stringify(merged, null, 2));
99
+ atomicWriteJSON(statePath, merged);
49
100
  return merged;
50
101
  }
51
102
 
@@ -64,7 +115,8 @@ function createStateManager(planningDir) {
64
115
  }
65
116
 
66
117
  /**
67
- * Reads and parses HANDOFF.json. Throws if missing or malformed.
118
+ * Reads and parses HANDOFF.json with schema validation (fail-open).
119
+ * Logs warnings for structural issues but always returns data if parseable.
68
120
  * @returns {object} Parsed handoff data (fresh object)
69
121
  */
70
122
  function readHandoff() {
@@ -79,8 +131,12 @@ function createStateManager(planningDir) {
79
131
  throw new Error(`HANDOFF.json is malformed: ${e.message}`);
80
132
  }
81
133
 
82
- if (!handoff.handoffs || !Array.isArray(handoff.handoffs)) {
83
- throw new Error('HANDOFF.json has no handoffs array');
134
+ // Fail-open schema validation — warn but never block
135
+ const { valid, warnings } = validateHandoff(handoff);
136
+ if (!valid) {
137
+ for (const warning of warnings) {
138
+ console.warn('[STATE] HANDOFF validation:', warning);
139
+ }
84
140
  }
85
141
 
86
142
  return handoff;
@@ -94,7 +150,7 @@ function createStateManager(planningDir) {
94
150
  const timestamped = Object.assign(Object.create(null), data, {
95
151
  last_updated: new Date().toISOString(),
96
152
  });
97
- fs.writeFileSync(handoffPath, JSON.stringify(timestamped, null, 2) + '\n');
153
+ atomicWriteJSON(handoffPath, timestamped);
98
154
  return timestamped;
99
155
  }
100
156
 
@@ -113,4 +169,4 @@ function sanitizeState(parsed) {
113
169
  return clean;
114
170
  }
115
171
 
116
- module.exports = { createStateManager };
172
+ module.exports = { createStateManager, validateHandoff };
@@ -96,12 +96,49 @@ class StuckMonitor {
96
96
  return identical.length >= 3;
97
97
  }
98
98
 
99
- isContentSimilar(a, b) {
99
+ isContentSimilar(a, b, threshold = 10) {
100
100
  if (!a || !b) return false;
101
101
  if (a === b) return true;
102
- // Simple similarity check (hardened from Roadmap requirement)
103
- const dist = this.levenshtein(a.slice(0, 100), b.slice(0, 100));
104
- return dist < 10;
102
+
103
+ const hashA = this._quickHash(a);
104
+ const hashB = this._quickHash(b);
105
+ if (hashA === hashB) return true;
106
+
107
+ const lenDiff = Math.abs(a.length - b.length);
108
+ if (lenDiff > Math.max(a.length, b.length) * 0.2) return false;
109
+
110
+ const cached = this._getCachedSimilarity(hashA, hashB);
111
+ if (cached !== undefined) return cached;
112
+
113
+ const truncA = a.substring(0, 100);
114
+ const truncB = b.substring(0, 100);
115
+ const result = this.levenshtein(truncA, truncB) <= threshold;
116
+
117
+ this._setCachedSimilarity(hashA, hashB, result);
118
+ return result;
119
+ }
120
+
121
+ _quickHash(str) {
122
+ let hash = 0;
123
+ for (let i = 0; i < str.length; i++) {
124
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
125
+ hash = hash & hash;
126
+ }
127
+ return hash;
128
+ }
129
+
130
+ _getCachedSimilarity(keyA, keyB) {
131
+ const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
132
+ return StuckMonitor._similarityCache.get(key);
133
+ }
134
+
135
+ _setCachedSimilarity(keyA, keyB, result) {
136
+ const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
137
+ if (StuckMonitor._similarityCache.size >= 200) {
138
+ const firstKey = StuckMonitor._similarityCache.keys().next().value;
139
+ StuckMonitor._similarityCache.delete(firstKey);
140
+ }
141
+ StuckMonitor._similarityCache.set(key, result);
105
142
  }
106
143
 
107
144
  levenshtein(a, b) {
@@ -109,12 +146,14 @@ class StuckMonitor {
109
146
  for (let i = 0; i <= a.length; i++) { tmp[i] = [i]; }
110
147
  for (let j = 0; j <= b.length; j++) { tmp[0][j] = j; }
111
148
  for (let i = 1; i <= a.length; i++) {
112
- for (let j = 1; j <= b.length; j++) {
113
- tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
114
- }
149
+ for (let j = 1; j <= b.length; j++) {
150
+ tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
151
+ }
115
152
  }
116
153
  return tmp[a.length][b.length];
117
154
  }
118
155
  }
119
156
 
157
+ StuckMonitor._similarityCache = new Map();
158
+
120
159
  module.exports = StuckMonitor;
@@ -7,6 +7,35 @@
7
7
 
8
8
  const crypto = require('crypto');
9
9
 
10
+ /**
11
+ * Semaphore for bounding concurrency within a wave.
12
+ * Tasks within a wave are independent — this limits how many run simultaneously.
13
+ */
14
+ class Semaphore {
15
+ constructor(max) {
16
+ this.max = max;
17
+ this.current = 0;
18
+ this.queue = [];
19
+ }
20
+
21
+ async acquire() {
22
+ if (this.current < this.max) {
23
+ this.current++;
24
+ return;
25
+ }
26
+ await new Promise(resolve => this.queue.push(resolve));
27
+ this.current++;
28
+ }
29
+
30
+ release() {
31
+ this.current--;
32
+ if (this.queue.length > 0) {
33
+ const next = this.queue.shift();
34
+ next();
35
+ }
36
+ }
37
+ }
38
+
10
39
  /**
11
40
  * Creates a wave executor with the given configuration.
12
41
  * @param {object} config
@@ -73,42 +102,54 @@ function createWaveExecutor(config = {}) {
73
102
  }
74
103
 
75
104
  /**
76
- * Executes a single wave — runs tasks sequentially, skipping already-completed ones.
105
+ * Executes a single wave — runs tasks in parallel (bounded by maxConcurrency),
106
+ * skipping already-completed ones.
77
107
  * @param {object} wave — A wave object from planWaves
78
108
  * @param {object} context — Execution context passed to callbacks
79
- * @param {object} context.executor — async function(task) => result (performs actual work)
109
+ * @param {function} context.executor — async function(task) => result (performs actual work)
110
+ * @param {number} [context.maxConcurrency=3] — Max parallel tasks within this wave
80
111
  * @returns {Promise<{ completed: string[], failed: string[], skipped: string[] }>}
81
112
  */
82
113
  async function executeWave(wave, context = {}) {
83
- const { executor = async () => {} } = context;
114
+ const { executor = async () => {}, maxConcurrency = 3 } = context;
84
115
  status = 'running';
85
116
 
86
117
  const pending = wave.tasks.filter(t => !completedTasks.has(t.id));
87
118
  const result = { completed: [], failed: [], skipped: [] };
119
+ const semaphore = new Semaphore(maxConcurrency);
88
120
 
89
121
  onWaveStart({ wave: wave.wave, taskCount: pending.length });
90
122
 
91
- for (const task of pending) {
92
- const taskStart = Date.now();
93
- onTaskStart({ task, wave: wave.wave });
94
-
95
- try {
96
- await executor(task);
97
-
98
- const duration = Date.now() - taskStart;
99
- completedTasks = new Set([...completedTasks, task.id]);
100
- result.completed.push(task.id);
101
-
102
- onTaskComplete({ task, wave: wave.wave, duration_ms: duration });
103
- } catch (err) {
104
- const duration = Date.now() - taskStart;
105
- result.failed.push(task.id);
106
-
107
- onTaskFail({ task, wave: wave.wave, error: err, duration_ms: duration });
108
-
109
- // Re-throw to let caller decide on retry/escalation strategy
110
- throw err;
111
- }
123
+ const settled = await Promise.allSettled(
124
+ pending.map(async (task) => {
125
+ await semaphore.acquire();
126
+ const taskStart = Date.now();
127
+ try {
128
+ onTaskStart({ task, wave: wave.wave });
129
+ await executor(task);
130
+
131
+ const duration = Date.now() - taskStart;
132
+ completedTasks = new Set([...completedTasks, task.id]);
133
+ result.completed.push(task.id);
134
+
135
+ onTaskComplete({ task, wave: wave.wave, duration_ms: duration });
136
+ return { task, status: 'fulfilled' };
137
+ } catch (err) {
138
+ const duration = Date.now() - taskStart;
139
+ result.failed.push(task.id);
140
+
141
+ onTaskFail({ task, wave: wave.wave, error: err, duration_ms: duration });
142
+ throw err;
143
+ } finally {
144
+ semaphore.release();
145
+ }
146
+ })
147
+ );
148
+
149
+ const failures = settled.filter(r => r.status === 'rejected');
150
+ if (failures.length > 0) {
151
+ const failMsg = failures.map(f => f.reason?.message || 'unknown').join(', ');
152
+ throw new Error(`${failures.length} task(s) failed in wave: ${failMsg}`);
112
153
  }
113
154
 
114
155
  currentWaveIndex++;
@@ -166,4 +207,4 @@ function normalizeTask(h) {
166
207
  });
167
208
  }
168
209
 
169
- module.exports = { createWaveExecutor };
210
+ module.exports = { createWaveExecutor, Semaphore };
@@ -192,6 +192,49 @@ function register(app) {
192
192
  app.get('/api/connections', (req, res) => {
193
193
  res.json({ clients: SSE.getClientCount() });
194
194
  });
195
+
196
+ // ── System observability ────────────────────────────────────────────────────
197
+ app.get('/api/v1/system', (req, res) => {
198
+ try {
199
+ const heapUsed = process.memoryUsage().heapUsed;
200
+ const heapTotal = process.memoryUsage().heapTotal;
201
+ const uptime = process.uptime();
202
+
203
+ let auditLines = 0;
204
+ try {
205
+ const auditPath = path.join(process.cwd(), '.planning', 'AUDIT.jsonl');
206
+ if (fs.existsSync(auditPath)) {
207
+ const content = fs.readFileSync(auditPath, 'utf8');
208
+ auditLines = content.split('\n').filter(l => l.trim()).length;
209
+ }
210
+ } catch { /* non-critical */ }
211
+
212
+ let snapshotCount = 0;
213
+ try {
214
+ const historyDir = path.join(process.cwd(), '.planning', 'history');
215
+ if (fs.existsSync(historyDir)) {
216
+ snapshotCount = fs.readdirSync(historyDir).length;
217
+ }
218
+ } catch { /* non-critical */ }
219
+
220
+ const heapHealth = Metrics.checkHeapHealth();
221
+
222
+ res.json({
223
+ heap_used_mb: Math.round(heapUsed / 1024 / 1024 * 100) / 100,
224
+ heap_total_mb: Math.round(heapTotal / 1024 / 1024 * 100) / 100,
225
+ heap_usage_pct: Math.round(heapUsed / heapTotal * 100),
226
+ heap_alert: heapHealth,
227
+ uptime_seconds: Math.round(uptime),
228
+ audit_lines: auditLines,
229
+ snapshot_count: snapshotCount,
230
+ sse_clients: SSE.getClientCount(),
231
+ node_version: process.version,
232
+ timestamp: new Date().toISOString()
233
+ });
234
+ } catch (err) {
235
+ res.status(500).json({ error: err.message });
236
+ }
237
+ });
195
238
  }
196
239
 
197
240
  module.exports = { register };
@@ -326,6 +326,32 @@ function getCosts(windowDays = 7) {
326
326
  return stats;
327
327
  }
328
328
 
329
+ // ── Heap Health ──────────────────────────────────────────────────────────────
330
+ function checkHeapHealth() {
331
+ const heapUsed = process.memoryUsage().heapUsed;
332
+ const maxHeap = getMaxOldSpaceSize();
333
+ const usagePct = Math.round(heapUsed / maxHeap * 100);
334
+
335
+ let status = 'healthy';
336
+ if (usagePct > 85) {
337
+ status = 'critical';
338
+ } else if (usagePct > 70) {
339
+ status = 'warning';
340
+ }
341
+
342
+ return { status, usage_pct: usagePct };
343
+ }
344
+
345
+ function getMaxOldSpaceSize() {
346
+ // Parse --max-old-space-size from process args, default 1.4GB
347
+ const flag = process.execArgv.find(a => a.startsWith('--max-old-space-size'));
348
+ if (flag) {
349
+ const mb = parseInt(flag.split('=')[1], 10);
350
+ if (mb > 0) return mb * 1024 * 1024;
351
+ }
352
+ return 1.4 * 1024 * 1024 * 1024;
353
+ }
354
+
329
355
  module.exports = {
330
356
  getStatus,
331
357
  getAuditEntries,
@@ -333,5 +359,6 @@ module.exports = {
333
359
  getApprovals,
334
360
  getTeamActivity,
335
361
  getMemory,
336
- getCosts
362
+ getCosts,
363
+ checkHeapHealth
337
364
  };
@@ -42,12 +42,18 @@ const RevOpsAPI = require('./revops-api');
42
42
  const app = express();
43
43
 
44
44
  // ── Bearer token authentication ──────────────────────────────────────────────
45
- const DASHBOARD_TOKEN = crypto.randomBytes(32).toString('hex');
45
+ let currentToken = crypto.randomBytes(32).toString('hex');
46
46
  const TOKEN_FILE = path.join(process.cwd(), '.mindforge', '.dashboard-token');
47
+ const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
48
+ let tokenCreatedAt = Date.now();
49
+
50
+ function isTokenExpired() {
51
+ return (Date.now() - tokenCreatedAt) > TOKEN_EXPIRY_MS;
52
+ }
47
53
 
48
54
  // Write token to file with restrictive permissions (owner-only read/write)
49
55
  fs.mkdirSync(path.dirname(TOKEN_FILE), { recursive: true });
50
- fs.writeFileSync(TOKEN_FILE, DASHBOARD_TOKEN, { mode: 0o600 });
56
+ fs.writeFileSync(TOKEN_FILE, currentToken, { mode: 0o600 });
51
57
 
52
58
  /**
53
59
  * requireAuth — Validates Bearer token on mutating requests (POST/PUT/DELETE).
@@ -56,6 +62,11 @@ fs.writeFileSync(TOKEN_FILE, DASHBOARD_TOKEN, { mode: 0o600 });
56
62
  function requireAuth(req, res, next) {
57
63
  if (req.method === 'GET' || req.method === 'OPTIONS') return next();
58
64
 
65
+ // Check token expiration first
66
+ if (isTokenExpired()) {
67
+ return res.status(401).json({ error: 'token_expired' });
68
+ }
69
+
59
70
  const authHeader = req.headers.authorization;
60
71
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
61
72
  return res.status(401).json({
@@ -65,7 +76,9 @@ function requireAuth(req, res, next) {
65
76
 
66
77
  const provided = authHeader.slice(7);
67
78
  // Constant-time comparison to prevent timing attacks
68
- if (!crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(DASHBOARD_TOKEN))) {
79
+ const tokenBuf = Buffer.from(currentToken);
80
+ const providedBuf = Buffer.from(provided);
81
+ if (tokenBuf.length !== providedBuf.length || !crypto.timingSafeEqual(providedBuf, tokenBuf)) {
69
82
  return res.status(401).json({
70
83
  error: 'Authentication required. Use the token printed at dashboard startup.'
71
84
  });
@@ -74,6 +87,44 @@ function requireAuth(req, res, next) {
74
87
  next();
75
88
  }
76
89
 
90
+ // ── Rate limiting (100 req/min/IP) ───────────────────────────────────────────
91
+ const rateLimitMap = new Map(); // ip -> { count, resetAt }
92
+ const RATE_LIMIT = 100;
93
+ const RATE_WINDOW_MS = 60000;
94
+
95
+ function rateLimitMiddleware(req, res, next) {
96
+ const ip = req.ip || req.connection.remoteAddress;
97
+ const now = Date.now();
98
+ let entry = rateLimitMap.get(ip);
99
+
100
+ if (!entry || now > entry.resetAt) {
101
+ entry = { count: 0, resetAt: now + RATE_WINDOW_MS };
102
+ rateLimitMap.set(ip, entry);
103
+ }
104
+
105
+ entry.count++;
106
+
107
+ if (entry.count > RATE_LIMIT) {
108
+ return res.status(429).json({
109
+ error: 'rate_limit_exceeded',
110
+ retry_after_ms: entry.resetAt - now
111
+ });
112
+ }
113
+
114
+ next();
115
+ }
116
+
117
+ // Periodically clean stale rate-limit entries to prevent memory growth
118
+ const rateLimitCleanupInterval = setInterval(() => {
119
+ const now = Date.now();
120
+ for (const [ip, entry] of rateLimitMap.entries()) {
121
+ if (now > entry.resetAt) {
122
+ rateLimitMap.delete(ip);
123
+ }
124
+ }
125
+ }, 60000);
126
+ if (rateLimitCleanupInterval.unref) rateLimitCleanupInterval.unref();
127
+
77
128
  // Security middleware
78
129
  app.use((req, res, next) => {
79
130
  const addr = req.socket.remoteAddress;
@@ -84,6 +135,9 @@ app.use((req, res, next) => {
84
135
  next();
85
136
  });
86
137
 
138
+ // ── Rate limiting — applied after localhost check, before auth ────────────────
139
+ app.use(rateLimitMiddleware);
140
+
87
141
  // CORS — restrict to dashboard's own origin only (prevent cross-origin attacks)
88
142
  const DASHBOARD_ORIGIN = `http://127.0.0.1:${PORT}`;
89
143
  app.use((req, res, next) => {
@@ -124,6 +178,15 @@ app.get('/', (req, res) => {
124
178
  res.sendFile(FRONTEND);
125
179
  });
126
180
 
181
+ // ── Token refresh endpoint (requires valid existing token) ───────────────────
182
+ app.post('/api/v1/token/refresh', requireAuth, (req, res) => {
183
+ const newToken = crypto.randomBytes(32).toString('hex');
184
+ fs.writeFileSync(TOKEN_FILE, newToken, { mode: 0o600 });
185
+ tokenCreatedAt = Date.now();
186
+ currentToken = newToken;
187
+ res.json({ success: true, token: newToken, expires_in_ms: TOKEN_EXPIRY_MS });
188
+ });
189
+
127
190
  // ── Register API routes ───────────────────────────────────────────────────────
128
191
  API.register(app);
129
192
  app.use('/api/temporal', TemporalAPI);
@@ -143,7 +206,7 @@ server.listen(PORT, '127.0.0.1', () => {
143
206
  console.log(` Status: http://localhost:${PORT}/api/status`);
144
207
  console.log(` Events: http://localhost:${PORT}/events`);
145
208
  console.log(` PID: ${process.pid}`);
146
- console.log(`[Dashboard] Auth token: ${DASHBOARD_TOKEN}`);
209
+ console.log('[Dashboard] Auth token written to token file (not logged for security).');
147
210
  console.log(` Token file: ${TOKEN_FILE}`);
148
211
  console.log('\n Press CTRL+C to stop\n');
149
212
 
@@ -80,14 +80,14 @@ function pollAuditLog() {
80
80
  const newSize = stat.size;
81
81
  const newIno = stat.ino;
82
82
 
83
- // File rotation detected: inode changed or new file is smaller
84
- if (newIno !== _auditInode && _auditInode !== 0) {
85
- process.stderr.write(`[sse-bridge] AUDIT.jsonl rotation detected (old ino: ${_auditInode}, new ino: ${newIno})\n`);
83
+ // File rotation detected: inode changed or file shrunk (truncated after archival)
84
+ if ((newIno !== _auditInode && _auditInode !== 0) || (newSize < _lastAuditSize)) {
85
+ process.stderr.write(`[sse-bridge] AUDIT.jsonl rotation detected (size: ${_lastAuditSize} -> ${newSize}, ino: ${_auditInode} -> ${newIno})\n`);
86
86
  _lastAuditSize = 0;
87
87
  }
88
88
  _auditInode = newIno;
89
89
 
90
- if (newSize <= _lastAuditSize) return; // No new data
90
+ if (newSize <= _lastAuditSize) return;
91
91
 
92
92
  // Read only the new bytes appended since last poll
93
93
  const fd = fs.openSync(AUDIT_PATH, 'r');
@@ -98,6 +98,14 @@ class WaveFeedbackLoop {
98
98
  return { shouldPause: false };
99
99
  }
100
100
 
101
+ recordRemediationOutcome(remediationId, outcome) {
102
+ this.recordPerformance(
103
+ outcome.strategy,
104
+ 'remediation',
105
+ outcome.effective
106
+ );
107
+ }
108
+
101
109
  reset() {
102
110
  this.waveState = { completed: 0, failed: 0, skipped: 0, total: 0 };
103
111
  }
@@ -15,24 +15,41 @@ class IntelligenceInterlock {
15
15
  this.UPGRADE_THRESHOLD = 0.50; // v6.3.0 Threshold (Recalibrated for IDC readiness)
16
16
  }
17
17
 
18
- /**
19
- * Evaluates if a model upgrade is required based on reasoning drift.
20
- * @param {string} spanId
21
- * @param {string} thought
22
- */
23
18
  evaluate(spanId, thought) {
24
- const analysis = driftDetector.analyze(spanId, thought);
25
-
26
- if (analysis.drift_score > this.UPGRADE_THRESHOLD) {
27
- console.log(`[IDC] Critical Drift Detected (${analysis.drift_score}). Recommending intelligence upgrade for Span ${spanId}.`);
28
- return {
29
- action: 'UPGRADE_MIR',
30
- new_mir: 99, // Force maximum intelligence (Tier 1+)
31
- reason: analysis.markers
32
- };
19
+ const driftReport = driftDetector.analyze(spanId, thought);
20
+ const driftScore = driftReport.drift_score;
21
+
22
+ if (driftScore <= this.UPGRADE_THRESHOLD) {
23
+ return { action: 'CONTINUE', drift_score: driftScore };
24
+ }
25
+
26
+ let tierIncrease;
27
+ if (driftScore > 0.80) {
28
+ tierIncrease = 'MAX';
29
+ } else if (driftScore > 0.65) {
30
+ tierIncrease = 2;
31
+ } else {
32
+ tierIncrease = 1;
33
33
  }
34
34
 
35
- return { action: 'CONTINUE', drift: analysis.drift_score };
35
+ let costWarning = false;
36
+ try {
37
+ const CostTracker = require('../models/cost-tracker');
38
+ const dailySpend = CostTracker.getDailySpend ? CostTracker.getDailySpend() : 0;
39
+ const hardLimit = CostTracker.getHardLimit ? CostTracker.getHardLimit() : Infinity;
40
+ if (dailySpend / hardLimit > 0.8) {
41
+ costWarning = true;
42
+ tierIncrease = Math.min(tierIncrease === 'MAX' ? 3 : tierIncrease, 1);
43
+ }
44
+ } catch { /* cost tracker unavailable */ }
45
+
46
+ return {
47
+ action: 'UPGRADE_MIR',
48
+ tier_increase: tierIncrease,
49
+ drift_score: driftScore,
50
+ cost_constrained: costWarning,
51
+ reason: driftReport.markers
52
+ };
36
53
  }
37
54
  }
38
55
 
@@ -8,10 +8,11 @@
8
8
  'use strict';
9
9
 
10
10
  const configManager = require('../governance/config-manager');
11
+ const { LRUMap } = require('../utils/index');
11
12
 
12
13
  class LogicDriftDetector {
13
14
  constructor() {
14
- this.sessionDriftHistory = new Map(); // spanId -> [scores]
15
+ this.sessionDriftHistory = new LRUMap(500);
15
16
  this.DRIFT_THRESHOLD = configManager.get('governance.drift_threshold', 0.75);
16
17
  this.CRITICAL_DRIFT_THRESHOLD = configManager.get('governance.critical_drift_threshold', 0.50);
17
18
  }
@@ -16,6 +16,7 @@ const remediationEngine = require('./remediation-engine'); // v6.1 Pillar X
16
16
  const logicValidator = require('./logic-validator'); // v7 Pillar X
17
17
  const vectorHub = require('../memory/vector-hub'); // v8 Pillar XV
18
18
  const { AuditWriter } = require('../utils/file-io');
19
+ const { LRUMap } = require('../utils/index');
19
20
 
20
21
  class NexusTracer {
21
22
  constructor(config = {}) {
@@ -29,8 +30,8 @@ class NexusTracer {
29
30
  this.vhInitialized = false;
30
31
 
31
32
  // v7: Centralized Thresholds
32
- this.RES_THRESHOLD = configManager.get('governance.res_threshold', 0.8);
33
- this.entropyCache = new Map();
33
+ this.RES_THRESHOLD = configManager.get('governance.res_threshold', 0.8);
34
+ this.entropyCache = new LRUMap(1000);
34
35
 
35
36
  // v9: Async Audit Writer (replaces sync appendFileSync)
36
37
  this._auditWriter = new AuditWriter(this.auditPath);