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.
Files changed (56) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/config.json +13 -4
  3. package/CHANGELOG.md +101 -0
  4. package/MINDFORGE.md +3 -3
  5. package/RELEASENOTES.md +1 -1
  6. package/bin/autonomous/audit-writer.js +108 -86
  7. package/bin/autonomous/auto-runner.js +304 -19
  8. package/bin/autonomous/dependency-dag.js +59 -0
  9. package/bin/autonomous/wave-executor.js +20 -1
  10. package/bin/council-cli.js +161 -0
  11. package/bin/dashboard/approval-handler.js +3 -1
  12. package/bin/dashboard/server.js +1 -1
  13. package/bin/dashboard/sse-bridge.js +9 -12
  14. package/bin/engine/council-runtime.js +124 -0
  15. package/bin/engine/otel-exporter.js +123 -0
  16. package/bin/engine/remediation-engine.js +1 -1
  17. package/bin/engine/self-corrective-synthesizer.js +1 -1
  18. package/bin/engine/temporal-cli.js +4 -2
  19. package/bin/engine/verification-runner.js +131 -0
  20. package/bin/engine/verify-cli.js +34 -0
  21. package/bin/eval/eval-harness.js +82 -0
  22. package/bin/eval/golden-set-retrieval.json +46 -0
  23. package/bin/governance/audit-hash.js +12 -0
  24. package/bin/governance/audit-verifier.js +60 -0
  25. package/bin/governance/quantum-crypto.js +63 -9
  26. package/bin/governance/ztai-manager.js +30 -2
  27. package/bin/hindsight-injector.js +5 -6
  28. package/bin/hooks/instinct-capture-hook.js +186 -0
  29. package/bin/memory/auto-shadow.js +32 -3
  30. package/bin/memory/identity-synthesizer.js +2 -2
  31. package/bin/memory/knowledge-store.js +30 -6
  32. package/bin/memory/retrieval-fusion.js +58 -0
  33. package/bin/memory/semantic-hub.js +2 -2
  34. package/bin/memory/vector-hub.js +111 -6
  35. package/bin/mindforge-cli.js +4 -5
  36. package/bin/models/anthropic-provider.js +13 -4
  37. package/bin/models/cost-tracker.js +3 -1
  38. package/bin/models/difficulty-scorer.js +54 -0
  39. package/bin/models/gemini-provider.js +6 -2
  40. package/bin/models/model-router.js +31 -18
  41. package/bin/models/openai-provider.js +6 -3
  42. package/bin/models/pricing-registry.js +128 -0
  43. package/bin/review/ads-engine.js +1 -1
  44. package/bin/security/trust-boundaries.js +102 -0
  45. package/bin/security/trust-gate-hook.js +39 -0
  46. package/bin/skill-registry.js +3 -2
  47. package/bin/skills-builder/marketplace-cli.js +5 -3
  48. package/bin/skills-builder/skill-registrar.js +4 -6
  49. package/bin/sre/sentinel.js +7 -5
  50. package/bin/utils/append-queue.js +55 -0
  51. package/bin/utils/file-io.js +27 -37
  52. package/bin/utils/version-check.js +59 -0
  53. package/bin/verify-audit.js +12 -0
  54. package/bin/wizard/theme.js +1 -2
  55. package/package.json +1 -1
  56. package/bin/dashboard/team-tracker.js +0 -0
@@ -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 data = this._db.export();
182
- const buffer = Buffer.from(data);
183
- fs.writeFileSync(this.dbPath, buffer);
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
  /**
@@ -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
- // Basic cost calculation (Sonnet 3.5 prices)
50
- const cost = (inputTokens * 0.000003) + (outputTokens * 0.000015);
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
- const cost = (inputTokens * 0.00000125) + (outputTokens * 0.00000375);
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
- return {
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
- return {
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
- return {
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
- const settingKey = 'EXECUTOR_MODEL';
110
- const model = settings[settingKey];
111
-
112
- return {
113
- model,
114
- setting: settingKey,
115
- reason: `Default EXECUTOR_MODEL for tier ${tier}`
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
- // Basic cost calculation (GPT-4o prices)
51
- const cost = (inputTokens * 0.000005) + (outputTokens * 0.000015);
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 };
@@ -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
- const redCritique = redResponse.content;
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
+ });