dual-brain 0.2.30 → 0.3.1
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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/precompact.mjs +3 -3
- package/hooks/session-end.mjs +3 -3
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/install.mjs +2 -2
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/recommendations.mjs
DELETED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
// recommendations.mjs — Proactive settings recommendations from HEAD
|
|
2
|
-
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
function readJSON(path) {
|
|
6
|
-
try {
|
|
7
|
-
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
8
|
-
} catch { return null; }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function dbPath(cwd, ...parts) {
|
|
12
|
-
return join(cwd || process.cwd(), '.dualbrain', ...parts);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// ─── Signal loaders ───────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
function loadRoutingState(cwd) { return readJSON(dbPath(cwd, 'routing-state.json')) || {}; }
|
|
18
|
-
function loadThinkMetrics(cwd) { return readJSON(dbPath(cwd, 'think-metrics.json')) || {}; }
|
|
19
|
-
function loadGovernance(cwd) { return readJSON(dbPath(cwd, 'governance-state.json')) || {}; }
|
|
20
|
-
function loadSubscription(cwd) { return readJSON(dbPath(cwd, 'subscription.json')) || {}; }
|
|
21
|
-
|
|
22
|
-
function loadOutcomes(cwd) {
|
|
23
|
-
try {
|
|
24
|
-
const dir = dbPath(cwd, 'outcomes');
|
|
25
|
-
if (!existsSync(dir)) return [];
|
|
26
|
-
return readdirSync(dir)
|
|
27
|
-
.filter(f => f.endsWith('.json'))
|
|
28
|
-
.map(f => readJSON(join(dir, f)))
|
|
29
|
-
.filter(Boolean);
|
|
30
|
-
} catch { return []; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── Recommendation rules ─────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
function thinkROI(metrics) {
|
|
36
|
-
const { hitRate, totalHits, totalMisses, avgTokensSaved } = metrics;
|
|
37
|
-
if (hitRate == null) return null;
|
|
38
|
-
const observations = (totalHits || 0) + (totalMisses || 0);
|
|
39
|
-
if (observations < 5) return null;
|
|
40
|
-
|
|
41
|
-
if (hitRate < 0.4) {
|
|
42
|
-
return {
|
|
43
|
-
id: 'think-roi-low',
|
|
44
|
-
priority: 'medium',
|
|
45
|
-
category: 'efficiency',
|
|
46
|
-
title: 'Think agent underperforming',
|
|
47
|
-
description: `${Math.round(hitRate * 100)}% hit rate — think preflight isn't saving tokens.`,
|
|
48
|
-
action: 'Consider disabling think triggers or narrowing trigger conditions.',
|
|
49
|
-
impact: 'Reduce latency and token overhead on low-complexity tasks.',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
if (hitRate > 0.7) {
|
|
53
|
-
const savings = avgTokensSaved ? `~${Math.round(avgTokensSaved / 1000)}K tokens` : 'tokens';
|
|
54
|
-
return {
|
|
55
|
-
id: 'think-roi-high',
|
|
56
|
-
priority: 'medium',
|
|
57
|
-
category: 'efficiency',
|
|
58
|
-
title: 'Think agent performing well',
|
|
59
|
-
description: `${Math.round(hitRate * 100)}% hit rate, saving ${savings} per refined task.`,
|
|
60
|
-
action: 'No action needed, keep enabled.',
|
|
61
|
-
impact: 'Sustained token efficiency on complex dispatches.',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function modelMismatch(routingState) {
|
|
68
|
-
const recs = [];
|
|
69
|
-
for (const [taskType, models] of Object.entries(routingState)) {
|
|
70
|
-
if (taskType.startsWith('_')) continue; // skip metadata keys
|
|
71
|
-
for (const [model, stats] of Object.entries(models)) {
|
|
72
|
-
const { ema, observations } = stats || {};
|
|
73
|
-
if (observations >= 10 && ema < 0.4) {
|
|
74
|
-
recs.push({
|
|
75
|
-
id: `model-mismatch-low-${taskType}-${model}`,
|
|
76
|
-
priority: 'high',
|
|
77
|
-
category: 'routing',
|
|
78
|
-
title: 'Model mismatch detected',
|
|
79
|
-
description: `${model} scores ${ema.toFixed(2)} on ${taskType} tasks.`,
|
|
80
|
-
action: `Route ${taskType} tasks away from ${model}.`,
|
|
81
|
-
impact: 'Better task outcomes by avoiding poor model-task fit.',
|
|
82
|
-
});
|
|
83
|
-
} else if (observations >= 10 && ema > 0.8 && (model === 'haiku' || model.includes('haiku'))) {
|
|
84
|
-
recs.push({
|
|
85
|
-
id: `model-mismatch-promote-${taskType}-${model}`,
|
|
86
|
-
priority: 'high',
|
|
87
|
-
category: 'routing',
|
|
88
|
-
title: 'Cheap model excelling',
|
|
89
|
-
description: `${model} scores ${ema.toFixed(2)} on ${taskType} tasks.`,
|
|
90
|
-
action: `Promote ${model} as default for ${taskType} tier — quality without the cost.`,
|
|
91
|
-
impact: 'Same output quality at lower token cost.',
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return recs;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function budgetTrajectory(governance) {
|
|
100
|
-
const { budgetUsedPct, sessionProgressPct, workStyle } = governance;
|
|
101
|
-
if (budgetUsedPct == null) return null;
|
|
102
|
-
|
|
103
|
-
if (budgetUsedPct > 60 && sessionProgressPct != null && sessionProgressPct < 50) {
|
|
104
|
-
return {
|
|
105
|
-
id: 'budget-critical',
|
|
106
|
-
priority: 'high',
|
|
107
|
-
category: 'budget',
|
|
108
|
-
title: 'Budget burning fast',
|
|
109
|
-
description: `${Math.round(budgetUsedPct)}% budget used, ~${Math.round(sessionProgressPct)}% through estimated work.`,
|
|
110
|
-
action: 'Switch to cost-saver mode: `dual-brain config set workStyle cost-saver`.',
|
|
111
|
-
impact: 'Avoid hitting budget ceiling before work completes.',
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
if (budgetUsedPct < 20 && workStyle === 'cost-saver') {
|
|
115
|
-
return {
|
|
116
|
-
id: 'budget-underutilized',
|
|
117
|
-
priority: 'low',
|
|
118
|
-
category: 'budget',
|
|
119
|
-
title: 'Budget well under control',
|
|
120
|
-
description: `Only ${Math.round(budgetUsedPct)}% budget used in cost-saver mode.`,
|
|
121
|
-
action: 'You could afford quality-first mode for this session.',
|
|
122
|
-
impact: 'Better output quality while staying within budget.',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function failurePattern(outcomes) {
|
|
129
|
-
if (!outcomes.length) return null;
|
|
130
|
-
const recent = outcomes.slice(-20);
|
|
131
|
-
const failures = recent.filter(o => o.success === false || (o.reward != null && o.reward < 0.3));
|
|
132
|
-
const failRate = failures.length / recent.length;
|
|
133
|
-
|
|
134
|
-
if (failRate > 0.3) {
|
|
135
|
-
const modelCounts = {};
|
|
136
|
-
failures.forEach(o => { if (o.model) modelCounts[o.model] = (modelCounts[o.model] || 0) + 1; });
|
|
137
|
-
const worstModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0];
|
|
138
|
-
const modelNote = worstModel && worstModel[1] >= 3
|
|
139
|
-
? ` Failures cluster on ${worstModel[0]}.`
|
|
140
|
-
: '';
|
|
141
|
-
return {
|
|
142
|
-
id: 'failure-pattern',
|
|
143
|
-
priority: 'high',
|
|
144
|
-
category: 'quality',
|
|
145
|
-
title: 'High failure rate detected',
|
|
146
|
-
description: `${Math.round(failRate * 100)}% of recent tasks failed.${modelNote}`,
|
|
147
|
-
action: worstModel && worstModel[1] >= 3
|
|
148
|
-
? `Route away from ${worstModel[0]} — or check task ambiguity.`
|
|
149
|
-
: 'Review task clarity and model-task fit.',
|
|
150
|
-
impact: 'Fewer retries, less wasted compute.',
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function subscriptionUtilization(subscription, routingState) {
|
|
157
|
-
const { tier, maxMultiplier } = subscription;
|
|
158
|
-
if (!tier) return null;
|
|
159
|
-
|
|
160
|
-
const routingCells = Object.entries(routingState)
|
|
161
|
-
.filter(([k]) => !k.startsWith('_')) // skip metadata keys
|
|
162
|
-
.map(([, v]) => v);
|
|
163
|
-
|
|
164
|
-
const opusUses = routingCells
|
|
165
|
-
.flatMap(m => Object.entries(m))
|
|
166
|
-
.filter(([model]) => model === 'opus' || model.includes('opus'))
|
|
167
|
-
.reduce((s, [, stats]) => s + (stats.observations || 0), 0);
|
|
168
|
-
|
|
169
|
-
const totalUses = routingCells
|
|
170
|
-
.flatMap(m => Object.values(m))
|
|
171
|
-
.reduce((s, stats) => s + (stats.observations || 0), 0);
|
|
172
|
-
|
|
173
|
-
if (!totalUses) return null;
|
|
174
|
-
const opusPct = opusUses / totalUses;
|
|
175
|
-
|
|
176
|
-
if ((tier === 'max' || (maxMultiplier && maxMultiplier >= 20)) && opusPct < 0.15) {
|
|
177
|
-
return {
|
|
178
|
-
id: 'subscription-underutilized',
|
|
179
|
-
priority: 'medium',
|
|
180
|
-
category: 'profile',
|
|
181
|
-
title: 'Subscription underutilized',
|
|
182
|
-
description: `Max ${maxMultiplier || ''}x plan but opus used only ${Math.round(opusPct * 100)}% of dispatches.`,
|
|
183
|
-
action: 'Consider quality-first mode for better output.',
|
|
184
|
-
impact: 'Get more value from your subscription tier.',
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if ((tier === 'free' || tier === 'pro') && opusPct > 0.4) {
|
|
188
|
-
return {
|
|
189
|
-
id: 'subscription-aggressive',
|
|
190
|
-
priority: 'medium',
|
|
191
|
-
category: 'profile',
|
|
192
|
-
title: 'Routing aggressively for plan',
|
|
193
|
-
description: `${Math.round(opusPct * 100)}% opus usage on a ${tier} plan.`,
|
|
194
|
-
action: 'Switch to balanced or cost-saver to stay within limits.',
|
|
195
|
-
impact: 'Avoid rate limits and unexpected cost overruns.',
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function cascadeEffectiveness(metrics, outcomes) {
|
|
202
|
-
const { cascadeHits } = metrics;
|
|
203
|
-
if (!cascadeHits || cascadeHits < 3) return null;
|
|
204
|
-
|
|
205
|
-
const cascaded = outcomes.filter(o => o.cascaded === true);
|
|
206
|
-
if (cascaded.length < 3) return null;
|
|
207
|
-
|
|
208
|
-
const avgReward = cascaded.reduce((s, o) => s + (o.reward || 0), 0) / cascaded.length;
|
|
209
|
-
if (avgReward > 0.7) {
|
|
210
|
-
return {
|
|
211
|
-
id: 'cascade-effective',
|
|
212
|
-
priority: 'low',
|
|
213
|
-
category: 'efficiency',
|
|
214
|
-
title: 'Cascade routing working well',
|
|
215
|
-
description: `${cascadeHits} cascade hits, ${avgReward.toFixed(2)} avg reward on cascaded tasks.`,
|
|
216
|
-
action: 'Keep cascade enabled — it\'s delivering quality results.',
|
|
217
|
-
impact: 'Continued token efficiency on eligible tasks.',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
return {
|
|
221
|
-
id: 'cascade-poor',
|
|
222
|
-
priority: 'low',
|
|
223
|
-
category: 'efficiency',
|
|
224
|
-
title: 'Cascade delivering poor results',
|
|
225
|
-
description: `${cascadeHits} cascade hits but only ${avgReward.toFixed(2)} avg reward.`,
|
|
226
|
-
action: 'Consider disabling cascade: `dual-brain config set cascade false`.',
|
|
227
|
-
impact: 'Better outcomes by routing cascade tasks to full models.',
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ─── Export 1: generateRecommendations ────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
export function generateRecommendations(cwd) {
|
|
234
|
-
try {
|
|
235
|
-
const routingState = loadRoutingState(cwd);
|
|
236
|
-
const thinkMetrics = loadThinkMetrics(cwd);
|
|
237
|
-
const governance = loadGovernance(cwd);
|
|
238
|
-
const subscription = loadSubscription(cwd);
|
|
239
|
-
const outcomes = loadOutcomes(cwd);
|
|
240
|
-
|
|
241
|
-
const recs = [
|
|
242
|
-
...modelMismatch(routingState),
|
|
243
|
-
failurePattern(outcomes),
|
|
244
|
-
budgetTrajectory(governance),
|
|
245
|
-
thinkROI(thinkMetrics),
|
|
246
|
-
subscriptionUtilization(subscription, routingState),
|
|
247
|
-
cascadeEffectiveness(thinkMetrics, outcomes),
|
|
248
|
-
].filter(Boolean);
|
|
249
|
-
|
|
250
|
-
const order = { high: 0, medium: 1, low: 2 };
|
|
251
|
-
return recs.sort((a, b) => (order[a.priority] ?? 9) - (order[b.priority] ?? 9));
|
|
252
|
-
} catch { return []; }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ─── Export 2: formatRecommendations ─────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
const ICONS = { high: '⚡', medium: '💡', low: '📊' };
|
|
258
|
-
|
|
259
|
-
export function formatRecommendations(recs) {
|
|
260
|
-
const top = recs.slice(0, 4);
|
|
261
|
-
if (!top.length) {
|
|
262
|
-
return '╭─ Recommendations ─────────────────────────────────────────────╮\n' +
|
|
263
|
-
'│ No recommendations — configuration looks healthy. │\n' +
|
|
264
|
-
'╰───────────────────────────────────────────────────────────────╯';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const WIDTH = 63;
|
|
268
|
-
// Truncate + pad to fit inside box: WIDTH - 4 accounts for '│ ' and ' │'
|
|
269
|
-
const INNER = WIDTH - 4;
|
|
270
|
-
const clip = (str) => str.length > INNER ? str.slice(0, INNER - 1) + '…' : str;
|
|
271
|
-
const pad = (str) => clip(str).padEnd(INNER);
|
|
272
|
-
const line = (content) => `│ ${pad(content)} │`;
|
|
273
|
-
|
|
274
|
-
const lines = [
|
|
275
|
-
'╭─ Recommendations ' + '─'.repeat(WIDTH - 20) + '╮',
|
|
276
|
-
line(''),
|
|
277
|
-
];
|
|
278
|
-
|
|
279
|
-
for (const rec of top) {
|
|
280
|
-
const icon = ICONS[rec.priority] || '•';
|
|
281
|
-
lines.push(line(`${icon} ${rec.priority.toUpperCase()}: ${rec.title}`));
|
|
282
|
-
lines.push(line(` ${rec.description}`));
|
|
283
|
-
lines.push(line(` → ${rec.action}`));
|
|
284
|
-
lines.push(line(''));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
lines.push('╰' + '─'.repeat(WIDTH - 2) + '╯');
|
|
288
|
-
return lines.join('\n');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── Export 3: getTopRecommendation ──────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
export function getTopRecommendation(cwd) {
|
|
294
|
-
const recs = generateRecommendations(cwd);
|
|
295
|
-
return recs.length ? recs[0] : null;
|
|
296
|
-
}
|
package/src/redact.mjs
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// redact.mjs — Secret redaction utility for dual-brain orchestrator.
|
|
3
|
-
// SAFETY-CRITICAL: nothing reaches AI dispatch without passing through redaction.
|
|
4
|
-
// All functions are synchronous and regex-based — no external dependencies.
|
|
5
|
-
// Exports: redact, redactFiles, isSecretFile
|
|
6
|
-
|
|
7
|
-
import { basename, extname } from 'node:path';
|
|
8
|
-
|
|
9
|
-
// ─── Non-secret placeholder values (skip redaction) ──────────────────────────
|
|
10
|
-
const PLACEHOLDER_PATTERN = /^(xxx+|changeme|placeholder|your[_-].+|example|fake|dummy|test|none|null|true|false|0|1|<[^>]+>|\$\{[^}]+\}|%[A-Z_]+%|\*+|\.+)$/i;
|
|
11
|
-
|
|
12
|
-
// ─── Secret patterns ──────────────────────────────────────────────────────────
|
|
13
|
-
// Each entry: { pattern: RegExp, replacer: Function|string }
|
|
14
|
-
// replacer receives the full match; return the redacted string.
|
|
15
|
-
// IMPORTANT: Only redact the value portion, not the key name.
|
|
16
|
-
|
|
17
|
-
const REDACT_PATTERNS = [
|
|
18
|
-
// .env-style: KEY=VALUE (key contains KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)
|
|
19
|
-
{
|
|
20
|
-
pattern: /\b([A-Z_]*(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*\s*=\s*)([^\s\n"'`]+)/gi,
|
|
21
|
-
replacer: (_m, key, _kw, val) => isPlaceholder(val) ? _m : `${key}[REDACTED]`,
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
// Explicit named keys (case-insensitive) with = or : assignment
|
|
25
|
-
// Covers: API_KEY=, OPENAI_API_KEY=, ANTHROPIC_API_KEY=, etc.
|
|
26
|
-
{
|
|
27
|
-
pattern: /\b((?:api[_-]?key|openai[_-]api[_-]key|anthropic[_-]api[_-]key|aws[_-]secret[_-]access[_-]key|aws[_-]access[_-]key[_-]id|private[_-]key|passwd)\s*[=:]\s*["'`]?)([^\s"'`\n,;]+)(["'`]?)/gi,
|
|
28
|
-
replacer: (_m, prefix, val, suffix) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]${suffix}`,
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
// Passwords: password=xxx / PASSWORD="xxx" / passwd: xxx
|
|
32
|
-
{
|
|
33
|
-
pattern: /\b(passwords?\s*[=:]\s*["'`]?)([^\s"'`\n,;]+)(["'`]?)/gi,
|
|
34
|
-
replacer: (_m, prefix, val, suffix) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]${suffix}`,
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
// Bearer tokens: Bearer xxx / Authorization: Bearer xxx
|
|
38
|
-
{
|
|
39
|
-
pattern: /(Bearer\s+)([A-Za-z0-9\-._~+/]+=*)/g,
|
|
40
|
-
replacer: (_m, prefix, val) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]`,
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
// Authorization header value (non-Bearer forms)
|
|
44
|
-
{
|
|
45
|
-
pattern: /(Authorization\s*:\s*)([^\s\n][^\n]*)/gi,
|
|
46
|
-
replacer: (_m, prefix, val) => {
|
|
47
|
-
const trimmed = val.trim();
|
|
48
|
-
if (isPlaceholder(trimmed)) return _m;
|
|
49
|
-
// Keep the auth scheme visible (Basic, Digest, etc.) but redact the credential
|
|
50
|
-
const schemeMatch = trimmed.match(/^(\w+)\s+(.+)$/);
|
|
51
|
-
if (schemeMatch) return `${prefix}${schemeMatch[1]} [REDACTED]`;
|
|
52
|
-
return `${prefix}[REDACTED]`;
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
// AWS credentials
|
|
57
|
-
{
|
|
58
|
-
pattern: /\b((?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key|AWS_ACCESS_KEY_ID|aws_access_key_id)\s*[=:]\s*["'`]?)([^\s"'`\n,;]+)(["'`]?)/g,
|
|
59
|
-
replacer: (_m, prefix, val, suffix) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]${suffix}`,
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
// Connection strings: ://user:password@host
|
|
63
|
-
{
|
|
64
|
-
pattern: /([\w+.-]+:\/\/[^:@\s]+:)([^@\s]+)(@)/g,
|
|
65
|
-
replacer: (_m, prefix, pass, at) => isPlaceholder(pass) ? _m : `${prefix}[REDACTED]${at}`,
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
// Inline JSON: "api_key": "value", "secret": "...", "token": "..."
|
|
69
|
-
{
|
|
70
|
-
pattern: /("(?:api[_-]?key|secret|token|password|passwd|credential|auth[_-]?key|private[_-]?key)"\s*:\s*")([^"]*?)(")/gi,
|
|
71
|
-
replacer: (_m, prefix, val, suffix) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]${suffix}`,
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// Inline JSON with single quotes
|
|
75
|
-
{
|
|
76
|
-
pattern: /('(?:api[_-]?key|secret|token|password|passwd|credential|auth[_-]?key|private[_-]?key)'\s*:\s*')([^']*?)(')/gi,
|
|
77
|
-
replacer: (_m, prefix, val, suffix) => isPlaceholder(val) ? _m : `${prefix}[REDACTED]${suffix}`,
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
// Common secret value prefixes: sk-, pk_, ghp_, gho_, npm_, pypi-
|
|
81
|
-
// Match these as standalone tokens (not inside process.env.X or function calls)
|
|
82
|
-
{
|
|
83
|
-
pattern: /(?<![.\w])(sk-[A-Za-z0-9\-_]{8,}|pk_(?:live|test)_[A-Za-z0-9]{8,}|ghp_[A-Za-z0-9]{8,}|gho_[A-Za-z0-9]{8,}|npm_[A-Za-z0-9]{8,}|pypi-[A-Za-z0-9\-]{8,})/g,
|
|
84
|
-
replacer: '[REDACTED]',
|
|
85
|
-
},
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
// ─── Secret file patterns ─────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
const SECRET_FILE_PATTERNS = [
|
|
91
|
-
// .env files
|
|
92
|
-
/(?:^|\/)\.env(?:\.[a-zA-Z0-9._-]+)?$/,
|
|
93
|
-
// Credential / service-account JSON files
|
|
94
|
-
/(?:^|\/)(?:credentials|service-account|serviceaccount)(?:\.[a-zA-Z0-9._-]+)?\.json$/i,
|
|
95
|
-
// Private key files
|
|
96
|
-
/\.pem$/i,
|
|
97
|
-
/\.key$/i,
|
|
98
|
-
// Git internals
|
|
99
|
-
/(?:^|\/)\.git\//,
|
|
100
|
-
// node_modules
|
|
101
|
-
/(?:^|\/)node_modules\//,
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
function isPlaceholder(value) {
|
|
107
|
-
if (!value) return true;
|
|
108
|
-
return PLACEHOLDER_PATTERN.test(value.trim());
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Scan text for common secret patterns and replace values with [REDACTED].
|
|
115
|
-
* Returns the cleaned text. Fast — pure regex, no I/O.
|
|
116
|
-
*
|
|
117
|
-
* @param {string} text
|
|
118
|
-
* @returns {string}
|
|
119
|
-
*/
|
|
120
|
-
function redact(text) {
|
|
121
|
-
if (!text || typeof text !== 'string') return text;
|
|
122
|
-
|
|
123
|
-
let result = text;
|
|
124
|
-
|
|
125
|
-
for (const { pattern, replacer } of REDACT_PATTERNS) {
|
|
126
|
-
// Reset lastIndex for global regexes to avoid skipped matches
|
|
127
|
-
pattern.lastIndex = 0;
|
|
128
|
-
|
|
129
|
-
if (typeof replacer === 'string') {
|
|
130
|
-
result = result.replace(pattern, replacer);
|
|
131
|
-
} else {
|
|
132
|
-
result = result.replace(pattern, replacer);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Reset again after use
|
|
136
|
-
pattern.lastIndex = 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Given a list of file paths, return a Set of paths that should NOT be sent
|
|
144
|
-
* as context to agents (secret files, .git, node_modules).
|
|
145
|
-
*
|
|
146
|
-
* @param {string[]} filePaths
|
|
147
|
-
* @param {string} [cwd]
|
|
148
|
-
* @returns {Set<string>}
|
|
149
|
-
*/
|
|
150
|
-
function redactFiles(filePaths, cwd) {
|
|
151
|
-
const blocked = new Set();
|
|
152
|
-
for (const fp of filePaths) {
|
|
153
|
-
if (isSecretFile(fp)) blocked.add(fp);
|
|
154
|
-
}
|
|
155
|
-
return blocked;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Returns true if the file path matches known secret/sensitive patterns.
|
|
160
|
-
*
|
|
161
|
-
* @param {string} filePath
|
|
162
|
-
* @returns {boolean}
|
|
163
|
-
*/
|
|
164
|
-
function isSecretFile(filePath) {
|
|
165
|
-
if (!filePath) return false;
|
|
166
|
-
// Normalise Windows separators
|
|
167
|
-
const normalised = filePath.replace(/\\/g, '/');
|
|
168
|
-
return SECRET_FILE_PATTERNS.some(p => p.test(normalised));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ─── CLI (smoke test) ─────────────────────────────────────────────────────────
|
|
172
|
-
if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
|
|
173
|
-
const samples = [
|
|
174
|
-
'OPENAI_API_KEY=sk-abc123secretvalue',
|
|
175
|
-
'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig',
|
|
176
|
-
'password=changeme',
|
|
177
|
-
'password=supersecret123',
|
|
178
|
-
'{"api_key": "sk-proj-abcdefgh12345678"}',
|
|
179
|
-
'process.env.API_KEY',
|
|
180
|
-
'getSecret("my-key")',
|
|
181
|
-
'connect postgresql://admin:s3cr3t@db.host.com/mydb',
|
|
182
|
-
'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
183
|
-
'ghp_ABCDEF1234567890abcdef1234567890',
|
|
184
|
-
];
|
|
185
|
-
for (const s of samples) {
|
|
186
|
-
console.log(`IN : ${s}`);
|
|
187
|
-
console.log(`OUT: ${redact(s)}`);
|
|
188
|
-
console.log();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export { redact, redactFiles, isSecretFile };
|