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,279 +0,0 @@
1
- // governance.mjs — Model tier enforcement + multi-model collaboration
2
-
3
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
4
- import { join } from 'node:path';
5
-
6
- // ─── Tier Definitions ────────────────────────────────────────────────────────
7
-
8
- export const MODEL_TIERS = Object.freeze({
9
- 1: {
10
- label: 'lightweight',
11
- models: ['claude-haiku-4-5-20251001', 'haiku', 'gpt-4o-mini', 'o4-mini'],
12
- autoApprove: true,
13
- },
14
- 2: {
15
- label: 'standard',
16
- models: ['claude-sonnet-4-6', 'sonnet', 'gpt-4o', 'gpt-4.1'],
17
- autoApprove: true,
18
- },
19
- 3: {
20
- label: 'heavy',
21
- models: ['claude-opus-4-6', 'claude-opus-4-7', 'opus', 'o3'],
22
- autoApprove: false, // requires consent check per profile
23
- },
24
- });
25
-
26
- // Reverse lookup: model ID → tier number
27
- export function getModelTier(modelId) {
28
- if (!modelId) return 2; // default to standard
29
- const normalized = String(modelId).toLowerCase();
30
- for (const [tier, def] of Object.entries(MODEL_TIERS)) {
31
- if (def.models.some(m => normalized.includes(m))) return Number(tier);
32
- }
33
- return 2; // unknown models default to standard
34
- }
35
-
36
- // ─── Task Scoring ────────────────────────────────────────────────────────────
37
-
38
- export function scoreTask(detection) {
39
- // detection comes from detect.mjs — has intent, risk, scope, files, etc.
40
- const scores = {
41
- complexity: 0, // 0-3
42
- risk: 0, // 0-3
43
- creativity: 0, // 0-2
44
- precision: 0, // 0-2
45
- contextVolume: 0, // 0-3
46
- };
47
-
48
- // Complexity from file count / scope
49
- const fileCount = detection?.files?.length || detection?.scope?.fileCount || 0;
50
- if (fileCount >= 6) scores.complexity = 3;
51
- else if (fileCount >= 3) scores.complexity = 2;
52
- else if (fileCount >= 1) scores.complexity = 1;
53
-
54
- // Risk from explicit risk field or keywords
55
- const risk = detection?.risk || detection?.riskLevel || 'low';
56
- const riskMap = { low: 0, medium: 1, high: 2, critical: 3 };
57
- scores.risk = riskMap[risk] ?? 0;
58
-
59
- // Boost risk for security/auth/billing keywords
60
- const text = (detection?.objective || detection?.intent || '').toLowerCase();
61
- if (/\b(auth|security|credential|secret|billing|payment|migration|delete|drop)\b/.test(text)) {
62
- scores.risk = Math.max(scores.risk, 2);
63
- }
64
-
65
- // Creativity from intent type
66
- const intent = (detection?.intent || detection?.type || '').toLowerCase();
67
- if (/\b(architect|design|brainstorm|explore|research)\b/.test(intent)) scores.creativity = 2;
68
- else if (/\b(refactor|plan|decide)\b/.test(intent)) scores.creativity = 1;
69
-
70
- // Precision — one-shot tasks need higher precision
71
- if (/\b(security|deploy|publish|migration)\b/.test(text)) scores.precision = 2;
72
- else if (/\b(implement|build|create)\b/.test(text)) scores.precision = 1;
73
-
74
- // Context volume
75
- const contextSize = detection?.contextTokens || detection?.estimatedContext || 0;
76
- if (contextSize > 200000) scores.contextVolume = 3;
77
- else if (contextSize > 50000) scores.contextVolume = 2;
78
- else if (contextSize > 10000) scores.contextVolume = 1;
79
-
80
- return scores;
81
- }
82
-
83
- export function computeRequiredTier(scores) {
84
- const total = Object.values(scores).reduce((a, b) => a + b, 0);
85
- if (total <= 2) return 1;
86
- if (total <= 6) return 2;
87
- return 3;
88
- }
89
-
90
- // ─── Governance Assessment ───────────────────────────────────────────────────
91
-
92
- // Profile governance defaults
93
- const GOVERNANCE_PERMISSIONS = {
94
- 'auto': { 1: 'auto', 2: 'auto', 3: 'ask' },
95
- 'balanced': { 1: 'auto', 2: 'auto', 3: 'ask' },
96
- 'cost-saver': { 1: 'auto', 2: 'auto', 3: 'deny' },
97
- 'quality-first': { 1: 'auto', 2: 'auto', 3: 'auto' },
98
- };
99
-
100
- // Pricing per million tokens (input/output) for cost estimation
101
- const MODEL_PRICING = {
102
- 'haiku': { input: 1.00, output: 5.00 },
103
- 'sonnet': { input: 3.00, output: 15.00 },
104
- 'opus': { input: 5.00, output: 25.00 },
105
- 'gpt-4o-mini': { input: 0.15, output: 0.60 },
106
- 'gpt-4o': { input: 2.50, output: 10.00 },
107
- 'gpt-4.1': { input: 2.00, output: 8.00 },
108
- 'o3': { input: 2.00, output: 8.00 },
109
- 'o4-mini': { input: 1.10, output: 4.40 },
110
- };
111
-
112
- function estimateCost(modelId, estimatedTokens = 8000) {
113
- const normalized = String(modelId).toLowerCase();
114
- let pricing = MODEL_PRICING['sonnet']; // default
115
- for (const [key, p] of Object.entries(MODEL_PRICING)) {
116
- if (normalized.includes(key)) { pricing = p; break; }
117
- }
118
- // Assume 20% input, 80% output for agent tasks
119
- const inputTokens = estimatedTokens * 0.2;
120
- const outputTokens = estimatedTokens * 0.8;
121
- return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
122
- }
123
-
124
- export function assessGovernance(model, detection, profile) {
125
- const tier = getModelTier(model);
126
- const scores = scoreTask(detection);
127
- const requiredTier = computeRequiredTier(scores);
128
- const workStyle = profile?.workStyle || profile?.name || 'auto';
129
- const permissions = GOVERNANCE_PERMISSIONS[workStyle] || GOVERNANCE_PERMISSIONS['auto'];
130
- const permission = permissions[tier] || 'ask';
131
- const estimatedCost = estimateCost(model);
132
-
133
- return {
134
- requestedTier: tier,
135
- requiredTier,
136
- overProvisioned: tier > requiredTier,
137
- underProvisioned: tier < requiredTier,
138
- permission, // 'auto' | 'ask' | 'deny'
139
- estimatedCost,
140
- scores,
141
- justification: buildJustification(scores, tier, requiredTier),
142
- };
143
- }
144
-
145
- function buildJustification(scores, requestedTier, requiredTier) {
146
- const parts = [];
147
- if (scores.risk >= 2) parts.push('high-risk');
148
- if (scores.complexity >= 2) parts.push('complex');
149
- if (scores.creativity >= 2) parts.push('creative/architectural');
150
- if (scores.contextVolume >= 2) parts.push('large-context');
151
- if (requestedTier > requiredTier) parts.push('over-provisioned');
152
- if (requestedTier < requiredTier) parts.push('under-provisioned');
153
- return parts.join(', ') || 'standard task';
154
- }
155
-
156
- // ─── Collaboration Assessment ────────────────────────────────────────────────
157
-
158
- export function shouldCollaborate(detection, governance, profile) {
159
- // Never collaborate on tier-1 tasks
160
- if (governance.requiredTier <= 1) return { collaborate: false };
161
-
162
- // Never collaborate in cost-saver mode unless explicitly requested
163
- const workStyle = profile?.workStyle || profile?.name || 'auto';
164
- if (workStyle === 'cost-saver') return { collaborate: false };
165
-
166
- // Check collaboration triggers (need ANY two)
167
- const triggers = [];
168
- const text = (detection?.objective || detection?.intent || '').toLowerCase();
169
-
170
- if (/\b(auth|security|credential|billing|migration)\b/.test(text)) triggers.push('irreversibility');
171
- if (detection?.ambiguity === 'high' || /\b(should we|how to|best approach|tradeoff)\b/.test(text)) triggers.push('ambiguity');
172
- if (detection?.novelty === 'high' || /\b(new|first time|never done|greenfield)\b/.test(text)) triggers.push('novelty');
173
- if ((detection?.files?.length || 0) >= 4 && /\b(security|performance|ux)\b/.test(text)) triggers.push('cross-domain');
174
- if (governance.requestedTier >= 3 && detection?.confidence && detection.confidence < 0.8) triggers.push('low-confidence');
175
-
176
- const shouldDo = triggers.length >= 2;
177
-
178
- return {
179
- collaborate: shouldDo,
180
- triggers,
181
- pattern: shouldDo ? selectPattern(triggers, detection) : null,
182
- estimatedOverhead: shouldDo ? estimateCost('gpt-4.1') : 0, // secondary model cost
183
- };
184
- }
185
-
186
- function selectPattern(triggers, detection) {
187
- const text = (detection?.objective || detection?.intent || '').toLowerCase();
188
-
189
- // Security → adversarial review
190
- if (triggers.includes('irreversibility') && /\b(auth|security|credential)\b/.test(text)) {
191
- return 'adversarial-review';
192
- }
193
-
194
- // Architecture/greenfield → second opinion (perspective rotation reserved for Phase 4)
195
- if (triggers.includes('novelty') || triggers.includes('ambiguity')) {
196
- return 'second-opinion';
197
- }
198
-
199
- // Default
200
- return 'second-opinion';
201
- }
202
-
203
- // ─── Governance State (Session Budget Tracking) ──────────────────────────────
204
-
205
- const STATE_FILE = '.dualbrain/governance-state.json';
206
- const SESSION_GAP_MS = 30 * 60 * 1000; // 30 min gap = new session
207
-
208
- export function loadGovernanceState(cwd) {
209
- const statePath = join(cwd, STATE_FILE);
210
- try {
211
- const raw = JSON.parse(readFileSync(statePath, 'utf8'));
212
- // Check if session is stale
213
- const lastDispatch = raw.dispatches?.[raw.dispatches.length - 1];
214
- if (lastDispatch && (Date.now() - Date.parse(lastDispatch.ts)) > SESSION_GAP_MS) {
215
- // Stale session — reset
216
- return freshState();
217
- }
218
- return raw;
219
- } catch {
220
- return freshState();
221
- }
222
- }
223
-
224
- function freshState() {
225
- return {
226
- sessionStartedAt: new Date().toISOString(),
227
- dispatches: [],
228
- totalEstimatedCost: 0,
229
- tierCounts: { 1: 0, 2: 0, 3: 0 },
230
- };
231
- }
232
-
233
- export function recordDispatch(cwd, tier, model, estimatedCost, approved = true) {
234
- const state = loadGovernanceState(cwd);
235
- state.dispatches.push({
236
- tier,
237
- model: String(model),
238
- estimatedCost,
239
- approved,
240
- ts: new Date().toISOString(),
241
- });
242
- state.totalEstimatedCost += estimatedCost;
243
- state.tierCounts[tier] = (state.tierCounts[tier] || 0) + 1;
244
-
245
- const dir = join(cwd, '.dualbrain');
246
- mkdirSync(dir, { recursive: true });
247
- writeFileSync(join(cwd, STATE_FILE), JSON.stringify(state, null, 2) + '\n');
248
- return state;
249
- }
250
-
251
- export function checkBudget(cwd, orchestratorConfig) {
252
- const state = loadGovernanceState(cwd);
253
- const sessionLimit = orchestratorConfig?.budgets?.session_limit_usd || 10;
254
- const remaining = sessionLimit - state.totalEstimatedCost;
255
-
256
- return {
257
- spent: state.totalEstimatedCost,
258
- remaining,
259
- limit: sessionLimit,
260
- warning: remaining < sessionLimit * 0.2, // <20% remaining
261
- blocked: remaining <= 0,
262
- tierCounts: state.tierCounts,
263
- };
264
- }
265
-
266
- // ─── Format for User Display ─────────────────────────────────────────────────
267
-
268
- export function formatGovernancePrompt(governance, collaboration) {
269
- const tierLabel = MODEL_TIERS[governance.requestedTier]?.label || 'unknown';
270
- const lines = [];
271
-
272
- lines.push(`[governance] Task requires ${tierLabel} model (tier ${governance.requestedTier}, ~$${governance.estimatedCost.toFixed(2)})`);
273
- if (governance.justification) lines.push(` Reason: ${governance.justification}`);
274
- if (collaboration?.collaborate) {
275
- lines.push(` + ${collaboration.pattern} with secondary model (+$${collaboration.estimatedOverhead.toFixed(2)})`);
276
- }
277
-
278
- return lines.join('\n');
279
- }
package/src/handoff.mjs DELETED
@@ -1,87 +0,0 @@
1
- // handoff.mjs — Typed handoffs between pipeline stages
2
- import { writeFileSync, readFileSync, existsSync, mkdirSync, renameSync, unlinkSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- export const HANDOFF_TYPES = {
6
- 'think-to-work': { required: ['objective', 'files', 'criteria'], optional: ['context', 'confidence'] },
7
- 'work-to-review': { required: ['filesChanged', 'objective'], optional: ['diff', 'criteria', 'testsRun'] },
8
- 'review-to-head': { required: ['pass'], optional: ['findings', 'recommendation', 'severity'] },
9
- };
10
-
11
- const hDir = (cwd) => join(cwd || process.cwd(), '.dualbrain', 'handoffs');
12
- const hPath = (id, f, t, cwd) => join(hDir(cwd), `${id}_${f}_${t}.json`);
13
-
14
- function validate(from, to, data) {
15
- const schema = HANDOFF_TYPES[`${from}-to-${to}`];
16
- if (!schema) return;
17
- for (const f of schema.required) {
18
- if (!(f in data)) process.stderr.write(`[handoff] warn: missing required field '${f}' in ${from}-to-${to}\n`);
19
- }
20
- }
21
-
22
- export function createHandoff(fromStage, toStage, data, runId, cwd) {
23
- try {
24
- validate(fromStage, toStage, data);
25
- const safeRunId = String(runId).replace(/[^a-z0-9_-]/gi, '_').slice(0, 50);
26
- const dir = hDir(cwd);
27
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
28
- const record = { fromStage, toStage, runId: safeRunId, createdAt: new Date().toISOString(), data };
29
- const dest = hPath(safeRunId, fromStage, toStage, cwd); const tmp = dest + '.tmp';
30
- writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf8');
31
- try { renameSync(tmp, dest); } catch { writeFileSync(dest, JSON.stringify(record, null, 2), 'utf8'); }
32
- return record;
33
- } catch { return null; }
34
- }
35
-
36
- export function consumeHandoff(runId, fromStage, toStage, cwd) {
37
- try {
38
- const safeRunId = String(runId).replace(/[^a-z0-9_-]/gi, '_').slice(0, 50);
39
- const p = hPath(safeRunId, fromStage, toStage, cwd);
40
- if (!existsSync(p)) return null;
41
- const record = JSON.parse(readFileSync(p, 'utf8'));
42
- try { unlinkSync(p); } catch { /* best-effort */ }
43
- return record;
44
- } catch { return null; }
45
- }
46
-
47
- export function buildHandoffContext(handoff, targetRole) {
48
- if (!handoff?.data) return '';
49
- const d = handoff.data;
50
- const lines = (...parts) => parts.filter(Boolean).join('\n');
51
- const list = (v) => Array.isArray(v) ? v.join(', ') : (v ?? '');
52
- const items = (v) => Array.isArray(v) ? v.map(x => ` - ${x}`).join('\n') : (v ?? '');
53
-
54
- if (targetRole === 'worker' && handoff.fromStage === 'thinker') return lines(
55
- '## Handoff from Think Stage',
56
- `**Objective:** ${d.objective ?? '(none)'}`,
57
- `**Files in scope:** ${list(d.files) || 'unspecified'}`,
58
- d.criteria ? `**Acceptance criteria:**\n${items(d.criteria)}` : '',
59
- d.context ? `**Context:** ${d.context}` : '',
60
- d.confidence != null ? `**Thinker confidence:** ${d.confidence}` : '',
61
- );
62
- if (targetRole === 'reviewer' && handoff.fromStage === 'worker') return lines(
63
- '## Handoff from Work Stage',
64
- `**Objective:** ${d.objective ?? '(none)'}`,
65
- `**Files changed:** ${list(d.filesChanged) || 'unknown'}`,
66
- d.criteria ? `**Original criteria:** ${Array.isArray(d.criteria) ? d.criteria.join('; ') : d.criteria}` : '',
67
- d.testsRun ? `**Tests run:** ${d.testsRun}` : '',
68
- d.diff ? `**Diff summary:**\n\`\`\`\n${d.diff.slice(0, 1200)}\n\`\`\`` : '',
69
- );
70
- if (targetRole === 'head' && handoff.fromStage === 'reviewer') return lines(
71
- `## Review Result: ${d.pass ? 'PASS' : 'FAIL'}`,
72
- d.findings ? `**Findings:**\n${items(d.findings)}` : '',
73
- d.recommendation ? `**Recommendation:** ${d.recommendation}` : '',
74
- d.severity ? `**Severity:** ${d.severity}` : '',
75
- );
76
- return JSON.stringify(handoff.data, null, 2);
77
- }
78
-
79
- export function cleanupHandoffs(runId, cwd) {
80
- try {
81
- const dir = hDir(cwd);
82
- if (!existsSync(dir)) return;
83
- for (const name of readdirSync(dir)) {
84
- if (name.startsWith(`${runId}_`)) try { unlinkSync(join(dir, name)); } catch { /* best-effort */ }
85
- }
86
- } catch { /* non-throwing */ }
87
- }
@@ -1,128 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { processTurn, loadState } from './head.mjs';
4
-
5
- const DUALBRAIN = join(process.cwd(), '.dualbrain');
6
- const DELIBERATION_FILE = join(DUALBRAIN, 'deliberation.json');
7
-
8
- /**
9
- * Write the deliberation artifact after running HEAD's cognitive pipeline.
10
- * This is the contract between HEAD's thinking and the deliberation-gate hook.
11
- *
12
- * @param {string} userMessage - The user's message to deliberate on
13
- * @param {object} context - Optional context (files, priorFailures, patterns, etc.)
14
- * @returns {object} The full deliberation result from processTurn
15
- */
16
- export function writeDeliberation(userMessage, context = {}) {
17
- const state = loadState();
18
- const result = processTurn(state, userMessage, context);
19
-
20
- // Determine if there are multiple independent sub-tasks that could be parallelized
21
- const dispatchPlan = _deriveDispatchPlan(result, context);
22
-
23
- // Build the artifact
24
- const artifact = {
25
- timestamp: Date.now(),
26
- createdAt: Date.now(),
27
- message: userMessage.slice(0, 500),
28
-
29
- // Core deliberation fields
30
- action: result.action,
31
- result: result.result,
32
- shouldAskUser: result.shouldAskUser,
33
- confidence: result.result?.confidence || null,
34
-
35
- // Obligations and noticings
36
- obligations: result.obligations || [],
37
- surfaceNoticings: result.result?.surfaceNoticings || [],
38
-
39
- // Dispatch plan (parallel-wave support)
40
- dispatchPlan,
41
-
42
- // Metadata
43
- depth: result.depth,
44
- rationale: result.rationale,
45
- situation: {
46
- taskShape: result.situation?.taskShape || null,
47
- urgency: result.situation?.urgency || null,
48
- scope: result.situation?.taskShape?.scope || null,
49
- },
50
- };
51
-
52
- // Write atomically
53
- mkdirSync(DUALBRAIN, { recursive: true });
54
- const tmpFile = DELIBERATION_FILE + '.tmp.' + process.pid;
55
- writeFileSync(tmpFile, JSON.stringify(artifact, null, 2));
56
- renameSync(tmpFile, DELIBERATION_FILE);
57
-
58
- return result;
59
- }
60
-
61
- /**
62
- * Read the current deliberation artifact.
63
- * @returns {object|null} The deliberation artifact, or null if not found/unreadable.
64
- */
65
- export function readDeliberation() {
66
- try {
67
- if (!existsSync(DELIBERATION_FILE)) return null;
68
- return JSON.parse(readFileSync(DELIBERATION_FILE, 'utf8'));
69
- } catch {
70
- return null;
71
- }
72
- }
73
-
74
- /**
75
- * Check if the current deliberation is fresh (within maxAgeMs).
76
- * @param {number} maxAgeMs - Maximum age in milliseconds (default 60000)
77
- * @returns {boolean} True if deliberation exists and is fresh.
78
- */
79
- export function isDeliberationFresh(maxAgeMs = 60_000) {
80
- const delib = readDeliberation();
81
- if (!delib) return false;
82
- const timestamp = delib.timestamp || delib.createdAt || 0;
83
- return (Date.now() - timestamp) <= maxAgeMs;
84
- }
85
-
86
- // ── Internal helpers ─────────────────────────────────────────────────────────
87
-
88
- /**
89
- * Derive a dispatch plan when the situation has multiple independent sub-tasks.
90
- * Returns null if parallel dispatch is not applicable.
91
- */
92
- function _deriveDispatchPlan(result, context) {
93
- const situation = result.situation;
94
- if (!situation) return null;
95
-
96
- // Only generate a parallel plan for multi-file, non-trivial work
97
- const scope = situation.taskShape?.scope;
98
- const actionType = result.action?.type;
99
-
100
- if (scope !== 'large' && scope !== 'medium') return null;
101
- if (actionType !== 'dispatch' && actionType !== 'proceed') return null;
102
-
103
- // Check for independent sub-tasks from context
104
- const subtasks = context.subtasks || [];
105
- if (subtasks.length < 2) {
106
- // Heuristic: large scope with multiple files could be parallel
107
- const fileCount = situation.material?.touchedFiles?.length || 0;
108
- if (fileCount < 3) return null;
109
-
110
- return {
111
- strategy: 'parallel-wave',
112
- id: Date.now().toString(36),
113
- expectedParallel: Math.min(fileCount, 5),
114
- waveSize: Math.min(fileCount, 5),
115
- reason: `${fileCount} independent files detected`,
116
- };
117
- }
118
-
119
- return {
120
- strategy: 'parallel-wave',
121
- id: Date.now().toString(36),
122
- expectedParallel: subtasks.length,
123
- waveSize: subtasks.length,
124
- subtasks: subtasks.map(t => typeof t === 'string' ? t : t.name || t.description),
125
- reason: `${subtasks.length} independent sub-tasks identified`,
126
- };
127
- }
128
-