dual-brain 7.1.25 → 7.1.26
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/package.json +7 -2
- package/src/awareness.mjs +343 -0
- package/src/models.mjs +363 -0
- package/src/prompt-intel.mjs +325 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.26",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"./decompose": "./src/decompose.mjs",
|
|
21
21
|
"./brief": "./src/brief.mjs",
|
|
22
22
|
"./redact": "./src/redact.mjs",
|
|
23
|
-
"./calibration": "./src/calibration.mjs"
|
|
23
|
+
"./calibration": "./src/calibration.mjs",
|
|
24
|
+
"./models": "./src/models.mjs",
|
|
25
|
+
"./prompt-intel": "./src/prompt-intel.mjs"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
26
28
|
"claude-code",
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
"src/brief.mjs",
|
|
61
63
|
"src/redact.mjs",
|
|
62
64
|
"src/calibration.mjs",
|
|
65
|
+
"src/models.mjs",
|
|
63
66
|
"src/pipeline.mjs",
|
|
64
67
|
"src/context.mjs",
|
|
65
68
|
"src/outcome.mjs",
|
|
@@ -71,10 +74,12 @@
|
|
|
71
74
|
"src/index.mjs",
|
|
72
75
|
"src/ledger.mjs",
|
|
73
76
|
"src/intelligence.mjs",
|
|
77
|
+
"src/awareness.mjs",
|
|
74
78
|
"src/tui.mjs",
|
|
75
79
|
"src/living-docs.mjs",
|
|
76
80
|
"src/install-hooks.mjs",
|
|
77
81
|
"src/update-check.mjs",
|
|
82
|
+
"src/prompt-intel.mjs",
|
|
78
83
|
"bin/*.mjs",
|
|
79
84
|
"hooks/enforce-tier.mjs",
|
|
80
85
|
"hooks/cost-logger.mjs",
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* awareness.mjs — Environment awareness layer for dual-brain.
|
|
3
|
+
* Scans runtime environment once on startup and caches results with TTL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { join, resolve } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
|
|
11
|
+
let _cache = null;
|
|
12
|
+
let _cacheTime = 0;
|
|
13
|
+
|
|
14
|
+
function safeExec(cmd, timeoutMs = 2000) {
|
|
15
|
+
try {
|
|
16
|
+
return execSync(cmd, {
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
19
|
+
timeout: timeoutMs,
|
|
20
|
+
}).trim();
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function extractVersion(output) {
|
|
27
|
+
if (!output) return null;
|
|
28
|
+
const m = output.match(/(\d+\.\d+[\.\d]*)/);
|
|
29
|
+
return m ? m[1] : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function probeToolAvailability(name) {
|
|
33
|
+
const path = safeExec(`which ${name}`);
|
|
34
|
+
if (!path) return { available: false, version: null };
|
|
35
|
+
if (name === 'rg' || name === 'replit' || name === 'gh') {
|
|
36
|
+
return { available: true };
|
|
37
|
+
}
|
|
38
|
+
const versionOutput = safeExec(`${name} --version`);
|
|
39
|
+
return { available: true, version: extractVersion(versionOutput) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectContainerType() {
|
|
43
|
+
const env = process.env;
|
|
44
|
+
if (env.REPL_ID || env.REPL_SLUG) return 'replit';
|
|
45
|
+
if (env.CODESPACES) return 'codespace';
|
|
46
|
+
if (env.CI || env.GITHUB_ACTIONS || env.GITLAB_CI || env.JENKINS_URL) return 'ci';
|
|
47
|
+
return 'local';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scanSecrets() {
|
|
51
|
+
const keys = [
|
|
52
|
+
'OPENAI_API_KEY',
|
|
53
|
+
'ANTHROPIC_API_KEY',
|
|
54
|
+
'NPM_TOKEN',
|
|
55
|
+
'DATABASE_URL',
|
|
56
|
+
'GITHUB_TOKEN',
|
|
57
|
+
'REPLIT_DB_URL',
|
|
58
|
+
];
|
|
59
|
+
const result = {};
|
|
60
|
+
for (const key of keys) {
|
|
61
|
+
result[key] = Boolean(process.env[key]);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scanReplitTools() {
|
|
67
|
+
const home = homedir();
|
|
68
|
+
const candidates = [
|
|
69
|
+
join(home, '.replit-tools'),
|
|
70
|
+
join('/home/runner/workspace', '.replit-tools'),
|
|
71
|
+
join(process.cwd(), '.replit-tools'),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
let toolsDir = null;
|
|
75
|
+
for (const c of candidates) {
|
|
76
|
+
if (existsSync(c)) {
|
|
77
|
+
toolsDir = c;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!toolsDir) {
|
|
83
|
+
return { installed: false, version: null, sessionArchivePath: null, capabilities: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let version = null;
|
|
87
|
+
const versionFile = join(toolsDir, '.version');
|
|
88
|
+
if (existsSync(versionFile)) {
|
|
89
|
+
try { version = readFileSync(versionFile, 'utf8').trim() || null; } catch { /* skip */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const persistentBase = join(toolsDir, '.claude-persistent');
|
|
93
|
+
const projectDir = join(persistentBase, 'projects');
|
|
94
|
+
let sessionArchivePath = null;
|
|
95
|
+
if (existsSync(projectDir)) {
|
|
96
|
+
sessionArchivePath = projectDir;
|
|
97
|
+
} else if (existsSync(persistentBase)) {
|
|
98
|
+
sessionArchivePath = persistentBase;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const capabilities = [];
|
|
102
|
+
if (existsSync(join(toolsDir, '.claude-persistent'))) capabilities.push('sessions');
|
|
103
|
+
const hasSearch = existsSync(join(toolsDir, 'search')) || existsSync(join(toolsDir, 'search.mjs'));
|
|
104
|
+
if (hasSearch) capabilities.push('search');
|
|
105
|
+
const hasContext = existsSync(join(toolsDir, 'context')) || existsSync(join(toolsDir, 'context.mjs'));
|
|
106
|
+
if (hasContext) capabilities.push('context');
|
|
107
|
+
const hasMcp = existsSync(join(toolsDir, 'mcp-server')) || existsSync(join(toolsDir, 'mcp'));
|
|
108
|
+
if (hasMcp) capabilities.push('mcp');
|
|
109
|
+
|
|
110
|
+
return { installed: true, version, sessionArchivePath, capabilities };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scanReplit(cwd) {
|
|
114
|
+
const env = process.env;
|
|
115
|
+
const isReplit = Boolean(env.REPL_ID || env.REPL_SLUG);
|
|
116
|
+
|
|
117
|
+
let hasDeployments = false;
|
|
118
|
+
const replitConfigPath = join(cwd, '.replit');
|
|
119
|
+
if (existsSync(replitConfigPath)) {
|
|
120
|
+
try {
|
|
121
|
+
const content = readFileSync(replitConfigPath, 'utf8');
|
|
122
|
+
hasDeployments = content.includes('[deployment]');
|
|
123
|
+
} catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
isReplit,
|
|
128
|
+
replId: env.REPL_ID || null,
|
|
129
|
+
replSlug: env.REPL_SLUG || null,
|
|
130
|
+
hasDatabase: Boolean(env.DATABASE_URL),
|
|
131
|
+
hasKV: Boolean(env.REPLIT_DB_URL),
|
|
132
|
+
hasObjectStorage: Boolean(env.REPLIT_BUCKET_URL || env.OBJECT_STORAGE_URL),
|
|
133
|
+
hasAuth: existsSync(join(cwd, '.replit-auth')) || Boolean(env.REPLIT_AUTH),
|
|
134
|
+
hasDeployments,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scanClaudeCode(cwd) {
|
|
139
|
+
const claudeDir = join(cwd, '.claude');
|
|
140
|
+
const homeClaudeDir = join(homedir(), '.claude');
|
|
141
|
+
const isInsideClaude = Boolean(
|
|
142
|
+
process.env.CLAUDE_CODE || process.env.CLAUDE_AGENT || process.env.ANTHROPIC_CLAUDE_CODE
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
let hooksDir = null;
|
|
146
|
+
const localHooks = join(claudeDir, 'hooks');
|
|
147
|
+
const rootHooks = join(cwd, 'hooks');
|
|
148
|
+
if (existsSync(localHooks)) {
|
|
149
|
+
hooksDir = localHooks;
|
|
150
|
+
} else if (existsSync(rootHooks)) {
|
|
151
|
+
hooksDir = rootHooks;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let mcpConfigured = false;
|
|
155
|
+
const mcpPaths = [
|
|
156
|
+
join(claudeDir, 'mcp.json'),
|
|
157
|
+
join(claudeDir, 'mcp_servers.json'),
|
|
158
|
+
join(homeClaudeDir, 'mcp.json'),
|
|
159
|
+
];
|
|
160
|
+
for (const p of mcpPaths) {
|
|
161
|
+
if (existsSync(p)) { mcpConfigured = true; break; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let settingsPath = null;
|
|
165
|
+
const settingsCandidates = [
|
|
166
|
+
join(claudeDir, 'settings.json'),
|
|
167
|
+
join(homeClaudeDir, 'settings.json'),
|
|
168
|
+
];
|
|
169
|
+
for (const p of settingsCandidates) {
|
|
170
|
+
if (existsSync(p)) { settingsPath = p; break; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { isInsideClaude, hooksDir, mcpConfigured, settingsPath };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scanDualBrain(cwd) {
|
|
177
|
+
let version = '0.0.0';
|
|
178
|
+
try {
|
|
179
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
180
|
+
version = pkg.version ?? '0.0.0';
|
|
181
|
+
} catch { /* skip */ }
|
|
182
|
+
|
|
183
|
+
const livingDocsDir = join(cwd, '.dual-brain');
|
|
184
|
+
const livingDocsInit = existsSync(livingDocsDir);
|
|
185
|
+
|
|
186
|
+
let sessionCount = 0;
|
|
187
|
+
const sessionDir = join(cwd, '.dualbrain', 'sessions');
|
|
188
|
+
if (existsSync(sessionDir)) {
|
|
189
|
+
try {
|
|
190
|
+
sessionCount = readdirSync(sessionDir).filter(f => f.endsWith('.jsonl')).length;
|
|
191
|
+
} catch { /* skip */ }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const hasLedger = existsSync(join(cwd, '.dualbrain', 'ledger.jsonl'));
|
|
195
|
+
const hasFailureMemory = existsSync(join(cwd, '.dualbrain', 'failures.jsonl'));
|
|
196
|
+
|
|
197
|
+
return { version, livingDocsInit, sessionCount, hasLedger, hasFailureMemory };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function scanEnvironment(cwd, options = {}) {
|
|
201
|
+
const ttl = options.ttl ?? 300000;
|
|
202
|
+
if (_cache && Date.now() - _cacheTime < ttl && !options.force) return _cache;
|
|
203
|
+
|
|
204
|
+
const resolvedCwd = resolve(cwd || process.cwd());
|
|
205
|
+
|
|
206
|
+
const container = {
|
|
207
|
+
type: detectContainerType(),
|
|
208
|
+
hostname: process.env.HOSTNAME || process.env.REPL_ID || 'unknown',
|
|
209
|
+
nodeVersion: process.version,
|
|
210
|
+
platform: process.platform,
|
|
211
|
+
arch: process.arch,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const toolNames = ['git', 'node', 'npm', 'codex', 'claude'];
|
|
215
|
+
const flagOnlyTools = ['rg', 'replit', 'gh'];
|
|
216
|
+
const tools = {};
|
|
217
|
+
|
|
218
|
+
for (const name of toolNames) {
|
|
219
|
+
tools[name] = probeToolAvailability(name);
|
|
220
|
+
}
|
|
221
|
+
for (const name of flagOnlyTools) {
|
|
222
|
+
const path = safeExec(`which ${name}`);
|
|
223
|
+
tools[name] = { available: Boolean(path) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const secrets = scanSecrets();
|
|
227
|
+
const replitTools = scanReplitTools();
|
|
228
|
+
const replit = scanReplit(resolvedCwd);
|
|
229
|
+
const claudeCode = scanClaudeCode(resolvedCwd);
|
|
230
|
+
const dualBrain = scanDualBrain(resolvedCwd);
|
|
231
|
+
|
|
232
|
+
const report = {
|
|
233
|
+
scannedAt: Date.now(),
|
|
234
|
+
ttl,
|
|
235
|
+
container,
|
|
236
|
+
tools,
|
|
237
|
+
secrets,
|
|
238
|
+
replitTools,
|
|
239
|
+
replit,
|
|
240
|
+
claudeCode,
|
|
241
|
+
dualBrain,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
_cache = report;
|
|
245
|
+
_cacheTime = Date.now();
|
|
246
|
+
return report;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function formatEnvironment(report) {
|
|
250
|
+
const { container, tools, secrets, replitTools, replit, dualBrain } = report;
|
|
251
|
+
|
|
252
|
+
const nodeShort = container.nodeVersion.replace(/^v/, '').split('.')[0];
|
|
253
|
+
const containerLabel = container.type === 'replit'
|
|
254
|
+
? 'Replit'
|
|
255
|
+
: container.type.charAt(0).toUpperCase() + container.type.slice(1);
|
|
256
|
+
|
|
257
|
+
const lines = [];
|
|
258
|
+
|
|
259
|
+
lines.push(`Environment: ${containerLabel} (node ${nodeShort}.x)`);
|
|
260
|
+
|
|
261
|
+
const toolEntries = [];
|
|
262
|
+
for (const [name, info] of Object.entries(tools)) {
|
|
263
|
+
if (info.available) toolEntries.push(`${name} ✓`);
|
|
264
|
+
}
|
|
265
|
+
if (toolEntries.length) lines.push(`Tools: ${toolEntries.join(' ')}`);
|
|
266
|
+
|
|
267
|
+
const secretMap = {
|
|
268
|
+
OPENAI_API_KEY: 'OpenAI',
|
|
269
|
+
ANTHROPIC_API_KEY: 'Anthropic',
|
|
270
|
+
NPM_TOKEN: 'npm',
|
|
271
|
+
GITHUB_TOKEN: 'GitHub',
|
|
272
|
+
DATABASE_URL: 'PostgreSQL',
|
|
273
|
+
REPLIT_DB_URL: 'KV',
|
|
274
|
+
};
|
|
275
|
+
const secretEntries = [];
|
|
276
|
+
for (const [key, label] of Object.entries(secretMap)) {
|
|
277
|
+
if (secrets[key]) secretEntries.push(`${label} ✓`);
|
|
278
|
+
}
|
|
279
|
+
if (secretEntries.length) lines.push(`Secrets: ${secretEntries.join(' ')}`);
|
|
280
|
+
|
|
281
|
+
const platformParts = [];
|
|
282
|
+
if (replit.hasDatabase) platformParts.push('PostgreSQL ✓');
|
|
283
|
+
if (replit.hasKV) platformParts.push('KV ✓');
|
|
284
|
+
if (replit.hasObjectStorage) platformParts.push('ObjectStorage ✓');
|
|
285
|
+
if (replit.hasAuth) platformParts.push('Auth ✓');
|
|
286
|
+
if (replit.hasDeployments) platformParts.push('Deployments ✓');
|
|
287
|
+
if (platformParts.length) lines.push(`Platform: ${platformParts.join(' ')}`);
|
|
288
|
+
|
|
289
|
+
if (replitTools.installed) {
|
|
290
|
+
const ver = replitTools.version ? `v${replitTools.version}` : 'installed';
|
|
291
|
+
const caps = replitTools.capabilities.join(', ');
|
|
292
|
+
lines.push(`replit-tools: ${ver}${caps ? ` (${caps})` : ''}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const dbFlag = dualBrain.hasLedger ? ', ledger ✓' : '';
|
|
296
|
+
const docsFlag = dualBrain.livingDocsInit ? 'living docs ✓' : 'living docs ✗';
|
|
297
|
+
lines.push(`dual-brain: v${dualBrain.version} (${docsFlag}${dbFlag})`);
|
|
298
|
+
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function getCapabilitySummary(report) {
|
|
303
|
+
const caps = [];
|
|
304
|
+
|
|
305
|
+
if (report.container.type !== 'unknown') {
|
|
306
|
+
caps.push(`${report.container.type}-container`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (report.replit.hasDatabase) caps.push('postgresql');
|
|
310
|
+
if (report.replit.hasKV) caps.push('replit-kv');
|
|
311
|
+
if (report.replit.hasObjectStorage) caps.push('object-storage');
|
|
312
|
+
if (report.replit.hasAuth) caps.push('replit-auth');
|
|
313
|
+
if (report.replit.hasDeployments) caps.push('replit-deployments');
|
|
314
|
+
|
|
315
|
+
if (report.tools.codex?.available) caps.push('codex-cli');
|
|
316
|
+
if (report.tools.claude?.available) caps.push('claude-cli');
|
|
317
|
+
if (report.tools.git?.available) caps.push('git');
|
|
318
|
+
if (report.tools.gh?.available) caps.push('github-cli');
|
|
319
|
+
if (report.tools.rg?.available) caps.push('ripgrep');
|
|
320
|
+
|
|
321
|
+
if (report.secrets.OPENAI_API_KEY) caps.push('openai-key');
|
|
322
|
+
if (report.secrets.ANTHROPIC_API_KEY) caps.push('anthropic-key');
|
|
323
|
+
|
|
324
|
+
if (report.replitTools.installed) {
|
|
325
|
+
for (const c of report.replitTools.capabilities) {
|
|
326
|
+
caps.push(`replit-tools-${c}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (report.claudeCode.mcpConfigured) caps.push('mcp-configured');
|
|
331
|
+
if (report.claudeCode.hooksDir) caps.push('claude-hooks');
|
|
332
|
+
|
|
333
|
+
if (report.dualBrain.hasLedger) caps.push('dual-brain-ledger');
|
|
334
|
+
if (report.dualBrain.hasFailureMemory) caps.push('dual-brain-failure-memory');
|
|
335
|
+
if (report.dualBrain.livingDocsInit) caps.push('dual-brain-living-docs');
|
|
336
|
+
|
|
337
|
+
return caps;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function invalidateCache() {
|
|
341
|
+
_cache = null;
|
|
342
|
+
_cacheTime = 0;
|
|
343
|
+
}
|
package/src/models.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models.mjs — Static model intelligence registry for the Dual-Brain Orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Pure in-memory registry of AI model capabilities used by routing modules.
|
|
5
|
+
* No file I/O, no API calls. Updated with each package release.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const REGISTRY_VERSION = '2026-05-15';
|
|
9
|
+
export const REGISTRY_UPDATED = '2026-05-15';
|
|
10
|
+
|
|
11
|
+
export const MODEL_REGISTRY = Object.freeze({
|
|
12
|
+
'claude-opus-4-6': {
|
|
13
|
+
provider: 'anthropic',
|
|
14
|
+
name: 'Claude Opus 4.6',
|
|
15
|
+
tier: 'frontier',
|
|
16
|
+
contextWindow: 200000,
|
|
17
|
+
maxOutput: 32000,
|
|
18
|
+
strengths: ['complex reasoning', 'architecture', 'code review', 'long context', 'multi-step planning'],
|
|
19
|
+
weaknesses: ['speed', 'cost'],
|
|
20
|
+
costTier: 'high',
|
|
21
|
+
bestFor: ['think', 'review', 'architecture', 'complex debugging'],
|
|
22
|
+
speed: 'slow',
|
|
23
|
+
reasoning: true,
|
|
24
|
+
vision: true,
|
|
25
|
+
tools: true,
|
|
26
|
+
agentCapable: true,
|
|
27
|
+
},
|
|
28
|
+
'claude-sonnet-4-6': {
|
|
29
|
+
provider: 'anthropic',
|
|
30
|
+
name: 'Claude Sonnet 4.6',
|
|
31
|
+
tier: 'workhorse',
|
|
32
|
+
contextWindow: 200000,
|
|
33
|
+
maxOutput: 16000,
|
|
34
|
+
strengths: ['balanced speed/quality', 'code generation', 'editing', 'testing'],
|
|
35
|
+
weaknesses: ['less depth on architecture decisions'],
|
|
36
|
+
costTier: 'medium',
|
|
37
|
+
bestFor: ['execute', 'implement', 'test', 'fix'],
|
|
38
|
+
speed: 'medium',
|
|
39
|
+
reasoning: true,
|
|
40
|
+
vision: true,
|
|
41
|
+
tools: true,
|
|
42
|
+
agentCapable: true,
|
|
43
|
+
},
|
|
44
|
+
'claude-haiku-4-5-20251001': {
|
|
45
|
+
provider: 'anthropic',
|
|
46
|
+
name: 'Claude Haiku 4.5',
|
|
47
|
+
tier: 'fast',
|
|
48
|
+
contextWindow: 200000,
|
|
49
|
+
maxOutput: 8192,
|
|
50
|
+
strengths: ['speed', 'low cost', 'simple tasks', 'search', 'classification'],
|
|
51
|
+
weaknesses: ['complex reasoning', 'multi-step tasks'],
|
|
52
|
+
costTier: 'low',
|
|
53
|
+
bestFor: ['search', 'classify', 'simple edits', 'grep'],
|
|
54
|
+
speed: 'fast',
|
|
55
|
+
reasoning: false,
|
|
56
|
+
vision: true,
|
|
57
|
+
tools: true,
|
|
58
|
+
agentCapable: true,
|
|
59
|
+
},
|
|
60
|
+
'gpt-5.5': {
|
|
61
|
+
provider: 'openai',
|
|
62
|
+
name: 'GPT-5.5',
|
|
63
|
+
tier: 'frontier',
|
|
64
|
+
contextWindow: 1050000,
|
|
65
|
+
maxOutput: 128000,
|
|
66
|
+
strengths: ['massive context', 'complex professional work', 'reasoning', 'web search', 'code interpreter'],
|
|
67
|
+
weaknesses: ['cost', 'latency on complex reasoning'],
|
|
68
|
+
costTier: 'premium',
|
|
69
|
+
bestFor: ['think', 'review', 'research', 'long-context analysis'],
|
|
70
|
+
speed: 'medium',
|
|
71
|
+
reasoning: true,
|
|
72
|
+
vision: true,
|
|
73
|
+
tools: true,
|
|
74
|
+
agentCapable: true,
|
|
75
|
+
},
|
|
76
|
+
'o3': {
|
|
77
|
+
provider: 'openai',
|
|
78
|
+
name: 'o3',
|
|
79
|
+
tier: 'reasoning',
|
|
80
|
+
contextWindow: 200000,
|
|
81
|
+
maxOutput: 100000,
|
|
82
|
+
strengths: ['deep reasoning', 'math', 'code', 'science', 'multi-step logic'],
|
|
83
|
+
weaknesses: ['speed', 'cost', 'simple tasks overkill'],
|
|
84
|
+
costTier: 'premium',
|
|
85
|
+
bestFor: ['think', 'complex debugging', 'algorithm design', 'security analysis'],
|
|
86
|
+
speed: 'slow',
|
|
87
|
+
reasoning: true,
|
|
88
|
+
vision: true,
|
|
89
|
+
tools: true,
|
|
90
|
+
agentCapable: true,
|
|
91
|
+
},
|
|
92
|
+
'gpt-4o': {
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
name: 'GPT-4o',
|
|
95
|
+
tier: 'workhorse',
|
|
96
|
+
contextWindow: 128000,
|
|
97
|
+
maxOutput: 16384,
|
|
98
|
+
strengths: ['balanced', 'fast', 'multimodal', 'tool use'],
|
|
99
|
+
weaknesses: ['less depth than frontier models'],
|
|
100
|
+
costTier: 'medium',
|
|
101
|
+
bestFor: ['execute', 'implement', 'chat', 'multimodal'],
|
|
102
|
+
speed: 'fast',
|
|
103
|
+
reasoning: false,
|
|
104
|
+
vision: true,
|
|
105
|
+
tools: true,
|
|
106
|
+
agentCapable: true,
|
|
107
|
+
},
|
|
108
|
+
'gpt-4o-mini': {
|
|
109
|
+
provider: 'openai',
|
|
110
|
+
name: 'GPT-4o Mini',
|
|
111
|
+
tier: 'fast',
|
|
112
|
+
contextWindow: 128000,
|
|
113
|
+
maxOutput: 16384,
|
|
114
|
+
strengths: ['very fast', 'very cheap', 'good enough for simple tasks'],
|
|
115
|
+
weaknesses: ['complex reasoning', 'nuanced code review'],
|
|
116
|
+
costTier: 'low',
|
|
117
|
+
bestFor: ['search', 'classify', 'simple tasks', 'formatting'],
|
|
118
|
+
speed: 'fast',
|
|
119
|
+
reasoning: false,
|
|
120
|
+
vision: true,
|
|
121
|
+
tools: true,
|
|
122
|
+
agentCapable: false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const COST_TIER_ORDER = ['low', 'medium', 'high', 'premium'];
|
|
127
|
+
const SPEED_ORDER = ['fast', 'medium', 'slow'];
|
|
128
|
+
const TIER_ORDER = ['fast', 'workhorse', 'reasoning', 'frontier'];
|
|
129
|
+
|
|
130
|
+
const MODEL_QUIRKS = {
|
|
131
|
+
'claude-opus-4-6': [
|
|
132
|
+
'Best for architecture decisions',
|
|
133
|
+
'Use for code review when quality matters',
|
|
134
|
+
'Expensive — reserve for high-value tasks',
|
|
135
|
+
],
|
|
136
|
+
'claude-sonnet-4-6': [
|
|
137
|
+
'Best general-purpose worker',
|
|
138
|
+
'Good balance of speed and quality',
|
|
139
|
+
'Use Agent(model: "sonnet") for work dispatch',
|
|
140
|
+
],
|
|
141
|
+
'claude-haiku-4-5-20251001': [
|
|
142
|
+
'Best for fast search and classification',
|
|
143
|
+
'Not reliable for multi-step edits',
|
|
144
|
+
'Use for disposable search agents',
|
|
145
|
+
],
|
|
146
|
+
'gpt-5.5': [
|
|
147
|
+
'Massive 1M+ context window',
|
|
148
|
+
'Good challenger for dual-brain think',
|
|
149
|
+
'Web search and code interpreter available',
|
|
150
|
+
],
|
|
151
|
+
'o3': [
|
|
152
|
+
'Purpose-built for deep reasoning chains',
|
|
153
|
+
'Strong challenger for security analysis',
|
|
154
|
+
'Slow — avoid for quick tasks',
|
|
155
|
+
],
|
|
156
|
+
'gpt-4o': [
|
|
157
|
+
'Fast and multimodal',
|
|
158
|
+
'Solid fallback for execute-tier GPT work',
|
|
159
|
+
'Native tool use support',
|
|
160
|
+
],
|
|
161
|
+
'gpt-4o-mini': [
|
|
162
|
+
'Cheapest GPT option for simple tasks',
|
|
163
|
+
'Not agent-capable — single-shot only',
|
|
164
|
+
'Good for classification and formatting',
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export function getModel(modelId) {
|
|
169
|
+
return MODEL_REGISTRY[modelId] ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getModelsForTask(taskType, provider = null) {
|
|
173
|
+
const entries = Object.entries(MODEL_REGISTRY);
|
|
174
|
+
|
|
175
|
+
const filtered = entries.filter(([, m]) => {
|
|
176
|
+
if (provider && m.provider !== provider) return false;
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
filtered.sort(([, a], [, b]) => {
|
|
181
|
+
const aMatch = a.bestFor.includes(taskType) ? 1 : 0;
|
|
182
|
+
const bMatch = b.bestFor.includes(taskType) ? 1 : 0;
|
|
183
|
+
if (bMatch !== aMatch) return bMatch - aMatch;
|
|
184
|
+
|
|
185
|
+
const aTier = TIER_ORDER.indexOf(a.tier);
|
|
186
|
+
const bTier = TIER_ORDER.indexOf(b.tier);
|
|
187
|
+
if (bTier !== aTier) return bTier - aTier;
|
|
188
|
+
|
|
189
|
+
return SPEED_ORDER.indexOf(a.speed) - SPEED_ORDER.indexOf(b.speed);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return filtered.map(([id, m]) => ({ id, ...m }));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getBestModel(taskType, constraints = {}) {
|
|
196
|
+
const { provider = null, maxCost = null, preferSpeed = false, requireReasoning = false, minContext = 0 } = constraints;
|
|
197
|
+
|
|
198
|
+
let candidates = getModelsForTask(taskType, provider);
|
|
199
|
+
|
|
200
|
+
if (requireReasoning) {
|
|
201
|
+
candidates = candidates.filter(m => m.reasoning);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (minContext > 0) {
|
|
205
|
+
candidates = candidates.filter(m => m.contextWindow >= minContext);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (maxCost) {
|
|
209
|
+
const maxIdx = COST_TIER_ORDER.indexOf(maxCost);
|
|
210
|
+
if (maxIdx !== -1) {
|
|
211
|
+
candidates = candidates.filter(m => COST_TIER_ORDER.indexOf(m.costTier) <= maxIdx);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (preferSpeed) {
|
|
216
|
+
candidates.sort((a, b) => SPEED_ORDER.indexOf(a.speed) - SPEED_ORDER.indexOf(b.speed));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return candidates[0] ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function compareModels(modelA, modelB) {
|
|
223
|
+
const a = MODEL_REGISTRY[modelA];
|
|
224
|
+
const b = MODEL_REGISTRY[modelB];
|
|
225
|
+
|
|
226
|
+
if (!a || !b) {
|
|
227
|
+
return {
|
|
228
|
+
contextAdvantage: 'tie',
|
|
229
|
+
speedAdvantage: 'tie',
|
|
230
|
+
costAdvantage: 'tie',
|
|
231
|
+
reasoningAdvantage: 'tie',
|
|
232
|
+
recommendation: 'One or both model IDs are unknown.',
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const ctxAdv = a.contextWindow > b.contextWindow ? 'a'
|
|
237
|
+
: b.contextWindow > a.contextWindow ? 'b'
|
|
238
|
+
: 'tie';
|
|
239
|
+
|
|
240
|
+
const spdA = SPEED_ORDER.indexOf(a.speed);
|
|
241
|
+
const spdB = SPEED_ORDER.indexOf(b.speed);
|
|
242
|
+
const spdAdv = spdA < spdB ? 'a' : spdB < spdA ? 'b' : 'tie';
|
|
243
|
+
|
|
244
|
+
const costA = COST_TIER_ORDER.indexOf(a.costTier);
|
|
245
|
+
const costB = COST_TIER_ORDER.indexOf(b.costTier);
|
|
246
|
+
const costAdv = costA < costB ? 'a' : costB < costA ? 'b' : 'tie';
|
|
247
|
+
|
|
248
|
+
const rsnAdv = (a.reasoning && !b.reasoning) ? 'a'
|
|
249
|
+
: (b.reasoning && !a.reasoning) ? 'b'
|
|
250
|
+
: 'tie';
|
|
251
|
+
|
|
252
|
+
const aAdvantages = [ctxAdv, spdAdv, costAdv, rsnAdv].filter(v => v === 'a').length;
|
|
253
|
+
const bAdvantages = [ctxAdv, spdAdv, costAdv, rsnAdv].filter(v => v === 'b').length;
|
|
254
|
+
|
|
255
|
+
let recommendation;
|
|
256
|
+
if (aAdvantages > bAdvantages) {
|
|
257
|
+
recommendation = `Use ${a.name} for ${a.bestFor.slice(0, 2).join('/')}; ${b.name} as a lighter alternative.`;
|
|
258
|
+
} else if (bAdvantages > aAdvantages) {
|
|
259
|
+
recommendation = `Use ${b.name} for ${b.bestFor.slice(0, 2).join('/')}; ${a.name} as a lighter alternative.`;
|
|
260
|
+
} else {
|
|
261
|
+
recommendation = `${a.name} and ${b.name} are comparable — choose based on provider availability.`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { contextAdvantage: ctxAdv, speedAdvantage: spdAdv, costAdvantage: costAdv, reasoningAdvantage: rsnAdv, recommendation };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function getRegistryAge() {
|
|
268
|
+
const updated = new Date(REGISTRY_UPDATED);
|
|
269
|
+
const now = new Date();
|
|
270
|
+
return Math.floor((now - updated) / (1000 * 60 * 60 * 24));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const COST_SYMBOL = { low: '$', medium: '$$', high: '$$$', premium: '$$$$' };
|
|
274
|
+
|
|
275
|
+
function fmtCtx(n) {
|
|
276
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M ctx`;
|
|
277
|
+
return `${Math.round(n / 1000)}K ctx`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function formatModelTable(provider = null) {
|
|
281
|
+
const groups = {};
|
|
282
|
+
for (const [id, m] of Object.entries(MODEL_REGISTRY)) {
|
|
283
|
+
if (provider && m.provider !== provider) continue;
|
|
284
|
+
if (!groups[m.provider]) groups[m.provider] = [];
|
|
285
|
+
groups[m.provider].push({ id, ...m });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const lines = [];
|
|
289
|
+
const providerLabels = { anthropic: 'Claude Models', openai: 'OpenAI Models' };
|
|
290
|
+
const providerOrder = ['anthropic', 'openai'];
|
|
291
|
+
|
|
292
|
+
for (const prov of providerOrder) {
|
|
293
|
+
if (!groups[prov]) continue;
|
|
294
|
+
lines.push(`${providerLabels[prov] ?? prov}:`);
|
|
295
|
+
for (const m of groups[prov]) {
|
|
296
|
+
const displayName = m.provider === 'anthropic' ? m.name.replace(/^Claude /, '') : m.name;
|
|
297
|
+
const name = displayName.padEnd(12);
|
|
298
|
+
const tier = m.tier.padEnd(10);
|
|
299
|
+
const ctx = fmtCtx(m.contextWindow).padEnd(10);
|
|
300
|
+
const speed = m.speed.padEnd(8);
|
|
301
|
+
const cost = (COST_SYMBOL[m.costTier] ?? '?').padEnd(6);
|
|
302
|
+
const tasks = m.bestFor.slice(0, 2).join('/');
|
|
303
|
+
lines.push(` ${name}${tier}${ctx}${speed}${cost}${tasks}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push('');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return lines.join('\n').trimEnd();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function getModelQuirks(modelId) {
|
|
312
|
+
return MODEL_QUIRKS[modelId] ?? [];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function suggestModel(intent, risk, complexity, availableProviders = ['anthropic', 'openai']) {
|
|
316
|
+
const intentLower = (intent || '').toLowerCase();
|
|
317
|
+
|
|
318
|
+
const TASK_MAP = {
|
|
319
|
+
think: ['architect', 'design', 'plan', 'review', 'security', 'audit', 'analysis', 'analyze'],
|
|
320
|
+
execute: ['implement', 'edit', 'fix', 'refactor', 'test', 'build', 'write', 'update', 'create'],
|
|
321
|
+
search: ['search', 'find', 'grep', 'look', 'explore', 'classify', 'format', 'list'],
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
let taskType = 'execute';
|
|
325
|
+
for (const [type, keywords] of Object.entries(TASK_MAP)) {
|
|
326
|
+
if (keywords.some(kw => intentLower.includes(kw))) {
|
|
327
|
+
taskType = type;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if ((risk === 'critical' || risk === 'high') && taskType !== 'think') {
|
|
333
|
+
taskType = 'think';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const requireReasoning = complexity === 'complex' || risk === 'critical';
|
|
337
|
+
const preferSpeed = risk === 'low' && complexity === 'simple';
|
|
338
|
+
const maxCost = risk === 'low' && complexity !== 'complex' ? 'medium' : null;
|
|
339
|
+
|
|
340
|
+
const candidates = getModelsForTask(taskType)
|
|
341
|
+
.filter(m => availableProviders.includes(m.provider))
|
|
342
|
+
.filter(m => !requireReasoning || m.reasoning)
|
|
343
|
+
.filter(m => !maxCost || COST_TIER_ORDER.indexOf(m.costTier) <= COST_TIER_ORDER.indexOf(maxCost));
|
|
344
|
+
|
|
345
|
+
if (preferSpeed) {
|
|
346
|
+
candidates.sort((a, b) => SPEED_ORDER.indexOf(a.speed) - SPEED_ORDER.indexOf(b.speed));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const best = candidates[0] ?? null;
|
|
350
|
+
if (!best) {
|
|
351
|
+
return { model: null, reason: 'No suitable model found for the given constraints.', alternatives: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const reason = [
|
|
355
|
+
`Selected ${best.name} for ${taskType}-tier work`,
|
|
356
|
+
risk !== 'low' ? `(${risk} risk)` : null,
|
|
357
|
+
requireReasoning ? '— reasoning required' : null,
|
|
358
|
+
].filter(Boolean).join(' ');
|
|
359
|
+
|
|
360
|
+
const alternatives = candidates.slice(1, 3).map(m => ({ id: m.id, name: m.name }));
|
|
361
|
+
|
|
362
|
+
return { model: best.id, reason, alternatives };
|
|
363
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// prompt-intel.mjs — Layer 3 prompt analysis, enrichment, risk detection, and intervention routing.
|
|
2
|
+
// Pure functions only. No I/O, no exec. Caller provides projectBrief and calibration.
|
|
3
|
+
|
|
4
|
+
const INTENT_PATTERNS = {
|
|
5
|
+
fix: /\b(?:fix|bug|broken|error|crash|failing|fails|wrong|issue|problem|broken|not\s+working|doesn't\s+work|doesn't\s+work)\b/i,
|
|
6
|
+
feature: /\b(?:add|create|build|implement|new|introduce|support|enable)\b/i,
|
|
7
|
+
refactor: /\b(?:refactor|clean\s+up|reorganize|simplify|extract|restructure|dedup|consolidate|move)\b/i,
|
|
8
|
+
review: /\b(?:review|check|audit|look\s+at|examine|inspect|assess|evaluate)\b/i,
|
|
9
|
+
ship: /\b(?:ship|deploy|publish|release|push|merge|go\s+live|launch)\b/i,
|
|
10
|
+
explore: /\b(?:what|how|why|where|find|search|explain|show\s+me|tell\s+me|list|which)\b/i,
|
|
11
|
+
test: /\b(?:test|spec|coverage|assert|jest|mocha|vitest|unit\s+test|integration)\b/i,
|
|
12
|
+
docs: /\b(?:doc|readme|comment|jsdoc|document|explain|annotate)\b/i,
|
|
13
|
+
deploy: /\b(?:deploy|provision|infrastructure|ci|cd|pipeline|k8s|kubernetes|docker)\b/i,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const RISK_PATTERNS = [
|
|
17
|
+
{ type: 'destructive', severity: 'block', re: /\b(?:delete\s+all|remove\s+all|drop\s+all|wipe|destroy|rm\s+-rf|truncate\s+all|nuke)\b/i, detail: 'Prompt contains mass-destructive operation' },
|
|
18
|
+
{ type: 'force_push', severity: 'block', re: /(?:force\s+push|--force|-f\s+origin|reset\s+--hard)/i, detail: 'Prompt implies forced git operation' },
|
|
19
|
+
{ type: 'secret', severity: 'warn', re: /\b(?:api\s+key|access\s+token|secret\s+key|\.env|private\s+key|bearer\s+token)\b/i, detail: 'Touches secret or credential material' },
|
|
20
|
+
{ type: 'auth', severity: 'warn', re: /\b(?:auth(?:entication)?|login|logout|password|credential|jwt|oauth|session\s+token)\b/i, detail: 'Touches authentication code' },
|
|
21
|
+
{ type: 'deploy', severity: 'warn', re: /\b(?:ship\s+to|deploy\s+to|release\s+to|push\s+to\s+prod)\b/i, detail: 'Targets a deployment action' },
|
|
22
|
+
{ type: 'data_loss', severity: 'warn', re: /\b(?:drop\s+table|alter\s+table|schema\s+migration|migrate\s+data|database\s+reset)\b/i, detail: 'Touches schema or migration — data loss risk' },
|
|
23
|
+
{ type: 'production', severity: 'warn', re: /\b(?:production|prod\b|live\s+environment|live\s+site)\b/i, detail: 'References production environment' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const FILE_REF_RE = /(?:src\/|\.mjs|\.tsx?|\.jsx?|\.json|\.ya?ml|\.sh|line\s+\d+|\bL\d+\b)/i;
|
|
27
|
+
const FUNC_REF_RE = /\b\w+\((?:\)|[^)]{0,40}\))/;
|
|
28
|
+
const STEP_RE = /\b(?:step\s+\d|first[,\s]|then[,\s]|finally[,\s]|must|should\s+(?:use|call|return|handle))\b/i;
|
|
29
|
+
const CRITERIA_RE = /\b(?:accept(?:ance)?\s+criteria|definition\s+of\s+done|constraints?|requirements?|must\s+(?:not|be|have)|should\s+not)\b/i;
|
|
30
|
+
|
|
31
|
+
function clamp(v, min = 1, max = 5) {
|
|
32
|
+
return Math.min(max, Math.max(min, v));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scoreSpecificity(prompt) {
|
|
36
|
+
const hasFile = FILE_REF_RE.test(prompt);
|
|
37
|
+
const hasFunc = FUNC_REF_RE.test(prompt);
|
|
38
|
+
const hasLine = /\bL\d+\b|\bline\s+\d+/i.test(prompt);
|
|
39
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
40
|
+
|
|
41
|
+
if (hasFile && (hasFunc || hasLine)) return 5;
|
|
42
|
+
if (hasFile || hasFunc) return 4;
|
|
43
|
+
if (words >= 10) return 3;
|
|
44
|
+
if (words >= 5) return 2;
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scoreActionability(prompt) {
|
|
49
|
+
const hasStep = STEP_RE.test(prompt);
|
|
50
|
+
const hasVerb = /\b(?:fix|add|remove|refactor|create|update|write|delete|move|rename|replace|ensure|make)\b/i.test(prompt);
|
|
51
|
+
const hasOutcome = /\b(?:so\s+that|in\s+order\s+to|result(?:ing)?\s+in|should\s+(?:return|output|produce|show))\b/i.test(prompt);
|
|
52
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
53
|
+
|
|
54
|
+
if (hasStep && hasVerb) return 5;
|
|
55
|
+
if (hasOutcome && hasVerb) return 4;
|
|
56
|
+
if (hasVerb && words >= 6) return 3;
|
|
57
|
+
if (hasVerb) return 2;
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function scoreSafety(risks) {
|
|
62
|
+
if (risks.some(r => r.severity === 'block')) return 1;
|
|
63
|
+
if (risks.some(r => r.type === 'auth' || r.type === 'secret' || r.type === 'production')) return 2;
|
|
64
|
+
if (risks.some(r => r.type === 'deploy' || r.type === 'data_loss')) return 3;
|
|
65
|
+
if (risks.length > 0) return 4;
|
|
66
|
+
return 5;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scoreCompleteness(prompt) {
|
|
70
|
+
const hasCriteria = CRITERIA_RE.test(prompt);
|
|
71
|
+
const hasContext = /\b(?:because|since|currently|right\s+now|the\s+issue\s+is|error\s+is|it\s+(?:crashes|fails|returns))\b/i.test(prompt);
|
|
72
|
+
const hasScope = FILE_REF_RE.test(prompt);
|
|
73
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
74
|
+
|
|
75
|
+
if (hasCriteria && hasContext && hasScope) return 5;
|
|
76
|
+
if ((hasCriteria || hasContext) && hasScope) return 4;
|
|
77
|
+
if (hasContext || (hasScope && words >= 8)) return 3;
|
|
78
|
+
if (words >= 6) return 2;
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detectIntent(prompt) {
|
|
83
|
+
const counts = {};
|
|
84
|
+
for (const [type, re] of Object.entries(INTENT_PATTERNS)) {
|
|
85
|
+
const matches = prompt.match(new RegExp(re.source, 'gi')) ?? [];
|
|
86
|
+
if (matches.length > 0) counts[type] = matches.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
return { type: 'unknown', confidence: 0, keywords: [] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const [topType, topCount] = entries[0];
|
|
95
|
+
const totalMatches = Object.values(counts).reduce((s, n) => s + n, 0);
|
|
96
|
+
const confidence = Math.min(1, topCount / Math.max(totalMatches, 1) * (entries.length === 1 ? 1.5 : 1));
|
|
97
|
+
|
|
98
|
+
const allWords = prompt.match(new RegExp(INTENT_PATTERNS[topType].source, 'gi')) ?? [];
|
|
99
|
+
const keywords = [...new Set(allWords.map(w => w.toLowerCase().trim()))].slice(0, 5);
|
|
100
|
+
|
|
101
|
+
return { type: topType, confidence: Math.round(confidence * 100) / 100, keywords };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function detectRisks(prompt) {
|
|
105
|
+
const found = [];
|
|
106
|
+
for (const { type, severity, re, detail } of RISK_PATTERNS) {
|
|
107
|
+
if (re.test(prompt)) {
|
|
108
|
+
found.push({ type, severity, detail });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findMissingInfo(prompt, intent, specificity, completeness) {
|
|
115
|
+
const missing = [];
|
|
116
|
+
|
|
117
|
+
if (intent.type === 'fix') {
|
|
118
|
+
if (!/error|exception|crash|fail|wrong|issue/i.test(prompt)) missing.push('error description or failure symptom');
|
|
119
|
+
if (!FILE_REF_RE.test(prompt)) missing.push('affected file or component');
|
|
120
|
+
if (!/repro|reproduce|steps|trigger|when\s+I/i.test(prompt)) missing.push('reproduction steps');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (intent.type === 'feature') {
|
|
124
|
+
if (!FILE_REF_RE.test(prompt)) missing.push('target file or module to add feature to');
|
|
125
|
+
if (!/user\s+(?:can|should|will)|should\s+(?:allow|enable|support)/i.test(prompt)) missing.push('user-facing outcome');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (intent.type === 'refactor' && !FILE_REF_RE.test(prompt)) {
|
|
129
|
+
missing.push('which file or function to refactor');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (specificity < 2) missing.push('file name or component reference');
|
|
133
|
+
if (completeness < 2) missing.push('context about current behavior');
|
|
134
|
+
|
|
135
|
+
return [...new Set(missing)].slice(0, 3);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function chooseIntervention(quality, risks, calibration) {
|
|
139
|
+
if (risks.some(r => r.severity === 'block')) return 'block';
|
|
140
|
+
|
|
141
|
+
const autonomy = calibration?.autonomy ?? 3;
|
|
142
|
+
const specificity = calibration?.specificity ?? 3;
|
|
143
|
+
|
|
144
|
+
if (quality.score >= 4 && risks.length === 0 && autonomy > 4) return 'pass';
|
|
145
|
+
if (quality.score < 2 && specificity < 3) return 'confirm_rewrite';
|
|
146
|
+
if (quality.score < 3 && quality.completeness <= 2) return 'clarify_once';
|
|
147
|
+
return 'silent_enrich';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function analyzePrompt(prompt, projectBrief, calibration) {
|
|
151
|
+
const risks = detectRisks(prompt);
|
|
152
|
+
const intent = detectIntent(prompt);
|
|
153
|
+
const specificity = scoreSpecificity(prompt);
|
|
154
|
+
const actionability= scoreActionability(prompt);
|
|
155
|
+
const safety = scoreSafety(risks);
|
|
156
|
+
const completeness = scoreCompleteness(prompt);
|
|
157
|
+
const score = Math.round(((specificity + actionability + safety + completeness) / 4) * 10) / 10;
|
|
158
|
+
|
|
159
|
+
const quality = { score, specificity, actionability, safety, completeness };
|
|
160
|
+
const missingInfo = findMissingInfo(prompt, intent, specificity, completeness);
|
|
161
|
+
const intervention = chooseIntervention(quality, risks, calibration);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
original: prompt,
|
|
165
|
+
quality,
|
|
166
|
+
intent,
|
|
167
|
+
risks,
|
|
168
|
+
missingInfo,
|
|
169
|
+
intervention,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function relevantDirtyFiles(dirtyFiles, intent) {
|
|
174
|
+
if (!Array.isArray(dirtyFiles) || dirtyFiles.length === 0) return [];
|
|
175
|
+
|
|
176
|
+
const INTENT_HINTS = {
|
|
177
|
+
fix: /\bfix|bug|error\b/i,
|
|
178
|
+
ship: /.*/,
|
|
179
|
+
review: /.*/,
|
|
180
|
+
feature: /src\//i,
|
|
181
|
+
refactor: /src\//i,
|
|
182
|
+
test: /test|spec/i,
|
|
183
|
+
docs: /\.md$|readme/i,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const re = INTENT_HINTS[intent.type] ?? /src\//i;
|
|
187
|
+
return dirtyFiles.filter(f => re.test(f)).slice(0, 4);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function enrichPrompt(prompt, projectBrief, analysis) {
|
|
191
|
+
if (!projectBrief) return prompt;
|
|
192
|
+
|
|
193
|
+
const lines = [prompt, ''];
|
|
194
|
+
const { branch, dirtyFiles = [], recentCommits = [], aheadOfRemote = 0, recentFailures = [] } = projectBrief;
|
|
195
|
+
const { intent } = analysis;
|
|
196
|
+
|
|
197
|
+
const uncommitted = dirtyFiles.length;
|
|
198
|
+
if (branch || uncommitted > 0) {
|
|
199
|
+
const parts = [];
|
|
200
|
+
if (branch) parts.push(`${branch} branch`);
|
|
201
|
+
if (uncommitted > 0) parts.push(`${uncommitted} uncommitted file${uncommitted !== 1 ? 's' : ''}`);
|
|
202
|
+
if (aheadOfRemote > 0) parts.push(`${aheadOfRemote} ahead of remote`);
|
|
203
|
+
lines.push(`[Context: ${parts.join(', ')}]`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const relFiles = relevantDirtyFiles(dirtyFiles, intent);
|
|
207
|
+
if (relFiles.length > 0) {
|
|
208
|
+
lines.push(`[Files: ${relFiles.join(', ')}]`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (recentCommits.length > 0 && ['fix', 'review', 'ship'].includes(intent.type)) {
|
|
212
|
+
lines.push(`[Recent: ${recentCommits[0].slice(0, 80)}]`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (recentFailures.length > 0) {
|
|
216
|
+
const related = recentFailures.find(f => {
|
|
217
|
+
const fp = (f.prompt ?? '').toLowerCase();
|
|
218
|
+
const pp = prompt.toLowerCase();
|
|
219
|
+
const words = pp.split(/\s+/).filter(w => w.length > 3);
|
|
220
|
+
return words.some(w => fp.includes(w));
|
|
221
|
+
});
|
|
222
|
+
if (related) {
|
|
223
|
+
lines.push(`[Failures: previous attempt failed — ${(related.error ?? 'unknown error').slice(0, 80)}]`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return lines.slice(0, lines.length).join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function formatRiskWarning(risks) {
|
|
231
|
+
if (!risks || risks.length === 0) return '';
|
|
232
|
+
const lines = ['⚠️ RISK DETECTED'];
|
|
233
|
+
for (const risk of risks) {
|
|
234
|
+
const icon = risk.severity === 'block' ? '🔴' : '🟡';
|
|
235
|
+
const suffix = risk.severity === 'block' ? ' (BLOCKED)' : ' (proceed with caution)';
|
|
236
|
+
lines.push(` ${icon} ${risk.type}: ${risk.detail}${suffix}`);
|
|
237
|
+
}
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function formatQuality(analysis) {
|
|
242
|
+
const { quality, intent, intervention } = analysis;
|
|
243
|
+
const q = quality;
|
|
244
|
+
return `Quality: ${q.score}/5 (specificity:${q.specificity} action:${q.actionability} safety:${q.safety} complete:${q.completeness})\nIntent: ${intent.type} (${intent.confidence}) | Intervention: ${intervention}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function suggestImprovement(analysis) {
|
|
248
|
+
const { intent, missingInfo, quality } = analysis;
|
|
249
|
+
|
|
250
|
+
const templates = {
|
|
251
|
+
fix: `Fix the [specific function or behavior] in [file path] — it [symptom/error], causing [impact]`,
|
|
252
|
+
feature: `Add [feature name] to [file/component] — it should [user outcome] when [condition]`,
|
|
253
|
+
refactor: `Refactor [function/module] in [file] to [desired outcome] — keep [what to preserve]`,
|
|
254
|
+
review: `Review [file or PR] for [concern] — focus on [specific area or risk]`,
|
|
255
|
+
ship: `Ship [branch/feature] — verify [test status], then merge and publish`,
|
|
256
|
+
explore: `Explain how [specific mechanism] works in [file/component]`,
|
|
257
|
+
test: `Write tests for [function/module] in [file] covering [edge cases]`,
|
|
258
|
+
docs: `Document [function or module] in [file] — cover [params, return, examples]`,
|
|
259
|
+
deploy: `Deploy [service] to [environment] — confirm [readiness checks]`,
|
|
260
|
+
unknown: `Describe what you want changed, which file it's in, and what the expected result is`,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const base = templates[intent.type] ?? templates.unknown;
|
|
264
|
+
const tips = missingInfo.length > 0
|
|
265
|
+
? ` (missing: ${missingInfo.join(', ')})`
|
|
266
|
+
: '';
|
|
267
|
+
|
|
268
|
+
return `Try: '${base}'${tips}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function getTaskTemplate(intentType) {
|
|
272
|
+
const templates = {
|
|
273
|
+
fix: {
|
|
274
|
+
needs: ['error_description', 'affected_files', 'reproduction_steps'],
|
|
275
|
+
auto_add: ['test_expectations', 'rollback_plan'],
|
|
276
|
+
},
|
|
277
|
+
feature: {
|
|
278
|
+
needs: ['feature_description', 'affected_area'],
|
|
279
|
+
auto_add: ['test_plan', 'acceptance_criteria'],
|
|
280
|
+
},
|
|
281
|
+
refactor: {
|
|
282
|
+
needs: ['target_code', 'desired_outcome'],
|
|
283
|
+
auto_add: ['test_preservation', 'scope_boundary'],
|
|
284
|
+
},
|
|
285
|
+
review: {
|
|
286
|
+
needs: ['scope'],
|
|
287
|
+
auto_add: ['recent_changes', 'risk_areas'],
|
|
288
|
+
},
|
|
289
|
+
ship: {
|
|
290
|
+
needs: ['readiness_check'],
|
|
291
|
+
auto_add: ['test_status', 'git_status', 'uncommitted_changes'],
|
|
292
|
+
},
|
|
293
|
+
explore: {
|
|
294
|
+
needs: ['topic'],
|
|
295
|
+
auto_add: ['related_files', 'recent_changes'],
|
|
296
|
+
},
|
|
297
|
+
test: {
|
|
298
|
+
needs: ['target_function', 'test_framework'],
|
|
299
|
+
auto_add: ['edge_cases', 'existing_coverage'],
|
|
300
|
+
},
|
|
301
|
+
docs: {
|
|
302
|
+
needs: ['target_module'],
|
|
303
|
+
auto_add: ['param_descriptions', 'usage_example'],
|
|
304
|
+
},
|
|
305
|
+
deploy: {
|
|
306
|
+
needs: ['target_environment', 'service_name'],
|
|
307
|
+
auto_add: ['pre_deploy_checks', 'rollback_plan'],
|
|
308
|
+
},
|
|
309
|
+
unknown: {
|
|
310
|
+
needs: ['prompt_clarification'],
|
|
311
|
+
auto_add: [],
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return templates[intentType] ?? templates.unknown;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function shouldBlock(analysis) {
|
|
319
|
+
return analysis.risks.some(r => r.severity === 'block');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function getBlockReason(analysis) {
|
|
323
|
+
const blocking = analysis.risks.find(r => r.severity === 'block');
|
|
324
|
+
return blocking ? blocking.detail : null;
|
|
325
|
+
}
|