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.
Files changed (85) hide show
  1. package/.agent/hooks/mindforge-statusline.js +2 -2
  2. package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
  3. package/.mindforge/config.json +18 -4
  4. package/CHANGELOG.md +165 -0
  5. package/MINDFORGE.md +3 -3
  6. package/README.md +49 -4
  7. package/RELEASENOTES.md +81 -1
  8. package/SECURITY.md +20 -8
  9. package/bin/autonomous/audit-writer.js +105 -70
  10. package/bin/autonomous/auto-runner.js +377 -34
  11. package/bin/autonomous/context-refactorer.js +26 -11
  12. package/bin/autonomous/dependency-dag.js +59 -0
  13. package/bin/autonomous/state-manager.js +62 -6
  14. package/bin/autonomous/stuck-monitor.js +46 -7
  15. package/bin/autonomous/wave-executor.js +86 -26
  16. package/bin/council-cli.js +161 -0
  17. package/bin/dashboard/api-router.js +43 -0
  18. package/bin/dashboard/approval-handler.js +3 -1
  19. package/bin/dashboard/metrics-aggregator.js +28 -1
  20. package/bin/dashboard/server.js +68 -5
  21. package/bin/dashboard/sse-bridge.js +10 -13
  22. package/bin/engine/council-runtime.js +124 -0
  23. package/bin/engine/feedback-loop.js +8 -0
  24. package/bin/engine/intelligence-interlock.js +32 -15
  25. package/bin/engine/logic-drift-detector.js +2 -1
  26. package/bin/engine/nexus-tracer.js +3 -2
  27. package/bin/engine/otel-exporter.js +123 -0
  28. package/bin/engine/remediation-engine.js +155 -32
  29. package/bin/engine/self-corrective-synthesizer.js +84 -10
  30. package/bin/engine/sre-manager.js +12 -4
  31. package/bin/engine/temporal-cli.js +4 -2
  32. package/bin/engine/temporal-hub.js +131 -34
  33. package/bin/engine/verification-runner.js +131 -0
  34. package/bin/engine/verify-cli.js +34 -0
  35. package/bin/eval/eval-harness.js +82 -0
  36. package/bin/eval/golden-set-retrieval.json +46 -0
  37. package/bin/governance/approve.js +41 -5
  38. package/bin/governance/audit-hash.js +12 -0
  39. package/bin/governance/audit-verifier.js +60 -0
  40. package/bin/governance/impact-analyzer.js +28 -0
  41. package/bin/governance/policy-engine.js +10 -3
  42. package/bin/governance/quantum-crypto.js +95 -28
  43. package/bin/governance/rbac-manager.js +74 -2
  44. package/bin/governance/ztai-manager.js +79 -9
  45. package/bin/hindsight-injector.js +8 -9
  46. package/bin/hooks/instinct-capture-hook.js +186 -0
  47. package/bin/memory/auto-shadow.js +32 -3
  48. package/bin/memory/eis-client.js +71 -34
  49. package/bin/memory/embedding-engine.js +61 -0
  50. package/bin/memory/identity-synthesizer.js +2 -2
  51. package/bin/memory/knowledge-graph.js +58 -5
  52. package/bin/memory/knowledge-indexer.js +53 -6
  53. package/bin/memory/knowledge-store.js +52 -6
  54. package/bin/memory/retrieval-fusion.js +58 -0
  55. package/bin/memory/semantic-hub.js +2 -2
  56. package/bin/memory/vector-hub.js +111 -6
  57. package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
  58. package/bin/migrations/schema-versions.js +13 -0
  59. package/bin/mindforge-cli.js +4 -5
  60. package/bin/models/anthropic-provider.js +58 -4
  61. package/bin/models/cloud-broker.js +68 -20
  62. package/bin/models/cost-tracker.js +3 -1
  63. package/bin/models/difficulty-scorer.js +54 -0
  64. package/bin/models/gemini-provider.js +57 -2
  65. package/bin/models/model-client.js +20 -0
  66. package/bin/models/model-router.js +59 -26
  67. package/bin/models/openai-provider.js +50 -3
  68. package/bin/models/pricing-registry.js +128 -0
  69. package/bin/review/ads-engine.js +1 -1
  70. package/bin/security/trust-boundaries.js +102 -0
  71. package/bin/security/trust-gate-hook.js +39 -0
  72. package/bin/skill-registry.js +3 -2
  73. package/bin/skills-builder/marketplace-cli.js +5 -3
  74. package/bin/skills-builder/skill-registrar.js +4 -6
  75. package/bin/sre/sentinel.js +7 -5
  76. package/bin/utils/append-queue.js +55 -0
  77. package/bin/utils/file-io.js +90 -38
  78. package/bin/utils/index.js +58 -0
  79. package/bin/utils/version-check.js +59 -0
  80. package/bin/verify-audit.js +12 -0
  81. package/bin/wizard/theme.js +1 -2
  82. package/docs/getting-started.md +1 -1
  83. package/docs/user-guide.md +2 -2
  84. package/package.json +2 -2
  85. package/bin/dashboard/team-tracker.js +0 -0
@@ -7,17 +7,37 @@
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
 
10
+ // Per-provider latency ring buffer (last 10 calls)
11
+ const latencyHistory = new Map();
12
+
13
+ function recordLatency(provider, durationMs) {
14
+ if (!latencyHistory.has(provider)) {
15
+ latencyHistory.set(provider, []);
16
+ }
17
+ const history = latencyHistory.get(provider);
18
+ history.push(durationMs);
19
+ if (history.length > 10) history.shift();
20
+ }
21
+
22
+ function getP95Latency(provider) {
23
+ const history = latencyHistory.get(provider);
24
+ if (!history || history.length === 0) return 500;
25
+ const sorted = [...history].sort((a, b) => a - b);
26
+ const idx = Math.ceil(sorted.length * 0.95) - 1;
27
+ return sorted[Math.min(idx, sorted.length - 1)];
28
+ }
29
+
10
30
  class CloudBroker {
11
31
  constructor(config = {}) {
12
32
  this.providers = config.providers || ['anthropic', 'google', 'aws', 'azure'];
13
33
  this.statsPath = config.statsPath || path.join(__dirname, 'performance-stats.json');
14
- this.blacklist = new Map(); // provider -> expiry (Date)
15
- this.failureWindow = new Map(); // provider:taskType -> count
34
+ this.blacklist = new Map();
35
+ this.failureWindow = new Map();
16
36
  this.state = {
17
- 'anthropic': { latency: 450, costMultiplier: 1.0, healthy: true },
18
- 'google': { latency: 600, costMultiplier: 0.85, healthy: true },
19
- 'aws': { latency: 550, costMultiplier: 0.95, healthy: true },
20
- 'azure': { latency: 650, costMultiplier: 1.1, healthy: true }
37
+ 'anthropic': { costMultiplier: 1.0, healthy: true },
38
+ 'google': { costMultiplier: 0.85, healthy: true },
39
+ 'aws': { costMultiplier: 0.95, healthy: true },
40
+ 'azure': { costMultiplier: 1.1, healthy: true }
21
41
  };
22
42
  this.reloadStats();
23
43
  }
@@ -71,21 +91,20 @@ class CloudBroker {
71
91
  return true;
72
92
  })
73
93
  .map(([id, data]) => {
74
- // Calculate Success Probability for this task
75
94
  const stats = this.performanceStats[id]?.[taskType] || { success: 1, failure: 0 };
76
95
  const totalTasks = stats.success + stats.failure;
77
96
  const successProb = totalTasks > 0 ? (stats.success / totalTasks) : 0.5;
78
97
 
79
- // Score Calculation (The "Affinity" Algorithm)
80
98
  const latencyWeight = 0.2;
81
99
  const costWeight = 0.3;
82
- const affinityWeight = 0.5;
100
+ const affinityWeight = 0.5;
83
101
 
84
- const score = (data.latency * latencyWeight) +
85
- (data.costMultiplier * 1000 * costWeight) +
102
+ const providerLatency = getP95Latency(id);
103
+ const score = (providerLatency * latencyWeight) +
104
+ (data.costMultiplier * 1000 * costWeight) +
86
105
  ((1.0 - successProb) * 2000 * affinityWeight);
87
106
 
88
- return { id, score, successProb: successProb.toFixed(2) };
107
+ return { id, score, successProb: successProb.toFixed(2), p95: providerLatency };
89
108
  });
90
109
 
91
110
  scored.sort((a, b) => a.score - b.score);
@@ -110,7 +129,7 @@ class CloudBroker {
110
129
 
111
130
  const fallback = Object.entries(this.state)
112
131
  .filter(([id, data]) => id !== failedProvider && data.healthy)
113
- .sort((a, b) => a[1].latency - b[1].latency)[0];
132
+ .sort((a, b) => getP95Latency(a[0]) - getP95Latency(b[0]))[0];
114
133
 
115
134
  return fallback ? fallback[0] : 'google'; // Default fallback
116
135
  }
@@ -130,33 +149,62 @@ class CloudBroker {
130
149
  return mappings[provider]?.[modelGroup] || mappings[provider]?.['sonnet'];
131
150
  }
132
151
 
152
+ /**
153
+ * Removes failure entries whose sliding window (5 min) has expired.
154
+ */
155
+ _pruneStaleFailures() {
156
+ const now = Date.now();
157
+ const WINDOW_MS = 5 * 60 * 1000;
158
+
159
+ for (const [key, entry] of this.failureWindow.entries()) {
160
+ if (now - entry.firstFailureAt > WINDOW_MS) {
161
+ this.failureWindow.delete(key);
162
+ }
163
+ }
164
+ }
165
+
133
166
  /**
134
167
  * Records a task failure and manages the circuit breaker.
135
168
  */
136
169
  recordFailure(provider, taskType = 'default') {
137
- const key = `${provider}:${taskType}`;
138
- const failures = (this.failureWindow.get(key) || 0) + 1;
139
- this.failureWindow.set(key, failures);
170
+ this._pruneStaleFailures();
140
171
 
141
- if (failures >= 3) {
142
- const expiry = new Date(Date.now() + 10 * 60 * 1000); // 10 min blacklist
172
+ const key = `${provider}:${taskType}`;
173
+ const existing = this.failureWindow.get(key);
174
+ const entry = existing
175
+ ? { count: existing.count + 1, firstFailureAt: existing.firstFailureAt }
176
+ : { count: 1, firstFailureAt: Date.now() };
177
+ this.failureWindow.set(key, entry);
178
+
179
+ if (entry.count >= 3) {
180
+ const expiry = new Date(Date.now() + 10 * 60 * 1000);
143
181
  this.blacklist.set(provider, expiry);
144
182
  console.warn(`[MCA-CIRCUIT-OPEN] Provider '${provider}' blacklisted for 10 min due to consecutive failures on '${taskType}'.`);
145
- this.failureWindow.set(key, 0); // Reset window upon blacklisting
183
+ this.failureWindow.delete(key);
146
184
  }
147
185
  }
148
186
 
149
187
  /**
150
188
  * Hardening: Simulate provider failures to verify Fallback Protocol.
151
189
  */
190
+ recordLatency(provider, durationMs) {
191
+ recordLatency(provider, durationMs);
192
+ }
193
+
194
+ getP95Latency(provider) {
195
+ return getP95Latency(provider);
196
+ }
197
+
152
198
  startChaosMode() {
153
199
  console.log('[ENTERPRISE-RESILIENCE] CloudBroker Chaos Mode ACTIVE. Simulating jitter and provider dropouts...');
154
200
  setInterval(() => {
155
201
  const providers = Object.keys(this.state);
156
202
  const randomProvider = providers[Math.floor(Math.random() * providers.length)];
157
- this.state[randomProvider].latency = Math.random() > 0.7 ? 5000 : 100;
203
+ recordLatency(randomProvider, Math.random() > 0.7 ? 5000 : 100);
158
204
  }, 10000);
159
205
  }
160
206
  }
161
207
 
162
208
  module.exports = CloudBroker;
209
+ module.exports.recordLatency = recordLatency;
210
+ module.exports.getP95Latency = getP95Latency;
@@ -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,6 +78,57 @@ class GeminiProvider {
74
78
  req.end();
75
79
  });
76
80
  }
81
+
82
+ async streamComplete(messages, options = {}) {
83
+ const model = options.model || 'gemini-2.5-pro';
84
+ const maxTokens = options.maxTokens || 8192;
85
+
86
+ const contents = messages.map(msg => ({
87
+ role: msg.role === 'assistant' ? 'model' : 'user',
88
+ parts: [{ text: msg.content }],
89
+ }));
90
+
91
+ const data = JSON.stringify({
92
+ contents,
93
+ generationConfig: {
94
+ maxOutputTokens: maxTokens,
95
+ },
96
+ });
97
+
98
+ const modelId = model.startsWith('models/') ? model : `models/${model}`;
99
+
100
+ return new Promise((resolve, reject) => {
101
+ const req = https.request({
102
+ hostname: 'generativelanguage.googleapis.com',
103
+ path: `/v1beta/${modelId}:streamGenerateContent?alt=sse`,
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ 'x-goog-api-key': this.apiKey,
108
+ 'Content-Length': Buffer.byteLength(data),
109
+ },
110
+ timeout: 300_000,
111
+ }, res => {
112
+ if (res.statusCode !== 200) {
113
+ let body = '';
114
+ res.on('data', chunk => body += chunk);
115
+ res.on('end', () => {
116
+ reject(new Error(`Gemini streaming failed: ${res.statusCode}`));
117
+ });
118
+ return;
119
+ }
120
+ resolve(res);
121
+ });
122
+
123
+ req.on('error', reject);
124
+ req.on('timeout', () => {
125
+ req.destroy();
126
+ reject(new Error('Gemini stream timeout'));
127
+ });
128
+ req.write(data);
129
+ req.end();
130
+ });
131
+ }
77
132
  }
78
133
 
79
134
  module.exports = GeminiProvider;
@@ -82,6 +82,26 @@ class ModelClient {
82
82
  }
83
83
  }
84
84
 
85
+ static async streamComplete(params) {
86
+ const {
87
+ persona = 'developer',
88
+ tier = 1,
89
+ messages,
90
+ maxTokens,
91
+ taskName = 'unknown',
92
+ } = params;
93
+
94
+ const routing = Router.route(persona, tier);
95
+ const modelId = routing.model;
96
+ const provider = this._getProvider(modelId);
97
+
98
+ if (!provider || !provider.streamComplete) {
99
+ throw new Error(`Streaming not supported for model: ${modelId}`);
100
+ }
101
+
102
+ return provider.streamComplete(messages, { ...params, model: modelId });
103
+ }
104
+
85
105
  static _getProvider(modelId) {
86
106
  if (modelId.startsWith('claude') || modelId.startsWith('anthropic.claude')) {
87
107
  if (!process.env.ANTHROPIC_API_KEY) return null;
@@ -36,15 +36,13 @@ const PERSONA_MAP = {
36
36
  };
37
37
 
38
38
  let _settingsCache = null;
39
+ let _settingsMtime = 0;
40
+ const CACHE_CHECK_INTERVAL_MS = 60000;
41
+ let _lastCacheCheck = 0;
39
42
 
40
- function readMindforgeSettings() {
41
- if (_settingsCache) return _settingsCache;
42
- const configPath = path.join(process.cwd(), 'MINDFORGE.md');
43
- if (!fs.existsSync(configPath)) return DEFAULTS;
44
-
45
- const content = fs.readFileSync(configPath, 'utf8');
43
+ function parseSettings(filePath) {
44
+ const content = fs.readFileSync(filePath, 'utf8');
46
45
  const settings = { ...DEFAULTS };
47
-
48
46
  const lines = content.split('\n');
49
47
  for (const line of lines) {
50
48
  const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
@@ -52,50 +50,83 @@ function readMindforgeSettings() {
52
50
  settings[match[1]] = match[2].trim();
53
51
  }
54
52
  }
55
- _settingsCache = settings;
56
53
  return settings;
57
54
  }
58
55
 
59
- function route(persona = 'developer', tier = 1) {
56
+ function readMindforgeSettings() {
57
+ const now = Date.now();
58
+ if (now - _lastCacheCheck < CACHE_CHECK_INTERVAL_MS && _settingsCache) {
59
+ return _settingsCache;
60
+ }
61
+ _lastCacheCheck = now;
62
+
63
+ const configPath = path.join(process.cwd(), 'MINDFORGE.md');
64
+ try {
65
+ const stat = fs.statSync(configPath);
66
+ if (stat.mtimeMs !== _settingsMtime) {
67
+ _settingsMtime = stat.mtimeMs;
68
+ _settingsCache = parseSettings(configPath);
69
+ }
70
+ } catch {
71
+ if (!_settingsCache) _settingsCache = { ...DEFAULTS };
72
+ }
73
+
74
+ return _settingsCache;
75
+ }
76
+
77
+ function route(persona = 'developer', tier = 1, taskContext) {
60
78
  const settings = readMindforgeSettings();
61
-
79
+ let result;
80
+
62
81
  // 1. Tier 3 override (Security/Privacy always uses SECURITY_MODEL)
63
82
  if (tier === 3) {
64
- return {
83
+ result = {
65
84
  model: settings.SECURITY_MODEL,
66
85
  setting: 'SECURITY_MODEL',
67
86
  reason: 'Tier 3 (Security/Privacy) override'
68
87
  };
69
88
  }
70
-
71
89
  // 2. Persona mapping (Specific personas like research, debug, qa)
72
- if (persona !== 'developer' && PERSONA_MAP[persona]) {
90
+ else if (persona !== 'developer' && PERSONA_MAP[persona]) {
73
91
  const settingKey = PERSONA_MAP[persona];
74
- return {
92
+ result = {
75
93
  model: settings[settingKey],
76
94
  setting: settingKey,
77
95
  reason: `Mapped from specific persona "${persona}"`
78
96
  };
79
97
  }
80
-
81
98
  // 3. Budget Bias (Tier 1 uses QUICK_MODEL for default developer tasks)
82
- if (tier === 1) {
83
- return {
99
+ else if (tier === 1) {
100
+ result = {
84
101
  model: settings.QUICK_MODEL,
85
102
  setting: 'QUICK_MODEL',
86
103
  reason: 'Tier 1 Budget Bias (efficiency mode)'
87
104
  };
88
105
  }
89
-
90
106
  // 4. Default mapping
91
- const settingKey = 'EXECUTOR_MODEL';
92
- const model = settings[settingKey];
93
-
94
- return {
95
- model,
96
- setting: settingKey,
97
- reason: `Default EXECUTOR_MODEL for tier ${tier}`
98
- };
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;
99
130
  }
100
131
 
101
132
  function getModel(settingKey) {
@@ -105,6 +136,8 @@ function getModel(settingKey) {
105
136
 
106
137
  function clearCache() {
107
138
  _settingsCache = null;
139
+ _settingsMtime = 0;
140
+ _lastCacheCheck = 0;
108
141
  }
109
142
 
110
143
  function getAllSettings() {
@@ -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,
@@ -73,6 +76,50 @@ class OpenAIProvider {
73
76
  req.end();
74
77
  });
75
78
  }
79
+
80
+ async streamComplete(messages, options = {}) {
81
+ const model = options.model || 'gpt-4o';
82
+ const maxTokens = options.maxTokens || 4096;
83
+
84
+ const data = JSON.stringify({
85
+ model,
86
+ messages,
87
+ max_tokens: maxTokens,
88
+ stream: true,
89
+ });
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const req = https.request({
93
+ hostname: 'api.openai.com',
94
+ path: '/v1/chat/completions',
95
+ method: 'POST',
96
+ headers: {
97
+ 'Content-Type': 'application/json',
98
+ 'Authorization': `Bearer ${this.apiKey}`,
99
+ 'Content-Length': Buffer.byteLength(data),
100
+ },
101
+ timeout: 300_000,
102
+ }, res => {
103
+ if (res.statusCode !== 200) {
104
+ let body = '';
105
+ res.on('data', chunk => body += chunk);
106
+ res.on('end', () => {
107
+ reject(new Error(`OpenAI streaming failed: ${res.statusCode}`));
108
+ });
109
+ return;
110
+ }
111
+ resolve(res);
112
+ });
113
+
114
+ req.on('error', reject);
115
+ req.on('timeout', () => {
116
+ req.destroy();
117
+ reject(new Error('OpenAI stream timeout'));
118
+ });
119
+ req.write(data);
120
+ req.end();
121
+ });
122
+ }
76
123
  }
77
124
 
78
125
  module.exports = OpenAIProvider;
@@ -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