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
package/bin/dashboard/server.js
CHANGED
|
@@ -42,12 +42,18 @@ const RevOpsAPI = require('./revops-api');
|
|
|
42
42
|
const app = express();
|
|
43
43
|
|
|
44
44
|
// ── Bearer token authentication ──────────────────────────────────────────────
|
|
45
|
-
|
|
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,
|
|
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
|
-
|
|
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) => {
|
|
@@ -107,7 +161,7 @@ app.use((req, res, next) => {
|
|
|
107
161
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
108
162
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
109
163
|
res.setHeader('Cache-Control', 'no-store'); // Never cache dashboard responses
|
|
110
|
-
res.setHeader('Content-Security-Policy',
|
|
164
|
+
res.setHeader('Content-Security-Policy', 'default-src \'self\'; script-src \'self\'; style-src \'self\' \'unsafe-inline\'; connect-src \'self\'');
|
|
111
165
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
112
166
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
113
167
|
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(
|
|
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
|
|
|
@@ -21,7 +21,6 @@ const APPROVAL_DIR = path.join(process.cwd(), '.planning', 'approvals');
|
|
|
21
21
|
const clients = new Set(); // Connected SSE response objects
|
|
22
22
|
|
|
23
23
|
let _lastAuditSize = 0;
|
|
24
|
-
let _auditInode = 0; // Track file inode for rotation detection
|
|
25
24
|
let _lastAutoState = '';
|
|
26
25
|
let _lastApprovals = '';
|
|
27
26
|
|
|
@@ -78,16 +77,18 @@ function pollAuditLog() {
|
|
|
78
77
|
try {
|
|
79
78
|
const stat = fs.statSync(AUDIT_PATH);
|
|
80
79
|
const newSize = stat.size;
|
|
81
|
-
const newIno = stat.ino;
|
|
82
80
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
// Truncation / recreation recovery (NOT rotation — audit rotation was retired in
|
|
82
|
+
// UC-04b because it broke the hash chain; AUDIT.jsonl grows unbounded). If the
|
|
83
|
+
// file ever SHRINKS (manually truncated, .planning wiped, or replaced), reset the
|
|
84
|
+
// read offset to 0 so the live tail keeps working instead of stalling forever on
|
|
85
|
+
// the `newSize <= _lastAuditSize` early-return below or reading at a stale offset.
|
|
86
|
+
if (newSize < _lastAuditSize) {
|
|
87
|
+
process.stderr.write(`[sse-bridge] AUDIT.jsonl shrank (size: ${_lastAuditSize} -> ${newSize}) — re-tailing from start\n`);
|
|
86
88
|
_lastAuditSize = 0;
|
|
87
89
|
}
|
|
88
|
-
_auditInode = newIno;
|
|
89
90
|
|
|
90
|
-
if (newSize <= _lastAuditSize) return;
|
|
91
|
+
if (newSize <= _lastAuditSize) return;
|
|
91
92
|
|
|
92
93
|
// Read only the new bytes appended since last poll
|
|
93
94
|
const fd = fs.openSync(AUDIT_PATH, 'r');
|
|
@@ -169,9 +170,7 @@ function startPolling() {
|
|
|
169
170
|
|
|
170
171
|
// Initialize AUDIT position on first start
|
|
171
172
|
if (!_initialized && fs.existsSync(AUDIT_PATH)) {
|
|
172
|
-
|
|
173
|
-
_lastAuditSize = stat.size;
|
|
174
|
-
_auditInode = stat.ino;
|
|
173
|
+
_lastAuditSize = fs.statSync(AUDIT_PATH).size;
|
|
175
174
|
_initialized = true;
|
|
176
175
|
}
|
|
177
176
|
|
|
@@ -207,9 +206,7 @@ function stopPolling() {
|
|
|
207
206
|
function start() {
|
|
208
207
|
// Pre-initialize AUDIT position so first client gets instant data
|
|
209
208
|
if (!_initialized && fs.existsSync(AUDIT_PATH)) {
|
|
210
|
-
|
|
211
|
-
_lastAuditSize = stat.size;
|
|
212
|
-
_auditInode = stat.ino;
|
|
209
|
+
_lastAuditSize = fs.statSync(AUDIT_PATH).size;
|
|
213
210
|
_initialized = true;
|
|
214
211
|
}
|
|
215
212
|
// Polling starts lazily when addClient() is called
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — Council Runtime (UC-10). Thin multi-voice decision harness (ADS).
|
|
4
|
+
*
|
|
5
|
+
* Activates the Adversarial Decision Loop (ADS) mandated by CLAUDE.md: instead of
|
|
6
|
+
* a multi-round debate simulator, this is a THIN runtime — parallel position
|
|
7
|
+
* collection (one per voice) + consensus scoring + dissent capture.
|
|
8
|
+
*
|
|
9
|
+
* The model is INJECTABLE (no hard LLM dependency) so callers/tests can supply a
|
|
10
|
+
* mock. The Semaphore from wave-executor is reused to bound concurrent voice calls.
|
|
11
|
+
*
|
|
12
|
+
* Design decision — NO challenge round (kept thin per adversarial review + YAGNI):
|
|
13
|
+
* A second model call per dissenter that folds a revised confidence back into the
|
|
14
|
+
* consensus adds ordering/weighting complexity and extra failure modes without
|
|
15
|
+
* changing the activation goal. Position-collection + consensus fully satisfies the
|
|
16
|
+
* ADS loop and the verdict contract. Add a challenge round only if a concrete need
|
|
17
|
+
* arises (an explicit `opts.challengeRound` flag would be the clean extension point).
|
|
18
|
+
*
|
|
19
|
+
* Filenames use opts.decisionId (NOT Date.now(), which is unavailable in some
|
|
20
|
+
* MindForge execution contexts); falls back to "council-latest.json".
|
|
21
|
+
*/
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { Semaphore } = require('../autonomous/wave-executor');
|
|
25
|
+
|
|
26
|
+
const DEFAULT_VOICES = ['architect', 'skeptic', 'pragmatist', 'critic'];
|
|
27
|
+
const VALID_RECOMMENDATIONS = ['PROCEED', 'REVISE'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates a single voice's position payload. Throws a clear, voice-named error
|
|
31
|
+
* on a malformed payload rather than letting it degrade into NaN consensus.
|
|
32
|
+
* @param {string} voice — The voice that produced the position (for the error message).
|
|
33
|
+
* @param {object} position — The raw position returned by the model.
|
|
34
|
+
*/
|
|
35
|
+
function validatePosition(voice, position) {
|
|
36
|
+
if (!position || typeof position !== 'object') {
|
|
37
|
+
throw new Error(`Council voice "${voice}" returned an invalid position: expected an object, got ${position === null ? 'null' : typeof position}`);
|
|
38
|
+
}
|
|
39
|
+
if (!VALID_RECOMMENDATIONS.includes(position.recommendation)) {
|
|
40
|
+
throw new Error(`Council voice "${voice}" returned an invalid position: recommendation must be one of ${VALID_RECOMMENDATIONS.join('/')}, got ${JSON.stringify(position.recommendation)}`);
|
|
41
|
+
}
|
|
42
|
+
if (typeof position.confidence !== 'number' || !Number.isFinite(position.confidence) || position.confidence < 0 || position.confidence > 1) {
|
|
43
|
+
throw new Error(`Council voice "${voice}" returned an invalid position: confidence must be a number in [0,1], got ${position.confidence}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Runs a thin adversarial council over a question.
|
|
49
|
+
* @param {string} question — The decision/question put to the council.
|
|
50
|
+
* @param {object} [opts]
|
|
51
|
+
* @param {string[]} [opts.voices] — Voice personas to consult (default: 4 ADS voices).
|
|
52
|
+
* @param {number} [opts.consensusThreshold=0.75] — Threshold for PROCEED/REVISE.
|
|
53
|
+
* @param {function} opts.model — REQUIRED. async ({voice, question}) =>
|
|
54
|
+
* { recommendation: 'PROCEED'|'REVISE', confidence: number(0..1), rationale: string }
|
|
55
|
+
* @param {number} [opts.maxConcurrency] — Bound on parallel voice calls (default: #voices).
|
|
56
|
+
* @param {boolean} [opts.writeDecision=true] — Persist a decision record to disk.
|
|
57
|
+
* @param {string} [opts.outputPath] — Directory for the record (default: .planning/decisions).
|
|
58
|
+
* @param {string} [opts.decisionId] — Stable id used in the filename (no Date.now()).
|
|
59
|
+
* @returns {Promise<{question,positions,consensus,verdict,dissent}>}
|
|
60
|
+
*/
|
|
61
|
+
async function runCouncil(question, opts = {}) {
|
|
62
|
+
const voices = Array.isArray(opts.voices) && opts.voices.length > 0
|
|
63
|
+
? opts.voices
|
|
64
|
+
: DEFAULT_VOICES;
|
|
65
|
+
const consensusThreshold = opts.consensusThreshold ?? 0.75;
|
|
66
|
+
const model = opts.model;
|
|
67
|
+
if (typeof model !== 'function') {
|
|
68
|
+
throw new Error('runCouncil requires an injectable model function (opts.model)');
|
|
69
|
+
}
|
|
70
|
+
const maxConcurrency = opts.maxConcurrency || voices.length;
|
|
71
|
+
|
|
72
|
+
// Parallel position collection — bounded by the reused Semaphore.
|
|
73
|
+
const sem = new Semaphore(maxConcurrency);
|
|
74
|
+
const positions = await Promise.all(voices.map(async (voice) => {
|
|
75
|
+
await sem.acquire();
|
|
76
|
+
try {
|
|
77
|
+
const position = await model({ voice, question });
|
|
78
|
+
// Validate the payload immediately — never silently swallow a malformed
|
|
79
|
+
// position into NaN consensus (which would collapse to NO_CONSENSUS and
|
|
80
|
+
// write NaN to the decision record).
|
|
81
|
+
validatePosition(voice, position);
|
|
82
|
+
return { voice, ...position };
|
|
83
|
+
} finally {
|
|
84
|
+
sem.release();
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Consensus = mean approval signal across voices.
|
|
89
|
+
// A PROCEED contributes its confidence; a REVISE contributes its inverse
|
|
90
|
+
// (so a high-confidence REVISE pulls consensus down hard).
|
|
91
|
+
const consensus = positions.reduce((sum, p) => {
|
|
92
|
+
const approval = p.recommendation === 'PROCEED' ? p.confidence : (1 - p.confidence);
|
|
93
|
+
return sum + approval;
|
|
94
|
+
}, 0) / positions.length;
|
|
95
|
+
|
|
96
|
+
const verdict = consensus >= consensusThreshold ? 'PROCEED'
|
|
97
|
+
: consensus <= (1 - consensusThreshold) ? 'REVISE'
|
|
98
|
+
: 'NO_CONSENSUS';
|
|
99
|
+
|
|
100
|
+
// Dissent capture:
|
|
101
|
+
// - For a decisive verdict (PROCEED/REVISE): the voices opposing that direction.
|
|
102
|
+
// - For NO_CONSENSUS (the deadlock ADS most needs documented): the FULL split —
|
|
103
|
+
// every voice's {voice, recommendation, rationale} — so the decision record
|
|
104
|
+
// preserves both camps rather than recording an empty dissent list.
|
|
105
|
+
const dissent = verdict === 'NO_CONSENSUS'
|
|
106
|
+
? positions.map((p) => ({ voice: p.voice, recommendation: p.recommendation, rationale: p.rationale }))
|
|
107
|
+
: positions.filter((p) =>
|
|
108
|
+
(verdict === 'PROCEED' && p.recommendation !== 'PROCEED') ||
|
|
109
|
+
(verdict === 'REVISE' && p.recommendation === 'PROCEED'))
|
|
110
|
+
.map((d) => ({ voice: d.voice, rationale: d.rationale }));
|
|
111
|
+
|
|
112
|
+
const result = { question, positions, consensus, verdict, dissent };
|
|
113
|
+
|
|
114
|
+
if (opts.writeDecision !== false) {
|
|
115
|
+
const dir = opts.outputPath || path.join(process.cwd(), '.planning', 'decisions');
|
|
116
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
117
|
+
const name = opts.decisionId ? `council-${opts.decisionId}.json` : 'council-latest.json';
|
|
118
|
+
fs.writeFileSync(path.join(dir, name), JSON.stringify(result, null, 2));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { runCouncil };
|
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — OTel GenAI Exporter (UC-18).
|
|
4
|
+
* Translates NexusTracer spans to OpenTelemetry GenAI semantic conventions.
|
|
5
|
+
* Active only when OTEL_EXPORTER_OTLP_ENDPOINT is set.
|
|
6
|
+
*
|
|
7
|
+
* NexusTracer span shape (from nexus-tracer.js startSpan/endSpan):
|
|
8
|
+
* {
|
|
9
|
+
* id: 'sp_<hex>',
|
|
10
|
+
* trace_id: 'tr_<hex>',
|
|
11
|
+
* parent_id: string|null,
|
|
12
|
+
* name: string,
|
|
13
|
+
* status: 'active'|'success'|'error',
|
|
14
|
+
* start_time: ISO-8601,
|
|
15
|
+
* end_time: ISO-8601,
|
|
16
|
+
* attributes: {
|
|
17
|
+
* service: string,
|
|
18
|
+
* host: string,
|
|
19
|
+
* pid: number,
|
|
20
|
+
* model_id?: string,
|
|
21
|
+
* skill?: string,
|
|
22
|
+
* input_tokens?: number,
|
|
23
|
+
* output_tokens?: number,
|
|
24
|
+
* ...
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Mapping to OTel GenAI semantic conventions:
|
|
29
|
+
* span.name → name
|
|
30
|
+
* span.attributes.model_id → gen_ai.request.model, gen_ai.response.model
|
|
31
|
+
* span.attributes.input_tokens → gen_ai.usage.input_tokens
|
|
32
|
+
* span.attributes.output_tokens → gen_ai.usage.output_tokens
|
|
33
|
+
* span.name → gen_ai.operation.name
|
|
34
|
+
* 'mindforge' → gen_ai.system (or span.attributes.provider if present)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const crypto = require('crypto');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if the OTel exporter is enabled (env var gate).
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function isEnabled() {
|
|
44
|
+
return !!process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Translate a NexusTracer span to OTel GenAI-compatible format.
|
|
49
|
+
* Produces a valid 16-byte hex traceId and 8-byte hex spanId.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} nexusSpan - A span object from NexusTracer
|
|
52
|
+
* @returns {object} OTel-compatible span object
|
|
53
|
+
*/
|
|
54
|
+
function toOtelSpan(nexusSpan) {
|
|
55
|
+
const attrs = nexusSpan.attributes || {};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
traceId: crypto.randomBytes(16).toString('hex'),
|
|
59
|
+
spanId: crypto.randomBytes(8).toString('hex'),
|
|
60
|
+
parentSpanId: nexusSpan.parent_id || '',
|
|
61
|
+
name: nexusSpan.name || 'unknown',
|
|
62
|
+
kind: 1, // SPAN_KIND_INTERNAL
|
|
63
|
+
startTimeUnixNano: nexusSpan.start_time
|
|
64
|
+
? BigInt(new Date(nexusSpan.start_time).getTime()) * 1_000_000n
|
|
65
|
+
: 0n,
|
|
66
|
+
endTimeUnixNano: nexusSpan.end_time
|
|
67
|
+
? BigInt(new Date(nexusSpan.end_time).getTime()) * 1_000_000n
|
|
68
|
+
: 0n,
|
|
69
|
+
status: nexusSpan.status === 'success' ? { code: 1 } : { code: 2 },
|
|
70
|
+
attributes: {
|
|
71
|
+
'gen_ai.system': attrs.provider || 'mindforge',
|
|
72
|
+
'gen_ai.request.model': attrs.model_id || '',
|
|
73
|
+
'gen_ai.response.model': attrs.model_id || '',
|
|
74
|
+
'gen_ai.usage.input_tokens': attrs.input_tokens || 0,
|
|
75
|
+
'gen_ai.usage.output_tokens': attrs.output_tokens || 0,
|
|
76
|
+
'gen_ai.operation.name': nexusSpan.name || '',
|
|
77
|
+
'service.name': attrs.service || 'mindforge-nexus',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Serialize BigInt values to strings for JSON compatibility.
|
|
84
|
+
* @param {object} otelSpan
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
function toJsonSafe(otelSpan) {
|
|
88
|
+
return {
|
|
89
|
+
...otelSpan,
|
|
90
|
+
startTimeUnixNano: String(otelSpan.startTimeUnixNano),
|
|
91
|
+
endTimeUnixNano: String(otelSpan.endTimeUnixNano),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Export a NexusTracer span to the OTel-compatible local file.
|
|
97
|
+
* In production, this would POST to OTEL_EXPORTER_OTLP_ENDPOINT/v1/traces.
|
|
98
|
+
* For now, appends to .mindforge/metrics/otel-spans.jsonl for verification.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} nexusSpan - A span from NexusTracer
|
|
101
|
+
*/
|
|
102
|
+
async function exportSpan(nexusSpan) {
|
|
103
|
+
if (!isEnabled()) return;
|
|
104
|
+
|
|
105
|
+
const otelSpan = toOtelSpan(nexusSpan);
|
|
106
|
+
const jsonSafe = toJsonSafe(otelSpan);
|
|
107
|
+
|
|
108
|
+
const fs = require('fs');
|
|
109
|
+
const path = require('path');
|
|
110
|
+
const outPath = path.join(process.cwd(), '.mindforge', 'metrics', 'otel-spans.jsonl');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const dir = path.dirname(outPath);
|
|
114
|
+
if (!fs.existsSync(dir)) {
|
|
115
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
fs.appendFileSync(outPath, JSON.stringify(jsonSafe) + '\n');
|
|
118
|
+
} catch {
|
|
119
|
+
// Non-fatal: observability export should never break the main flow
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { isEnabled, toOtelSpan, toJsonSafe, exportSpan };
|