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,204 +0,0 @@
1
- // routing-advisor.mjs — EMA + epsilon-greedy routing advisor
2
- // Learns which model works best for which task type from outcome signals.
3
-
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
5
- import { checkFileSurvival } from './outcome.mjs';
6
- import { join } from 'node:path';
7
-
8
- const ALPHA = 0.3;
9
- const MIN_EPSILON = 0.1;
10
- const MIN_OBSERVATIONS = 5;
11
- const PRIOR_WEIGHT = 5;
12
-
13
- const STATIC_PRIORS = {
14
- 'search:haiku': 0.85, 'search:sonnet': 0.70, 'search:opus': 0.50,
15
- 'execute:haiku': 0.55, 'execute:sonnet': 0.80, 'execute:opus': 0.85,
16
- 'think:haiku': 0.30, 'think:sonnet': 0.70, 'think:opus': 0.90,
17
- 'review:haiku': 0.40, 'review:sonnet': 0.75, 'review:opus': 0.85,
18
- };
19
-
20
- const VALID_MODELS = {
21
- search: ['haiku', 'sonnet'],
22
- execute: ['haiku', 'sonnet', 'opus'],
23
- think: ['sonnet', 'opus'],
24
- review: ['sonnet', 'opus'],
25
- };
26
-
27
- function stateFile(cwd) { return join(cwd || process.cwd(), '.dualbrain', 'routing-state.json'); }
28
-
29
- function loadState(cwd) {
30
- try {
31
- const p = stateFile(cwd);
32
- if (!existsSync(p)) return {};
33
- const raw = readFileSync(p, 'utf8');
34
- const parsed = JSON.parse(raw);
35
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
36
- return parsed;
37
- } catch { return {}; }
38
- }
39
-
40
- function saveState(state, cwd) {
41
- try {
42
- const dir = join(cwd || process.cwd(), '.dualbrain');
43
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
44
- const p = stateFile(cwd), tmp = p + '.tmp';
45
- writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf8');
46
- renameSync(tmp, p);
47
- } catch { /* non-throwing */ }
48
- }
49
-
50
- /** Cross-cell bias: average EMA from same-tier cells that have >= 8 observations. */
51
- function getCrossCellBias(state, cellKey, model) {
52
- const [tier] = cellKey.split(':');
53
- let biasSum = 0, biasCount = 0;
54
- for (const [key, models] of Object.entries(state)) {
55
- if (key.startsWith(tier + ':') && key !== cellKey && models[model]) {
56
- const entry = models[model];
57
- if ((entry.observations ?? 0) >= 8) { biasSum += entry.ema; biasCount++; }
58
- }
59
- }
60
- return biasCount > 0 ? biasSum / biasCount : null;
61
- }
62
-
63
- const staticPrior = (tier, model) => STATIC_PRIORS[`${tier}:${model}`] ?? 0.5;
64
- const cellObs = (state, key) => Object.values(state[key] ?? {}).reduce((s, m) => s + (m.observations ?? 0), 0);
65
- const blended = (ema, n, tier, model) =>
66
- (n / (n + PRIOR_WEIGHT)) * ema + (PRIOR_WEIGHT / (n + PRIOR_WEIGHT)) * staticPrior(tier, model);
67
-
68
- // taskProfile: { intent, tier, risk, files?, complexity? }
69
- // Returns: { model, reason, confidence, explored }
70
- export function adviseModel(taskProfile, cwd) {
71
- try {
72
- const { tier, intent } = taskProfile ?? {};
73
- const validTier = tier && VALID_MODELS[tier] ? tier : 'execute';
74
- const cellKey = `${validTier}:${intent ?? 'implement'}`;
75
- const models = VALID_MODELS[validTier];
76
-
77
- const state = loadState(cwd);
78
- const totalObs = cellObs(state, cellKey);
79
- const grandTotal = Object.values(state).reduce((s, cell) =>
80
- s + Object.values(cell).reduce((t, e) => t + (e.observations ?? 0), 0), 0);
81
-
82
- if (totalObs < MIN_OBSERVATIONS) {
83
- // When enough global data exists, blend cross-cell bias with static prior
84
- if (grandTotal > 100) {
85
- let bestModel = models[0], bestScore = -Infinity;
86
- for (const m of models) {
87
- const xbias = getCrossCellBias(state, cellKey, m);
88
- const prior = staticPrior(validTier, m);
89
- const score = xbias != null ? (xbias + prior) / 2 : prior;
90
- if (score > bestScore) { bestScore = score; bestModel = m; }
91
- }
92
- return { model: bestModel, reason: 'cross-cell bias', confidence: 0.4, explored: false };
93
- }
94
- const best = models.reduce((a, b) => staticPrior(validTier, a) >= staticPrior(validTier, b) ? a : b);
95
- return { model: best, reason: 'insufficient data, using heuristic', confidence: 0.3, explored: false };
96
- }
97
-
98
- const epsilon = Math.max(MIN_EPSILON, 0.5 * Math.pow(0.9, totalObs));
99
- const explored = Math.random() < epsilon;
100
-
101
- if (explored) {
102
- const model = models[Math.floor(Math.random() * models.length)];
103
- return { model, reason: 'exploration', confidence: epsilon, explored: true };
104
- }
105
-
106
- // Exploitation: pick highest blended score
107
- const cell = state[cellKey] ?? {};
108
- let bestModel = models[0];
109
- let bestScore = -Infinity;
110
- for (const m of models) {
111
- const entry = cell[m];
112
- const ema = entry?.ema ?? staticPrior(validTier, m);
113
- const n = entry?.observations ?? 0;
114
- const score = blended(ema, n, validTier, m);
115
- if (score > bestScore) { bestScore = score; bestModel = m; }
116
- }
117
-
118
- return { model: bestModel, reason: 'exploitation', confidence: 1 - epsilon, explored: false };
119
- } catch {
120
- return { model: 'sonnet', reason: 'error fallback', confidence: 0.1, explored: false };
121
- }
122
- }
123
-
124
- // reward: number in [0, 1]
125
- export function recordReward(cellKey, model, reward, cwd) {
126
- try {
127
- const state = loadState(cwd);
128
- if (!state[cellKey]) state[cellKey] = {};
129
- const entry = state[cellKey][model] ?? { ema: reward, observations: 0 };
130
- entry.ema = ALPHA * reward + (1 - ALPHA) * entry.ema;
131
- entry.observations = (entry.observations ?? 0) + 1;
132
- entry.lastUpdated = new Date().toISOString();
133
- entry.lastReward = reward;
134
- state[cellKey][model] = entry;
135
- saveState(state, cwd);
136
- } catch {
137
- // non-throwing
138
- }
139
- }
140
-
141
- export function getRoutingStats(cwd) {
142
- try {
143
- const state = loadState(cwd);
144
- const cells = {}, flat = [];
145
- let totalObservations = 0;
146
- for (const [cellKey, models] of Object.entries(state)) {
147
- cells[cellKey] ??= {};
148
- for (const [model, entry] of Object.entries(models)) {
149
- const obs = entry.observations ?? 0;
150
- cells[cellKey][model] = { ema: entry.ema, observations: obs };
151
- totalObservations += obs;
152
- flat.push({ cell: cellKey, model, ema: entry.ema, observations: obs });
153
- }
154
- }
155
- flat.sort((a, b) => b.ema - a.ema);
156
- return { cells, totalObservations, topPerformers: flat.slice(0, 5), worstPerformers: flat.slice(-5).reverse() };
157
- } catch {
158
- return { cells: {}, totalObservations: 0, topPerformers: [], worstPerformers: [] };
159
- }
160
- }
161
-
162
- /**
163
- * Loads cross-session routing state. If the state was last updated in a prior session,
164
- * applies a mild decay (×0.95) to all EMA scores to account for staleness.
165
- */
166
- export function loadCrossSessionPriors(cwd) {
167
- try {
168
- const state = loadState(cwd);
169
- const sessionStart = state._sessionStart;
170
- if (!sessionStart) return state; // no prior session marker
171
- const lastMs = new Date(sessionStart).getTime();
172
- if (isNaN(lastMs)) return state;
173
- const stale = (Date.now() - lastMs) > 60_000; // more than 1 min old = different session
174
- if (!stale) return state;
175
- for (const [cellKey, models] of Object.entries(state)) {
176
- if (cellKey.startsWith('_')) continue;
177
- for (const entry of Object.values(models)) {
178
- if (typeof entry.ema === 'number') entry.ema = entry.ema * 0.95;
179
- }
180
- }
181
- return state;
182
- } catch { return {}; }
183
- }
184
-
185
- /**
186
- * Records session start timestamp and triggers file survival checks.
187
- * Call once at CLI session start.
188
- */
189
- export async function markSessionStart(cwd) {
190
- try {
191
- const state = loadState(cwd);
192
- state._sessionStart = new Date().toISOString();
193
- saveState(state, cwd);
194
- await checkFileSurvival(cwd).catch(() => {});
195
- } catch { /* non-throwing */ }
196
- }
197
-
198
- export function resetAdvisor(cwd) {
199
- try {
200
- saveState({}, cwd);
201
- } catch {
202
- // non-throwing
203
- }
204
- }
@@ -1,147 +0,0 @@
1
- // self-correct.mjs — Failure analysis and retry strategy selection
2
-
3
- const MODEL_TIER = { 'haiku': 1, 'sonnet': 2, 'opus': 3 };
4
- const TIER_MODEL = { 1: 'haiku', 2: 'sonnet', 3: 'opus' };
5
- const MAX_ATTEMPTS = 3;
6
-
7
- function modelTier(model = '') {
8
- const m = model.toLowerCase();
9
- if (m.includes('haiku')) return 1;
10
- if (m.includes('opus')) return 3;
11
- return 2; // sonnet default
12
- }
13
-
14
- function matchesAny(text, keywords) {
15
- const t = text.toLowerCase();
16
- return keywords.some(k => t.includes(k));
17
- }
18
-
19
- // Export 1: classifyFailure(result)
20
- export function classifyFailure(result) {
21
- try {
22
- const err = String(result?.error || result?.stderr || '');
23
- const out = String(result?.output || result?.stdout || '');
24
- const combined = err + ' ' + out;
25
- const duration = result?.durationMs ?? 0;
26
- const timeoutThreshold = result?.timeoutMs ?? 60_000;
27
-
28
- if (matchesAny(combined, ['rate limit', 'ratelimit', '429', 'quota exceeded', 'capacity'])) {
29
- return { type: 'rate-limit', confidence: 0.95, retryable: true };
30
- }
31
- if (matchesAny(combined, ['timeout', 'timed out']) || duration > timeoutThreshold) {
32
- return { type: 'timeout', confidence: 0.9, retryable: true };
33
- }
34
- if (matchesAny(combined, ['context length', 'token limit', 'too long', 'maximum context', 'context window'])) {
35
- return { type: 'context-overflow', confidence: 0.9, retryable: true };
36
- }
37
- if (matchesAny(combined, ['ambiguous', 'unclear', 'did you mean', 'which one', 'could you clarify', 'please clarify'])) {
38
- return { type: 'specification', confidence: 0.85, retryable: false };
39
- }
40
- if (matchesAny(combined, ['unable to', "i don't know how", 'beyond my', 'cannot complete', 'incomplete'])) {
41
- return { type: 'capability', confidence: 0.8, retryable: true };
42
- }
43
- // Heuristic: low quality output without explicit error signals capability gap
44
- const quality = result?.quality ?? result?.score ?? null;
45
- if (quality !== null && quality < 0.5) {
46
- return { type: 'capability', confidence: 0.7, retryable: true };
47
- }
48
-
49
- return { type: 'unknown', confidence: 0.5, retryable: true };
50
- } catch {
51
- return { type: 'unknown', confidence: 0, retryable: true };
52
- }
53
- }
54
-
55
- // Export 2: selectStrategy(failure, originalDecision, attemptNumber)
56
- export function selectStrategy(failure, originalDecision, attemptNumber) {
57
- try {
58
- if (!failure.retryable) {
59
- return { strategy: 'give-up', reason: `failure type '${failure.type}' requires user input` };
60
- }
61
- if (attemptNumber >= MAX_ATTEMPTS) {
62
- return { strategy: 'give-up', reason: `max attempts (${MAX_ATTEMPTS}) reached` };
63
- }
64
-
65
- const tier = modelTier(originalDecision?.model);
66
-
67
- if (attemptNumber === 1) {
68
- switch (failure.type) {
69
- case 'capability':
70
- if (tier >= 3) return { strategy: 'split', newDecision: originalDecision, reason: 'already at max tier; decompose task' };
71
- return { strategy: 'escalate', newDecision: originalDecision, reason: 'model lacked capability; escalating tier' };
72
- case 'timeout':
73
- return { strategy: 'wait-retry', newDecision: originalDecision, reason: 'timed out; retrying with delay' };
74
- case 'rate-limit':
75
- return { strategy: 'wait-retry', newDecision: originalDecision, reason: 'rate limited; retrying after delay' };
76
- case 'context-overflow':
77
- return { strategy: 'compress', newDecision: originalDecision, reason: 'context too large; compressing' };
78
- case 'specification':
79
- return { strategy: 'give-up', reason: 'ambiguous specification; user clarification needed' };
80
- default: // unknown
81
- if (tier >= 3) return { strategy: 'split', newDecision: originalDecision, reason: 'unknown failure at max tier; decomposing' };
82
- return { strategy: 'escalate', newDecision: originalDecision, reason: 'unknown failure; escalating as precaution' };
83
- }
84
- }
85
-
86
- if (attemptNumber === 2) {
87
- if (tier >= 3) {
88
- return { strategy: 'split', newDecision: originalDecision, reason: 'max tier reached; splitting task' };
89
- }
90
- return { strategy: 'escalate', newDecision: originalDecision, reason: 'retry failed; escalating one final tier' };
91
- }
92
-
93
- return { strategy: 'give-up', reason: 'exhausted retry budget' };
94
- } catch {
95
- return { strategy: 'give-up', reason: 'internal error in strategy selection' };
96
- }
97
- }
98
-
99
- // Export 3: buildRetryDecision(originalDecision, strategy, failure)
100
- export function buildRetryDecision(originalDecision, strategy, failure) {
101
- try {
102
- const base = {
103
- ...originalDecision,
104
- _retryAttempt: (originalDecision?._retryAttempt ?? 0) + 1,
105
- _retryReason: failure.type,
106
- _retryStrategy: strategy,
107
- };
108
-
109
- switch (strategy) {
110
- case 'escalate': {
111
- const tier = modelTier(originalDecision?.model);
112
- const nextTier = Math.min(tier + 1, 3);
113
- return { ...base, model: TIER_MODEL[nextTier] };
114
- }
115
- case 'compress':
116
- return { ...base, _contextBudget: 0.5 };
117
- case 'wait-retry':
118
- return { ...base, _delayMs: 5000 };
119
- case 'rethink':
120
- return { ...base, tier: 'think', _retryAsThink: true };
121
- case 'split':
122
- return { ...base, _shouldDecompose: true };
123
- default:
124
- return base;
125
- }
126
- } catch {
127
- return { ...originalDecision, _retryAttempt: 1, _retryReason: 'error', _retryStrategy: strategy };
128
- }
129
- }
130
-
131
- // Export 4: shouldRetry(result, originalDecision, attemptNumber)
132
- export function shouldRetry(result, originalDecision, attemptNumber = 1) {
133
- try {
134
- if (attemptNumber >= MAX_ATTEMPTS) return { retry: false, reason: `max attempts (${MAX_ATTEMPTS}) reached`, strategy: 'give-up' };
135
- const failure = classifyFailure(result);
136
- const { strategy, newDecision, reason } = selectStrategy(failure, originalDecision, attemptNumber);
137
-
138
- if (strategy === 'give-up') {
139
- return { retry: false, reason, strategy };
140
- }
141
-
142
- const decision = buildRetryDecision(newDecision ?? originalDecision, strategy, failure);
143
- return { retry: true, decision, reason, strategy };
144
- } catch {
145
- return { retry: false, reason: 'internal error in shouldRetry', strategy: 'give-up' };
146
- }
147
- }
@@ -1,160 +0,0 @@
1
- // session-lock.mjs — Ensures one active HEAD session at a time.
2
- // If two shells/chats open, only one owns the cognitive state.
3
- // The other gets read-only access (can observe but not dispatch).
4
- //
5
- // "One ring rules them all" — no split-brain.
6
-
7
- import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
8
- import { join } from 'node:path';
9
-
10
- const STATE_DIR = join(process.cwd(), '.dualbrain');
11
- const LOCK_FILE = join(STATE_DIR, 'session.lock');
12
-
13
- const STALE_THRESHOLD_MS = 90_000; // 90 seconds without heartbeat = stale
14
- const HEARTBEAT_INTERVAL_MS = 30_000;
15
-
16
- let _heartbeatTimer = null;
17
- let _sessionId = null;
18
-
19
- /**
20
- * @typedef {object} LockResult
21
- * @property {boolean} acquired - Whether this session owns HEAD
22
- * @property {string} sessionId - This session's ID
23
- * @property {string|null} existingSession - ID of the session that already holds the lock (if not acquired)
24
- * @property {string} mode - 'primary' | 'takeover' | 'readonly'
25
- */
26
-
27
- /**
28
- * Attempt to acquire the session lock.
29
- * - If no lock exists or lock is stale: acquire as primary
30
- * - If lock is fresh and held by another: return readonly
31
- *
32
- * @param {object} opts
33
- * @param {boolean} opts.force - Force takeover even if existing session is fresh
34
- * @returns {LockResult}
35
- */
36
- export function acquire({ force = false } = {}) {
37
- mkdirSync(STATE_DIR, { recursive: true });
38
- _sessionId = _generateSessionId();
39
-
40
- const existing = _readLock();
41
-
42
- if (!existing) {
43
- // No lock — claim it
44
- _writeLock(_sessionId);
45
- _startHeartbeat();
46
- return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
47
- }
48
-
49
- // Same process (re-entry within same session) — always grant
50
- if (existing.pid === process.pid) {
51
- _sessionId = existing.sessionId;
52
- return { acquired: true, sessionId: _sessionId, existingSession: null, mode: 'primary' };
53
- }
54
-
55
- const age = Date.now() - existing.heartbeat;
56
-
57
- if (age > STALE_THRESHOLD_MS || force) {
58
- // Stale or forced takeover
59
- _writeLock(_sessionId);
60
- _startHeartbeat();
61
- return { acquired: true, sessionId: _sessionId, existingSession: existing.sessionId, mode: 'takeover' };
62
- }
63
-
64
- // Another session is active — go readonly
65
- return { acquired: false, sessionId: _sessionId, existingSession: existing.sessionId, mode: 'readonly' };
66
- }
67
-
68
- /**
69
- * Release the session lock (called at session end).
70
- */
71
- export function release() {
72
- if (_heartbeatTimer) {
73
- clearInterval(_heartbeatTimer);
74
- _heartbeatTimer = null;
75
- }
76
-
77
- try {
78
- const existing = _readLock();
79
- if (existing && existing.sessionId === _sessionId) {
80
- unlinkSync(LOCK_FILE);
81
- }
82
- } catch {}
83
- }
84
-
85
- /**
86
- * Check if this session currently holds the lock.
87
- * @returns {boolean}
88
- */
89
- export function isOwner() {
90
- const existing = _readLock();
91
- return existing?.sessionId === _sessionId;
92
- }
93
-
94
- /**
95
- * Get current lock status without modifying it.
96
- * @returns {{active: boolean, sessionId: string|null, age: number|null}}
97
- */
98
- export function status() {
99
- const existing = _readLock();
100
- if (!existing) return { active: false, sessionId: null, age: null };
101
- return {
102
- active: (Date.now() - existing.heartbeat) < STALE_THRESHOLD_MS,
103
- sessionId: existing.sessionId,
104
- age: Date.now() - existing.heartbeat,
105
- };
106
- }
107
-
108
- /**
109
- * Manually heartbeat (useful if the automatic timer isn't running).
110
- */
111
- export function heartbeat() {
112
- if (!_sessionId) return;
113
- const existing = _readLock();
114
- if (existing && existing.sessionId === _sessionId) {
115
- _writeLock(_sessionId);
116
- }
117
- }
118
-
119
- // ── Internal ──────────────────────────────────────────────────────────────────
120
-
121
- function _generateSessionId() {
122
- return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
123
- }
124
-
125
- function _readLock() {
126
- try {
127
- if (!existsSync(LOCK_FILE)) return null;
128
- return JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- function _writeLock(sessionId) {
135
- const lock = {
136
- sessionId,
137
- heartbeat: Date.now(),
138
- pid: process.pid,
139
- };
140
- writeFileSync(LOCK_FILE, JSON.stringify(lock));
141
- }
142
-
143
- function _startHeartbeat() {
144
- if (_heartbeatTimer) clearInterval(_heartbeatTimer);
145
- _heartbeatTimer = setInterval(() => {
146
- try {
147
- const existing = _readLock();
148
- if (existing && existing.sessionId === _sessionId) {
149
- _writeLock(_sessionId);
150
- } else {
151
- // Someone else took over — stop heartbeating
152
- clearInterval(_heartbeatTimer);
153
- _heartbeatTimer = null;
154
- }
155
- } catch {}
156
- }, HEARTBEAT_INTERVAL_MS);
157
-
158
- // Don't keep the process alive just for heartbeats
159
- if (_heartbeatTimer.unref) _heartbeatTimer.unref();
160
- }