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