mindforge-cc 10.7.0 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/hooks/mindforge-statusline.js +2 -2
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +43 -10
- package/.mindforge/config.json +18 -4
- package/CHANGELOG.md +165 -0
- package/MINDFORGE.md +3 -3
- package/README.md +49 -4
- package/RELEASENOTES.md +81 -1
- package/SECURITY.md +20 -8
- package/bin/autonomous/audit-writer.js +105 -70
- package/bin/autonomous/auto-runner.js +377 -34
- package/bin/autonomous/context-refactorer.js +26 -11
- package/bin/autonomous/dependency-dag.js +59 -0
- package/bin/autonomous/state-manager.js +62 -6
- package/bin/autonomous/stuck-monitor.js +46 -7
- package/bin/autonomous/wave-executor.js +86 -26
- package/bin/council-cli.js +161 -0
- package/bin/dashboard/api-router.js +43 -0
- package/bin/dashboard/approval-handler.js +3 -1
- package/bin/dashboard/metrics-aggregator.js +28 -1
- package/bin/dashboard/server.js +68 -5
- package/bin/dashboard/sse-bridge.js +10 -13
- package/bin/engine/council-runtime.js +124 -0
- package/bin/engine/feedback-loop.js +8 -0
- package/bin/engine/intelligence-interlock.js +32 -15
- package/bin/engine/logic-drift-detector.js +2 -1
- package/bin/engine/nexus-tracer.js +3 -2
- package/bin/engine/otel-exporter.js +123 -0
- package/bin/engine/remediation-engine.js +155 -32
- package/bin/engine/self-corrective-synthesizer.js +84 -10
- package/bin/engine/sre-manager.js +12 -4
- package/bin/engine/temporal-cli.js +4 -2
- package/bin/engine/temporal-hub.js +131 -34
- package/bin/engine/verification-runner.js +131 -0
- package/bin/engine/verify-cli.js +34 -0
- package/bin/eval/eval-harness.js +82 -0
- package/bin/eval/golden-set-retrieval.json +46 -0
- package/bin/governance/approve.js +41 -5
- package/bin/governance/audit-hash.js +12 -0
- package/bin/governance/audit-verifier.js +60 -0
- package/bin/governance/impact-analyzer.js +28 -0
- package/bin/governance/policy-engine.js +10 -3
- package/bin/governance/quantum-crypto.js +95 -28
- package/bin/governance/rbac-manager.js +74 -2
- package/bin/governance/ztai-manager.js +79 -9
- package/bin/hindsight-injector.js +8 -9
- package/bin/hooks/instinct-capture-hook.js +186 -0
- package/bin/memory/auto-shadow.js +32 -3
- package/bin/memory/eis-client.js +71 -34
- package/bin/memory/embedding-engine.js +61 -0
- package/bin/memory/identity-synthesizer.js +2 -2
- package/bin/memory/knowledge-graph.js +58 -5
- package/bin/memory/knowledge-indexer.js +53 -6
- package/bin/memory/knowledge-store.js +52 -6
- package/bin/memory/retrieval-fusion.js +58 -0
- package/bin/memory/semantic-hub.js +2 -2
- package/bin/memory/vector-hub.js +111 -6
- package/bin/migrations/10.7.0-to-11.0.0.js +110 -0
- package/bin/migrations/schema-versions.js +13 -0
- package/bin/mindforge-cli.js +4 -5
- package/bin/models/anthropic-provider.js +58 -4
- package/bin/models/cloud-broker.js +68 -20
- package/bin/models/cost-tracker.js +3 -1
- package/bin/models/difficulty-scorer.js +54 -0
- package/bin/models/gemini-provider.js +57 -2
- package/bin/models/model-client.js +20 -0
- package/bin/models/model-router.js +59 -26
- package/bin/models/openai-provider.js +50 -3
- package/bin/models/pricing-registry.js +128 -0
- package/bin/review/ads-engine.js +1 -1
- package/bin/security/trust-boundaries.js +102 -0
- package/bin/security/trust-gate-hook.js +39 -0
- package/bin/skill-registry.js +3 -2
- package/bin/skills-builder/marketplace-cli.js +5 -3
- package/bin/skills-builder/skill-registrar.js +4 -6
- package/bin/sre/sentinel.js +7 -5
- package/bin/utils/append-queue.js +55 -0
- package/bin/utils/file-io.js +90 -38
- package/bin/utils/index.js +58 -0
- package/bin/utils/version-check.js +59 -0
- package/bin/verify-audit.js +12 -0
- package/bin/wizard/theme.js +1 -2
- package/docs/getting-started.md +1 -1
- package/docs/user-guide.md +2 -2
- package/package.json +2 -2
- package/bin/dashboard/team-tracker.js +0 -0
|
@@ -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();
|
|
15
|
-
this.failureWindow = new Map();
|
|
34
|
+
this.blacklist = new Map();
|
|
35
|
+
this.failureWindow = new Map();
|
|
16
36
|
this.state = {
|
|
17
|
-
'anthropic': {
|
|
18
|
-
'google': {
|
|
19
|
-
'aws': {
|
|
20
|
-
'azure': {
|
|
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
|
|
85
|
-
|
|
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[
|
|
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
|
-
|
|
138
|
-
const failures = (this.failureWindow.get(key) || 0) + 1;
|
|
139
|
-
this.failureWindow.set(key, failures);
|
|
170
|
+
this._pruneStaleFailures();
|
|
140
171
|
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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 };
|
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
|