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,545 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- // ── Blackboard: shared state across collaborating agents ────────────────────
5
-
6
- /**
7
- * Create a fresh collaboration session.
8
- * All agents in a multi-agent task share this blackboard.
9
- */
10
- export function createSession(taskId, objective, opts = {}) {
11
- return {
12
- id: taskId || Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
13
- objective,
14
- created: Date.now(),
15
- status: 'active',
16
-
17
- // Shared knowledge — agents write findings here, others read them
18
- blackboard: {
19
- findings: [], // { agentId, type, content, confidence, timestamp }
20
- files: new Set(), // files discovered or changed (serialized as array)
21
- decisions: [], // { agentId, decision, rationale, timestamp }
22
- warnings: [], // { agentId, severity, message, timestamp }
23
- context: {}, // arbitrary key-value context any agent can set
24
- },
25
-
26
- // Agent tracking
27
- agents: [], // { id, role, provider, model, status, startedAt, completedAt, result }
28
-
29
- // Event log — HEAD reads this to know what happened
30
- events: [], // { type, agentId, data, timestamp }
31
-
32
- // Chain configuration
33
- chain: opts.chain || null, // ordered list of stages if chained execution
34
- currentStage: 0,
35
-
36
- // Cross-review config
37
- crossReview: opts.crossReview ?? false,
38
- };
39
- }
40
-
41
- // ── Blackboard operations ───────────────────────────────────────────────────
42
-
43
- export function addFinding(session, agentId, type, content, confidence = 0.8) {
44
- session.blackboard.findings.push({
45
- agentId, type, content, confidence, timestamp: Date.now(),
46
- });
47
- _emitEvent(session, 'finding', agentId, { type, content, confidence });
48
- }
49
-
50
- export function addDecision(session, agentId, decision, rationale) {
51
- session.blackboard.decisions.push({
52
- agentId, decision, rationale, timestamp: Date.now(),
53
- });
54
- _emitEvent(session, 'decision', agentId, { decision, rationale });
55
- }
56
-
57
- export function addWarning(session, agentId, severity, message) {
58
- session.blackboard.warnings.push({
59
- agentId, severity, message, timestamp: Date.now(),
60
- });
61
- _emitEvent(session, 'warning', agentId, { severity, message });
62
- }
63
-
64
- export function setContext(session, key, value, agentId = 'head') {
65
- session.blackboard.context[key] = value;
66
- _emitEvent(session, 'context-set', agentId, { key });
67
- }
68
-
69
- export function trackFile(session, filePath, agentId) {
70
- if (typeof session.blackboard.files === 'object' && session.blackboard.files instanceof Set) {
71
- session.blackboard.files.add(filePath);
72
- } else {
73
- if (!Array.isArray(session.blackboard.files)) session.blackboard.files = [];
74
- if (!session.blackboard.files.includes(filePath)) session.blackboard.files.push(filePath);
75
- }
76
- _emitEvent(session, 'file-tracked', agentId, { filePath });
77
- }
78
-
79
- // ── Agent lifecycle ─────────────────────────────────────────────────────────
80
-
81
- export function registerAgent(session, agentId, role, provider, model) {
82
- const agent = {
83
- id: agentId,
84
- role,
85
- provider,
86
- model,
87
- status: 'registered',
88
- startedAt: null,
89
- completedAt: null,
90
- result: null,
91
- summary: null,
92
- };
93
- session.agents.push(agent);
94
- _emitEvent(session, 'agent-registered', agentId, { role, provider, model });
95
- return agent;
96
- }
97
-
98
- export function startAgent(session, agentId) {
99
- const agent = session.agents.find(a => a.id === agentId);
100
- if (agent) {
101
- agent.status = 'running';
102
- agent.startedAt = Date.now();
103
- _emitEvent(session, 'agent-started', agentId, {});
104
- }
105
- }
106
-
107
- export function completeAgent(session, agentId, result, summary) {
108
- const agent = session.agents.find(a => a.id === agentId);
109
- if (agent) {
110
- agent.status = result?.error ? 'failed' : 'completed';
111
- agent.completedAt = Date.now();
112
- agent.result = result;
113
- agent.summary = summary || _extractSummary(result);
114
- _emitEvent(session, 'agent-completed', agentId, {
115
- status: agent.status,
116
- durationMs: agent.completedAt - agent.startedAt,
117
- summary: agent.summary,
118
- });
119
- }
120
- }
121
-
122
- // ── Context builder: what an agent sees from prior agents ───────────────────
123
-
124
- /**
125
- * Build a context injection string for the next agent in the collaboration.
126
- * Contains: blackboard findings, decisions, warnings, and prior agent summaries.
127
- * Token-budgeted to stay compact.
128
- */
129
- export function buildAgentContext(session, forAgentId, maxTokens = 2000) {
130
- const lines = [];
131
- const charBudget = maxTokens * 4;
132
-
133
- lines.push('[COLLABORATION CONTEXT]');
134
-
135
- // Prior agent summaries (most valuable — what others already did)
136
- const completedAgents = session.agents.filter(a => a.status === 'completed' && a.id !== forAgentId);
137
- if (completedAgents.length > 0) {
138
- lines.push('');
139
- lines.push('Prior work:');
140
- for (const a of completedAgents) {
141
- const duration = a.completedAt - a.startedAt;
142
- const durationLabel = duration > 60000 ? `${Math.round(duration / 60000)}m` : `${Math.round(duration / 1000)}s`;
143
- lines.push(`- ${a.role} (${a.provider}/${a.model}, ${durationLabel}): ${(a.summary || 'completed').slice(0, 200)}`);
144
- }
145
- }
146
-
147
- // Key findings (high confidence first)
148
- const findings = [...session.blackboard.findings]
149
- .sort((a, b) => b.confidence - a.confidence)
150
- .slice(0, 10);
151
- if (findings.length > 0) {
152
- lines.push('');
153
- lines.push('Findings:');
154
- for (const f of findings) {
155
- lines.push(`- [${f.type}] ${f.content.slice(0, 150)}`);
156
- }
157
- }
158
-
159
- // Decisions made
160
- if (session.blackboard.decisions.length > 0) {
161
- lines.push('');
162
- lines.push('Decisions:');
163
- for (const d of session.blackboard.decisions.slice(-5)) {
164
- lines.push(`- ${d.decision}: ${d.rationale.slice(0, 100)}`);
165
- }
166
- }
167
-
168
- // Active warnings
169
- const activeWarnings = session.blackboard.warnings.filter(w => w.severity === 'high' || w.severity === 'critical');
170
- if (activeWarnings.length > 0) {
171
- lines.push('');
172
- lines.push('Warnings:');
173
- for (const w of activeWarnings) {
174
- lines.push(`- [${w.severity}] ${w.message.slice(0, 120)}`);
175
- }
176
- }
177
-
178
- // Files touched
179
- const files = session.blackboard.files instanceof Set
180
- ? [...session.blackboard.files]
181
- : (Array.isArray(session.blackboard.files) ? session.blackboard.files : []);
182
- if (files.length > 0) {
183
- lines.push('');
184
- lines.push(`Files in play: ${files.slice(0, 15).join(', ')}${files.length > 15 ? ` (+${files.length - 15} more)` : ''}`);
185
- }
186
-
187
- lines.push('[/COLLABORATION CONTEXT]');
188
-
189
- let result = lines.join('\n');
190
- if (result.length > charBudget) {
191
- result = result.slice(0, charBudget - 20) + '\n[...truncated]';
192
- }
193
- return result;
194
- }
195
-
196
- // ── Chain execution: ordered multi-stage pipelines ──────────────────────────
197
-
198
- /**
199
- * Define a chain of agent stages.
200
- * Each stage runs after the previous completes, with full blackboard access.
201
- *
202
- * @param {Array<{ role: string, tier: string, promptTemplate: Function, provider?: string, model?: string }>} stages
203
- */
204
- export function defineChain(stages) {
205
- return stages.map((s, i) => ({
206
- index: i,
207
- role: s.role,
208
- tier: s.tier || 'execute',
209
- promptTemplate: s.promptTemplate,
210
- provider: s.provider || 'claude',
211
- model: s.model || null,
212
- dependsOn: i > 0 ? [i - 1] : [],
213
- }));
214
- }
215
-
216
- /**
217
- * Get the next stage to execute in a chain.
218
- * Returns null when all stages are complete or if dependencies aren't met.
219
- */
220
- export function getNextStage(session) {
221
- if (!session.chain) return null;
222
-
223
- const stage = session.chain[session.currentStage];
224
- if (!stage) return null;
225
-
226
- // Check dependencies
227
- for (const depIdx of stage.dependsOn || []) {
228
- const depAgent = session.agents.find(a => a.role === session.chain[depIdx]?.role);
229
- if (!depAgent || depAgent.status !== 'completed') return null;
230
- }
231
-
232
- return stage;
233
- }
234
-
235
- /**
236
- * Advance the chain to the next stage.
237
- */
238
- export function advanceChain(session) {
239
- session.currentStage++;
240
- return session.currentStage < (session.chain?.length || 0);
241
- }
242
-
243
- /**
244
- * Build the prompt for a chain stage, injecting collaboration context.
245
- */
246
- export function buildChainPrompt(session, stage) {
247
- const context = buildAgentContext(session, `chain-${stage.index}`);
248
- const basePrompt = stage.promptTemplate(session);
249
- return `${context}\n\n${basePrompt}`;
250
- }
251
-
252
- // ── Cross-review: opposite provider reviews the work ────────────────────────
253
-
254
- /**
255
- * Build a cross-review prompt for an agent's output.
256
- * Symmetric: works in both directions (Claude→OpenAI and OpenAI→Claude).
257
- * Falls back to same-provider review with a different model if the opposite
258
- * provider isn't available.
259
- *
260
- * @param {object} session
261
- * @param {string} agentId
262
- * @param {string[]} [availableProviders] Which providers are online
263
- */
264
- export function buildCrossReviewPrompt(session, agentId, availableProviders) {
265
- const agent = session.agents.find(a => a.id === agentId);
266
- if (!agent || !agent.result) return null;
267
-
268
- // Symmetric provider swap — respects availability
269
- const opposite = agent.provider === 'claude' ? 'openai' : 'claude';
270
- const reviewProvider = (!availableProviders || availableProviders.includes(opposite))
271
- ? opposite
272
- : agent.provider;
273
-
274
- // When same-provider review, use a different model tier
275
- const sameProvider = reviewProvider === agent.provider;
276
- const reviewModel = sameProvider
277
- ? (agent.model === 'opus' ? 'sonnet' : 'opus')
278
- : null;
279
-
280
- const prompt = [
281
- `Review the following work by ${agent.provider}/${agent.model} (${agent.role}):`,
282
- '',
283
- `Objective: ${session.objective}`,
284
- '',
285
- `Result summary: ${(agent.summary || '').slice(0, 500)}`,
286
- '',
287
- 'Check for:',
288
- '- Correctness: does the output match the objective?',
289
- '- Missed edge cases or risks',
290
- '- Anything the next agent should know',
291
- '',
292
- 'Be concise. Return: assessment (pass/flag/fail), key concerns, and suggestions.',
293
- sameProvider ? '\nNote: You are reviewing work done by the same provider but a different model. Be especially critical.' : '',
294
- ].join('\n');
295
-
296
- return { prompt, provider: reviewProvider, model: reviewModel, tier: 'search' };
297
- }
298
-
299
- // ── HEAD observation: synthesize what happened ──────────────────────────────
300
-
301
- /**
302
- * Generate a compact summary of the collaboration session for HEAD.
303
- * HEAD uses this to understand what happened without reading raw outputs.
304
- */
305
- export function synthesize(session) {
306
- const completed = session.agents.filter(a => a.status === 'completed');
307
- const failed = session.agents.filter(a => a.status === 'failed');
308
- const running = session.agents.filter(a => a.status === 'running');
309
-
310
- const totalDuration = completed.reduce((sum, a) => sum + (a.completedAt - a.startedAt), 0);
311
-
312
- const files = session.blackboard.files instanceof Set
313
- ? [...session.blackboard.files]
314
- : (Array.isArray(session.blackboard.files) ? session.blackboard.files : []);
315
-
316
- return {
317
- sessionId: session.id,
318
- objective: session.objective,
319
- status: failed.length > 0 ? 'partial' : running.length > 0 ? 'in-progress' : 'complete',
320
- agents: {
321
- total: session.agents.length,
322
- completed: completed.length,
323
- failed: failed.length,
324
- running: running.length,
325
- },
326
- summaries: completed.map(a => ({
327
- role: a.role,
328
- provider: a.provider,
329
- model: a.model,
330
- summary: a.summary,
331
- durationMs: a.completedAt - a.startedAt,
332
- })),
333
- findings: session.blackboard.findings.length,
334
- decisions: session.blackboard.decisions,
335
- warnings: session.blackboard.warnings.filter(w => w.severity !== 'low'),
336
- filesAffected: files,
337
- totalDurationMs: totalDuration,
338
- eventCount: session.events.length,
339
- };
340
- }
341
-
342
- // ── Preset collaboration patterns ───────────────────────────────────────────
343
-
344
- /**
345
- * Plan-Code-Review: the Devin-style self-review loop.
346
- * 1. Plan agent outlines the approach
347
- * 2. Code agent implements
348
- * 3. Review agent checks the work
349
- * 4. If review fails, code agent gets another pass with review feedback
350
- */
351
- export function planCodeReviewChain(objective, scope, opts = {}) {
352
- return defineChain([
353
- {
354
- role: 'planner',
355
- tier: 'think',
356
- provider: opts.planProvider || 'claude',
357
- model: opts.planModel || 'opus',
358
- promptTemplate: (session) => {
359
- return [
360
- `Plan the implementation for: ${objective}`,
361
- '',
362
- `Scope: ${scope.join(', ')}`,
363
- '',
364
- 'Return: step-by-step plan, files to modify, risks, and acceptance criteria.',
365
- 'Do NOT implement — only plan.',
366
- ].join('\n');
367
- },
368
- },
369
- {
370
- role: 'implementer',
371
- tier: 'execute',
372
- provider: opts.codeProvider || 'claude',
373
- model: opts.codeModel || 'sonnet',
374
- promptTemplate: (session) => {
375
- const planAgent = session.agents.find(a => a.role === 'planner');
376
- const plan = planAgent?.summary || 'No plan available — use best judgment.';
377
- return [
378
- `Implement: ${objective}`,
379
- '',
380
- `Plan: ${plan}`,
381
- '',
382
- `Scope: ${scope.join(', ')}`,
383
- '',
384
- 'Follow the plan exactly. Report files changed and tests run.',
385
- ].join('\n');
386
- },
387
- },
388
- {
389
- role: 'reviewer',
390
- tier: 'review',
391
- provider: opts.reviewProvider || (opts.codeProvider === 'claude' ? 'openai' : 'claude'),
392
- model: opts.reviewModel || null,
393
- promptTemplate: (session) => {
394
- const implAgent = session.agents.find(a => a.role === 'implementer');
395
- return [
396
- `Review the implementation of: ${objective}`,
397
- '',
398
- `What was done: ${implAgent?.summary || 'unknown'}`,
399
- '',
400
- `Scope: ${scope.join(', ')}`,
401
- '',
402
- 'Check: correctness, edge cases, security, test coverage, architectural drift.',
403
- 'Return: pass/fail, findings with severity, and fixes needed.',
404
- ].join('\n');
405
- },
406
- },
407
- ]);
408
- }
409
-
410
- /**
411
- * Research-Synthesize: multiple agents research in parallel, one synthesizes.
412
- */
413
- export function researchSynthesizePattern(question, sources, opts = {}) {
414
- const researchStages = sources.map((source, i) => ({
415
- role: `researcher-${i}`,
416
- tier: 'search',
417
- provider: i % 2 === 0 ? 'claude' : (opts.altProvider || 'claude'),
418
- model: opts.researchModel || 'haiku',
419
- promptTemplate: () => `Research: ${question}\nFocus on: ${source}\nReturn: key findings, file references, confidence level.`,
420
- }));
421
-
422
- return defineChain([
423
- ...researchStages,
424
- {
425
- role: 'synthesizer',
426
- tier: 'think',
427
- provider: opts.synthProvider || 'claude',
428
- model: opts.synthModel || 'sonnet',
429
- promptTemplate: (session) => {
430
- const researchFindings = session.agents
431
- .filter(a => a.role.startsWith('researcher-') && a.status === 'completed')
432
- .map(a => `[${a.role}]: ${a.summary || 'no findings'}`)
433
- .join('\n');
434
- return [
435
- `Synthesize research on: ${question}`,
436
- '',
437
- 'Research findings:',
438
- researchFindings,
439
- '',
440
- 'Combine findings into a coherent answer. Note disagreements between sources.',
441
- 'Return: synthesis, confidence level, remaining unknowns.',
442
- ].join('\n');
443
- },
444
- },
445
- ]);
446
- }
447
-
448
- /**
449
- * Dual-Review: two providers independently review, then a third reconciles.
450
- */
451
- export function dualReviewPattern(files, context, opts = {}) {
452
- return defineChain([
453
- {
454
- role: 'reviewer-claude',
455
- tier: 'review',
456
- provider: 'claude',
457
- model: opts.claudeModel || 'sonnet',
458
- promptTemplate: () => `Review these files: ${files.join(', ')}\nContext: ${context}\nReturn: findings with severity and line references.`,
459
- },
460
- {
461
- role: 'reviewer-openai',
462
- tier: 'review',
463
- provider: 'openai',
464
- model: opts.openaiModel || 'gpt-4o',
465
- promptTemplate: () => `Review these files: ${files.join(', ')}\nContext: ${context}\nReturn: findings with severity and line references.`,
466
- },
467
- {
468
- role: 'reconciler',
469
- tier: 'think',
470
- provider: 'claude',
471
- model: opts.reconcileModel || 'opus',
472
- promptTemplate: (session) => {
473
- const reviews = session.agents
474
- .filter(a => a.role.startsWith('reviewer-') && a.status === 'completed')
475
- .map(a => `[${a.provider}]: ${a.summary || 'no findings'}`)
476
- .join('\n\n');
477
- return [
478
- 'Reconcile two independent code reviews:',
479
- '',
480
- reviews,
481
- '',
482
- 'Identify: agreements (high confidence), disagreements (need resolution), and missed items.',
483
- 'Return: final consolidated review with severity ratings.',
484
- ].join('\n');
485
- },
486
- },
487
- ]);
488
- }
489
-
490
- // ── Persistence ─────────────────────────────────────────────────────────────
491
-
492
- export function saveSession(session, cwd) {
493
- const dir = join(cwd || process.cwd(), '.dual-brain', 'collaborations');
494
- mkdirSync(dir, { recursive: true });
495
-
496
- // Convert Set to Array for JSON serialization
497
- const serializable = {
498
- ...session,
499
- blackboard: {
500
- ...session.blackboard,
501
- files: session.blackboard.files instanceof Set
502
- ? [...session.blackboard.files]
503
- : session.blackboard.files,
504
- },
505
- };
506
-
507
- writeFileSync(join(dir, `${session.id}.json`), JSON.stringify(serializable, null, 2));
508
- }
509
-
510
- export function loadSession(sessionId, cwd) {
511
- const path = join(cwd || process.cwd(), '.dual-brain', 'collaborations', `${sessionId}.json`);
512
- if (!existsSync(path)) return null;
513
- try {
514
- const data = JSON.parse(readFileSync(path, 'utf8'));
515
- data.blackboard.files = new Set(data.blackboard.files || []);
516
- return data;
517
- } catch {
518
- return null;
519
- }
520
- }
521
-
522
- // ── Event bus (internal) ────────────────────────────────────────────────────
523
-
524
- function _emitEvent(session, type, agentId, data) {
525
- session.events.push({ type, agentId, data, timestamp: Date.now() });
526
- }
527
-
528
- function _extractSummary(result) {
529
- if (!result) return null;
530
- if (typeof result === 'string') return result.slice(0, 300);
531
- if (result.summary) return String(result.summary).slice(0, 300);
532
- if (result.rawOutput) return String(result.rawOutput).slice(0, 300);
533
- return null;
534
- }
535
-
536
- // ── Event log persistence (append-only JSONL) ───────────────────────────────
537
-
538
- export function persistEvents(session, cwd) {
539
- const dir = join(cwd || process.cwd(), '.dual-brain', 'collaborations');
540
- mkdirSync(dir, { recursive: true });
541
- const logPath = join(dir, `${session.id}.events.jsonl`);
542
- for (const event of session.events) {
543
- appendFileSync(logPath, JSON.stringify(event) + '\n');
544
- }
545
- }
@@ -1,158 +0,0 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
-
4
- export const MODEL_FORMAT = {
5
- claude: 'xml', sonnet: 'xml', haiku: 'xml', opus: 'xml',
6
- gpt: 'markdown', 'o4-mini': 'markdown',
7
- o3: 'prose',
8
- };
9
-
10
- function detectFormat(targetModel, role) {
11
- const m = targetModel.toLowerCase();
12
- if (m.includes('o3') || (m.includes('opus') && role === 'thinker')) return 'prose';
13
- if (m.includes('gpt') || m.includes('o3') || m.includes('o4')) return 'markdown';
14
- return 'xml';
15
- }
16
-
17
- export function selectRelevant(pack, role) {
18
- if (!pack) return { intent: '', constraints: [], acceptanceCriteria: [] };
19
- const { intent, constraints, priorAttempts, repoState, fileSummaries,
20
- acceptanceCriteria, files } = pack;
21
- if (role === 'thinker') {
22
- return { intent, constraints, priorAttempts, repoState,
23
- fileSummaries, acceptanceCriteria };
24
- }
25
- if (role === 'worker') {
26
- const inScope = [...(files?.explicit || []), ...(files?.gitChanged || [])];
27
- return { intent, acceptanceCriteria, constraints, inScope };
28
- }
29
- // reviewer
30
- return { intent, acceptanceCriteria, constraints, fileSummaries, repoState };
31
- }
32
-
33
- function readFiles(paths, cwd) {
34
- const base = cwd || process.cwd();
35
- const out = {};
36
- for (const p of paths) {
37
- const abs = resolve(base, p);
38
- if (existsSync(abs)) {
39
- try { out[p] = readFileSync(abs, 'utf8'); } catch { out[p] = '(unreadable)'; }
40
- }
41
- }
42
- return out;
43
- }
44
-
45
- export function renderForModel(sections, targetModel, role) {
46
- const fmt = detectFormat(targetModel, role);
47
-
48
- if (fmt === 'prose') {
49
- const parts = [];
50
- if (sections.intent) parts.push(`Task: ${sections.intent}`);
51
- if (sections.acceptanceCriteria?.length)
52
- parts.push(`Success looks like: ${sections.acceptanceCriteria.join('; ')}`);
53
- if (sections.constraints?.length)
54
- parts.push(`Constraints: ${sections.constraints.join('; ')}`);
55
- if (sections.repoState) parts.push(`Repo: ${JSON.stringify(sections.repoState)}`);
56
- if (sections.fileSummaries) parts.push(`Files: ${JSON.stringify(sections.fileSummaries)}`);
57
- if (sections.priorAttempts?.length)
58
- parts.push(`Prior attempts: ${JSON.stringify(sections.priorAttempts)}`);
59
- return parts.join('\n\n');
60
- }
61
-
62
- if (fmt === 'markdown') {
63
- const lines = [];
64
- if (sections.intent) lines.push(`## Objective\n${sections.intent}`);
65
- if (sections.constraints?.length)
66
- lines.push(`## Constraints\n${sections.constraints.map(c => `- ${c}`).join('\n')}`);
67
- if (sections.acceptanceCriteria?.length)
68
- lines.push(`## Acceptance Criteria\n${sections.acceptanceCriteria.map(c => `- ${c}`).join('\n')}`);
69
- if (sections.repoState) lines.push(`## Repo State\n\`\`\`json\n${JSON.stringify(sections.repoState, null, 2)}\n\`\`\``);
70
- if (sections.fileSummaries) lines.push(`## Files\n\`\`\`json\n${JSON.stringify(sections.fileSummaries, null, 2)}\n\`\`\``);
71
- if (sections.fileContents) {
72
- lines.push('## File Contents');
73
- for (const [p, content] of Object.entries(sections.fileContents))
74
- lines.push(`### ${p}\n\`\`\`\n${content}\n\`\`\``);
75
- }
76
- if (sections.inScope?.length)
77
- lines.push(`## In-Scope Files\n${sections.inScope.map(f => `- ${f}`).join('\n')}`);
78
- if (sections.priorAttempts?.length)
79
- lines.push(`## Prior Attempts\n${JSON.stringify(sections.priorAttempts, null, 2)}`);
80
- return lines.join('\n\n');
81
- }
82
-
83
- // xml (Claude models)
84
- const tags = [];
85
- if (sections.intent) tags.push(`<objective>${sections.intent}</objective>`);
86
- if (sections.constraints?.length)
87
- tags.push(`<constraints>\n${sections.constraints.map(c => ` <constraint>${c}</constraint>`).join('\n')}\n</constraints>`);
88
- if (sections.acceptanceCriteria?.length)
89
- tags.push(`<criteria>\n${sections.acceptanceCriteria.map(c => ` <criterion>${c}</criterion>`).join('\n')}\n</criteria>`);
90
- if (sections.repoState)
91
- tags.push(`<repo_state>${JSON.stringify(sections.repoState)}</repo_state>`);
92
- if (sections.fileSummaries)
93
- tags.push(`<files>${JSON.stringify(sections.fileSummaries)}</files>`);
94
- if (sections.fileContents) {
95
- const fc = Object.entries(sections.fileContents)
96
- .map(([p, c]) => ` <file path="${p}">\n${c}\n </file>`).join('\n');
97
- tags.push(`<file_contents>\n${fc}\n</file_contents>`);
98
- }
99
- if (sections.inScope?.length)
100
- tags.push(`<in_scope_files>\n${sections.inScope.map(f => ` <file>${f}</file>`).join('\n')}\n</in_scope_files>`);
101
- if (sections.priorAttempts?.length)
102
- tags.push(`<prior_attempts>${JSON.stringify(sections.priorAttempts)}</prior_attempts>`);
103
- return `<context>\n${tags.join('\n')}\n</context>`;
104
- }
105
-
106
- export function enforceTokenBudget(rendered, budget) {
107
- const chars = budget * 4;
108
- const originalTokens = Math.ceil(rendered.length / 4);
109
- if (rendered.length <= chars) return { text: rendered, truncated: false, originalTokens, finalTokens: originalTokens };
110
-
111
- // Try dropping prior_attempts / prior attempts block
112
- let text = rendered
113
- .replace(/<prior_attempts>[\s\S]*?<\/prior_attempts>/g, '')
114
- .replace(/## Prior Attempts[\s\S]*?(?=\n## |$)/, '')
115
- .replace(/Prior attempts:.*?(?=\n\n|$)/s, '');
116
-
117
- if (text.length <= chars) return { text: text.trim(), truncated: true, originalTokens, finalTokens: Math.ceil(text.length / 4) };
118
-
119
- // Summarize git/repo state
120
- text = text
121
- .replace(/<repo_state>[\s\S]*?<\/repo_state>/g, '<repo_state>(truncated)</repo_state>')
122
- .replace(/## Repo State[\s\S]*?(?=\n## |$)/, '## Repo State\n(truncated)');
123
-
124
- if (text.length <= chars) return { text: text.trim(), truncated: true, originalTokens, finalTokens: Math.ceil(text.length / 4) };
125
-
126
- // Hard truncate
127
- text = text.slice(0, chars) + '\n...(truncated)';
128
- return { text, truncated: true, originalTokens, finalTokens: Math.ceil(text.length / 4) };
129
- }
130
-
131
- export function attachOutputSchema(role) {
132
- if (role === 'thinker')
133
- return 'Return JSON: { decision: string, confidence: 0-1, reasoning: string, workSpec: { objective, files, criteria } }';
134
- if (role === 'worker')
135
- return 'Return JSON: { filesChanged: string[], testsRun: boolean, issues: string[] }';
136
- return 'Return JSON: { pass: boolean, findings: [{ severity, file, line, issue, fix }] }';
137
- }
138
-
139
- export function shapeForRole(pack, role, targetModel = 'sonnet', tokenBudget = 8000) {
140
- const sections = selectRelevant(pack, role);
141
-
142
- if (role === 'worker' && sections.inScope?.length) {
143
- const cwd = pack.repoState?.cwd;
144
- sections.fileContents = readFiles(sections.inScope, cwd);
145
- }
146
-
147
- const rendered = renderForModel(sections, targetModel, role);
148
- const { text, truncated, originalTokens, finalTokens } = enforceTokenBudget(rendered, tokenBudget);
149
- const sectionKeys = Object.keys(sections);
150
-
151
- return { shaped: text, role, model: targetModel, tokenEstimate: finalTokens, sections: sectionKeys };
152
- }
153
-
154
- export function compilePacket(pack, role, targetModel, tokenBudget) {
155
- const { shaped } = shapeForRole(pack, role, targetModel, tokenBudget);
156
- const schema = attachOutputSchema(role);
157
- return `${shaped}\n\n${schema}`;
158
- }