dual-brain 0.2.30 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/.dual-brain/docs/claude-code-extension-points.md +32 -0
  2. package/.dual-brain/docs/data-tools-capabilities.md +181 -0
  3. package/.dual-brain/docs/ecosystem-tools.md +91 -0
  4. package/.dual-brain/docs/panel-handoff.md +124 -0
  5. package/.dual-brain/docs/ruflo-analysis.md +48 -0
  6. package/bin/dual-brain.mjs +56 -56
  7. package/dist/mcp-server/index.d.ts +27 -0
  8. package/dist/mcp-server/index.js +359 -0
  9. package/dist/mcp-server/index.js.map +1 -0
  10. package/dist/src/agent-protocol.d.ts +163 -0
  11. package/dist/src/agent-protocol.js +368 -0
  12. package/dist/src/agent-protocol.js.map +1 -0
  13. package/dist/src/agents/registry.d.ts +52 -0
  14. package/dist/src/agents/registry.js +393 -0
  15. package/dist/src/agents/registry.js.map +1 -0
  16. package/dist/src/awareness.d.ts +93 -0
  17. package/dist/src/awareness.js +406 -0
  18. package/dist/src/awareness.js.map +1 -0
  19. package/dist/src/brief.d.ts +48 -0
  20. package/dist/src/brief.js +179 -0
  21. package/dist/src/brief.js.map +1 -0
  22. package/dist/src/calibration.d.ts +32 -0
  23. package/dist/src/calibration.js +133 -0
  24. package/dist/src/calibration.js.map +1 -0
  25. package/dist/src/checkpoint.d.ts +33 -0
  26. package/dist/src/checkpoint.js +99 -0
  27. package/dist/src/checkpoint.js.map +1 -0
  28. package/dist/src/ci-triage.d.ts +33 -0
  29. package/dist/src/ci-triage.js +193 -0
  30. package/dist/src/ci-triage.js.map +1 -0
  31. package/dist/src/cognitive-loop.d.ts +56 -0
  32. package/dist/src/cognitive-loop.js +495 -0
  33. package/dist/src/cognitive-loop.js.map +1 -0
  34. package/dist/src/collaboration.d.ts +147 -0
  35. package/dist/src/collaboration.js +438 -0
  36. package/dist/src/collaboration.js.map +1 -0
  37. package/dist/src/context-intel.d.ts +47 -0
  38. package/dist/src/context-intel.js +156 -0
  39. package/dist/src/context-intel.js.map +1 -0
  40. package/dist/src/context.d.ts +53 -0
  41. package/dist/src/context.js +332 -0
  42. package/dist/src/context.js.map +1 -0
  43. package/dist/src/continuity.d.ts +89 -0
  44. package/dist/src/continuity.js +230 -0
  45. package/dist/src/continuity.js.map +1 -0
  46. package/dist/src/cost-tracker.d.ts +47 -0
  47. package/dist/src/cost-tracker.js +170 -0
  48. package/dist/src/cost-tracker.js.map +1 -0
  49. package/dist/src/debrief.d.ts +53 -0
  50. package/dist/src/debrief.js +222 -0
  51. package/dist/src/debrief.js.map +1 -0
  52. package/dist/src/decide.d.ts +96 -0
  53. package/dist/src/decide.js +744 -0
  54. package/dist/src/decide.js.map +1 -0
  55. package/dist/src/decompose.d.ts +39 -0
  56. package/dist/src/decompose.js +218 -0
  57. package/dist/src/decompose.js.map +1 -0
  58. package/dist/src/detect.d.ts +91 -0
  59. package/dist/src/detect.js +544 -0
  60. package/dist/src/detect.js.map +1 -0
  61. package/dist/src/dispatch.d.ts +154 -0
  62. package/dist/src/dispatch.js +1306 -0
  63. package/dist/src/dispatch.js.map +1 -0
  64. package/dist/src/doctor.d.ts +421 -0
  65. package/dist/src/doctor.js +1689 -0
  66. package/dist/src/doctor.js.map +1 -0
  67. package/dist/src/engine.d.ts +70 -0
  68. package/dist/src/engine.js +155 -0
  69. package/dist/src/engine.js.map +1 -0
  70. package/dist/src/envelope.d.ts +36 -0
  71. package/dist/src/envelope.js +80 -0
  72. package/dist/src/envelope.js.map +1 -0
  73. package/dist/src/failure-memory.d.ts +55 -0
  74. package/dist/src/failure-memory.js +175 -0
  75. package/dist/src/failure-memory.js.map +1 -0
  76. package/dist/src/fx.d.ts +87 -0
  77. package/dist/src/fx.js +272 -0
  78. package/dist/src/fx.js.map +1 -0
  79. package/dist/src/governance.d.ts +93 -0
  80. package/dist/src/governance.js +261 -0
  81. package/dist/src/governance.js.map +1 -0
  82. package/dist/src/handoff.d.ts +11 -0
  83. package/dist/src/handoff.js +90 -0
  84. package/dist/src/handoff.js.map +1 -0
  85. package/dist/src/head-protocol.d.ts +76 -0
  86. package/dist/src/head-protocol.js +109 -0
  87. package/dist/src/head-protocol.js.map +1 -0
  88. package/dist/src/head.d.ts +222 -0
  89. package/dist/src/head.js +765 -0
  90. package/dist/src/head.js.map +1 -0
  91. package/dist/src/health.d.ts +132 -0
  92. package/dist/src/health.js +435 -0
  93. package/dist/src/health.js.map +1 -0
  94. package/dist/src/inbox.d.ts +70 -0
  95. package/dist/src/inbox.js +218 -0
  96. package/dist/src/inbox.js.map +1 -0
  97. package/dist/src/index.d.ts +33 -0
  98. package/dist/src/index.js +38 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/install-hooks.d.ts +13 -0
  101. package/dist/src/install-hooks.js +88 -0
  102. package/dist/src/install-hooks.js.map +1 -0
  103. package/dist/src/integrity.d.ts +59 -0
  104. package/dist/src/integrity.js +206 -0
  105. package/dist/src/integrity.js.map +1 -0
  106. package/dist/src/intelligence.d.ts +104 -0
  107. package/dist/src/intelligence.js +391 -0
  108. package/dist/src/intelligence.js.map +1 -0
  109. package/dist/src/ledger.d.ts +54 -0
  110. package/dist/src/ledger.js +179 -0
  111. package/dist/src/ledger.js.map +1 -0
  112. package/dist/src/living-docs.d.ts +14 -0
  113. package/dist/src/living-docs.js +197 -0
  114. package/dist/src/living-docs.js.map +1 -0
  115. package/dist/src/memory-tiers.d.ts +37 -0
  116. package/dist/src/memory-tiers.js +160 -0
  117. package/dist/src/memory-tiers.js.map +1 -0
  118. package/dist/src/model-profiles.d.ts +65 -0
  119. package/dist/src/model-profiles.js +568 -0
  120. package/dist/src/model-profiles.js.map +1 -0
  121. package/dist/src/models.d.ts +58 -0
  122. package/dist/src/models.js +327 -0
  123. package/dist/src/models.js.map +1 -0
  124. package/dist/src/narrative.d.ts +54 -0
  125. package/dist/src/narrative.js +163 -0
  126. package/dist/src/narrative.js.map +1 -0
  127. package/dist/src/nextstep.d.ts +16 -0
  128. package/dist/src/nextstep.js +103 -0
  129. package/dist/src/nextstep.js.map +1 -0
  130. package/dist/src/observer.d.ts +18 -0
  131. package/dist/src/observer.js +251 -0
  132. package/dist/src/observer.js.map +1 -0
  133. package/dist/src/outcome.d.ts +110 -0
  134. package/dist/src/outcome.js +377 -0
  135. package/dist/src/outcome.js.map +1 -0
  136. package/dist/src/pipeline.d.ts +167 -0
  137. package/dist/src/pipeline.js +1503 -0
  138. package/dist/src/pipeline.js.map +1 -0
  139. package/dist/src/playbook.d.ts +59 -0
  140. package/dist/src/playbook.js +238 -0
  141. package/dist/src/playbook.js.map +1 -0
  142. package/dist/src/pr-agent.d.ts +97 -0
  143. package/dist/src/pr-agent.js +195 -0
  144. package/dist/src/pr-agent.js.map +1 -0
  145. package/dist/src/predictive.d.ts +57 -0
  146. package/dist/src/predictive.js +230 -0
  147. package/dist/src/predictive.js.map +1 -0
  148. package/dist/src/profile.d.ts +294 -0
  149. package/dist/src/profile.js +1347 -0
  150. package/dist/src/profile.js.map +1 -0
  151. package/dist/src/prompt-audit.d.ts +22 -0
  152. package/dist/src/prompt-audit.js +194 -0
  153. package/dist/src/prompt-audit.js.map +1 -0
  154. package/dist/src/prompt-intel.d.ts +12 -0
  155. package/dist/src/prompt-intel.js +321 -0
  156. package/dist/src/prompt-intel.js.map +1 -0
  157. package/dist/src/provider-context.d.ts +121 -0
  158. package/dist/src/provider-context.js +222 -0
  159. package/dist/src/provider-context.js.map +1 -0
  160. package/dist/src/provider-manager.d.ts +92 -0
  161. package/dist/src/provider-manager.js +428 -0
  162. package/dist/src/provider-manager.js.map +1 -0
  163. package/dist/src/receipt.d.ts +87 -0
  164. package/dist/src/receipt.js +326 -0
  165. package/dist/src/receipt.js.map +1 -0
  166. package/dist/src/recommendations.d.ts +13 -0
  167. package/dist/src/recommendations.js +291 -0
  168. package/dist/src/recommendations.js.map +1 -0
  169. package/dist/src/redact.d.ts +15 -0
  170. package/dist/src/redact.js +129 -0
  171. package/dist/src/redact.js.map +1 -0
  172. package/dist/src/replit.d.ts +397 -0
  173. package/dist/src/replit.js +1160 -0
  174. package/dist/src/replit.js.map +1 -0
  175. package/dist/src/repo.d.ts +149 -0
  176. package/dist/src/repo.js +416 -0
  177. package/dist/src/repo.js.map +1 -0
  178. package/dist/src/revert.d.ts +30 -0
  179. package/dist/src/revert.js +166 -0
  180. package/dist/src/revert.js.map +1 -0
  181. package/dist/src/room.d.ts +102 -0
  182. package/dist/src/room.js +212 -0
  183. package/dist/src/room.js.map +1 -0
  184. package/dist/src/routing-advisor.d.ts +57 -0
  185. package/dist/src/routing-advisor.js +221 -0
  186. package/dist/src/routing-advisor.js.map +1 -0
  187. package/dist/src/self-correct.d.ts +40 -0
  188. package/dist/src/self-correct.js +137 -0
  189. package/dist/src/self-correct.js.map +1 -0
  190. package/dist/src/session-lock.d.ts +35 -0
  191. package/dist/src/session-lock.js +134 -0
  192. package/dist/src/session-lock.js.map +1 -0
  193. package/dist/src/session.d.ts +267 -0
  194. package/dist/src/session.js +1660 -0
  195. package/dist/src/session.js.map +1 -0
  196. package/dist/src/settings-tui.d.ts +5 -0
  197. package/dist/src/settings-tui.js +422 -0
  198. package/dist/src/settings-tui.js.map +1 -0
  199. package/dist/src/setup-flow.d.ts +63 -0
  200. package/dist/src/setup-flow.js +233 -0
  201. package/dist/src/setup-flow.js.map +1 -0
  202. package/dist/src/signal.d.ts +19 -0
  203. package/dist/src/signal.js +122 -0
  204. package/dist/src/signal.js.map +1 -0
  205. package/dist/src/simmer.d.ts +85 -0
  206. package/dist/src/simmer.js +224 -0
  207. package/dist/src/simmer.js.map +1 -0
  208. package/dist/src/state-export.d.ts +129 -0
  209. package/dist/src/state-export.js +233 -0
  210. package/dist/src/state-export.js.map +1 -0
  211. package/dist/src/strategy.d.ts +54 -0
  212. package/dist/src/strategy.js +95 -0
  213. package/dist/src/strategy.js.map +1 -0
  214. package/dist/src/subscription.d.ts +40 -0
  215. package/dist/src/subscription.js +189 -0
  216. package/dist/src/subscription.js.map +1 -0
  217. package/dist/src/templates.d.ts +208 -0
  218. package/dist/src/templates.js +238 -0
  219. package/dist/src/templates.js.map +1 -0
  220. package/dist/src/test.d.ts +9 -0
  221. package/dist/src/test.js +1173 -0
  222. package/dist/src/test.js.map +1 -0
  223. package/dist/src/think-engine.d.ts +67 -0
  224. package/dist/src/think-engine.js +412 -0
  225. package/dist/src/think-engine.js.map +1 -0
  226. package/dist/src/tui.d.ts +71 -0
  227. package/dist/src/tui.js +242 -0
  228. package/dist/src/tui.js.map +1 -0
  229. package/dist/src/types.d.ts +177 -0
  230. package/dist/src/types.js +6 -0
  231. package/dist/src/types.js.map +1 -0
  232. package/dist/src/update-check.d.ts +7 -0
  233. package/dist/src/update-check.js +36 -0
  234. package/dist/src/update-check.js.map +1 -0
  235. package/dist/src/wave-planner.d.ts +30 -0
  236. package/dist/src/wave-planner.js +281 -0
  237. package/dist/src/wave-planner.js.map +1 -0
  238. package/hooks/head-guard.sh +41 -0
  239. package/hooks/task-classifier.mjs +328 -0
  240. package/hooks/vibe-router.mjs +387 -0
  241. package/package.json +29 -153
  242. package/src/agents/registry.mjs +0 -405
  243. package/src/awareness.mjs +0 -425
  244. package/src/brief.mjs +0 -266
  245. package/src/calibration.mjs +0 -148
  246. package/src/checkpoint.mjs +0 -109
  247. package/src/ci-triage.mjs +0 -191
  248. package/src/cognitive-loop.mjs +0 -562
  249. package/src/collaboration.mjs +0 -545
  250. package/src/context-intel.mjs +0 -158
  251. package/src/context.mjs +0 -389
  252. package/src/continuity.mjs +0 -298
  253. package/src/cost-tracker.mjs +0 -184
  254. package/src/debrief.mjs +0 -228
  255. package/src/decide.mjs +0 -1099
  256. package/src/decompose.mjs +0 -331
  257. package/src/detect.mjs +0 -702
  258. package/src/dispatch.mjs +0 -1447
  259. package/src/doctor.mjs +0 -1607
  260. package/src/envelope.mjs +0 -139
  261. package/src/failure-memory.mjs +0 -178
  262. package/src/fx.mjs +0 -276
  263. package/src/governance.mjs +0 -279
  264. package/src/handoff.mjs +0 -87
  265. package/src/head-protocol.mjs +0 -128
  266. package/src/head.mjs +0 -952
  267. package/src/health.mjs +0 -528
  268. package/src/inbox.mjs +0 -195
  269. package/src/index.mjs +0 -44
  270. package/src/install-hooks.mjs +0 -100
  271. package/src/integrity.mjs +0 -245
  272. package/src/intelligence.mjs +0 -447
  273. package/src/ledger.mjs +0 -196
  274. package/src/living-docs.mjs +0 -210
  275. package/src/memory-tiers.mjs +0 -193
  276. package/src/models.mjs +0 -363
  277. package/src/narrative.mjs +0 -169
  278. package/src/nextstep.mjs +0 -100
  279. package/src/observer.mjs +0 -241
  280. package/src/outcome.mjs +0 -400
  281. package/src/pipeline.mjs +0 -1711
  282. package/src/playbook.mjs +0 -257
  283. package/src/pr-agent.mjs +0 -214
  284. package/src/predictive.mjs +0 -250
  285. package/src/profile.mjs +0 -1411
  286. package/src/prompt-audit.mjs +0 -231
  287. package/src/prompt-intel.mjs +0 -325
  288. package/src/provider-context.mjs +0 -257
  289. package/src/receipt.mjs +0 -344
  290. package/src/recommendations.mjs +0 -296
  291. package/src/redact.mjs +0 -192
  292. package/src/replit.mjs +0 -1210
  293. package/src/repo.mjs +0 -445
  294. package/src/revert.mjs +0 -149
  295. package/src/routing-advisor.mjs +0 -204
  296. package/src/self-correct.mjs +0 -147
  297. package/src/session-lock.mjs +0 -160
  298. package/src/session.mjs +0 -1655
  299. package/src/settings-tui.mjs +0 -373
  300. package/src/setup-flow.mjs +0 -223
  301. package/src/signal.mjs +0 -115
  302. package/src/simmer.mjs +0 -241
  303. package/src/strategy.mjs +0 -235
  304. package/src/subscription.mjs +0 -212
  305. package/src/templates.mjs +0 -260
  306. package/src/think-engine.mjs +0 -428
  307. package/src/tui.mjs +0 -276
  308. package/src/update-check.mjs +0 -35
  309. package/src/wave-planner.mjs +0 -294
@@ -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 };