dual-brain 0.1.23 → 0.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/bin/dual-brain.mjs +673 -262
- package/package.json +16 -2
- package/src/awareness.mjs +343 -0
- package/src/calibration.mjs +148 -0
- package/src/cost-tracker.mjs +184 -0
- package/src/decide.mjs +162 -10
- package/src/dispatch.mjs +40 -2
- package/src/doctor.mjs +716 -1
- package/src/fx.mjs +276 -0
- package/src/intelligence.mjs +423 -0
- package/src/ledger.mjs +196 -0
- package/src/living-docs.mjs +210 -0
- package/src/models.mjs +363 -0
- package/src/pipeline.mjs +367 -8
- package/src/prompt-intel.mjs +325 -0
- package/src/think-engine.mjs +428 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
"./session": "./src/session.mjs",
|
|
20
20
|
"./decompose": "./src/decompose.mjs",
|
|
21
21
|
"./brief": "./src/brief.mjs",
|
|
22
|
-
"./redact": "./src/redact.mjs"
|
|
22
|
+
"./redact": "./src/redact.mjs",
|
|
23
|
+
"./calibration": "./src/calibration.mjs",
|
|
24
|
+
"./models": "./src/models.mjs",
|
|
25
|
+
"./prompt-intel": "./src/prompt-intel.mjs"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
28
|
"claude-code",
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
"scripts": {
|
|
41
44
|
"test": "node .claude/hooks/test-orchestrator.mjs",
|
|
42
45
|
"test:core": "node --test src/test.mjs",
|
|
46
|
+
"prepublishOnly": "node .claude/hooks/repo-doctor.mjs",
|
|
43
47
|
"postinstall": "echo 'dual-brain installed. Run: dual-brain install (in your project) to set up hooks.'",
|
|
44
48
|
"postpublish": "node scripts/verify-publish.mjs"
|
|
45
49
|
},
|
|
@@ -58,6 +62,8 @@
|
|
|
58
62
|
"src/decompose.mjs",
|
|
59
63
|
"src/brief.mjs",
|
|
60
64
|
"src/redact.mjs",
|
|
65
|
+
"src/calibration.mjs",
|
|
66
|
+
"src/models.mjs",
|
|
61
67
|
"src/pipeline.mjs",
|
|
62
68
|
"src/context.mjs",
|
|
63
69
|
"src/outcome.mjs",
|
|
@@ -67,9 +73,17 @@
|
|
|
67
73
|
"src/receipt.mjs",
|
|
68
74
|
"src/failure-memory.mjs",
|
|
69
75
|
"src/index.mjs",
|
|
76
|
+
"src/ledger.mjs",
|
|
77
|
+
"src/intelligence.mjs",
|
|
78
|
+
"src/awareness.mjs",
|
|
70
79
|
"src/tui.mjs",
|
|
80
|
+
"src/living-docs.mjs",
|
|
81
|
+
"src/cost-tracker.mjs",
|
|
82
|
+
"src/think-engine.mjs",
|
|
71
83
|
"src/install-hooks.mjs",
|
|
72
84
|
"src/update-check.mjs",
|
|
85
|
+
"src/prompt-intel.mjs",
|
|
86
|
+
"src/fx.mjs",
|
|
73
87
|
"bin/*.mjs",
|
|
74
88
|
"hooks/enforce-tier.mjs",
|
|
75
89
|
"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
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// User calibration module — tracks specificity, corrections, and autonomy signals
|
|
2
|
+
// to adapt dual-brain behavior to any user from vague vibe-coder to precise expert.
|
|
3
|
+
|
|
4
|
+
const FILE_REF_RE = /(?:src\/|\.mjs|\.tsx?|\.jsx?|\.json|\.ya?ml|\.sh|line\s+\d+|\bL\d+\b)/i;
|
|
5
|
+
const TECH_TERM_RE = /\b(?:regex|middleware|api|endpoint|refactor|migration|schema|auth(?:entication)?|jwt|token|hook|dispatch|pipeline|module|function|class|interface|type|import|export|async|await|promise|callback|handler|router|controller|service|repository|factory|singleton|decorator|mixin|proxy|guard|interceptor|serializer|validator|transformer|adapter|facade|strategy|observer|subscriber|emitter|stream|buffer|cache|queue|worker|thread|mutex|semaphore|socket|websocket|http|grpc|graphql|rest|orm|sql|nosql|index|query|transaction|migration|seed|fixture|mock|stub|spy|assertion|coverage|lint|typecheck|bundle|compile|transpile|minify|tree.shake|dead.code|chunk|lazy.load|ssr|csr|hydrat)\b/i;
|
|
6
|
+
const VAGUE_RE = /\b(?:idk|just|make\s+it|fix\s+it|whatever|vibes?|better|nicer|faster|cleaner|improve|help|do\s+it|yeah|sure|ok|okay|hmm|uh|er|um)\b/i;
|
|
7
|
+
const AUTONOMY_HIGH_RE = /\b(?:just\s+do\s+it|go(?:\s+ahead)?|build\s+it|ship\s+it|run\s+it|execute|do\s+it|proceed|continue|carry\s+on|handle\s+it|take\s+care|make\s+it\s+happen|yolo)\b/i;
|
|
8
|
+
const QUESTION_RE = /\?|^(?:how|what|why|when|where|which|should|can|could|would|is|are|do|does|did|will|won't|don't)\b/i;
|
|
9
|
+
const CORRECTION_RE = /^(?:no[,.]?|not\s|wrong|stop|don't|that'?s?\s+not|i\s+said|i\s+meant)\b|(?:\binstead\b|\brather\b|\bactually\b)/i;
|
|
10
|
+
|
|
11
|
+
export function analyzeInput(input) {
|
|
12
|
+
const text = (input || '').trim();
|
|
13
|
+
const lower = text.toLowerCase();
|
|
14
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
15
|
+
const wordCount = words.length;
|
|
16
|
+
|
|
17
|
+
const hasFileRefs = FILE_REF_RE.test(text);
|
|
18
|
+
const hasTechTerms = TECH_TERM_RE.test(text);
|
|
19
|
+
const hasExactInstructions = /\b(?:step\s+\d|first[,\s]|then[,\s]|finally[,\s]|specifically|exactly|must|should\s+(?:use|call|return|handle)|(?:use|call|return|throw|emit|dispatch)\s+\w)/i.test(text);
|
|
20
|
+
const isVague = VAGUE_RE.test(lower) || wordCount <= 3;
|
|
21
|
+
|
|
22
|
+
let specificity;
|
|
23
|
+
if (hasFileRefs || /\bline\s*\d+\b|\bL\d+\b/i.test(text) || (hasTechTerms && hasExactInstructions)) {
|
|
24
|
+
specificity = 5;
|
|
25
|
+
} else if (hasTechTerms && wordCount >= 6) {
|
|
26
|
+
specificity = 4;
|
|
27
|
+
} else if (!isVague && wordCount >= 8) {
|
|
28
|
+
specificity = 3;
|
|
29
|
+
} else if (wordCount >= 4 && !isVague) {
|
|
30
|
+
specificity = 2;
|
|
31
|
+
} else {
|
|
32
|
+
specificity = 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
specificity,
|
|
37
|
+
signals: {
|
|
38
|
+
hasFileRefs,
|
|
39
|
+
hasTechTerms,
|
|
40
|
+
hasExactInstructions,
|
|
41
|
+
isVague,
|
|
42
|
+
wordCount
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function detectCorrection(input) {
|
|
48
|
+
return CORRECTION_RE.test((input || '').trim());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clamp(value, min = 1, max = 5) {
|
|
52
|
+
return Math.min(max, Math.max(min, value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function round1(value) {
|
|
56
|
+
return Math.round(value * 10) / 10;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function detectAutonomySignal(input) {
|
|
60
|
+
const text = (input || '').trim();
|
|
61
|
+
if (AUTONOMY_HIGH_RE.test(text)) return 5;
|
|
62
|
+
if (QUESTION_RE.test(text)) return 2;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function updateCalibration(calibration, input, correction = false) {
|
|
67
|
+
const { specificity: newSpec } = analyzeInput(input);
|
|
68
|
+
const autonomySignal = detectAutonomySignal(input);
|
|
69
|
+
|
|
70
|
+
const prev = {
|
|
71
|
+
specificity: calibration.specificity ?? 3,
|
|
72
|
+
corrections: calibration.corrections ?? 3,
|
|
73
|
+
autonomy: calibration.autonomy ?? 3,
|
|
74
|
+
interactions: calibration.interactions ?? 0
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const specificity = round1(clamp(prev.specificity * 0.7 + newSpec * 0.3));
|
|
78
|
+
|
|
79
|
+
const corrections = round1(clamp(
|
|
80
|
+
correction ? prev.corrections - 0.3 : prev.corrections + 0.1
|
|
81
|
+
));
|
|
82
|
+
|
|
83
|
+
let autonomy = prev.autonomy;
|
|
84
|
+
if (autonomySignal !== null) {
|
|
85
|
+
autonomy = round1(clamp(prev.autonomy * 0.7 + autonomySignal * 0.3));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
specificity,
|
|
90
|
+
corrections,
|
|
91
|
+
autonomy,
|
|
92
|
+
interactions: prev.interactions + 1,
|
|
93
|
+
lastUpdated: new Date().toISOString()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getAdaptation(calibration) {
|
|
98
|
+
const s = calibration.specificity ?? 3;
|
|
99
|
+
const c = calibration.corrections ?? 3;
|
|
100
|
+
const a = calibration.autonomy ?? 3;
|
|
101
|
+
|
|
102
|
+
const clarifyBeforeActing = s < 2.5 || c > 3;
|
|
103
|
+
const explainReasoning = a < 3;
|
|
104
|
+
const suggestNextSteps = a < 4;
|
|
105
|
+
const askForApproval = c < 2.5;
|
|
106
|
+
const autoExecute = a > 4 && c > 3.5;
|
|
107
|
+
|
|
108
|
+
let responseStyle;
|
|
109
|
+
const combined = (s + c + a) / 3;
|
|
110
|
+
if (combined >= 4) {
|
|
111
|
+
responseStyle = 'terse';
|
|
112
|
+
} else if (combined >= 2.5) {
|
|
113
|
+
responseStyle = 'normal';
|
|
114
|
+
} else {
|
|
115
|
+
responseStyle = 'detailed';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let userLevel;
|
|
119
|
+
if (s >= 4) {
|
|
120
|
+
userLevel = 'advanced';
|
|
121
|
+
} else if (s >= 2.5) {
|
|
122
|
+
userLevel = 'intermediate';
|
|
123
|
+
} else {
|
|
124
|
+
userLevel = 'beginner';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
clarifyBeforeActing,
|
|
129
|
+
explainReasoning,
|
|
130
|
+
suggestNextSteps,
|
|
131
|
+
askForApproval,
|
|
132
|
+
autoExecute,
|
|
133
|
+
responseStyle,
|
|
134
|
+
userLevel
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatCalibration(calibration) {
|
|
139
|
+
const { userLevel, responseStyle } = getAdaptation(calibration);
|
|
140
|
+
const s = calibration.specificity ?? 3;
|
|
141
|
+
const c = calibration.corrections ?? 3;
|
|
142
|
+
const a = calibration.autonomy ?? 3;
|
|
143
|
+
|
|
144
|
+
const autonomyLabel = a >= 4 ? 'high autonomy' : a >= 2.5 ? 'normal autonomy' : 'low autonomy';
|
|
145
|
+
const trustLabel = c >= 4 ? 'high trust' : c >= 2.5 ? 'good trust' : 'low trust';
|
|
146
|
+
|
|
147
|
+
return `User: ${userLevel} · ${autonomyLabel} · ${trustLabel}\n (specificity: ${s}, corrections: ${c}, autonomy: ${a})`;
|
|
148
|
+
}
|