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
package/src/signal.mjs DELETED
@@ -1,115 +0,0 @@
1
- // signal.mjs — Compound outcome signal scoring
2
- // Combines multiple weak signals into one reliable reward score.
3
-
4
- import { existsSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { execSync } from 'node:child_process';
7
-
8
- export const EXPECTED_DURATION_MS = { search: 15000, execute: 45000, think: 30000, review: 40000 };
9
-
10
- export function scoreDurationRatio(durationMs, tier) {
11
- try {
12
- if (durationMs <= 0) return null;
13
- const expectedMs = EXPECTED_DURATION_MS[tier] || EXPECTED_DURATION_MS.execute;
14
- const ratio = durationMs / expectedMs;
15
- if (ratio >= 0.5 && ratio <= 1.5) return 1.0;
16
- if (ratio < 0.2) return 0.5;
17
- if (ratio > 3.0) return 0.3;
18
- if (ratio < 0.5) return 0.5 + ((ratio - 0.2) / (0.5 - 0.2)) * 0.5;
19
- // ratio 1.5–3.0
20
- return 1.0 - ((ratio - 1.5) / (3.0 - 1.5)) * 0.7;
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export function measureFileSurvival(outcome, cwd) {
27
- try {
28
- const files = Array.isArray(outcome.filesChanged)
29
- ? outcome.filesChanged
30
- : [];
31
- if (files.length === 0) return 1.0;
32
-
33
- let changed;
34
- try {
35
- changed = new Set(
36
- execSync('git diff --name-only', { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
37
- .split('\n')
38
- .map(f => f.trim())
39
- .filter(Boolean)
40
- );
41
- } catch {
42
- changed = new Set();
43
- }
44
-
45
- const survived = files.filter(f => {
46
- const abs = join(cwd, f);
47
- return existsSync(abs) && !changed.has(f);
48
- });
49
- return survived.length / files.length;
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- export function scoreOutcome(outcome, context = {}) {
56
- try {
57
- const tier = outcome.tier ?? 'execute';
58
- const signals = [];
59
-
60
- // Signal 1: exit success (weight 0.3)
61
- let exitVal;
62
- if (outcome.success === true) exitVal = 1.0;
63
- else if (outcome.status === 'partial') exitVal = 0.4;
64
- else exitVal = 0.0;
65
- signals.push({ name: 'exitSuccess', value: exitVal, weight: 0.3 });
66
-
67
- // Signal 2: duration ratio (weight 0.25)
68
- const durationMs = outcome.durationMs ?? 0;
69
- const durVal = durationMs > 0 ? scoreDurationRatio(durationMs, tier) : null;
70
- signals.push({ name: 'durationRatio', value: durVal, weight: 0.25 });
71
-
72
- // Signal 3: token efficiency (weight 0.25)
73
- let effVal = null;
74
- const filesChanged = outcome.filesChanged ?? 0;
75
- const fileCount = Array.isArray(filesChanged) ? filesChanged.length : (typeof filesChanged === 'number' ? filesChanged : 0);
76
- if (!(fileCount === 0 && tier === 'think')) {
77
- const tokensUsed =
78
- outcome.tokensUsed?.output ??
79
- (durationMs > 0 ? Math.round(durationMs / 100) : null);
80
- if (tokensUsed !== null) {
81
- const efficiency = fileCount / Math.max(1, tokensUsed / 1000);
82
- if (efficiency > 2) effVal = 1.0;
83
- else if (efficiency >= 0.5) effVal = 0.5 + ((efficiency - 0.5) / 1.5) * 0.5;
84
- else if (efficiency < 0.1) effVal = 0.2;
85
- else effVal = 0.2 + ((efficiency - 0.1) / 0.4) * 0.3;
86
- }
87
- }
88
- signals.push({ name: 'tokenEfficiency', value: effVal, weight: 0.25 });
89
-
90
- // Signal 4: file survival (weight 0.2) — delayed, may be null
91
- const survivalVal = context.fileSurvival ?? null;
92
- signals.push({ name: 'fileSurvival', value: survivalVal, weight: 0.2 });
93
-
94
- // Compound score with weight redistribution
95
- const active = signals.filter(s => s.value !== null);
96
- const totalWeight = active.reduce((sum, s) => sum + s.weight, 0);
97
- const reward = totalWeight > 0
98
- ? active.reduce((sum, s) => sum + (s.value * s.weight / totalWeight), 0)
99
- : 0;
100
- const confidence = totalWeight;
101
-
102
- return {
103
- reward: Math.min(1, Math.max(0, reward)),
104
- confidence: Math.min(1, confidence),
105
- signals: {
106
- exitSuccess: exitVal,
107
- durationRatio: durVal,
108
- tokenEfficiency: effVal,
109
- fileSurvival: survivalVal,
110
- },
111
- };
112
- } catch {
113
- return { reward: 0, confidence: 0, signals: { exitSuccess: false, durationRatio: null, tokenEfficiency: null, fileSurvival: null } };
114
- }
115
- }
package/src/simmer.mjs DELETED
@@ -1,241 +0,0 @@
1
- // simmer.mjs — Ideas that aren't tasks yet. They sit, gather heat, and crystallize.
2
- //
3
- // The "song" insight: users drop ideas casually. HEAD tends to acknowledge them
4
- // verbally then move on. The simmer buffer catches these — every idea gets stored
5
- // with a heat score. Heat rises when: the idea recurs, evidence supports it,
6
- // adjacent work makes it more relevant, or time passes and it keeps nagging.
7
- // When heat crosses a threshold, the idea crystallizes into an actionable item
8
- // and surfaces to HEAD during deliberation.
9
-
10
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
- import { join } from 'node:path';
12
-
13
- const STATE_DIR = join(process.cwd(), '.dualbrain');
14
- const SIMMER_FILE = join(STATE_DIR, 'simmer.json');
15
-
16
- const CRYSTALLIZE_THRESHOLD = 5;
17
- const MAX_ITEMS = 30;
18
- const HEAT_DECAY_PER_HOUR = 0.3;
19
-
20
- /**
21
- * @typedef {object} SimmerItem
22
- * @property {string} id
23
- * @property {string} idea - The raw idea in prose
24
- * @property {string} origin - Where it came from (user quote, observation, debrief finding)
25
- * @property {number} heat - Current heat score
26
- * @property {number} createdAt
27
- * @property {number} lastHeated - Last time heat was added
28
- * @property {string[]} signals - Evidence trail (why heat was added)
29
- * @property {boolean} crystallized - Whether it's crossed the threshold
30
- * @property {string|null} crystallizedAs - What it became (task description, architecture decision, etc)
31
- */
32
-
33
- /**
34
- * Add a new idea to the simmer buffer.
35
- * If a similar idea already exists (fuzzy match), heat it instead of duplicating.
36
- *
37
- * @param {string} idea - The idea in natural language
38
- * @param {object} opts
39
- * @param {string} opts.origin - Where this came from
40
- * @param {number} opts.initialHeat - Starting heat (default 1)
41
- * @returns {SimmerItem} The created or heated item
42
- */
43
- export function add(idea, { origin = 'observation', initialHeat = 1 } = {}) {
44
- const items = _load();
45
-
46
- // Check for similar existing idea
47
- const existing = _findSimilar(items, idea);
48
- if (existing) {
49
- return heat(existing.id, initialHeat, `Recurrence: "${idea.slice(0, 60)}"`);
50
- }
51
-
52
- const item = {
53
- id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
54
- idea,
55
- origin,
56
- heat: initialHeat,
57
- createdAt: Date.now(),
58
- lastHeated: Date.now(),
59
- signals: [`Created from: ${origin}`],
60
- crystallized: false,
61
- crystallizedAs: null,
62
- };
63
-
64
- items.push(item);
65
- _save(items);
66
- return item;
67
- }
68
-
69
- /**
70
- * Add heat to an existing item. If it crosses the threshold, mark as crystallized.
71
- *
72
- * @param {string} id
73
- * @param {number} amount - Heat to add (default 1)
74
- * @param {string} signal - Why heat is being added
75
- * @returns {SimmerItem|null}
76
- */
77
- export function heat(id, amount = 1, signal = '') {
78
- const items = _load();
79
- const item = items.find(i => i.id === id);
80
- if (!item) return null;
81
-
82
- item.heat += amount;
83
- item.lastHeated = Date.now();
84
- if (signal) item.signals.push(signal);
85
-
86
- // Cap signals array
87
- if (item.signals.length > 10) {
88
- item.signals = item.signals.slice(-10);
89
- }
90
-
91
- // Check crystallization
92
- if (!item.crystallized && item.heat >= CRYSTALLIZE_THRESHOLD) {
93
- item.crystallized = true;
94
- }
95
-
96
- _save(items);
97
- return item;
98
- }
99
-
100
- /**
101
- * Get all items that have crystallized but haven't been surfaced yet.
102
- * These should be presented to HEAD during deliberation.
103
- *
104
- * @returns {SimmerItem[]}
105
- */
106
- export function harvest() {
107
- const items = _load();
108
- return items.filter(i => i.crystallized && !i.crystallizedAs);
109
- }
110
-
111
- /**
112
- * Mark a crystallized item as actioned — record what it became.
113
- *
114
- * @param {string} id
115
- * @param {string} became - Description of what action was taken
116
- */
117
- export function resolve(id, became) {
118
- const items = _load();
119
- const item = items.find(i => i.id === id);
120
- if (!item) return;
121
- item.crystallizedAs = became;
122
- _save(items);
123
- }
124
-
125
- /**
126
- * Get all active (non-resolved) simmering items, sorted by heat descending.
127
- * Used by the narrative to include "what's brewing" context.
128
- *
129
- * @returns {SimmerItem[]}
130
- */
131
- export function active() {
132
- const items = _load();
133
- _applyDecay(items);
134
- return items
135
- .filter(i => !i.crystallizedAs)
136
- .sort((a, b) => b.heat - a.heat);
137
- }
138
-
139
- /**
140
- * Check if an idea already exists in the buffer (for deduplication).
141
- * @param {string} idea
142
- * @returns {SimmerItem|null}
143
- */
144
- export function find(idea) {
145
- const items = _load();
146
- return _findSimilar(items, idea);
147
- }
148
-
149
- /**
150
- * Generate a brief for HEAD showing what's simmering.
151
- * Included in the narrative context so HEAD is aware of brewing ideas.
152
- *
153
- * @returns {string} Prose summary of active simmer items, or empty string
154
- */
155
- export function brief() {
156
- const items = active();
157
- if (items.length === 0) return '';
158
-
159
- const crystallized = items.filter(i => i.crystallized);
160
- const hot = items.filter(i => !i.crystallized && i.heat >= 3);
161
- const warm = items.filter(i => !i.crystallized && i.heat >= 1.5 && i.heat < 3);
162
-
163
- const parts = [];
164
-
165
- if (crystallized.length > 0) {
166
- parts.push(`Crystallized (ready to act): ${crystallized.map(i => i.idea.slice(0, 80)).join('; ')}`);
167
- }
168
- if (hot.length > 0) {
169
- parts.push(`Hot (building momentum): ${hot.map(i => `${i.idea.slice(0, 60)} [heat:${i.heat.toFixed(1)}]`).join('; ')}`);
170
- }
171
- if (warm.length > 0 && parts.length < 2) {
172
- parts.push(`Warm: ${warm.slice(0, 3).map(i => i.idea.slice(0, 50)).join('; ')}`);
173
- }
174
-
175
- return parts.join('\n');
176
- }
177
-
178
- /**
179
- * Prune resolved and cold-dead items.
180
- */
181
- export function prune() {
182
- let items = _load();
183
- _applyDecay(items);
184
- // Remove: resolved items older than 1h, or items with heat <= 0
185
- const cutoff = Date.now() - 60 * 60 * 1000;
186
- items = items.filter(i => {
187
- if (i.crystallizedAs && i.lastHeated < cutoff) return false;
188
- if (i.heat <= 0) return false;
189
- return true;
190
- });
191
- _save(items);
192
- }
193
-
194
- // ── Internal ──────────────────────────────────────────────────────────────────
195
-
196
- function _load() {
197
- try {
198
- if (existsSync(SIMMER_FILE)) {
199
- return JSON.parse(readFileSync(SIMMER_FILE, 'utf8'));
200
- }
201
- } catch {}
202
- return [];
203
- }
204
-
205
- function _save(items) {
206
- // Cap total items
207
- if (items.length > MAX_ITEMS) {
208
- items.sort((a, b) => b.heat - a.heat);
209
- items = items.slice(0, MAX_ITEMS);
210
- }
211
- mkdirSync(STATE_DIR, { recursive: true });
212
- writeFileSync(SIMMER_FILE, JSON.stringify(items, null, 2));
213
- }
214
-
215
- function _applyDecay(items) {
216
- const now = Date.now();
217
- for (const item of items) {
218
- if (item.crystallized) continue; // Crystallized items don't decay
219
- const hoursSinceHeat = (now - item.lastHeated) / (60 * 60 * 1000);
220
- if (hoursSinceHeat > 1) {
221
- item.heat -= HEAT_DECAY_PER_HOUR * hoursSinceHeat;
222
- if (item.heat < 0) item.heat = 0;
223
- }
224
- }
225
- }
226
-
227
- function _findSimilar(items, idea) {
228
- const normalized = idea.toLowerCase().replace(/[^a-z0-9\s]/g, '');
229
- const words = normalized.split(/\s+/).filter(w => w.length > 4);
230
- if (words.length === 0) return null;
231
-
232
- for (const item of items) {
233
- if (item.crystallizedAs) continue; // Skip resolved
234
- const itemNorm = item.idea.toLowerCase().replace(/[^a-z0-9\s]/g, '');
235
- const matchCount = words.filter(w => itemNorm.includes(w)).length;
236
- if (matchCount >= Math.ceil(words.length * 0.5)) {
237
- return item;
238
- }
239
- }
240
- return null;
241
- }
package/src/strategy.mjs DELETED
@@ -1,235 +0,0 @@
1
- // strategy.mjs — Dispatch strategy library + selection
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- // ─── Strategy definitions ──────────────────────────────────────────────────────
6
-
7
- export const STRATEGIES = {
8
- direct: {
9
- id: 'direct',
10
- label: 'Direct dispatch',
11
- description: 'Single agent, single task. Best for clear, focused work.',
12
- applicability: { maxFiles: 3, maxComplexity: 'moderate', maxRisk: 'medium' },
13
- cost: 1.0,
14
- },
15
- cascade: {
16
- id: 'cascade',
17
- label: 'Think → Execute cascade',
18
- description: 'Cheap thinker refines spec, then worker executes. Best for routine-but-multi-step tasks.',
19
- applicability: { minFiles: 1, minComplexity: 'moderate', maxRisk: 'high' },
20
- cost: 1.3,
21
- },
22
- split: {
23
- id: 'split',
24
- label: 'Decompose → parallel dispatch',
25
- description: 'Break into sub-tasks, dispatch each at optimal tier. Best for large multi-file changes.',
26
- applicability: { minFiles: 4, minComplexity: 'complex' },
27
- cost: 2.0,
28
- },
29
- 'dual-review': {
30
- id: 'dual-review',
31
- label: 'Execute → adversarial review',
32
- description: 'Worker implements, second model reviews. Best for high-risk/security code.',
33
- applicability: { minRisk: 'high' },
34
- cost: 1.5,
35
- },
36
- 'architect-editor': {
37
- id: 'architect-editor',
38
- label: 'Architect reasons → editor implements',
39
- description: 'Opus/o3 reasons freely, sonnet/haiku formats the edits. Best for complex architecture + implementation.',
40
- applicability: { minComplexity: 'complex', minFiles: 3 },
41
- cost: 1.8,
42
- },
43
- };
44
-
45
- // ─── Helpers ───────────────────────────────────────────────────────────────────
46
-
47
- const COMPLEXITY_RANK = { trivial: 0, simple: 1, moderate: 2, complex: 3 };
48
- const RISK_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
49
-
50
- const COST_CAPS = {
51
- frugal: 1.0,
52
- 'cost-saver': 1.3,
53
- balanced: 2.0,
54
- 'quality-first': 3.0,
55
- maximum: Infinity,
56
- aggressive: Infinity, // maps to maximum behaviour
57
- fullpower: Infinity,
58
- fast: 1.3,
59
- };
60
-
61
- const SECURITY_KEYWORDS = /\b(auth|security|billing|payment|credential|secret|token|encrypt|permission|oauth|jwt)\b/i;
62
-
63
- function costCap(workStyle) {
64
- return COST_CAPS[workStyle] ?? 2.0;
65
- }
66
-
67
- function fileCount(detection) {
68
- return detection?.fileCount ?? detection?.files ?? 0;
69
- }
70
-
71
- function complexityRank(detection) {
72
- return COMPLEXITY_RANK[detection?.complexity] ?? 1;
73
- }
74
-
75
- function riskRank(detection) {
76
- return RISK_RANK[detection?.risk] ?? 0;
77
- }
78
-
79
- function prompt(detection) {
80
- return detection?.prompt ?? detection?.description ?? '';
81
- }
82
-
83
- // ─── Scoring ───────────────────────────────────────────────────────────────────
84
-
85
- function scoreStrategies(detection, workStyle) {
86
- const files = fileCount(detection);
87
- const cRank = complexityRank(detection);
88
- const rRank = riskRank(detection);
89
- const text = prompt(detection);
90
- const frugal = workStyle === 'frugal';
91
- const saver = workStyle === 'cost-saver' || workStyle === 'fast';
92
-
93
- return {
94
- direct: 0.5,
95
-
96
- cascade: 0
97
- + (cRank >= COMPLEXITY_RANK.moderate ? 0.3 : 0)
98
- + (files >= 2 ? 0.2 : 0)
99
- - (frugal ? 0.5 : 0),
100
-
101
- split: 0
102
- + (files >= 4 ? 0.4 : 0)
103
- + (cRank >= COMPLEXITY_RANK.complex ? 0.3 : 0)
104
- - (frugal || saver ? 0.5 : 0),
105
-
106
- 'dual-review': 0
107
- + (rRank >= RISK_RANK.high ? 0.5 : 0)
108
- + (SECURITY_KEYWORDS.test(text) ? 0.3 : 0)
109
- - (frugal ? 0.3 : 0),
110
-
111
- 'architect-editor': 0
112
- + (cRank >= COMPLEXITY_RANK.complex && files >= 3 ? 0.4 : 0)
113
- - (saver ? 0.3 : 0),
114
- };
115
- }
116
-
117
- // ─── Export 1: selectStrategy ─────────────────────────────────────────────────
118
-
119
- /**
120
- * Select the best dispatch strategy for a task.
121
- * @param {object} detection — from detect.mjs (detectTask output)
122
- * @param {object} decision — from decide.mjs (decideRoute output)
123
- * @param {object} profile — user profile (workStyle, etc.)
124
- * @returns {{ strategy: string, reason: string, alternatives: string[] }}
125
- */
126
- export function selectStrategy(detection, decision, profile) {
127
- try {
128
- const workStyle = profile?.workStyle ?? profile?.bias ?? 'balanced';
129
- const cap = costCap(workStyle);
130
- const scores = scoreStrategies(detection, workStyle);
131
-
132
- // Filter by cost cap, then rank
133
- const ranked = Object.entries(scores)
134
- .filter(([id]) => STRATEGIES[id].cost <= cap)
135
- .sort(([, a], [, b]) => b - a);
136
-
137
- if (!ranked.length) {
138
- // Fallback — always allow direct
139
- return { strategy: 'direct', reason: 'Cost cap allows only direct dispatch.', alternatives: [] };
140
- }
141
-
142
- const [bestId] = ranked[0];
143
- const alternatives = ranked.slice(1).map(([id]) => id);
144
-
145
- const reasons = {
146
- direct: 'Clear, focused task within single-agent scope.',
147
- cascade: 'Multi-step task benefits from spec refinement before execution.',
148
- split: 'Large file count warrants decomposition into parallel sub-tasks.',
149
- 'dual-review': 'High-risk or security-sensitive work requires adversarial review.',
150
- 'architect-editor': 'Complex architecture + implementation benefits from dual-model reasoning.',
151
- };
152
-
153
- return {
154
- strategy: bestId,
155
- reason: reasons[bestId] ?? 'Best match for task profile.',
156
- alternatives,
157
- };
158
- } catch {
159
- return { strategy: 'direct', reason: 'Fallback to direct dispatch.', alternatives: [] };
160
- }
161
- }
162
-
163
- // ─── Export 2: describeStrategy ───────────────────────────────────────────────
164
-
165
- /**
166
- * Human-readable description of a strategy.
167
- * @param {string} strategyId
168
- * @returns {string}
169
- */
170
- export function describeStrategy(strategyId) {
171
- const s = STRATEGIES[strategyId];
172
- if (!s) return `Unknown strategy: ${strategyId}`;
173
- return `${s.label} (cost ×${s.cost})\n${s.description}`;
174
- }
175
-
176
- // ─── Export 3: getStrategyForTask ─────────────────────────────────────────────
177
-
178
- /**
179
- * Convenience: load profile + decision context, select strategy, return with execution plan.
180
- * @param {object} detection — from detect.mjs
181
- * @param {string} [cwd] — working directory (for profile loading)
182
- * @returns {{ strategy: string, reason: string, alternatives: string[], plan: { steps: object[] } }}
183
- */
184
- export function getStrategyForTask(detection, cwd) {
185
- const dir = cwd ?? process.cwd();
186
- let profile = {};
187
- try {
188
- const p = join(dir, '.dualbrain', 'config.json');
189
- if (existsSync(p)) profile = JSON.parse(readFileSync(p, 'utf8'));
190
- } catch { /* non-throwing */ }
191
-
192
- // Minimal decision stub (model resolved from profile if available)
193
- const decision = { model: profile?.models?.execute ?? 'sonnet' };
194
- const selected = selectStrategy(detection, decision, profile);
195
-
196
- return { ...selected, plan: buildPlan(selected.strategy, decision) };
197
- }
198
-
199
- // ─── Plan builder ─────────────────────────────────────────────────────────────
200
-
201
- function buildPlan(strategyId, decision) {
202
- const m = decision?.model ?? 'sonnet';
203
- const plans = {
204
- direct: [
205
- { role: 'worker', model: m, description: 'Execute task' },
206
- ],
207
- cascade: [
208
- { role: 'thinker', model: 'sonnet', description: 'Refine spec' },
209
- { role: 'worker', model: 'from-think', description: 'Execute refined spec' },
210
- ],
211
- split: [
212
- { role: 'thinker', model: 'sonnet', description: 'Decompose into sub-tasks' },
213
- { role: 'worker', model: 'varies', description: 'Execute each sub-task' },
214
- ],
215
- 'dual-review': [
216
- { role: 'worker', model: m, description: 'Implement' },
217
- { role: 'reviewer', model: 'sonnet', description: 'Adversarial review' },
218
- ],
219
- 'architect-editor': [
220
- { role: 'thinker', model: 'opus', description: 'Architect solution' },
221
- { role: 'worker', model: 'haiku', description: 'Format edits' },
222
- ],
223
- };
224
- return { steps: plans[strategyId] ?? plans.direct };
225
- }
226
-
227
- // ─── Export 4: listStrategies ─────────────────────────────────────────────────
228
-
229
- /**
230
- * List all strategies for display.
231
- * @returns {{ id: string, label: string, description: string, cost: number }[]}
232
- */
233
- export function listStrategies() {
234
- return Object.values(STRATEGIES).map(({ id, label, description, cost }) => ({ id, label, description, cost }));
235
- }