dual-brain 0.2.30 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/.dual-brain/docs/claude-code-extension-points.md +32 -0
  2. package/.dual-brain/docs/data-tools-capabilities.md +181 -0
  3. package/.dual-brain/docs/ecosystem-tools.md +91 -0
  4. package/.dual-brain/docs/panel-handoff.md +124 -0
  5. package/.dual-brain/docs/ruflo-analysis.md +48 -0
  6. package/bin/dual-brain.mjs +56 -56
  7. package/dist/mcp-server/index.d.ts +27 -0
  8. package/dist/mcp-server/index.js +359 -0
  9. package/dist/mcp-server/index.js.map +1 -0
  10. package/dist/src/agent-protocol.d.ts +163 -0
  11. package/dist/src/agent-protocol.js +368 -0
  12. package/dist/src/agent-protocol.js.map +1 -0
  13. package/dist/src/agents/registry.d.ts +52 -0
  14. package/dist/src/agents/registry.js +393 -0
  15. package/dist/src/agents/registry.js.map +1 -0
  16. package/dist/src/awareness.d.ts +93 -0
  17. package/dist/src/awareness.js +406 -0
  18. package/dist/src/awareness.js.map +1 -0
  19. package/dist/src/brief.d.ts +48 -0
  20. package/dist/src/brief.js +179 -0
  21. package/dist/src/brief.js.map +1 -0
  22. package/dist/src/calibration.d.ts +32 -0
  23. package/dist/src/calibration.js +133 -0
  24. package/dist/src/calibration.js.map +1 -0
  25. package/dist/src/checkpoint.d.ts +33 -0
  26. package/dist/src/checkpoint.js +99 -0
  27. package/dist/src/checkpoint.js.map +1 -0
  28. package/dist/src/ci-triage.d.ts +33 -0
  29. package/dist/src/ci-triage.js +193 -0
  30. package/dist/src/ci-triage.js.map +1 -0
  31. package/dist/src/cognitive-loop.d.ts +56 -0
  32. package/dist/src/cognitive-loop.js +495 -0
  33. package/dist/src/cognitive-loop.js.map +1 -0
  34. package/dist/src/collaboration.d.ts +147 -0
  35. package/dist/src/collaboration.js +438 -0
  36. package/dist/src/collaboration.js.map +1 -0
  37. package/dist/src/context-intel.d.ts +47 -0
  38. package/dist/src/context-intel.js +156 -0
  39. package/dist/src/context-intel.js.map +1 -0
  40. package/dist/src/context.d.ts +53 -0
  41. package/dist/src/context.js +332 -0
  42. package/dist/src/context.js.map +1 -0
  43. package/dist/src/continuity.d.ts +89 -0
  44. package/dist/src/continuity.js +230 -0
  45. package/dist/src/continuity.js.map +1 -0
  46. package/dist/src/cost-tracker.d.ts +47 -0
  47. package/dist/src/cost-tracker.js +170 -0
  48. package/dist/src/cost-tracker.js.map +1 -0
  49. package/dist/src/debrief.d.ts +53 -0
  50. package/dist/src/debrief.js +222 -0
  51. package/dist/src/debrief.js.map +1 -0
  52. package/dist/src/decide.d.ts +96 -0
  53. package/dist/src/decide.js +744 -0
  54. package/dist/src/decide.js.map +1 -0
  55. package/dist/src/decompose.d.ts +39 -0
  56. package/dist/src/decompose.js +218 -0
  57. package/dist/src/decompose.js.map +1 -0
  58. package/dist/src/detect.d.ts +91 -0
  59. package/dist/src/detect.js +544 -0
  60. package/dist/src/detect.js.map +1 -0
  61. package/dist/src/dispatch.d.ts +154 -0
  62. package/dist/src/dispatch.js +1306 -0
  63. package/dist/src/dispatch.js.map +1 -0
  64. package/dist/src/doctor.d.ts +421 -0
  65. package/dist/src/doctor.js +1689 -0
  66. package/dist/src/doctor.js.map +1 -0
  67. package/dist/src/engine.d.ts +70 -0
  68. package/dist/src/engine.js +155 -0
  69. package/dist/src/engine.js.map +1 -0
  70. package/dist/src/envelope.d.ts +36 -0
  71. package/dist/src/envelope.js +80 -0
  72. package/dist/src/envelope.js.map +1 -0
  73. package/dist/src/failure-memory.d.ts +55 -0
  74. package/dist/src/failure-memory.js +175 -0
  75. package/dist/src/failure-memory.js.map +1 -0
  76. package/dist/src/fx.d.ts +87 -0
  77. package/dist/src/fx.js +272 -0
  78. package/dist/src/fx.js.map +1 -0
  79. package/dist/src/governance.d.ts +93 -0
  80. package/dist/src/governance.js +261 -0
  81. package/dist/src/governance.js.map +1 -0
  82. package/dist/src/handoff.d.ts +11 -0
  83. package/dist/src/handoff.js +90 -0
  84. package/dist/src/handoff.js.map +1 -0
  85. package/dist/src/head-protocol.d.ts +76 -0
  86. package/dist/src/head-protocol.js +109 -0
  87. package/dist/src/head-protocol.js.map +1 -0
  88. package/dist/src/head.d.ts +222 -0
  89. package/dist/src/head.js +765 -0
  90. package/dist/src/head.js.map +1 -0
  91. package/dist/src/health.d.ts +132 -0
  92. package/dist/src/health.js +435 -0
  93. package/dist/src/health.js.map +1 -0
  94. package/dist/src/inbox.d.ts +70 -0
  95. package/dist/src/inbox.js +218 -0
  96. package/dist/src/inbox.js.map +1 -0
  97. package/dist/src/index.d.ts +33 -0
  98. package/dist/src/index.js +38 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/install-hooks.d.ts +13 -0
  101. package/dist/src/install-hooks.js +88 -0
  102. package/dist/src/install-hooks.js.map +1 -0
  103. package/dist/src/integrity.d.ts +59 -0
  104. package/dist/src/integrity.js +206 -0
  105. package/dist/src/integrity.js.map +1 -0
  106. package/dist/src/intelligence.d.ts +104 -0
  107. package/dist/src/intelligence.js +391 -0
  108. package/dist/src/intelligence.js.map +1 -0
  109. package/dist/src/ledger.d.ts +54 -0
  110. package/dist/src/ledger.js +179 -0
  111. package/dist/src/ledger.js.map +1 -0
  112. package/dist/src/living-docs.d.ts +14 -0
  113. package/dist/src/living-docs.js +197 -0
  114. package/dist/src/living-docs.js.map +1 -0
  115. package/dist/src/memory-tiers.d.ts +37 -0
  116. package/dist/src/memory-tiers.js +160 -0
  117. package/dist/src/memory-tiers.js.map +1 -0
  118. package/dist/src/model-profiles.d.ts +65 -0
  119. package/dist/src/model-profiles.js +568 -0
  120. package/dist/src/model-profiles.js.map +1 -0
  121. package/dist/src/models.d.ts +58 -0
  122. package/dist/src/models.js +327 -0
  123. package/dist/src/models.js.map +1 -0
  124. package/dist/src/narrative.d.ts +54 -0
  125. package/dist/src/narrative.js +163 -0
  126. package/dist/src/narrative.js.map +1 -0
  127. package/dist/src/nextstep.d.ts +16 -0
  128. package/dist/src/nextstep.js +103 -0
  129. package/dist/src/nextstep.js.map +1 -0
  130. package/dist/src/observer.d.ts +18 -0
  131. package/dist/src/observer.js +251 -0
  132. package/dist/src/observer.js.map +1 -0
  133. package/dist/src/outcome.d.ts +110 -0
  134. package/dist/src/outcome.js +377 -0
  135. package/dist/src/outcome.js.map +1 -0
  136. package/dist/src/pipeline.d.ts +167 -0
  137. package/dist/src/pipeline.js +1503 -0
  138. package/dist/src/pipeline.js.map +1 -0
  139. package/dist/src/playbook.d.ts +59 -0
  140. package/dist/src/playbook.js +238 -0
  141. package/dist/src/playbook.js.map +1 -0
  142. package/dist/src/pr-agent.d.ts +97 -0
  143. package/dist/src/pr-agent.js +195 -0
  144. package/dist/src/pr-agent.js.map +1 -0
  145. package/dist/src/predictive.d.ts +57 -0
  146. package/dist/src/predictive.js +230 -0
  147. package/dist/src/predictive.js.map +1 -0
  148. package/dist/src/profile.d.ts +294 -0
  149. package/dist/src/profile.js +1347 -0
  150. package/dist/src/profile.js.map +1 -0
  151. package/dist/src/prompt-audit.d.ts +22 -0
  152. package/dist/src/prompt-audit.js +194 -0
  153. package/dist/src/prompt-audit.js.map +1 -0
  154. package/dist/src/prompt-intel.d.ts +12 -0
  155. package/dist/src/prompt-intel.js +321 -0
  156. package/dist/src/prompt-intel.js.map +1 -0
  157. package/dist/src/provider-context.d.ts +121 -0
  158. package/dist/src/provider-context.js +222 -0
  159. package/dist/src/provider-context.js.map +1 -0
  160. package/dist/src/provider-manager.d.ts +92 -0
  161. package/dist/src/provider-manager.js +428 -0
  162. package/dist/src/provider-manager.js.map +1 -0
  163. package/dist/src/receipt.d.ts +87 -0
  164. package/dist/src/receipt.js +326 -0
  165. package/dist/src/receipt.js.map +1 -0
  166. package/dist/src/recommendations.d.ts +13 -0
  167. package/dist/src/recommendations.js +291 -0
  168. package/dist/src/recommendations.js.map +1 -0
  169. package/dist/src/redact.d.ts +15 -0
  170. package/dist/src/redact.js +129 -0
  171. package/dist/src/redact.js.map +1 -0
  172. package/dist/src/replit.d.ts +397 -0
  173. package/dist/src/replit.js +1160 -0
  174. package/dist/src/replit.js.map +1 -0
  175. package/dist/src/repo.d.ts +149 -0
  176. package/dist/src/repo.js +416 -0
  177. package/dist/src/repo.js.map +1 -0
  178. package/dist/src/revert.d.ts +30 -0
  179. package/dist/src/revert.js +166 -0
  180. package/dist/src/revert.js.map +1 -0
  181. package/dist/src/room.d.ts +102 -0
  182. package/dist/src/room.js +212 -0
  183. package/dist/src/room.js.map +1 -0
  184. package/dist/src/routing-advisor.d.ts +57 -0
  185. package/dist/src/routing-advisor.js +221 -0
  186. package/dist/src/routing-advisor.js.map +1 -0
  187. package/dist/src/self-correct.d.ts +40 -0
  188. package/dist/src/self-correct.js +137 -0
  189. package/dist/src/self-correct.js.map +1 -0
  190. package/dist/src/session-lock.d.ts +35 -0
  191. package/dist/src/session-lock.js +134 -0
  192. package/dist/src/session-lock.js.map +1 -0
  193. package/dist/src/session.d.ts +267 -0
  194. package/dist/src/session.js +1660 -0
  195. package/dist/src/session.js.map +1 -0
  196. package/dist/src/settings-tui.d.ts +5 -0
  197. package/dist/src/settings-tui.js +422 -0
  198. package/dist/src/settings-tui.js.map +1 -0
  199. package/dist/src/setup-flow.d.ts +63 -0
  200. package/dist/src/setup-flow.js +233 -0
  201. package/dist/src/setup-flow.js.map +1 -0
  202. package/dist/src/signal.d.ts +19 -0
  203. package/dist/src/signal.js +122 -0
  204. package/dist/src/signal.js.map +1 -0
  205. package/dist/src/simmer.d.ts +85 -0
  206. package/dist/src/simmer.js +224 -0
  207. package/dist/src/simmer.js.map +1 -0
  208. package/dist/src/state-export.d.ts +129 -0
  209. package/dist/src/state-export.js +233 -0
  210. package/dist/src/state-export.js.map +1 -0
  211. package/dist/src/strategy.d.ts +54 -0
  212. package/dist/src/strategy.js +95 -0
  213. package/dist/src/strategy.js.map +1 -0
  214. package/dist/src/subscription.d.ts +40 -0
  215. package/dist/src/subscription.js +189 -0
  216. package/dist/src/subscription.js.map +1 -0
  217. package/dist/src/templates.d.ts +208 -0
  218. package/dist/src/templates.js +238 -0
  219. package/dist/src/templates.js.map +1 -0
  220. package/dist/src/test.d.ts +9 -0
  221. package/dist/src/test.js +1173 -0
  222. package/dist/src/test.js.map +1 -0
  223. package/dist/src/think-engine.d.ts +67 -0
  224. package/dist/src/think-engine.js +412 -0
  225. package/dist/src/think-engine.js.map +1 -0
  226. package/dist/src/tui.d.ts +71 -0
  227. package/dist/src/tui.js +242 -0
  228. package/dist/src/tui.js.map +1 -0
  229. package/dist/src/types.d.ts +177 -0
  230. package/dist/src/types.js +6 -0
  231. package/dist/src/types.js.map +1 -0
  232. package/dist/src/update-check.d.ts +7 -0
  233. package/dist/src/update-check.js +36 -0
  234. package/dist/src/update-check.js.map +1 -0
  235. package/dist/src/wave-planner.d.ts +30 -0
  236. package/dist/src/wave-planner.js +281 -0
  237. package/dist/src/wave-planner.js.map +1 -0
  238. package/hooks/head-guard.sh +41 -0
  239. package/hooks/precompact.mjs +3 -3
  240. package/hooks/session-end.mjs +3 -3
  241. package/hooks/task-classifier.mjs +328 -0
  242. package/hooks/vibe-router.mjs +387 -0
  243. package/install.mjs +2 -2
  244. package/package.json +29 -153
  245. package/src/agents/registry.mjs +0 -405
  246. package/src/awareness.mjs +0 -425
  247. package/src/brief.mjs +0 -266
  248. package/src/calibration.mjs +0 -148
  249. package/src/checkpoint.mjs +0 -109
  250. package/src/ci-triage.mjs +0 -191
  251. package/src/cognitive-loop.mjs +0 -562
  252. package/src/collaboration.mjs +0 -545
  253. package/src/context-intel.mjs +0 -158
  254. package/src/context.mjs +0 -389
  255. package/src/continuity.mjs +0 -298
  256. package/src/cost-tracker.mjs +0 -184
  257. package/src/debrief.mjs +0 -228
  258. package/src/decide.mjs +0 -1099
  259. package/src/decompose.mjs +0 -331
  260. package/src/detect.mjs +0 -702
  261. package/src/dispatch.mjs +0 -1447
  262. package/src/doctor.mjs +0 -1607
  263. package/src/envelope.mjs +0 -139
  264. package/src/failure-memory.mjs +0 -178
  265. package/src/fx.mjs +0 -276
  266. package/src/governance.mjs +0 -279
  267. package/src/handoff.mjs +0 -87
  268. package/src/head-protocol.mjs +0 -128
  269. package/src/head.mjs +0 -952
  270. package/src/health.mjs +0 -528
  271. package/src/inbox.mjs +0 -195
  272. package/src/index.mjs +0 -44
  273. package/src/install-hooks.mjs +0 -100
  274. package/src/integrity.mjs +0 -245
  275. package/src/intelligence.mjs +0 -447
  276. package/src/ledger.mjs +0 -196
  277. package/src/living-docs.mjs +0 -210
  278. package/src/memory-tiers.mjs +0 -193
  279. package/src/models.mjs +0 -363
  280. package/src/narrative.mjs +0 -169
  281. package/src/nextstep.mjs +0 -100
  282. package/src/observer.mjs +0 -241
  283. package/src/outcome.mjs +0 -400
  284. package/src/pipeline.mjs +0 -1711
  285. package/src/playbook.mjs +0 -257
  286. package/src/pr-agent.mjs +0 -214
  287. package/src/predictive.mjs +0 -250
  288. package/src/profile.mjs +0 -1411
  289. package/src/prompt-audit.mjs +0 -231
  290. package/src/prompt-intel.mjs +0 -325
  291. package/src/provider-context.mjs +0 -257
  292. package/src/receipt.mjs +0 -344
  293. package/src/recommendations.mjs +0 -296
  294. package/src/redact.mjs +0 -192
  295. package/src/replit.mjs +0 -1210
  296. package/src/repo.mjs +0 -445
  297. package/src/revert.mjs +0 -149
  298. package/src/routing-advisor.mjs +0 -204
  299. package/src/self-correct.mjs +0 -147
  300. package/src/session-lock.mjs +0 -160
  301. package/src/session.mjs +0 -1655
  302. package/src/settings-tui.mjs +0 -373
  303. package/src/setup-flow.mjs +0 -223
  304. package/src/signal.mjs +0 -115
  305. package/src/simmer.mjs +0 -241
  306. package/src/strategy.mjs +0 -235
  307. package/src/subscription.mjs +0 -212
  308. package/src/templates.mjs +0 -260
  309. package/src/think-engine.mjs +0 -428
  310. package/src/tui.mjs +0 -276
  311. package/src/update-check.mjs +0 -35
  312. package/src/wave-planner.mjs +0 -294
package/src/context.mjs DELETED
@@ -1,389 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
- import { execSync } from 'node:child_process';
3
- import { join, resolve, dirname, extname, relative } from 'node:path';
4
-
5
- import { detectTask } from './detect.mjs';
6
-
7
- // ─── Language detection ───────────────────────────────────────────────────────
8
-
9
- const EXT_LANG = {
10
- '.mjs': 'javascript', '.js': 'javascript', '.cjs': 'javascript',
11
- '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript',
12
- '.py': 'python', '.pyx': 'python', '.pyi': 'python',
13
- '.rs': 'rust',
14
- '.go': 'go',
15
- '.rb': 'ruby',
16
- '.java': 'java',
17
- '.kt': 'kotlin', '.kts': 'kotlin',
18
- '.swift': 'swift',
19
- '.c': 'c', '.h': 'c',
20
- '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp',
21
- '.cs': 'csharp',
22
- '.php': 'php',
23
- '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
24
- '.html': 'html', '.htm': 'html',
25
- '.css': 'css', '.scss': 'scss', '.sass': 'sass', '.less': 'less',
26
- '.json': 'json', '.jsonl': 'json',
27
- '.yaml': 'yaml', '.yml': 'yaml',
28
- '.toml': 'toml',
29
- '.md': 'markdown', '.mdx': 'markdown',
30
- '.sql': 'sql',
31
- '.graphql': 'graphql', '.gql': 'graphql',
32
- '.dockerfile': 'dockerfile',
33
- };
34
-
35
- function detectLanguage(filePath) {
36
- const ext = extname(filePath).toLowerCase();
37
- if (!ext && filePath.toLowerCase().endsWith('dockerfile')) return 'dockerfile';
38
- return EXT_LANG[ext] || 'unknown';
39
- }
40
-
41
- // ─── Git helpers ──────────────────────────────────────────────────────────────
42
-
43
- function git(cmd, cwd) {
44
- return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
45
- }
46
-
47
- function safeGit(cmd, cwd, fallback = '') {
48
- try { return git(cmd, cwd); } catch { return fallback; }
49
- }
50
-
51
- function getGitChangedFiles(cwd) {
52
- const raw = safeGit('git status --porcelain', cwd, '');
53
- if (!raw) return { files: [], statusMap: {} };
54
-
55
- const statusMap = {};
56
- const files = [];
57
-
58
- for (const line of raw.split('\n')) {
59
- if (!line.trim()) continue;
60
- const code = line.slice(0, 2).trim() || '?';
61
- const filePath = line.slice(3).trim().replace(/^"(.*)"$/, '$1');
62
- if (filePath) {
63
- statusMap[filePath] = code;
64
- files.push(filePath);
65
- }
66
- }
67
-
68
- return { files, statusMap };
69
- }
70
-
71
- function getRepoState(cwd) {
72
- const branch = safeGit('git branch --show-current', cwd, 'unknown');
73
-
74
- const statusRaw = safeGit('git status --porcelain', cwd, '');
75
- const uncommittedCount = statusRaw
76
- ? statusRaw.split('\n').filter(l => l.trim()).length
77
- : 0;
78
-
79
- const lastCommitMessage = safeGit('git log -1 --pretty=format:%s', cwd, '');
80
-
81
- let lastCommitAge = 'unknown';
82
- try {
83
- const epochStr = git('git log -1 --pretty=format:%ct', cwd);
84
- const epoch = parseInt(epochStr, 10);
85
- if (!isNaN(epoch)) {
86
- lastCommitAge = formatTimeAgo(epoch * 1000);
87
- }
88
- } catch { /* non-fatal */ }
89
-
90
- return { branch, uncommittedCount, lastCommitMessage, lastCommitAge };
91
- }
92
-
93
- function formatTimeAgo(timestampMs) {
94
- const diff = Date.now() - timestampMs;
95
- const mins = Math.floor(diff / 60_000);
96
- if (mins < 1) return 'just now';
97
- if (mins < 60) return `${mins}m ago`;
98
- const hours = Math.floor(mins / 60);
99
- if (hours < 24) return `${hours}h ago`;
100
- const days = Math.floor(hours / 24);
101
- return `${days}d ago`;
102
- }
103
-
104
- // ─── Related files (import graph, one hop) ───────────────────────────────────
105
-
106
- const IMPORT_RE = /(?:import\s+.*?from\s+|require\s*\(\s*)['"]([^'"]+)['"]/g;
107
-
108
- function findRelatedFiles(explicitFiles, cwd) {
109
- const related = new Set();
110
-
111
- for (const filePath of explicitFiles) {
112
- const absPath = resolve(cwd, filePath);
113
- if (!existsSync(absPath)) continue;
114
-
115
- let content;
116
- try { content = readFileSync(absPath, 'utf8'); } catch { continue; }
117
-
118
- const fileDir = dirname(absPath);
119
- let match;
120
- IMPORT_RE.lastIndex = 0;
121
-
122
- while ((match = IMPORT_RE.exec(content)) !== null) {
123
- const specifier = match[1];
124
- if (!specifier.startsWith('.')) continue; // skip node_modules / bare specifiers
125
-
126
- // Try common extensions in order
127
- const candidates = [
128
- specifier,
129
- specifier + '.mjs', specifier + '.js', specifier + '.ts',
130
- specifier + '/index.mjs', specifier + '/index.js', specifier + '/index.ts',
131
- ];
132
-
133
- for (const candidate of candidates) {
134
- const abs = resolve(fileDir, candidate);
135
- if (existsSync(abs)) {
136
- const rel = relative(cwd, abs);
137
- if (!explicitFiles.includes(rel)) related.add(rel);
138
- break;
139
- }
140
- }
141
- }
142
- }
143
-
144
- return [...related];
145
- }
146
-
147
- // ─── File summaries ───────────────────────────────────────────────────────────
148
-
149
- function buildFileSummary(filePath, cwd, statusMap = {}) {
150
- const absPath = resolve(cwd, filePath);
151
- const language = detectLanguage(filePath);
152
-
153
- let lines = 0;
154
- try {
155
- const content = readFileSync(absPath, 'utf8');
156
- lines = content.split('\n').length;
157
- } catch { /* file missing or unreadable */ }
158
-
159
- const rawStatus = statusMap[filePath] || statusMap[filePath.replace(/\\/g, '/')];
160
- const gitStatus = rawStatus || 'clean';
161
-
162
- return { path: filePath, language, lines, gitStatus };
163
- }
164
-
165
- // ─── Constraints from CLAUDE.md ───────────────────────────────────────────────
166
-
167
- const CONSTRAINT_RE = /\b(must|never|always|require[sd]?|do not|don't)\b/i;
168
-
169
- function extractConstraints(cwd) {
170
- const candidates = [
171
- join(cwd, 'CLAUDE.md'),
172
- join(cwd, '.claude', 'CLAUDE.md'),
173
- ];
174
-
175
- const constraints = [];
176
-
177
- for (const p of candidates) {
178
- if (!existsSync(p)) continue;
179
- try {
180
- const lines = readFileSync(p, 'utf8').split('\n');
181
- for (const line of lines) {
182
- const trimmed = line.trim();
183
- if (trimmed && CONSTRAINT_RE.test(trimmed) && trimmed.length < 200) {
184
- constraints.push(trimmed.replace(/^[-*#\s]+/, '').trim());
185
- }
186
- }
187
- } catch { /* non-fatal */ }
188
- }
189
-
190
- return constraints;
191
- }
192
-
193
- // ─── Prior attempts from .dualbrain/outcomes/ ────────────────────────────────
194
-
195
- function loadPriorAttempts(prompt, cwd) {
196
- const outcomesDir = join(cwd, '.dualbrain', 'outcomes');
197
- if (!existsSync(outcomesDir)) return [];
198
-
199
- const promptWords = new Set(
200
- prompt.toLowerCase().split(/\W+/).filter(w => w.length > 3),
201
- );
202
-
203
- const attempts = [];
204
-
205
- let entries;
206
- try { entries = readdirSync(outcomesDir); } catch { return []; }
207
-
208
- for (const entry of entries) {
209
- if (!entry.endsWith('.json')) continue;
210
- try {
211
- const raw = JSON.parse(readFileSync(join(outcomesDir, entry), 'utf8'));
212
- if (!raw.prompt) continue;
213
-
214
- // Simple word-overlap similarity
215
- const entryWords = raw.prompt.toLowerCase().split(/\W+/).filter(w => w.length > 3);
216
- const overlap = entryWords.filter(w => promptWords.has(w)).length;
217
- const similarity = overlap / Math.max(promptWords.size, entryWords.length, 1);
218
-
219
- if (similarity >= 0.3) {
220
- attempts.push({
221
- timestamp: raw.timestamp || 0,
222
- prompt: raw.prompt,
223
- success: raw.success ?? false,
224
- lesson: raw.lesson || raw.summary || '',
225
- });
226
- }
227
- } catch { /* non-fatal */ }
228
- }
229
-
230
- return attempts.sort((a, b) => b.timestamp - a.timestamp).slice(0, 5);
231
- }
232
-
233
- // ─── Related sessions ─────────────────────────────────────────────────────────
234
-
235
- async function loadRelatedSessions(prompt, files, cwd) {
236
- try {
237
- // Dynamic import so missing module doesn't break the whole pack
238
- const { findRelatedSessions } = await import('./session.mjs');
239
- const raw = findRelatedSessions(prompt, files, cwd);
240
- return raw.map(s => ({
241
- id: s.sessionId,
242
- name: s.smartName || s.sessionId.slice(0, 8),
243
- score: s.score,
244
- }));
245
- } catch {
246
- return [];
247
- }
248
- }
249
-
250
- // ─── Acceptance criteria ──────────────────────────────────────────────────────
251
-
252
- const CRITERIA_PATTERNS = [
253
- { re: /\btests?\s+pass\b/i, label: 'tests pass' },
254
- { re: /\bno\s+regression[s]?\b/i, label: 'no regressions' },
255
- { re: /\bbuilds?\s+clean\b/i, label: 'builds clean' },
256
- { re: /\bbuild[s]?\b/i, label: 'builds clean' },
257
- { re: /\blint\s+clean\b/i, label: 'lint clean' },
258
- { re: /\bno\s+error[s]?\b/i, label: 'no errors' },
259
- { re: /\btype.?check\b/i, label: 'type-check passes' },
260
- { re: /\bworks?\s+on\s+\w+/i, label: (m) => `works on ${m[0].match(/works?\s+on\s+(\w+)/i)?.[1]}` },
261
- { re: /\bcompatible\s+with\s+\w+/i, label: (m) => `compatible with ${m[0].match(/compatible\s+with\s+(\w+)/i)?.[1]}` },
262
- { re: /\bno\s+breaking\s+change[s]?\b/i, label: 'no breaking changes' },
263
- { re: /\bbackward[s]?\s+compat/i, label: 'backward compatible' },
264
- { re: /\ball\s+tests?\s+pass/i, label: 'all tests pass' },
265
- { re: /\bci\s+pass(?:es)?\b/i, label: 'CI passes' },
266
- { re: /\bcoverage\b/i, label: 'coverage maintained' },
267
- ];
268
-
269
- function inferAcceptanceCriteria(prompt) {
270
- const found = new Set();
271
- for (const { re, label } of CRITERIA_PATTERNS) {
272
- const m = prompt.match(re);
273
- if (m) {
274
- const criterion = typeof label === 'function' ? label([m[0]]) : label;
275
- if (criterion) found.add(criterion);
276
- }
277
- }
278
- return [...found];
279
- }
280
-
281
- // ─── Main export ──────────────────────────────────────────────────────────────
282
-
283
- /**
284
- * Build a structured context pack for a task. All fields are best-effort —
285
- * missing git, missing files, and missing optional modules all degrade gracefully.
286
- *
287
- * @param {string} prompt
288
- * @param {string[]} files - Explicitly mentioned file paths (may be relative)
289
- * @param {string} cwd - Working directory (absolute)
290
- * @param {object} options
291
- * @param {number} [options.priorFailures=0]
292
- * @returns {Promise<object>}
293
- */
294
- export async function buildContextPack(prompt = '', files = [], cwd = process.cwd(), options = {}) {
295
- const { priorFailures = 0 } = options;
296
-
297
- // 1. Detection (intent / tier / risk)
298
- const detection = detectTask({ prompt, files, priorFailures });
299
-
300
- // 2. Git changed files + status map
301
- const { files: gitChanged, statusMap } = getGitChangedFiles(cwd);
302
-
303
- // 3. Related files (import graph, one hop from explicit files)
304
- const relatedFiles = findRelatedFiles(files, cwd);
305
-
306
- const filesPack = {
307
- explicit: files,
308
- gitChanged,
309
- related: relatedFiles,
310
- };
311
-
312
- // 4. File summaries — explicit + gitChanged, deduped
313
- const summaryTargets = [...new Set([...files, ...gitChanged])];
314
- const fileSummaries = summaryTargets.map(f => buildFileSummary(f, cwd, statusMap));
315
-
316
- // 5. Repo state
317
- const repoState = getRepoState(cwd);
318
-
319
- // 6. Constraints from CLAUDE.md
320
- const constraints = extractConstraints(cwd);
321
-
322
- // 7. Prior attempts
323
- const priorAttempts = loadPriorAttempts(prompt, cwd);
324
-
325
- // 8. Related sessions (async, may fail silently)
326
- const allFiles = [...new Set([...files, ...gitChanged])];
327
- const relatedSessions = await loadRelatedSessions(prompt, allFiles, cwd);
328
-
329
- // 9. Acceptance criteria
330
- const acceptanceCriteria = inferAcceptanceCriteria(prompt);
331
-
332
- return {
333
- intent: detection.intent,
334
- prompt,
335
- tier: detection.tier,
336
- risk: detection.risk,
337
- files: filesPack,
338
- fileSummaries,
339
- repoState,
340
- constraints,
341
- priorAttempts,
342
- relatedSessions,
343
- acceptanceCriteria,
344
- };
345
- }
346
-
347
- // ─── Summarizer ───────────────────────────────────────────────────────────────
348
-
349
- /**
350
- * Return a human-readable 3-5 line summary of a context pack for logging/display.
351
- *
352
- * @param {object} pack - Result of buildContextPack()
353
- * @returns {string}
354
- */
355
- export function summarizeContextPack(pack) {
356
- const lines = [];
357
-
358
- lines.push(
359
- `Task: ${pack.intent} (${pack.tier} tier, ${pack.risk} risk)`,
360
- );
361
-
362
- const explicit = pack.files?.explicit?.length ?? 0;
363
- const changed = pack.files?.gitChanged?.length ?? 0;
364
- const related = pack.files?.related?.length ?? 0;
365
- lines.push(`Files: ${explicit} explicit, ${changed} changed, ${related} related`);
366
-
367
- const { branch, uncommittedCount, lastCommitAge } = pack.repoState ?? {};
368
- const branchStr = branch ? `${branch} branch` : 'unknown branch';
369
- const uncommittedStr = uncommittedCount != null
370
- ? `${uncommittedCount} uncommitted file${uncommittedCount === 1 ? '' : 's'}`
371
- : 'commit count unknown';
372
- const ageStr = lastCommitAge && lastCommitAge !== 'unknown' ? `, last commit ${lastCommitAge}` : '';
373
- lines.push(`Repo: ${branchStr}, ${uncommittedStr}${ageStr}`);
374
-
375
- if (pack.priorAttempts?.length > 0) {
376
- const failed = pack.priorAttempts.filter(a => !a.success).length;
377
- const total = pack.priorAttempts.length;
378
- const label = failed > 0
379
- ? `${failed} failed attempt${failed === 1 ? '' : 's'} on similar task`
380
- : `${total} prior attempt${total === 1 ? '' : 's'} on similar task`;
381
- lines.push(`Prior: ${label}`);
382
- }
383
-
384
- if (pack.acceptanceCriteria?.length > 0) {
385
- lines.push(`Criteria: ${pack.acceptanceCriteria.join(', ')}`);
386
- }
387
-
388
- return lines.join('\n');
389
- }
@@ -1,298 +0,0 @@
1
- #!/usr/bin/env node
2
- // continuity.mjs — Session continuity for dual-brain.
3
- // Generates handoff receipts so the next session can pick up seamlessly
4
- // when a session hits context limits, crashes, or is manually ended.
5
- //
6
- // Exports: generateHandoff, saveHandoff, getLatestHandoff, getHandoffAge,
7
- // buildCompactionSurvivalKit, buildResumeBrief, pruneHandoffs,
8
- // extractRoutingPatterns
9
-
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { load as loadNarrative } from './narrative.mjs';
13
-
14
- // ─── Session chaining ─────────────────────────────────────────────────────────
15
-
16
- /**
17
- * Generate a compact handoff object from current session state.
18
- * Designed to fit in ~500 tokens when serialized.
19
- *
20
- * @param {object} sessionState
21
- * @param {string} [sessionState.taskDescription]
22
- * @param {string[]} [sessionState.filesChanged]
23
- * @param {string[]} [sessionState.testsRun]
24
- * @param {object[]} [sessionState.decisions] Most recent routing decisions
25
- * @param {string[]} [sessionState.unresolved] Open questions / blockers
26
- * @param {object} [sessionState.routingHistory]
27
- * @param {string} [sessionState.routingHistory.lastProvider]
28
- * @param {string} [sessionState.routingHistory.lastModel]
29
- * @param {string[]} [sessionState.routingHistory.failedProviders]
30
- * @param {string[]} [sessionState.activePreferences]
31
- * @param {string} [sessionState.resumeHint] e.g. "continue implementing auth refactor"
32
- * @returns {object}
33
- */
34
- export function generateHandoff(sessionState) {
35
- return {
36
- version: 2,
37
- timestamp: new Date().toISOString(),
38
- task: sessionState.taskDescription || null,
39
- progress: {
40
- filesChanged: (sessionState.filesChanged || []).slice(0, 20),
41
- testsRun: sessionState.testsRun || [],
42
- decisions: (sessionState.decisions || []).slice(0, 5),
43
- },
44
- unresolved: (sessionState.unresolved || []).slice(0, 5),
45
- routing: {
46
- lastProvider: sessionState.routingHistory?.lastProvider || null,
47
- lastModel: sessionState.routingHistory?.lastModel || null,
48
- failedProviders: sessionState.routingHistory?.failedProviders || [],
49
- },
50
- preferences: sessionState.activePreferences || [],
51
- resumeHint: sessionState.resumeHint || null,
52
- narrative: sessionState.narrative || loadNarrative() || null,
53
- };
54
- }
55
-
56
- // ─── Handoff persistence ──────────────────────────────────────────────────────
57
-
58
- /**
59
- * Persist a handoff object to .dualbrain/handoffs/.
60
- * @param {object} handoff Result of generateHandoff()
61
- * @param {string} [cwd] Project root (defaults to process.cwd())
62
- * @returns {string} Absolute path of the written file
63
- */
64
- export function saveHandoff(handoff, cwd) {
65
- const dir = join(cwd || process.cwd(), '.dualbrain', 'handoffs');
66
- mkdirSync(dir, { recursive: true });
67
- const filename = `handoff-${Date.now()}.json`;
68
- writeFileSync(join(dir, filename), JSON.stringify(handoff, null, 2));
69
- return join(dir, filename);
70
- }
71
-
72
- /**
73
- * Load the most recent handoff from .dualbrain/handoffs/.
74
- * Returns null when no handoffs exist or all are unreadable.
75
- * @param {string} [cwd]
76
- * @returns {object|null}
77
- */
78
- export function getLatestHandoff(cwd) {
79
- const dir = join(cwd || process.cwd(), '.dualbrain', 'handoffs');
80
- if (!existsSync(dir)) return null;
81
- const files = readdirSync(dir)
82
- .filter(f => f.startsWith('handoff-') && f.endsWith('.json'))
83
- .sort()
84
- .reverse();
85
- if (files.length === 0) return null;
86
- try {
87
- return JSON.parse(readFileSync(join(dir, files[0]), 'utf8'));
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- /**
94
- * Return the age of a handoff in hours.
95
- * Returns Infinity when the handoff has no timestamp.
96
- * @param {object|null} handoff
97
- * @returns {number} Hours since handoff was generated
98
- */
99
- export function getHandoffAge(handoff) {
100
- if (!handoff?.timestamp) return Infinity;
101
- return (Date.now() - Date.parse(handoff.timestamp)) / 3600000;
102
- }
103
-
104
- // ─── Smart compaction ─────────────────────────────────────────────────────────
105
-
106
- /**
107
- * Build a compaction-safe summary string to inject before context compression.
108
- * The content must survive being summarised by a compression pass, so keep it
109
- * terse, high-signal, and easy to re-state.
110
- *
111
- * @param {object} state
112
- * @param {string} [state.activeTask]
113
- * @param {string[]} [state.routingRules]
114
- * @param {string[]} [state.criticalDecisions]
115
- * @param {string[]} [state.filesInProgress]
116
- * @param {string[]} [state.preferences]
117
- * @param {string[]} [state.warnings]
118
- * @returns {string}
119
- */
120
- export function buildCompactionSurvivalKit(state) {
121
- const lines = [];
122
- lines.push('[DUAL-BRAIN CONTINUITY]');
123
-
124
- if (state.activeTask) {
125
- lines.push(`TASK: ${state.activeTask}`);
126
- }
127
- if (state.routingRules?.length) {
128
- lines.push(`ROUTING: ${state.routingRules.join('; ')}`);
129
- }
130
- if (state.criticalDecisions?.length) {
131
- lines.push(`DECISIONS: ${state.criticalDecisions.join('; ')}`);
132
- }
133
- if (state.filesInProgress?.length) {
134
- lines.push(`FILES: ${state.filesInProgress.join(', ')}`);
135
- }
136
- if (state.preferences?.length) {
137
- lines.push(`PREFS: ${state.preferences.join('; ')}`);
138
- }
139
- if (state.warnings?.length) {
140
- lines.push(`WARNINGS: ${state.warnings.join('; ')}`);
141
- }
142
-
143
- lines.push('[/DUAL-BRAIN CONTINUITY]');
144
- return lines.join('\n');
145
- }
146
-
147
- // ─── Resume brief builder ─────────────────────────────────────────────────────
148
-
149
- /**
150
- * Check for a recent handoff and build a resume context string for a new session.
151
- * Returns null when no usable handoff exists (missing, too stale, or unreadable).
152
- *
153
- * @param {string} [cwd]
154
- * @returns {string|null}
155
- */
156
- export function buildResumeBrief(cwd) {
157
- const handoff = getLatestHandoff(cwd);
158
- if (!handoff) return null;
159
-
160
- const ageHours = getHandoffAge(handoff);
161
- if (ageHours > 48) return null; // too stale to be useful
162
-
163
- const lines = [];
164
- const ageLabel =
165
- ageHours < 1
166
- ? 'just now'
167
- : ageHours < 24
168
- ? `${Math.round(ageHours)}h ago`
169
- : `${Math.round(ageHours / 24)}d ago`;
170
-
171
- lines.push(`Resuming from previous session (${ageLabel}):`);
172
-
173
- // Narrative first — most valuable context for immersion
174
- if (handoff.narrative) {
175
- lines.push(` Context: ${handoff.narrative.slice(0, 300)}`);
176
- }
177
-
178
- if (handoff.task) lines.push(` Task: ${handoff.task}`);
179
- if (handoff.resumeHint) lines.push(` Next: ${handoff.resumeHint}`);
180
- if (handoff.progress?.filesChanged?.length) {
181
- const shown = handoff.progress.filesChanged.slice(0, 5);
182
- const extra = handoff.progress.filesChanged.length > 5
183
- ? ` (+${handoff.progress.filesChanged.length - 5} more)`
184
- : '';
185
- lines.push(` Changed: ${shown.join(', ')}${extra}`);
186
- }
187
- if (handoff.unresolved?.length) {
188
- lines.push(` Unresolved: ${handoff.unresolved.join('; ')}`);
189
- }
190
- if (handoff.routing?.failedProviders?.length) {
191
- lines.push(` Note: ${handoff.routing.failedProviders.join(', ')} failed last session`);
192
- }
193
-
194
- return lines.join('\n');
195
- }
196
-
197
- // ─── Handoff cleanup ──────────────────────────────────────────────────────────
198
-
199
- /**
200
- * Remove old handoff files, keeping only the most recent `keep` entries.
201
- * @param {string} [cwd]
202
- * @param {number} [keep=10]
203
- * @returns {number} Count of files pruned
204
- */
205
- export function pruneHandoffs(cwd, keep = 10) {
206
- const dir = join(cwd || process.cwd(), '.dualbrain', 'handoffs');
207
- if (!existsSync(dir)) return 0;
208
- const files = readdirSync(dir)
209
- .filter(f => f.startsWith('handoff-') && f.endsWith('.json'))
210
- .sort()
211
- .reverse();
212
- let pruned = 0;
213
- for (const f of files.slice(keep)) {
214
- try {
215
- unlinkSync(join(dir, f));
216
- pruned++;
217
- } catch {
218
- // Skip files that can't be removed — best-effort
219
- }
220
- }
221
- return pruned;
222
- }
223
-
224
- // ─── Cross-session learning ───────────────────────────────────────────────────
225
-
226
- /**
227
- * Extract routing patterns from handoff history to inform provider/model selection.
228
- *
229
- * @param {string} [cwd]
230
- * @returns {{
231
- * patterns: Array<{ type: string, value: string, count: number }>,
232
- * confidence: number,
233
- * sampleSize: number
234
- * }}
235
- */
236
- export function extractRoutingPatterns(cwd) {
237
- const dir = join(cwd || process.cwd(), '.dualbrain', 'handoffs');
238
- if (!existsSync(dir)) return { patterns: [], confidence: 0, sampleSize: 0 };
239
-
240
- const files = readdirSync(dir)
241
- .filter(f => f.startsWith('handoff-') && f.endsWith('.json'))
242
- .sort()
243
- .reverse()
244
- .slice(0, 20);
245
-
246
- const handoffs = files
247
- .map(f => {
248
- try {
249
- return JSON.parse(readFileSync(join(dir, f), 'utf8'));
250
- } catch {
251
- return null;
252
- }
253
- })
254
- .filter(Boolean);
255
-
256
- // Count provider/model usage patterns
257
- const providerCounts = {};
258
- const modelCounts = {};
259
- const failureCounts = {};
260
-
261
- for (const h of handoffs) {
262
- if (h.routing?.lastProvider) {
263
- providerCounts[h.routing.lastProvider] = (providerCounts[h.routing.lastProvider] || 0) + 1;
264
- }
265
- if (h.routing?.lastModel) {
266
- modelCounts[h.routing.lastModel] = (modelCounts[h.routing.lastModel] || 0) + 1;
267
- }
268
- for (const fp of (h.routing?.failedProviders || [])) {
269
- failureCounts[fp] = (failureCounts[fp] || 0) + 1;
270
- }
271
- }
272
-
273
- const patterns = [];
274
-
275
- // Most used provider
276
- const topProvider = Object.entries(providerCounts).sort((a, b) => b[1] - a[1])[0];
277
- if (topProvider) {
278
- patterns.push({ type: 'preferred_provider', value: topProvider[0], count: topProvider[1] });
279
- }
280
-
281
- // Most used model
282
- const topModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0];
283
- if (topModel) {
284
- patterns.push({ type: 'preferred_model', value: topModel[0], count: topModel[1] });
285
- }
286
-
287
- // Frequently failing provider (threshold: 3+ failures)
288
- const topFailure = Object.entries(failureCounts).sort((a, b) => b[1] - a[1])[0];
289
- if (topFailure && topFailure[1] >= 3) {
290
- patterns.push({ type: 'unreliable_provider', value: topFailure[0], count: topFailure[1] });
291
- }
292
-
293
- return {
294
- patterns,
295
- confidence: Math.min(1, handoffs.length / 10),
296
- sampleSize: handoffs.length,
297
- };
298
- }