mindforge-cc 11.0.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/config.json +13 -4
- package/CHANGELOG.md +101 -0
- package/MINDFORGE.md +3 -3
- 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/wave-executor.js +20 -1
- 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/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +1 -1
- package/bin/engine/self-corrective-synthesizer.js +1 -1
- 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/quantum-crypto.js +63 -9
- package/bin/governance/ztai-manager.js +30 -2
- package/bin/hindsight-injector.js +5 -6
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- 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 +111 -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/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 +27 -37
- package/bin/utils/version-check.js +59 -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() {
|
|
@@ -167,22 +191,74 @@ class VectorHub {
|
|
|
167
191
|
this._db.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_migrations_name ON _migrations(name)');
|
|
168
192
|
|
|
169
193
|
this.initialized = true;
|
|
194
|
+
this._installExitGuard();
|
|
170
195
|
this.save();
|
|
171
196
|
console.log(`[VectorHub] Initialized WASM SQLite persistence at ${this.dbPath}`);
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
/**
|
|
175
|
-
* Persist the in-memory database to disk.
|
|
200
|
+
* Persist the in-memory database to disk (UC-09).
|
|
201
|
+
*
|
|
202
|
+
* sql.js export() is intrinsically synchronous, but the (potentially large)
|
|
203
|
+
* FILE WRITE no longer blocks the event loop: we snapshot the bytes
|
|
204
|
+
* synchronously, then write+fsync them asynchronously. Successive saves are
|
|
205
|
+
* serialized on a single chain so two exports never write the .db file
|
|
206
|
+
* concurrently. The write is crash-safe (tmp file + atomic rename + fsync),
|
|
207
|
+
* so a partial write can never leave a corrupted database on disk.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Promise<void>} Resolves once the snapshot is durably on disk.
|
|
176
210
|
*/
|
|
177
211
|
save() {
|
|
212
|
+
if (!this._db) return Promise.resolve();
|
|
213
|
+
|
|
214
|
+
let buffer;
|
|
215
|
+
try {
|
|
216
|
+
this._ensureDir();
|
|
217
|
+
// Snapshot the DB synchronously so the bytes reflect this exact moment.
|
|
218
|
+
buffer = Buffer.from(this._db.export());
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.warn(`[VectorHub] Failed to export database: ${err.message}`);
|
|
221
|
+
return Promise.resolve();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const dbPath = this.dbPath;
|
|
225
|
+
// Increment when SCHEDULED; decrement only once this specific save has
|
|
226
|
+
// COMPLETED (success or failure). The exit guard fires saveSync() while any
|
|
227
|
+
// scheduled save is still outstanding — see _installExitGuard().
|
|
228
|
+
this._pendingSaves++;
|
|
229
|
+
this._saveChain = this._saveChain.then(() => writeDbDurable(dbPath, buffer))
|
|
230
|
+
.catch((err) => {
|
|
231
|
+
console.warn(`[VectorHub] Failed to save database: ${err.message}`);
|
|
232
|
+
})
|
|
233
|
+
.then(() => { this._pendingSaves--; });
|
|
234
|
+
return this._saveChain;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Synchronous, crash-safe persistence — used only on shutdown to GUARANTEE
|
|
239
|
+
* no acknowledged write is lost if the process exits before the async save
|
|
240
|
+
* chain drains. Correctness over non-blocking here.
|
|
241
|
+
*/
|
|
242
|
+
saveSync() {
|
|
178
243
|
if (!this._db) return;
|
|
179
244
|
try {
|
|
180
245
|
this._ensureDir();
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
fs.
|
|
246
|
+
const buffer = Buffer.from(this._db.export());
|
|
247
|
+
const tmpPath = `${this.dbPath}.tmp.${process.pid}`;
|
|
248
|
+
const fd = fs.openSync(tmpPath, 'w');
|
|
249
|
+
try {
|
|
250
|
+
fs.writeSync(fd, buffer);
|
|
251
|
+
fs.fsyncSync(fd);
|
|
252
|
+
} finally {
|
|
253
|
+
fs.closeSync(fd);
|
|
254
|
+
}
|
|
255
|
+
fs.renameSync(tmpPath, this.dbPath);
|
|
256
|
+
// A sync export captures the full in-memory DB — a superset of anything the
|
|
257
|
+
// outstanding async saves would have written — so the pending work is now
|
|
258
|
+
// durably satisfied. Clearing the counter prevents a redundant second flush.
|
|
259
|
+
this._pendingSaves = 0;
|
|
184
260
|
} catch (err) {
|
|
185
|
-
console.warn(`[VectorHub] Failed to save database: ${err.message}`);
|
|
261
|
+
console.warn(`[VectorHub] Failed to save database (sync): ${err.message}`);
|
|
186
262
|
}
|
|
187
263
|
}
|
|
188
264
|
|
|
@@ -199,10 +275,13 @@ class VectorHub {
|
|
|
199
275
|
|
|
200
276
|
/**
|
|
201
277
|
* Close the database and save final state to disk.
|
|
278
|
+
* Drains any pending async saves, then performs a guaranteed synchronous
|
|
279
|
+
* durable write so no acknowledged data is lost on shutdown (UC-09).
|
|
202
280
|
*/
|
|
203
281
|
async close() {
|
|
204
282
|
if (this._db) {
|
|
205
|
-
this.save()
|
|
283
|
+
try { await this._saveChain; } catch { /* logged in save() */ }
|
|
284
|
+
this.saveSync();
|
|
206
285
|
this._db.close();
|
|
207
286
|
this._db = null;
|
|
208
287
|
this.initialized = false;
|
|
@@ -455,6 +534,32 @@ class VectorHub {
|
|
|
455
534
|
}
|
|
456
535
|
}
|
|
457
536
|
|
|
537
|
+
// ── Durable async DB file write (UC-09) ───────────────────────────────────────
|
|
538
|
+
// Crash-safe: write to a tmp file, fsync, then atomically rename over the target.
|
|
539
|
+
// A crash mid-write leaves the previous good .db intact (rename is atomic on POSIX).
|
|
540
|
+
function writeDbDurable(dbPath, buffer) {
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
const tmpPath = `${dbPath}.tmp.${process.pid}`;
|
|
543
|
+
const fail = (err) => { fs.unlink(tmpPath, () => reject(err)); };
|
|
544
|
+
fs.open(tmpPath, 'w', (openErr, fd) => {
|
|
545
|
+
if (openErr) return reject(openErr);
|
|
546
|
+
fs.write(fd, buffer, 0, buffer.length, 0, (writeErr) => {
|
|
547
|
+
if (writeErr) { fs.close(fd, () => fail(writeErr)); return; }
|
|
548
|
+
fs.fsync(fd, (syncErr) => {
|
|
549
|
+
fs.close(fd, (closeErr) => {
|
|
550
|
+
if (syncErr) return fail(syncErr);
|
|
551
|
+
if (closeErr) return fail(closeErr);
|
|
552
|
+
fs.rename(tmpPath, dbPath, (renameErr) => {
|
|
553
|
+
if (renameErr) return fail(renameErr);
|
|
554
|
+
resolve();
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
458
563
|
// ── Factory Function ──────────────────────────────────────────────────────────
|
|
459
564
|
|
|
460
565
|
/**
|
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
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively sorts object keys for deterministic JSON serialization.
|
|
7
|
+
* Arrays are preserved in order; nested objects get sorted keys.
|
|
8
|
+
*/
|
|
9
|
+
function stableStringify(value) {
|
|
10
|
+
if (value === null || typeof value !== 'object') {
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return '[' + value.map(item => stableStringify(item)).join(',') + ']';
|
|
15
|
+
}
|
|
16
|
+
const sortedKeys = Object.keys(value).sort();
|
|
17
|
+
const pairs = sortedKeys.map(key => {
|
|
18
|
+
return JSON.stringify(key) + ':' + stableStringify(value[key]);
|
|
19
|
+
});
|
|
20
|
+
return '{' + pairs.join(',') + '}';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Computes SHA-256 hash of a manifest using stable key-sorted serialization.
|
|
25
|
+
* Returns { name, hash, pinnedAt }.
|
|
26
|
+
*/
|
|
27
|
+
function pinManifest(manifest) {
|
|
28
|
+
const serialized = stableStringify(manifest);
|
|
29
|
+
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
30
|
+
return {
|
|
31
|
+
name: manifest.name,
|
|
32
|
+
hash,
|
|
33
|
+
pinnedAt: Date.now()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Verifies a manifest against a previously pinned hash.
|
|
39
|
+
* Returns { valid: true } or { valid: false, reason }.
|
|
40
|
+
*/
|
|
41
|
+
function verifyManifest(manifest, pin) {
|
|
42
|
+
const serialized = stableStringify(manifest);
|
|
43
|
+
const computed = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
44
|
+
if (computed === pin.hash) {
|
|
45
|
+
return { valid: true };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
reason: `hash mismatch: expected ${pin.hash}, got ${computed}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wraps content with untrusted provenance metadata.
|
|
55
|
+
* Returns { content, trusted: false, provenance: { source, tool, timestamp } }.
|
|
56
|
+
*/
|
|
57
|
+
function tagUntrusted(content, meta) {
|
|
58
|
+
return {
|
|
59
|
+
content,
|
|
60
|
+
trusted: false,
|
|
61
|
+
provenance: {
|
|
62
|
+
source: meta.source,
|
|
63
|
+
tool: meta.tool,
|
|
64
|
+
timestamp: Date.now()
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Null byte (char code 0). Built via fromCharCode so we never embed a control
|
|
70
|
+
// character in a regex literal (eslint no-control-regex).
|
|
71
|
+
const NUL = String.fromCharCode(0);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Detects high-impact / destructive commands via case-insensitive pattern matching.
|
|
75
|
+
* Returns true if the command matches known destructive patterns.
|
|
76
|
+
*/
|
|
77
|
+
function isHighImpact(command) {
|
|
78
|
+
// Strip null bytes first — shells ignore them, so an attacker must not be
|
|
79
|
+
// able to use a NUL to split a destructive token and slip past the patterns.
|
|
80
|
+
const sanitized = String(command).split(NUL).join('');
|
|
81
|
+
const patterns = [
|
|
82
|
+
/rm\s+(-\w*r\w*\s+-\w*f|(-\w*f\w*\s+-\w*r)|-\w*rf|-\w*fr)/i,
|
|
83
|
+
/git\s+push\s+.*--force/i,
|
|
84
|
+
/git\s+push\s+.*-f/i,
|
|
85
|
+
/drop\s+(table|database)/i,
|
|
86
|
+
/git\s+reset\s+--hard/i,
|
|
87
|
+
/delete\s+from/i,
|
|
88
|
+
/truncate\s+table/i,
|
|
89
|
+
/\bmkfs(\.\w+)?\s+\/dev\//i,
|
|
90
|
+
/\bdd\b.*\bof=\/dev\//i,
|
|
91
|
+
/\b(curl|wget)\b.*\|\s*(bash|sh|zsh)\b/i,
|
|
92
|
+
/^\s*find\s+.*-delete\b/i,
|
|
93
|
+
];
|
|
94
|
+
return patterns.some(pattern => pattern.test(sanitized));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
pinManifest,
|
|
99
|
+
verifyManifest,
|
|
100
|
+
tagUntrusted,
|
|
101
|
+
isHighImpact
|
|
102
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { isHighImpact } = require('./trust-boundaries');
|
|
5
|
+
|
|
6
|
+
let input = '';
|
|
7
|
+
process.stdin.setEncoding('utf8');
|
|
8
|
+
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
9
|
+
process.stdin.on('end', () => {
|
|
10
|
+
try {
|
|
11
|
+
const event = JSON.parse(input);
|
|
12
|
+
|
|
13
|
+
// Only gate Bash tool calls
|
|
14
|
+
if (event.tool_name !== 'Bash') {
|
|
15
|
+
process.exit(0); // allow
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fullCommand = event.tool_input?.command || '';
|
|
19
|
+
const command = fullCommand.split('\n')[0];
|
|
20
|
+
|
|
21
|
+
if (isHighImpact(command)) {
|
|
22
|
+
// Output a block reason (Claude Code shows this to the user)
|
|
23
|
+
process.stdout.write(JSON.stringify({
|
|
24
|
+
decision: 'block',
|
|
25
|
+
reason: `[TrustGate] High-impact command detected: "${command.substring(0, 80)}..." — requires explicit user approval`
|
|
26
|
+
}));
|
|
27
|
+
process.exit(2); // block
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(0); // allow
|
|
31
|
+
} catch (e) {
|
|
32
|
+
process.stderr.write('[trust-gate-hook] parse error (BLOCKING): ' + e.message + '\n');
|
|
33
|
+
process.stdout.write(JSON.stringify({
|
|
34
|
+
decision: 'block',
|
|
35
|
+
reason: '[TrustGate] Could not verify command safety — parse error'
|
|
36
|
+
}));
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
});
|