mindforge-cc 11.0.0 → 11.2.1
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/config.json +14 -4
- package/CHANGELOG.md +137 -0
- package/MINDFORGE.md +5 -5
- package/RELEASENOTES.md +1 -1
- package/bin/autonomous/audit-writer.js +108 -86
- package/bin/autonomous/auto-runner.js +304 -19
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/mesh-self-healer.js +101 -28
- package/bin/autonomous/wave-executor.js +20 -1
- package/bin/browser/regression-writer.js +45 -3
- package/bin/browser/session-manager.js +21 -17
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/server.js +1 -1
- package/bin/dashboard/sse-bridge.js +9 -12
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/logic-drift-detector.js +14 -6
- package/bin/engine/logic-validator.js +155 -25
- package/bin/engine/orbital-guardian.js +56 -10
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/reason-source-aligner.js +19 -6
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- package/bin/engine/sre-manager.js +33 -6
- package/bin/engine/temporal-cli.js +4 -2
- 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/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/policy-engine.js +17 -4
- package/bin/governance/quantum-crypto.js +63 -9
- package/bin/governance/ztai-archiver.js +74 -9
- package/bin/governance/ztai-manager.js +33 -5
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/installer-core.js +31 -2
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +45 -4
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-store.js +30 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +143 -6
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +13 -4
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +6 -2
- package/bin/models/model-router.js +31 -18
- package/bin/models/openai-provider.js +6 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/review/finding-synthesizer.js +35 -6
- package/bin/security/trust-boundaries.js +194 -0
- package/bin/security/trust-gate-hook.js +49 -0
- package/bin/skill-registry.js +34 -22
- 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/sre/shadow-mirror.js +90 -40
- package/bin/utils/append-queue.js +67 -0
- package/bin/utils/file-io.js +29 -80
- package/bin/utils/version-check.js +75 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/package.json +1 -1
- package/bin/dashboard/team-tracker.js +0 -0
package/bin/memory/vector-hub.js
CHANGED
|
@@ -23,6 +23,30 @@ class VectorHub {
|
|
|
23
23
|
this.initialized = false;
|
|
24
24
|
this._writeCount = 0;
|
|
25
25
|
this._batchSize = 10;
|
|
26
|
+
// UC-09: serialized async persistence chain. Successive save() calls queue
|
|
27
|
+
// behind one another so two exports never write the .db file concurrently
|
|
28
|
+
// (a corrupted half-written database would otherwise be possible).
|
|
29
|
+
this._saveChain = Promise.resolve();
|
|
30
|
+
// Count of async save()s that have been SCHEDULED but not yet COMPLETED their
|
|
31
|
+
// durable disk write. A boolean here is unsafe: with two rapid saves the chain
|
|
32
|
+
// is [writeA → clear → writeB → clear], leaving a window where the flag reads
|
|
33
|
+
// "clean" while writeB is still pending — a hard process.exit() in that window
|
|
34
|
+
// would make the exit guard skip saveSync() and lose the last batch (the exact
|
|
35
|
+
// data loss this guard exists to prevent). A counter has no such gap: it only
|
|
36
|
+
// returns to 0 once EVERY scheduled save has completed. saveSync() always
|
|
37
|
+
// exports the current in-memory DB, so over-flushing on exit is harmless — we
|
|
38
|
+
// deliberately bias toward flushing.
|
|
39
|
+
this._pendingSaves = 0;
|
|
40
|
+
this._exitGuardInstalled = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_installExitGuard() {
|
|
44
|
+
if (this._exitGuardInstalled) return;
|
|
45
|
+
this._exitGuardInstalled = true;
|
|
46
|
+
// 'exit' handlers can only run synchronous code — saveSync() fits exactly.
|
|
47
|
+
process.once('exit', () => {
|
|
48
|
+
if (this._db && this._pendingSaves > 0) this.saveSync();
|
|
49
|
+
});
|
|
26
50
|
}
|
|
27
51
|
|
|
28
52
|
_ensureDir() {
|
|
@@ -32,6 +56,26 @@ class VectorHub {
|
|
|
32
56
|
}
|
|
33
57
|
}
|
|
34
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Idempotently add a column to an existing table (lightweight migration).
|
|
61
|
+
* SQLite has no "ADD COLUMN IF NOT EXISTS", so we run the ALTER and swallow
|
|
62
|
+
* only the "duplicate column name" error — which simply means the column is
|
|
63
|
+
* already present (the table was created with it, or a prior run added it).
|
|
64
|
+
* Any other error is re-thrown so genuine schema problems surface loudly.
|
|
65
|
+
* @param {string} table
|
|
66
|
+
* @param {string} column
|
|
67
|
+
* @param {string} typeDecl - e.g. 'TEXT', 'INTEGER DEFAULT 0'
|
|
68
|
+
*/
|
|
69
|
+
_addColumnIfMissing(table, column, typeDecl) {
|
|
70
|
+
try {
|
|
71
|
+
this._db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${typeDecl}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (!/duplicate column name/i.test(err && err.message)) {
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
35
79
|
/**
|
|
36
80
|
* Initialize the WASM SQLite database and create tables + indexes.
|
|
37
81
|
*/
|
|
@@ -100,11 +144,23 @@ class VectorHub {
|
|
|
100
144
|
id TEXT PRIMARY KEY,
|
|
101
145
|
request_id TEXT NOT NULL,
|
|
102
146
|
status TEXT NOT NULL,
|
|
147
|
+
did TEXT,
|
|
148
|
+
signed_message TEXT,
|
|
103
149
|
attestation_payload TEXT,
|
|
104
150
|
timestamp TEXT NOT NULL
|
|
105
151
|
)
|
|
106
152
|
`);
|
|
107
153
|
|
|
154
|
+
// UC-22 (audit finding #2): orbital attestations must carry the signer DID
|
|
155
|
+
// and the EXACT canonical message that was signed so verify() can re-check
|
|
156
|
+
// the cryptographic signature instead of trusting status='APPROVED' alone.
|
|
157
|
+
// CREATE TABLE IF NOT EXISTS won't add columns to a database created before
|
|
158
|
+
// this fix, so back-fill them with guarded ALTER TABLE statements. SQLite
|
|
159
|
+
// throws "duplicate column name" when the column already exists — that case
|
|
160
|
+
// is the success path (already migrated), so it is swallowed.
|
|
161
|
+
this._addColumnIfMissing('attestations', 'did', 'TEXT');
|
|
162
|
+
this._addColumnIfMissing('attestations', 'signed_message', 'TEXT');
|
|
163
|
+
|
|
108
164
|
this._db.run(`
|
|
109
165
|
CREATE TABLE IF NOT EXISTS mesh_config (
|
|
110
166
|
key TEXT PRIMARY KEY,
|
|
@@ -167,22 +223,74 @@ class VectorHub {
|
|
|
167
223
|
this._db.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_name ON _migrations(name)');
|
|
168
224
|
|
|
169
225
|
this.initialized = true;
|
|
226
|
+
this._installExitGuard();
|
|
170
227
|
this.save();
|
|
171
228
|
console.log(`[VectorHub] Initialized WASM SQLite persistence at ${this.dbPath}`);
|
|
172
229
|
}
|
|
173
230
|
|
|
174
231
|
/**
|
|
175
|
-
* Persist the in-memory database to disk.
|
|
232
|
+
* Persist the in-memory database to disk (UC-09).
|
|
233
|
+
*
|
|
234
|
+
* sql.js export() is intrinsically synchronous, but the (potentially large)
|
|
235
|
+
* FILE WRITE no longer blocks the event loop: we snapshot the bytes
|
|
236
|
+
* synchronously, then write+fsync them asynchronously. Successive saves are
|
|
237
|
+
* serialized on a single chain so two exports never write the .db file
|
|
238
|
+
* concurrently. The write is crash-safe (tmp file + atomic rename + fsync),
|
|
239
|
+
* so a partial write can never leave a corrupted database on disk.
|
|
240
|
+
*
|
|
241
|
+
* @returns {Promise<void>} Resolves once the snapshot is durably on disk.
|
|
176
242
|
*/
|
|
177
243
|
save() {
|
|
244
|
+
if (!this._db) return Promise.resolve();
|
|
245
|
+
|
|
246
|
+
let buffer;
|
|
247
|
+
try {
|
|
248
|
+
this._ensureDir();
|
|
249
|
+
// Snapshot the DB synchronously so the bytes reflect this exact moment.
|
|
250
|
+
buffer = Buffer.from(this._db.export());
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.warn(`[VectorHub] Failed to export database: ${err.message}`);
|
|
253
|
+
return Promise.resolve();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dbPath = this.dbPath;
|
|
257
|
+
// Increment when SCHEDULED; decrement only once this specific save has
|
|
258
|
+
// COMPLETED (success or failure). The exit guard fires saveSync() while any
|
|
259
|
+
// scheduled save is still outstanding — see _installExitGuard().
|
|
260
|
+
this._pendingSaves++;
|
|
261
|
+
this._saveChain = this._saveChain.then(() => writeDbDurable(dbPath, buffer))
|
|
262
|
+
.catch((err) => {
|
|
263
|
+
console.warn(`[VectorHub] Failed to save database: ${err.message}`);
|
|
264
|
+
})
|
|
265
|
+
.then(() => { this._pendingSaves--; });
|
|
266
|
+
return this._saveChain;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Synchronous, crash-safe persistence — used only on shutdown to GUARANTEE
|
|
271
|
+
* no acknowledged write is lost if the process exits before the async save
|
|
272
|
+
* chain drains. Correctness over non-blocking here.
|
|
273
|
+
*/
|
|
274
|
+
saveSync() {
|
|
178
275
|
if (!this._db) return;
|
|
179
276
|
try {
|
|
180
277
|
this._ensureDir();
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
fs.
|
|
278
|
+
const buffer = Buffer.from(this._db.export());
|
|
279
|
+
const tmpPath = `${this.dbPath}.tmp.${process.pid}`;
|
|
280
|
+
const fd = fs.openSync(tmpPath, 'w');
|
|
281
|
+
try {
|
|
282
|
+
fs.writeSync(fd, buffer);
|
|
283
|
+
fs.fsyncSync(fd);
|
|
284
|
+
} finally {
|
|
285
|
+
fs.closeSync(fd);
|
|
286
|
+
}
|
|
287
|
+
fs.renameSync(tmpPath, this.dbPath);
|
|
288
|
+
// A sync export captures the full in-memory DB — a superset of anything the
|
|
289
|
+
// outstanding async saves would have written — so the pending work is now
|
|
290
|
+
// durably satisfied. Clearing the counter prevents a redundant second flush.
|
|
291
|
+
this._pendingSaves = 0;
|
|
184
292
|
} catch (err) {
|
|
185
|
-
console.warn(`[VectorHub] Failed to save database: ${err.message}`);
|
|
293
|
+
console.warn(`[VectorHub] Failed to save database (sync): ${err.message}`);
|
|
186
294
|
}
|
|
187
295
|
}
|
|
188
296
|
|
|
@@ -199,10 +307,13 @@ class VectorHub {
|
|
|
199
307
|
|
|
200
308
|
/**
|
|
201
309
|
* Close the database and save final state to disk.
|
|
310
|
+
* Drains any pending async saves, then performs a guaranteed synchronous
|
|
311
|
+
* durable write so no acknowledged data is lost on shutdown (UC-09).
|
|
202
312
|
*/
|
|
203
313
|
async close() {
|
|
204
314
|
if (this._db) {
|
|
205
|
-
this.save()
|
|
315
|
+
try { await this._saveChain; } catch { /* logged in save() */ }
|
|
316
|
+
this.saveSync();
|
|
206
317
|
this._db.close();
|
|
207
318
|
this._db = null;
|
|
208
319
|
this.initialized = false;
|
|
@@ -455,6 +566,32 @@ class VectorHub {
|
|
|
455
566
|
}
|
|
456
567
|
}
|
|
457
568
|
|
|
569
|
+
// ── Durable async DB file write (UC-09) ───────────────────────────────────────
|
|
570
|
+
// Crash-safe: write to a tmp file, fsync, then atomically rename over the target.
|
|
571
|
+
// A crash mid-write leaves the previous good .db intact (rename is atomic on POSIX).
|
|
572
|
+
function writeDbDurable(dbPath, buffer) {
|
|
573
|
+
return new Promise((resolve, reject) => {
|
|
574
|
+
const tmpPath = `${dbPath}.tmp.${process.pid}`;
|
|
575
|
+
const fail = (err) => { fs.unlink(tmpPath, () => reject(err)); };
|
|
576
|
+
fs.open(tmpPath, 'w', (openErr, fd) => {
|
|
577
|
+
if (openErr) return reject(openErr);
|
|
578
|
+
fs.write(fd, buffer, 0, buffer.length, 0, (writeErr) => {
|
|
579
|
+
if (writeErr) { fs.close(fd, () => fail(writeErr)); return; }
|
|
580
|
+
fs.fsync(fd, (syncErr) => {
|
|
581
|
+
fs.close(fd, (closeErr) => {
|
|
582
|
+
if (syncErr) return fail(syncErr);
|
|
583
|
+
if (closeErr) return fail(closeErr);
|
|
584
|
+
fs.rename(tmpPath, dbPath, (renameErr) => {
|
|
585
|
+
if (renameErr) return fail(renameErr);
|
|
586
|
+
resolve();
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
458
595
|
// ── Factory Function ──────────────────────────────────────────────────────────
|
|
459
596
|
|
|
460
597
|
/**
|
package/bin/mindforge-cli.js
CHANGED
|
@@ -115,11 +115,6 @@ const COMMANDS = {
|
|
|
115
115
|
script: 'bin/autonomous/mesh-self-healer.js',
|
|
116
116
|
description: 'Auto-detect and repair reasoning drifts in the active swarm'
|
|
117
117
|
},
|
|
118
|
-
'quantum-verify': {
|
|
119
|
-
script: 'bin/governance/quantum-crypto.js',
|
|
120
|
-
description: 'Verify framework integrity using post-quantum signatures',
|
|
121
|
-
defaultArgs: ['--verify', '.mindforge/engine/']
|
|
122
|
-
},
|
|
123
118
|
// Planned: jira-sync, confluence-sync (not yet implemented)
|
|
124
119
|
'metrics': {
|
|
125
120
|
script: 'bin/dashboard/metrics-aggregator.js',
|
|
@@ -138,6 +133,10 @@ const COMMANDS = {
|
|
|
138
133
|
script: 'bin/engine/learning-manager.js',
|
|
139
134
|
description: 'Append a new Learning Entry to the Evolution Log',
|
|
140
135
|
defaultArgs: ['record']
|
|
136
|
+
},
|
|
137
|
+
'verify': {
|
|
138
|
+
script: 'bin/engine/verify-cli.js',
|
|
139
|
+
description: 'Run unified verification (tests, lint, audit, typecheck) and write report'
|
|
141
140
|
}
|
|
142
141
|
};
|
|
143
142
|
|
|
@@ -15,7 +15,7 @@ class AnthropicProvider {
|
|
|
15
15
|
|
|
16
16
|
const data = JSON.stringify({
|
|
17
17
|
model,
|
|
18
|
-
system: systemPrompt,
|
|
18
|
+
system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
|
|
19
19
|
messages: [{ role: 'user', content: userMessage }],
|
|
20
20
|
max_tokens: maxTokens,
|
|
21
21
|
temperature,
|
|
@@ -45,15 +45,24 @@ class AnthropicProvider {
|
|
|
45
45
|
|
|
46
46
|
const inputTokens = json.usage.input_tokens;
|
|
47
47
|
const outputTokens = json.usage.output_tokens;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const cacheRead = json.usage.cache_read_input_tokens || 0;
|
|
49
|
+
const cacheCreate = json.usage.cache_creation_input_tokens || 0;
|
|
50
|
+
|
|
51
|
+
const { priceCall } = require('./pricing-registry');
|
|
52
|
+
const cost = priceCall(json.model, {
|
|
53
|
+
input_tokens: inputTokens,
|
|
54
|
+
output_tokens: outputTokens,
|
|
55
|
+
cache_read_input_tokens: cacheRead,
|
|
56
|
+
cache_creation_input_tokens: cacheCreate,
|
|
57
|
+
});
|
|
51
58
|
|
|
52
59
|
resolve({
|
|
53
60
|
model: json.model,
|
|
54
61
|
content: json.content[0].text,
|
|
55
62
|
input_tokens: inputTokens,
|
|
56
63
|
output_tokens: outputTokens,
|
|
64
|
+
cache_read_input_tokens: cacheRead,
|
|
65
|
+
cache_creation_input_tokens: cacheCreate,
|
|
57
66
|
cost_usd: cost,
|
|
58
67
|
provider: 'anthropic'
|
|
59
68
|
});
|
|
@@ -101,10 +101,12 @@ function getSummary(params = { days: 7 }) {
|
|
|
101
101
|
result.calls++;
|
|
102
102
|
|
|
103
103
|
const model = entry.model || 'unknown';
|
|
104
|
-
if (!result.by_model[model]) result.by_model[model] = { cost: 0, calls: 0, tokens: 0 };
|
|
104
|
+
if (!result.by_model[model]) result.by_model[model] = { cost: 0, calls: 0, tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0 };
|
|
105
105
|
result.by_model[model].cost += cost;
|
|
106
106
|
result.by_model[model].calls++;
|
|
107
107
|
result.by_model[model].tokens += (entry.input_tokens || 0) + (entry.output_tokens || 0);
|
|
108
|
+
result.by_model[model].cache_read_tokens += (entry.cache_read_input_tokens || 0);
|
|
109
|
+
result.by_model[model].cache_creation_tokens += (entry.cache_creation_input_tokens || 0);
|
|
108
110
|
|
|
109
111
|
const phase = entry.phase || 'unknown';
|
|
110
112
|
if (!result.by_phase[phase]) result.by_phase[phase] = 0;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* MindForge — Difficulty Scorer (UC-06). Pure heuristic 1-10.
|
|
4
|
+
* Used by model-router in SHADOW MODE to log intended routing
|
|
5
|
+
* without altering actual model selection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const HIGH_KW = /auth|jwt|oauth|crypto|security|payment|pii|gdpr|hipaa|encrypt|secret|credential/i;
|
|
9
|
+
const MED_KW = /refactor|migrate|architect|design|performance|concurrency|async/i;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Score a task for difficulty on a 1-10 scale.
|
|
13
|
+
* @param {object} task
|
|
14
|
+
* @param {string} [task.description] — free-text task description
|
|
15
|
+
* @param {string[]} [task.files] — files involved
|
|
16
|
+
* @param {number} [task.tier] — security tier (1-3)
|
|
17
|
+
* @returns {number} integer difficulty score in [1, 10]
|
|
18
|
+
*/
|
|
19
|
+
function score(task = {}) {
|
|
20
|
+
const desc = task.description || '';
|
|
21
|
+
const files = task.files || [];
|
|
22
|
+
const tier = task.tier || 0;
|
|
23
|
+
|
|
24
|
+
let s = 3; // baseline
|
|
25
|
+
|
|
26
|
+
// Keyword analysis (description + file paths)
|
|
27
|
+
if (HIGH_KW.test(desc) || files.some(f => HIGH_KW.test(f))) {
|
|
28
|
+
s += 4;
|
|
29
|
+
} else if (MED_KW.test(desc)) {
|
|
30
|
+
s += 2;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// File count complexity
|
|
34
|
+
if (files.length > 10) {
|
|
35
|
+
s += 2;
|
|
36
|
+
} else if (files.length > 5) {
|
|
37
|
+
s += 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Long description signals complexity
|
|
41
|
+
if (desc.length > 500) {
|
|
42
|
+
s += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tier-3 floor: security/privacy tasks never score below 7
|
|
46
|
+
if (tier >= 3) {
|
|
47
|
+
s = Math.max(s, 7);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Clamp to [1, 10]
|
|
51
|
+
return Math.min(Math.max(s, 1), 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { score };
|
|
@@ -46,10 +46,14 @@ class GeminiProvider {
|
|
|
46
46
|
return reject(Object.assign(new Error(json.error?.message || 'Gemini API error'), { status: res.statusCode }));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Gemini 1.5 Pro billing is complex; using $1.25 / 1M input as baseline
|
|
50
49
|
const inputTokens = json.usageMetadata.promptTokenCount;
|
|
51
50
|
const outputTokens = json.usageMetadata.candidatesTokenCount;
|
|
52
|
-
|
|
51
|
+
|
|
52
|
+
const { priceCall } = require('./pricing-registry');
|
|
53
|
+
const cost = priceCall(modelId, {
|
|
54
|
+
input_tokens: inputTokens,
|
|
55
|
+
output_tokens: outputTokens,
|
|
56
|
+
});
|
|
53
57
|
|
|
54
58
|
resolve({
|
|
55
59
|
model: modelId,
|
|
@@ -74,46 +74,59 @@ function readMindforgeSettings() {
|
|
|
74
74
|
return _settingsCache;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function route(persona = 'developer', tier = 1) {
|
|
77
|
+
function route(persona = 'developer', tier = 1, taskContext) {
|
|
78
78
|
const settings = readMindforgeSettings();
|
|
79
|
-
|
|
79
|
+
let result;
|
|
80
|
+
|
|
80
81
|
// 1. Tier 3 override (Security/Privacy always uses SECURITY_MODEL)
|
|
81
82
|
if (tier === 3) {
|
|
82
|
-
|
|
83
|
+
result = {
|
|
83
84
|
model: settings.SECURITY_MODEL,
|
|
84
85
|
setting: 'SECURITY_MODEL',
|
|
85
86
|
reason: 'Tier 3 (Security/Privacy) override'
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
|
-
|
|
89
89
|
// 2. Persona mapping (Specific personas like research, debug, qa)
|
|
90
|
-
if (persona !== 'developer' && PERSONA_MAP[persona]) {
|
|
90
|
+
else if (persona !== 'developer' && PERSONA_MAP[persona]) {
|
|
91
91
|
const settingKey = PERSONA_MAP[persona];
|
|
92
|
-
|
|
92
|
+
result = {
|
|
93
93
|
model: settings[settingKey],
|
|
94
94
|
setting: settingKey,
|
|
95
95
|
reason: `Mapped from specific persona "${persona}"`
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
|
-
|
|
99
98
|
// 3. Budget Bias (Tier 1 uses QUICK_MODEL for default developer tasks)
|
|
100
|
-
if (tier === 1) {
|
|
101
|
-
|
|
99
|
+
else if (tier === 1) {
|
|
100
|
+
result = {
|
|
102
101
|
model: settings.QUICK_MODEL,
|
|
103
102
|
setting: 'QUICK_MODEL',
|
|
104
103
|
reason: 'Tier 1 Budget Bias (efficiency mode)'
|
|
105
104
|
};
|
|
106
105
|
}
|
|
107
|
-
|
|
108
106
|
// 4. Default mapping
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
107
|
+
else {
|
|
108
|
+
const settingKey = 'EXECUTOR_MODEL';
|
|
109
|
+
result = {
|
|
110
|
+
model: settings[settingKey],
|
|
111
|
+
setting: settingKey,
|
|
112
|
+
reason: `Default EXECUTOR_MODEL for tier ${tier}`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Shadow-mode: difficulty-aware routing (UC-06)
|
|
117
|
+
// Logs what model the difficulty scorer WOULD select, without changing the result.
|
|
118
|
+
if (taskContext) {
|
|
119
|
+
const { score: scoreDifficulty } = require('./difficulty-scorer');
|
|
120
|
+
const difficulty = scoreDifficulty(taskContext);
|
|
121
|
+
const shadowModel = difficulty <= 3 ? settings.QUICK_MODEL
|
|
122
|
+
: difficulty >= 8 ? settings.PLANNER_MODEL
|
|
123
|
+
: settings.EXECUTOR_MODEL;
|
|
124
|
+
if (shadowModel !== result.model) {
|
|
125
|
+
process.stderr.write(`[model-router:shadow] difficulty=${difficulty} would route to ${shadowModel} (actual: ${result.model})\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
function getModel(settingKey) {
|
|
@@ -46,9 +46,12 @@ class OpenAIProvider {
|
|
|
46
46
|
|
|
47
47
|
const inputTokens = json.usage.prompt_tokens;
|
|
48
48
|
const outputTokens = json.usage.completion_tokens;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const cost = (
|
|
49
|
+
|
|
50
|
+
const { priceCall } = require('./pricing-registry');
|
|
51
|
+
const cost = priceCall(json.model, {
|
|
52
|
+
input_tokens: inputTokens,
|
|
53
|
+
output_tokens: outputTokens,
|
|
54
|
+
});
|
|
52
55
|
|
|
53
56
|
resolve({
|
|
54
57
|
model: json.model,
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Pricing Registry (UC-05)
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all model pricing. Loads from
|
|
5
|
+
* .mindforge/config.json `revops.market_registry` and normalizes
|
|
6
|
+
* to per-1M-token units. All providers and cost-tracker MUST
|
|
7
|
+
* query this module instead of hardcoding rates.
|
|
8
|
+
*
|
|
9
|
+
* Buckets: input, output, cache_read, cache_creation
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const CONFIG_PATH = path.join(__dirname, '..', '..', '.mindforge', 'config.json');
|
|
17
|
+
|
|
18
|
+
// Fallback per-1M rates when model is unknown (generous estimate to avoid under-billing)
|
|
19
|
+
const FALLBACK_RATES = {
|
|
20
|
+
input: 5.0,
|
|
21
|
+
output: 15.0,
|
|
22
|
+
cache_read: 0.5,
|
|
23
|
+
cache_creation: 6.25,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let _priceTable = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load and normalize the market_registry from config.json.
|
|
30
|
+
* Config values are in per-1K-token units. We multiply by 1000 to get per-1M.
|
|
31
|
+
* Cache buckets: cache_read = 10% of input, cache_creation = 125% of input
|
|
32
|
+
* (unless explicitly provided in config).
|
|
33
|
+
*/
|
|
34
|
+
function loadPriceTable() {
|
|
35
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
36
|
+
const config = JSON.parse(raw);
|
|
37
|
+
const registry = config.revops && config.revops.market_registry;
|
|
38
|
+
|
|
39
|
+
if (!registry || typeof registry !== 'object') {
|
|
40
|
+
process.stderr.write('[pricing-registry] WARN: market_registry missing from config.json, using fallbacks\n');
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const table = {};
|
|
45
|
+
for (const [modelId, entry] of Object.entries(registry)) {
|
|
46
|
+
const inputPer1M = (entry.cost_input || 0) * 1000;
|
|
47
|
+
const outputPer1M = (entry.cost_output || 0) * 1000;
|
|
48
|
+
|
|
49
|
+
// Cache bucket derivation: use explicit config fields if present,
|
|
50
|
+
// otherwise derive from Anthropic-standard ratios
|
|
51
|
+
const cacheReadPer1M = entry.cost_cache_read != null
|
|
52
|
+
? entry.cost_cache_read * 1000
|
|
53
|
+
: inputPer1M * 0.1;
|
|
54
|
+
const cacheCreationPer1M = entry.cost_cache_creation != null
|
|
55
|
+
? entry.cost_cache_creation * 1000
|
|
56
|
+
: inputPer1M * 1.25;
|
|
57
|
+
|
|
58
|
+
table[modelId] = {
|
|
59
|
+
input: inputPer1M,
|
|
60
|
+
output: outputPer1M,
|
|
61
|
+
cache_read: cacheReadPer1M,
|
|
62
|
+
cache_creation: cacheCreationPer1M,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return table;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensureLoaded() {
|
|
69
|
+
if (_priceTable === null) {
|
|
70
|
+
_priceTable = loadPriceTable();
|
|
71
|
+
}
|
|
72
|
+
return _priceTable;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the per-1M-token price for a model+bucket.
|
|
77
|
+
* @param {string} modelId - e.g. 'claude-sonnet-4-6'
|
|
78
|
+
* @param {'input'|'output'|'cache_read'|'cache_creation'} bucket
|
|
79
|
+
* @returns {number} USD per 1M tokens
|
|
80
|
+
*/
|
|
81
|
+
function getPrice(modelId, bucket) {
|
|
82
|
+
const table = ensureLoaded();
|
|
83
|
+
const entry = table[modelId];
|
|
84
|
+
if (!entry) {
|
|
85
|
+
process.stderr.write(`[pricing-registry] WARN: unknown model "${modelId}", using fallback rates\n`);
|
|
86
|
+
return FALLBACK_RATES[bucket] || FALLBACK_RATES.input;
|
|
87
|
+
}
|
|
88
|
+
return entry[bucket] != null ? entry[bucket] : (FALLBACK_RATES[bucket] || 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Calculate total cost for a single API call.
|
|
93
|
+
* @param {string} modelId
|
|
94
|
+
* @param {object} usage
|
|
95
|
+
* @param {number} usage.input_tokens
|
|
96
|
+
* @param {number} usage.output_tokens
|
|
97
|
+
* @param {number} [usage.cache_read_input_tokens=0]
|
|
98
|
+
* @param {number} [usage.cache_creation_input_tokens=0]
|
|
99
|
+
* @returns {number} Total USD cost
|
|
100
|
+
*/
|
|
101
|
+
function priceCall(modelId, usage) {
|
|
102
|
+
const inputTokens = usage.input_tokens || 0;
|
|
103
|
+
const outputTokens = usage.output_tokens || 0;
|
|
104
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
105
|
+
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
106
|
+
|
|
107
|
+
const inputRate = getPrice(modelId, 'input');
|
|
108
|
+
const outputRate = getPrice(modelId, 'output');
|
|
109
|
+
const cacheReadRate = getPrice(modelId, 'cache_read');
|
|
110
|
+
const cacheCreationRate = getPrice(modelId, 'cache_creation');
|
|
111
|
+
|
|
112
|
+
const cost =
|
|
113
|
+
(inputTokens / 1_000_000) * inputRate +
|
|
114
|
+
(outputTokens / 1_000_000) * outputRate +
|
|
115
|
+
(cacheReadTokens / 1_000_000) * cacheReadRate +
|
|
116
|
+
(cacheCreationTokens / 1_000_000) * cacheCreationRate;
|
|
117
|
+
|
|
118
|
+
return cost;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Clear the cached price table (for testing or config reload).
|
|
123
|
+
*/
|
|
124
|
+
function clearCache() {
|
|
125
|
+
_priceTable = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { getPrice, priceCall, clearCache };
|
package/bin/review/ads-engine.js
CHANGED
|
@@ -49,7 +49,7 @@ Include a [ADS_METRICS] block for your counter-proposal or critique logic.`,
|
|
|
49
49
|
sessionId,
|
|
50
50
|
phaseNum
|
|
51
51
|
});
|
|
52
|
-
|
|
52
|
+
let redCritique = redResponse.content;
|
|
53
53
|
process.stdout.write('done.\n');
|
|
54
54
|
|
|
55
55
|
// Red-Team Jailbreak: Force higher-fidelity critiques if Auditor is too lenient
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
const SEVERITY_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
|
|
8
8
|
|
|
9
|
+
// A severity spread of this many levels (or more) within a single location-group
|
|
10
|
+
// is treated as a contradiction (e.g. CRITICAL=3 vs LOW=0 → gap 3).
|
|
11
|
+
const CONTRADICTION_GAP_THRESHOLD = 2;
|
|
12
|
+
|
|
9
13
|
function synthesizeFindings(reviews) {
|
|
10
14
|
const allFindings = [];
|
|
11
15
|
const modelSpecific = {};
|
|
@@ -18,8 +22,9 @@ function synthesizeFindings(reviews) {
|
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
// Detect consensus
|
|
25
|
+
// Detect consensus and contradictions from the same location-groups.
|
|
22
26
|
const consensus = [];
|
|
27
|
+
const contradictions = [];
|
|
23
28
|
const processed = new Set();
|
|
24
29
|
|
|
25
30
|
for (let i = 0; i < allFindings.length; i++) {
|
|
@@ -31,7 +36,7 @@ function synthesizeFindings(reviews) {
|
|
|
31
36
|
for (let j = i + 1; j < allFindings.length; j++) {
|
|
32
37
|
if (processed.has(j)) continue;
|
|
33
38
|
const f2 = allFindings[j];
|
|
34
|
-
|
|
39
|
+
|
|
35
40
|
if (isSameFinding(f1, f2)) {
|
|
36
41
|
group.push(f2);
|
|
37
42
|
processed.add(j);
|
|
@@ -45,13 +50,12 @@ function synthesizeFindings(reviews) {
|
|
|
45
50
|
severity: getHighestSeverity(group.map(f => f.severity)),
|
|
46
51
|
models: group.map(f => f.model),
|
|
47
52
|
});
|
|
53
|
+
|
|
54
|
+
const contradiction = detectContradiction(f1.location, group);
|
|
55
|
+
if (contradiction) contradictions.push(contradiction);
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
// Detect contradictions (large severity gap on same finding)
|
|
52
|
-
const contradictions = [];
|
|
53
|
-
// (In a real implementation, we'd more deeply analyze conflicting logic)
|
|
54
|
-
|
|
55
59
|
return {
|
|
56
60
|
consensus,
|
|
57
61
|
model_specific: modelSpecific,
|
|
@@ -92,6 +96,31 @@ function normalizeLocation(loc) {
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
function severityRank(severity) {
|
|
100
|
+
const idx = SEVERITY_ORDER.indexOf(severity);
|
|
101
|
+
return idx < 0 ? 0 : idx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A location-group is contradictory when its reviews disagree on severity by
|
|
105
|
+
// CONTRADICTION_GAP_THRESHOLD levels or more (e.g. CRITICAL vs LOW). Reuses the
|
|
106
|
+
// already-computed location-group rather than re-deriving it.
|
|
107
|
+
function detectContradiction(location, group) {
|
|
108
|
+
const ranks = group.map(f => severityRank(f.severity));
|
|
109
|
+
const maxRank = Math.max(...ranks);
|
|
110
|
+
const minRank = Math.min(...ranks);
|
|
111
|
+
|
|
112
|
+
if (maxRank - minRank < CONTRADICTION_GAP_THRESHOLD) return null;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
location,
|
|
116
|
+
severities: group.map(f => f.severity),
|
|
117
|
+
models: group.map(f => f.model),
|
|
118
|
+
description: `Severity disagreement at ${location}: ` +
|
|
119
|
+
`${SEVERITY_ORDER[minRank]} vs ${SEVERITY_ORDER[maxRank]} ` +
|
|
120
|
+
`(${maxRank - minRank}-level gap across ${group.length} reviews)`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
function getHighestSeverity(severities) {
|
|
96
125
|
let highest = 0;
|
|
97
126
|
for (const s of severities) {
|