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.
Files changed (312) 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/precompact.mjs +3 -3
  240. package/hooks/session-end.mjs +3 -3
  241. package/hooks/task-classifier.mjs +328 -0
  242. package/hooks/vibe-router.mjs +387 -0
  243. package/install.mjs +2 -2
  244. package/package.json +29 -153
  245. package/src/agents/registry.mjs +0 -405
  246. package/src/awareness.mjs +0 -425
  247. package/src/brief.mjs +0 -266
  248. package/src/calibration.mjs +0 -148
  249. package/src/checkpoint.mjs +0 -109
  250. package/src/ci-triage.mjs +0 -191
  251. package/src/cognitive-loop.mjs +0 -562
  252. package/src/collaboration.mjs +0 -545
  253. package/src/context-intel.mjs +0 -158
  254. package/src/context.mjs +0 -389
  255. package/src/continuity.mjs +0 -298
  256. package/src/cost-tracker.mjs +0 -184
  257. package/src/debrief.mjs +0 -228
  258. package/src/decide.mjs +0 -1099
  259. package/src/decompose.mjs +0 -331
  260. package/src/detect.mjs +0 -702
  261. package/src/dispatch.mjs +0 -1447
  262. package/src/doctor.mjs +0 -1607
  263. package/src/envelope.mjs +0 -139
  264. package/src/failure-memory.mjs +0 -178
  265. package/src/fx.mjs +0 -276
  266. package/src/governance.mjs +0 -279
  267. package/src/handoff.mjs +0 -87
  268. package/src/head-protocol.mjs +0 -128
  269. package/src/head.mjs +0 -952
  270. package/src/health.mjs +0 -528
  271. package/src/inbox.mjs +0 -195
  272. package/src/index.mjs +0 -44
  273. package/src/install-hooks.mjs +0 -100
  274. package/src/integrity.mjs +0 -245
  275. package/src/intelligence.mjs +0 -447
  276. package/src/ledger.mjs +0 -196
  277. package/src/living-docs.mjs +0 -210
  278. package/src/memory-tiers.mjs +0 -193
  279. package/src/models.mjs +0 -363
  280. package/src/narrative.mjs +0 -169
  281. package/src/nextstep.mjs +0 -100
  282. package/src/observer.mjs +0 -241
  283. package/src/outcome.mjs +0 -400
  284. package/src/pipeline.mjs +0 -1711
  285. package/src/playbook.mjs +0 -257
  286. package/src/pr-agent.mjs +0 -214
  287. package/src/predictive.mjs +0 -250
  288. package/src/profile.mjs +0 -1411
  289. package/src/prompt-audit.mjs +0 -231
  290. package/src/prompt-intel.mjs +0 -325
  291. package/src/provider-context.mjs +0 -257
  292. package/src/receipt.mjs +0 -344
  293. package/src/recommendations.mjs +0 -296
  294. package/src/redact.mjs +0 -192
  295. package/src/replit.mjs +0 -1210
  296. package/src/repo.mjs +0 -445
  297. package/src/revert.mjs +0 -149
  298. package/src/routing-advisor.mjs +0 -204
  299. package/src/self-correct.mjs +0 -147
  300. package/src/session-lock.mjs +0 -160
  301. package/src/session.mjs +0 -1655
  302. package/src/settings-tui.mjs +0 -373
  303. package/src/setup-flow.mjs +0 -223
  304. package/src/signal.mjs +0 -115
  305. package/src/simmer.mjs +0 -241
  306. package/src/strategy.mjs +0 -235
  307. package/src/subscription.mjs +0 -212
  308. package/src/templates.mjs +0 -260
  309. package/src/think-engine.mjs +0 -428
  310. package/src/tui.mjs +0 -276
  311. package/src/update-check.mjs +0 -35
  312. 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 };