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,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
- }