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/repo.mjs DELETED
@@ -1,445 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * repo.mjs — Auto-detect project type and commands without asking the user.
4
- *
5
- * Exports:
6
- * detectRepo(cwd) → repo descriptor object
7
- * loadRepoCache(cwd) → cached detection (re-detects if >1 hour old)
8
- * getTestCommand(cwd) → convenience: test command string or null
9
- * getLintCommand(cwd) → convenience: lint command string or null
10
- */
11
-
12
- import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
13
- import { join } from 'node:path';
14
- import { execSync } from 'node:child_process';
15
-
16
- // ─── Constants ────────────────────────────────────────────────────────────────
17
-
18
- const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
19
- const CACHE_FILE = '.dualbrain/repo.json';
20
-
21
- // npm init placeholder — skip this as a real test command
22
- const NPM_PLACEHOLDER = 'echo "Error: no test specified"';
23
-
24
- // ─── Git helpers ──────────────────────────────────────────────────────────────
25
-
26
- function gitBranch(cwd) {
27
- try {
28
- return execSync('git rev-parse --abbrev-ref HEAD', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
29
- .toString().trim() || null;
30
- } catch { return null; }
31
- }
32
-
33
- function gitDirty(cwd) {
34
- try {
35
- const out = execSync('git status --porcelain', { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
36
- .toString();
37
- return out.trim().length > 0;
38
- } catch { return false; }
39
- }
40
-
41
- // ─── Node.js detection ────────────────────────────────────────────────────────
42
-
43
- function detectNode(cwd) {
44
- const pkgPath = join(cwd, 'package.json');
45
- if (!existsSync(pkgPath)) return null;
46
-
47
- let pkg = {};
48
- try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch { return null; }
49
-
50
- const scripts = pkg.scripts || {};
51
-
52
- // Package manager detection (order matters: most specific first)
53
- let packageManager = 'npm';
54
- if (existsSync(join(cwd, 'bun.lockb'))) packageManager = 'bun';
55
- else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';
56
- else if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';
57
-
58
- // Monorepo detection
59
- const monorepo = Boolean(
60
- pkg.workspaces ||
61
- existsSync(join(cwd, 'pnpm-workspace.yaml'))
62
- );
63
-
64
- // Extract commands from scripts (skip npm init placeholder for test)
65
- const rawTest = scripts.test || null;
66
- const test = (rawTest && !rawTest.includes(NPM_PLACEHOLDER) && !rawTest.toLowerCase().startsWith('echo'))
67
- ? rawTest
68
- : null;
69
-
70
- const lint = scripts.lint || null;
71
- const build = scripts.build || null;
72
-
73
- // Typecheck: explicit script or infer from tsconfig
74
- let typecheck = scripts.typecheck || scripts['type-check'] || null;
75
- if (!typecheck && existsSync(join(cwd, 'tsconfig.json'))) {
76
- typecheck = 'npx tsc --noEmit';
77
- }
78
-
79
- return {
80
- type: 'node',
81
- name: pkg.name || null,
82
- packageManager,
83
- commands: { test, lint, build, typecheck },
84
- monorepo,
85
- };
86
- }
87
-
88
- // ─── Go detection ─────────────────────────────────────────────────────────────
89
-
90
- function detectGo(cwd) {
91
- const modPath = join(cwd, 'go.mod');
92
- if (!existsSync(modPath)) return null;
93
-
94
- let name = null;
95
- try {
96
- const content = readFileSync(modPath, 'utf8');
97
- const match = content.match(/^module\s+(\S+)/m);
98
- if (match) name = match[1].split('/').pop(); // last segment of module path
99
- } catch { /* skip */ }
100
-
101
- return {
102
- type: 'go',
103
- name,
104
- packageManager: null,
105
- commands: { test: 'go test ./...', lint: null, build: 'go build ./...', typecheck: null },
106
- monorepo: false,
107
- };
108
- }
109
-
110
- // ─── Rust detection ───────────────────────────────────────────────────────────
111
-
112
- function detectRust(cwd) {
113
- const cargoPath = join(cwd, 'Cargo.toml');
114
- if (!existsSync(cargoPath)) return null;
115
-
116
- let name = null;
117
- try {
118
- const content = readFileSync(cargoPath, 'utf8');
119
- const match = content.match(/^\[package\][^\[]*name\s*=\s*"([^"]+)"/ms);
120
- if (match) name = match[1];
121
- } catch { /* skip */ }
122
-
123
- return {
124
- type: 'rust',
125
- name,
126
- packageManager: null,
127
- commands: { test: 'cargo test', lint: 'cargo clippy', build: 'cargo build', typecheck: null },
128
- monorepo: false,
129
- };
130
- }
131
-
132
- // ─── Python detection ─────────────────────────────────────────────────────────
133
-
134
- function detectPython(cwd) {
135
- const hasPyproject = existsSync(join(cwd, 'pyproject.toml'));
136
- const hasSetupPy = existsSync(join(cwd, 'setup.py'));
137
- if (!hasPyproject && !hasSetupPy) return null;
138
-
139
- let name = null;
140
- let test = 'pytest';
141
- let lint = null;
142
-
143
- if (hasPyproject) {
144
- try {
145
- const content = readFileSync(join(cwd, 'pyproject.toml'), 'utf8');
146
- const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
147
- if (nameMatch) name = nameMatch[1];
148
- if (content.includes('pytest')) test = 'pytest';
149
- if (content.includes('ruff')) lint = 'ruff check .';
150
- if (content.includes('flake8')) lint = lint || 'flake8';
151
- } catch { /* skip */ }
152
- }
153
-
154
- return {
155
- type: 'python',
156
- name,
157
- packageManager: null,
158
- commands: { test, lint, build: null, typecheck: null },
159
- monorepo: false,
160
- };
161
- }
162
-
163
- // ─── Ruby detection ───────────────────────────────────────────────────────────
164
-
165
- function detectRuby(cwd) {
166
- const gemfilePath = join(cwd, 'Gemfile');
167
- if (!existsSync(gemfilePath)) return null;
168
-
169
- let name = null;
170
- let test = null;
171
-
172
- try {
173
- const content = readFileSync(gemfilePath, 'utf8');
174
- if (content.includes('rspec')) test = 'bundle exec rspec';
175
- else if (content.includes('minitest')) test = 'bundle exec rake test';
176
- } catch { /* skip */ }
177
-
178
- // Try gemspec for name
179
- try {
180
- const gemspecFiles = readdirSync(cwd).filter(f => f.endsWith('.gemspec'));
181
- if (gemspecFiles.length > 0) {
182
- const spec = readFileSync(join(cwd, gemspecFiles[0]), 'utf8');
183
- const match = spec.match(/\.name\s*=\s*["']([^"']+)["']/);
184
- if (match) name = match[1];
185
- }
186
- } catch { /* skip */ }
187
-
188
- return {
189
- type: 'ruby',
190
- name,
191
- packageManager: null,
192
- commands: { test, lint: null, build: null, typecheck: null },
193
- monorepo: false,
194
- };
195
- }
196
-
197
- // ─── Main detection ───────────────────────────────────────────────────────────
198
-
199
- /**
200
- * Detect the project type, name, package manager, and common commands.
201
- * @param {string} [cwd]
202
- * @returns {object} Repo descriptor
203
- */
204
- export function detectRepo(cwd = process.cwd()) {
205
- // Try detectors in priority order
206
- const detected =
207
- detectNode(cwd) ||
208
- detectGo(cwd) ||
209
- detectRust(cwd) ||
210
- detectPython(cwd) ||
211
- detectRuby(cwd) ||
212
- {
213
- type: 'unknown',
214
- name: null,
215
- packageManager: null,
216
- commands: { test: null, lint: null, build: null, typecheck: null },
217
- monorepo: false,
218
- };
219
-
220
- return {
221
- ...detected,
222
- branch: gitBranch(cwd),
223
- dirty: gitDirty(cwd),
224
- };
225
- }
226
-
227
- // ─── Cache ────────────────────────────────────────────────────────────────────
228
-
229
- /**
230
- * Load cached repo detection if <1 hour old, otherwise re-detect and cache.
231
- * @param {string} [cwd]
232
- * @returns {object} Repo descriptor
233
- */
234
- export function loadRepoCache(cwd = process.cwd()) {
235
- const cachePath = join(cwd, CACHE_FILE);
236
-
237
- if (existsSync(cachePath)) {
238
- try {
239
- const cached = JSON.parse(readFileSync(cachePath, 'utf8'));
240
- const age = Date.now() - Date.parse(cached._cachedAt || 0);
241
- if (age < CACHE_TTL_MS && cached.type) {
242
- // Re-detect git state (branch/dirty) which changes frequently
243
- return {
244
- ...cached,
245
- branch: gitBranch(cwd),
246
- dirty: gitDirty(cwd),
247
- };
248
- }
249
- } catch { /* fall through to re-detect */ }
250
- }
251
-
252
- const repo = detectRepo(cwd);
253
- const toWrite = { ...repo, _cachedAt: new Date().toISOString() };
254
-
255
- try {
256
- const dir = join(cwd, '.dualbrain');
257
- mkdirSync(dir, { recursive: true });
258
- const tmp = cachePath + '.tmp.' + process.pid;
259
- writeFileSync(tmp, JSON.stringify(toWrite, null, 2) + '\n');
260
- renameSync(tmp, cachePath);
261
- } catch { /* non-fatal: cache miss is fine */ }
262
-
263
- return repo;
264
- }
265
-
266
- // ─── Convenience helpers ──────────────────────────────────────────────────────
267
-
268
- /**
269
- * Returns the detected test command or null.
270
- * @param {string} [cwd]
271
- * @returns {string|null}
272
- */
273
- export function getTestCommand(cwd = process.cwd()) {
274
- return detectRepo(cwd).commands.test;
275
- }
276
-
277
- /**
278
- * Returns the detected lint command or null.
279
- * @param {string} [cwd]
280
- * @returns {string|null}
281
- */
282
- export function getLintCommand(cwd = process.cwd()) {
283
- return detectRepo(cwd).commands.lint;
284
- }
285
-
286
- // ─── Ownership hints ──────────────────────────────────────────────────────────
287
-
288
- /**
289
- * Return the last git author, last-modified date, and commit count for a file.
290
- * @param {string} filePath
291
- * @param {string} [cwd]
292
- * @returns {{ lastAuthor: string, lastModified: string, totalCommits: number }|null}
293
- */
294
- export function getFileOwnership(filePath, cwd) {
295
- try {
296
- const blame = execSync(`git log --format="%an" -1 -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim();
297
- const lastDate = execSync(`git log --format="%ci" -1 -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim();
298
- const commitCount = parseInt(execSync(`git rev-list --count HEAD -- "${filePath}"`, { cwd, encoding: 'utf8', timeout: 5000 }).trim()) || 0;
299
- return { lastAuthor: blame, lastModified: lastDate, totalCommits: commitCount };
300
- } catch { return null; }
301
- }
302
-
303
- // ─── Dependency edges ─────────────────────────────────────────────────────────
304
-
305
- /**
306
- * Extract import/require edges from a source file.
307
- * @param {string} filePath — relative path from cwd
308
- * @param {string} [cwd]
309
- * @returns {{ local: string[], external: string[], total: number }}
310
- */
311
- export function getDependencyEdges(filePath, cwd) {
312
- try {
313
- const content = readFileSync(join(cwd || process.cwd(), filePath), 'utf8');
314
- const imports = [];
315
- // ES module imports
316
- for (const match of content.matchAll(/import\s+.*?from\s+['"]([^'"]+)['"]/g)) {
317
- imports.push(match[1]);
318
- }
319
- // Dynamic imports
320
- for (const match of content.matchAll(/import\(['"]([^'"]+)['"]\)/g)) {
321
- imports.push(match[1]);
322
- }
323
- // CommonJS requires
324
- for (const match of content.matchAll(/require\(['"]([^'"]+)['"]\)/g)) {
325
- imports.push(match[1]);
326
- }
327
- const local = imports.filter(i => i.startsWith('.') || i.startsWith('/'));
328
- const external = imports.filter(i => !i.startsWith('.') && !i.startsWith('/'));
329
- return { local, external, total: imports.length };
330
- } catch { return { local: [], external: [], total: 0 }; }
331
- }
332
-
333
- // ─── Test mapping ─────────────────────────────────────────────────────────────
334
-
335
- /**
336
- * Find test files whose name matches the source file's base name.
337
- * @param {string} filePath
338
- * @param {string} [cwd]
339
- * @returns {string[]}
340
- */
341
- export function findRelatedTests(filePath, cwd) {
342
- const root = cwd || process.cwd();
343
- const base = filePath.replace(/\.(mjs|js|ts|tsx|jsx)$/, '');
344
- const name = base.split('/').pop();
345
-
346
- const found = [];
347
- try {
348
- const allTests = execSync(
349
- `find . -type f \\( -name "*.test.*" -o -name "*.spec.*" -o -path "*/tests/*" -o -path "*/test/*" -o -path "*/__tests__/*" \\) -not -path "*/node_modules/*"`,
350
- { cwd: root, encoding: 'utf8', timeout: 5000 }
351
- ).trim().split('\n').filter(Boolean);
352
-
353
- for (const t of allTests) {
354
- if (t.includes(name)) found.push(t.replace(/^\.\//, ''));
355
- }
356
- } catch {}
357
-
358
- return found;
359
- }
360
-
361
- // ─── Risk hotspots ────────────────────────────────────────────────────────────
362
-
363
- /**
364
- * Return the files with highest churn × complexity risk in the last N days.
365
- * @param {string} [cwd]
366
- * @param {{ days?: number, limit?: number }} [opts]
367
- * @returns {Array<{ file: string, changeCount: number, lineCount: number, risk: number }>}
368
- */
369
- export function getRiskHotspots(cwd, opts = {}) {
370
- const { days = 30, limit = 10 } = opts;
371
- const root = cwd || process.cwd();
372
- try {
373
- const since = new Date(Date.now() - days * 86400000).toISOString().split('T')[0];
374
- const log = execSync(
375
- `git log --since="${since}" --name-only --pretty=format: | sort | uniq -c | sort -rn | head -${limit * 2}`,
376
- { cwd: root, encoding: 'utf8', timeout: 10000 }
377
- ).trim();
378
-
379
- const hotspots = [];
380
- for (const line of log.split('\n').filter(Boolean)) {
381
- const match = line.trim().match(/^(\d+)\s+(.+)$/);
382
- if (match) {
383
- const changeCount = parseInt(match[1]);
384
- const file = match[2];
385
- if (changeCount >= 3 && existsSync(join(root, file))) {
386
- let lineCount = 0;
387
- try {
388
- lineCount = readFileSync(join(root, file), 'utf8').split('\n').length;
389
- } catch {}
390
- hotspots.push({ file, changeCount, lineCount, risk: changeCount * Math.log2(Math.max(lineCount, 1)) });
391
- }
392
- }
393
- }
394
-
395
- return hotspots.sort((a, b) => b.risk - a.risk).slice(0, limit);
396
- } catch { return []; }
397
- }
398
-
399
- // ─── Primary language detection ───────────────────────────────────────────────
400
-
401
- function detectPrimaryLanguage(cwd) {
402
- try {
403
- const files = execSync(
404
- 'git ls-files --cached | grep -oE "\\.[a-zA-Z]+$" | sort | uniq -c | sort -rn | head -5',
405
- { cwd, encoding: 'utf8', timeout: 5000 }
406
- ).trim();
407
- const match = files.split('\n')[0]?.trim().match(/^\d+\s+\.(.+)$/);
408
- const ext = match?.[1];
409
- const langMap = {
410
- js: 'JavaScript', mjs: 'JavaScript', ts: 'TypeScript', tsx: 'TypeScript',
411
- py: 'Python', rb: 'Ruby', go: 'Go', rs: 'Rust', java: 'Java',
412
- kt: 'Kotlin', swift: 'Swift', cpp: 'C++', c: 'C',
413
- };
414
- return langMap[ext] || ext || 'unknown';
415
- } catch { return 'unknown'; }
416
- }
417
-
418
- // ─── Repo intelligence ────────────────────────────────────────────────────────
419
-
420
- /**
421
- * Return consolidated repo intelligence for routing decisions.
422
- * @param {string} [cwd]
423
- * @returns {object}
424
- */
425
- export function getRepoIntelligence(cwd) {
426
- const root = cwd || process.cwd();
427
- const cache = loadRepoCache(root);
428
- const hotspots = getRiskHotspots(root);
429
-
430
- return {
431
- ...cache,
432
- hotspots,
433
- hasTests: hotspots.some(h => h.file.includes('test')),
434
- primaryLanguage: detectPrimaryLanguage(root),
435
- repoSize: cache?.fileCount || 0,
436
- };
437
- }
438
-
439
- // ─── CLI (direct invocation) ──────────────────────────────────────────────────
440
-
441
- const isMain = process.argv[1]?.endsWith('repo.mjs');
442
- if (isMain) {
443
- const repo = detectRepo(process.cwd());
444
- process.stdout.write(JSON.stringify(repo, null, 2) + '\n');
445
- }
package/src/revert.mjs DELETED
@@ -1,149 +0,0 @@
1
- // revert.mjs — Undo recent auto-adjustments and applied recommendations
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createInterface } from 'node:readline';
5
-
6
- function dbDir(cwd) { return join(cwd || process.cwd(), '.dualbrain'); }
7
- function changesPath(cwd) { return join(dbDir(cwd), 'changes.jsonl'); }
8
- function configPath(cwd) { return join(dbDir(cwd), 'config.json'); }
9
-
10
- function genId() { return 'chg_' + Math.random().toString(36).slice(2, 9); }
11
-
12
- function readChanges(cwd) {
13
- try {
14
- if (!existsSync(changesPath(cwd))) return [];
15
- return readFileSync(changesPath(cwd), 'utf8')
16
- .split('\n').filter(Boolean).map(l => JSON.parse(l));
17
- } catch { return []; }
18
- }
19
-
20
- function writeChanges(records, cwd) {
21
- try {
22
- mkdirSync(dbDir(cwd), { recursive: true });
23
- writeFileSync(changesPath(cwd), records.map(r => JSON.stringify(r)).join('\n') + '\n');
24
- } catch {}
25
- }
26
-
27
- function applyRevert(changeRecord, cwd) {
28
- let config = {};
29
- try { config = JSON.parse(readFileSync(configPath(cwd), 'utf8')); } catch {}
30
- Object.assign(config, changeRecord.previousValue);
31
- writeFileSync(configPath(cwd), JSON.stringify(config, null, 2) + '\n');
32
- }
33
-
34
- function relativeTime(iso) {
35
- const diff = Date.now() - new Date(iso).getTime();
36
- const m = Math.floor(diff / 60000);
37
- if (m < 60) return `${m}m ago`;
38
- const h = Math.floor(m / 60);
39
- if (h < 24) return `${h}h ago`;
40
- return `${Math.floor(h / 24)}d ago`;
41
- }
42
-
43
- export function formatChange(change) {
44
- const badge = change.type === 'auto' ? '(auto)' : change.type === 'recommendation' ? '(rec)' : '(manual)';
45
- return `${relativeTime(change.timestamp).padEnd(8)} ${change.description} ${badge}`;
46
- }
47
-
48
- export function recordChange({ type, category, description, previousValue, newValue }, cwd) {
49
- try {
50
- mkdirSync(dbDir(cwd), { recursive: true });
51
- const record = {
52
- id: genId(),
53
- timestamp: new Date().toISOString(),
54
- type, category, description, previousValue, newValue,
55
- reverted: false,
56
- };
57
- writeFileSync(changesPath(cwd), JSON.stringify(record) + '\n', { flag: 'a' });
58
- return record;
59
- } catch { return null; }
60
- }
61
-
62
- export function getRecentChanges(cwd, limit = 10) {
63
- try {
64
- return readChanges(cwd)
65
- .filter(r => !r.reverted)
66
- .reverse()
67
- .slice(0, limit);
68
- } catch { return []; }
69
- }
70
-
71
- export function revertChange(changeId, cwd) {
72
- try {
73
- const records = readChanges(cwd);
74
- const idx = records.findIndex(r => r.id === changeId);
75
- if (idx === -1) return { success: false, description: 'Change not found' };
76
- const record = records[idx];
77
- if (record.reverted) return { success: false, description: 'Already reverted' };
78
- applyRevert(record, cwd);
79
- records[idx] = { ...record, reverted: true };
80
- writeChanges(records, cwd);
81
- return { success: true, description: record.description };
82
- } catch (e) { return { success: false, description: e.message }; }
83
- }
84
-
85
- export function revertAll(since, cwd) {
86
- try {
87
- const records = readChanges(cwd);
88
- const cutoff = since ? new Date(since).getTime() : 0;
89
- let count = 0;
90
- for (let i = 0; i < records.length; i++) {
91
- const r = records[i];
92
- if (!r.reverted && new Date(r.timestamp).getTime() >= cutoff) {
93
- applyRevert(r, cwd);
94
- records[i] = { ...r, reverted: true };
95
- count++;
96
- }
97
- }
98
- writeChanges(records, cwd);
99
- return { success: true, count };
100
- } catch (e) { return { success: false, count: 0, error: e.message }; }
101
- }
102
-
103
- export async function runRevert(cwd) {
104
- const changes = getRecentChanges(cwd, 10);
105
- const W = 59;
106
- const border = '─'.repeat(W - 2);
107
- const pad = s => '│ ' + s.padEnd(W - 4) + ' │';
108
-
109
- console.log(`╭${border}╮`);
110
- console.log(pad('Recent Changes'));
111
- console.log(pad(''));
112
- if (!changes.length) {
113
- console.log(pad(' No recent changes to revert.'));
114
- } else {
115
- changes.forEach((c, i) => console.log(pad(` [${i + 1}] ${formatChange(c)}`)));
116
- }
117
- console.log(pad(''));
118
- console.log(pad(' [number] revert [a] revert all [q] quit'));
119
- console.log(pad(''));
120
- console.log(`╰${border}╯`);
121
-
122
- if (!changes.length) return;
123
-
124
- const rl = createInterface({ input: process.stdin, output: process.stdout });
125
- const answer = await new Promise(res => rl.question('> ', res));
126
- rl.close();
127
-
128
- const input = answer.trim().toLowerCase();
129
- if (input === 'q' || input === '') return;
130
- if (input === 'a') {
131
- const confirm = await new Promise(res => {
132
- const r2 = createInterface({ input: process.stdin, output: process.stdout });
133
- r2.question(`Revert all ${changes.length} changes? (y/N) `, ans => { r2.close(); res(ans); });
134
- });
135
- if (confirm.trim().toLowerCase() === 'y') {
136
- const result = revertAll(null, cwd);
137
- console.log(result.success ? `Reverted ${result.count} changes.` : `Error: ${result.error}`);
138
- }
139
- return;
140
- }
141
- const n = parseInt(input, 10);
142
- if (!isNaN(n) && n >= 1 && n <= changes.length) {
143
- const target = changes[n - 1];
144
- const result = revertChange(target.id, cwd);
145
- console.log(result.success ? `Reverted: ${result.description}` : `Error: ${result.description}`);
146
- } else {
147
- console.log('Invalid selection.');
148
- }
149
- }