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