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,428 +0,0 @@
1
- // think-engine.mjs — Adaptive thinking ladder: recall → triage → tier decision.
2
- // Replaces fixed "always dual-brain" with knowledge preflight + heuristic classification.
3
- // Zero network calls. All matching is keyword-based.
4
-
5
- import { readFileSync, appendFileSync, mkdirSync, existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
-
8
- const DOCS_DIR = '.dual-brain';
9
- const DECISIONS_FILE = 'decisions.jsonl';
10
-
11
- const STOP_WORDS = new Set([
12
- 'a','an','the','and','or','but','in','on','at','to','for','of','with',
13
- 'by','from','is','it','its','be','as','are','was','were','been','has',
14
- 'have','had','do','does','did','will','would','could','should','may',
15
- 'might','shall','can','this','that','these','those','i','we','you',
16
- 'he','she','they','my','our','your','his','her','their','what','how',
17
- 'when','where','why','which','who','all','any','more','most','also',
18
- 'not','no','so','if','then','than','into','up','out','about','just',
19
- 'after','before','between','through','during','each','get','use',
20
- ]);
21
-
22
- const HARD_ESCALATION_KEYWORDS = [
23
- 'auth','credential','secret','token','security','migration','billing',
24
- 'payment','deploy production','delete','drop','force push','routing logic',
25
- 'dispatcher','pipeline gate',
26
- ];
27
-
28
- const TIER_TOKENS = {
29
- recall: 0,
30
- quick: 2000,
31
- standard: 8000,
32
- deep: 20000,
33
- ultra: 50000,
34
- };
35
-
36
- const TIER_COST = {
37
- recall: 'zero',
38
- quick: 'minimal',
39
- standard: 'moderate',
40
- deep: 'significant',
41
- ultra: 'heavy',
42
- };
43
-
44
- export function normalizeIntent(text) {
45
- if (!text || typeof text !== 'string') return [];
46
- return text
47
- .toLowerCase()
48
- .replace(/[^a-z0-9\s]/g, ' ')
49
- .split(/\s+/)
50
- .filter(w => w.length > 2 && !STOP_WORDS.has(w));
51
- }
52
-
53
- function decisionsPath(cwd) {
54
- return join(cwd, DOCS_DIR, DECISIONS_FILE);
55
- }
56
-
57
- function readDecisions(cwd) {
58
- const path = decisionsPath(cwd);
59
- if (!existsSync(path)) return [];
60
- try {
61
- const raw = readFileSync(path, 'utf8');
62
- return raw
63
- .split('\n')
64
- .filter(l => l.trim())
65
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
66
- .filter(Boolean);
67
- } catch {
68
- return [];
69
- }
70
- }
71
-
72
- function getFreshness(timestamp) {
73
- if (!timestamp) return 'stale';
74
- const ageMs = Date.now() - new Date(timestamp).getTime();
75
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
76
- if (ageDays < 7) return 'current';
77
- if (ageDays < 30) return 'aging';
78
- return 'stale';
79
- }
80
-
81
- function keywordOverlap(kwA, kwB) {
82
- if (!kwA.length || !kwB.length) return 0;
83
- const setA = new Set(kwA);
84
- const matches = kwB.filter(w => setA.has(w)).length;
85
- return matches / Math.max(kwA.length, kwB.length);
86
- }
87
-
88
- function getApplicability(relevance, freshness) {
89
- if (relevance > 0.8 && freshness === 'current') return 'exact_reuse';
90
- if (relevance > 0.8 && freshness === 'aging') return 'reuse_with_validation';
91
- if (relevance > 0.8 && freshness === 'stale') return 'stale';
92
- if (relevance >= 0.4) return 'related_precedent';
93
- return null;
94
- }
95
-
96
- export function lookupDecision(intent, tags = [], cwd = process.cwd()) {
97
- const queryKw = normalizeIntent(intent);
98
- const queryTags = tags.map(t => t.toLowerCase());
99
- const decisions = readDecisions(cwd);
100
-
101
- const candidates = [];
102
- for (const dec of decisions) {
103
- const decKw = dec.normalizedIntent
104
- ? dec.normalizedIntent.split(' ').filter(Boolean)
105
- : normalizeIntent(dec.question || dec.decision || '');
106
-
107
- let relevance = keywordOverlap(queryKw, decKw);
108
-
109
- const decTags = (dec.tags || []).map(t => t.toLowerCase());
110
- const tagMatch = queryTags.some(t => decTags.includes(t));
111
- if (tagMatch) relevance = Math.min(1, relevance + 0.15);
112
-
113
- if (relevance < 0.4) continue;
114
-
115
- const freshness = getFreshness(dec.timestamp);
116
- const applicability = getApplicability(relevance, freshness);
117
- if (!applicability) continue;
118
-
119
- candidates.push({ decision: dec, relevance, freshness, applicability });
120
- }
121
-
122
- candidates.sort((a, b) => b.relevance - a.relevance);
123
-
124
- const highRelevance = candidates.filter(c => c.relevance > 0.8);
125
- let recommendation = 'new_thinking_needed';
126
-
127
- if (highRelevance.length > 1) {
128
- const decisions_set = highRelevance.map(c =>
129
- normalizeIntent(typeof c.decision.decision === 'string' ? c.decision.decision : JSON.stringify(c.decision.decision)).join(' ')
130
- );
131
- const pairOverlap = keywordOverlap(
132
- normalizeIntent(decisions_set[0]),
133
- normalizeIntent(decisions_set[1])
134
- );
135
- if (pairOverlap < 0.3) {
136
- for (const c of highRelevance) c.applicability = 'conflicting';
137
- recommendation = 'new_thinking_needed';
138
- } else if (candidates[0]?.applicability === 'exact_reuse') {
139
- recommendation = 'reuse';
140
- } else {
141
- recommendation = 'validate';
142
- }
143
- } else if (candidates[0]?.applicability === 'exact_reuse') {
144
- recommendation = 'reuse';
145
- } else if (candidates[0]?.applicability === 'reuse_with_validation') {
146
- recommendation = 'validate';
147
- } else if (candidates.length > 0) {
148
- recommendation = 'new_thinking_needed';
149
- }
150
-
151
- return {
152
- found: candidates.length > 0,
153
- candidates: candidates.slice(0, 5),
154
- recommendation,
155
- };
156
- }
157
-
158
- function detectRisk(question) {
159
- const q = question.toLowerCase();
160
- const critical = ['auth','credential','secret','token','security','billing','payment','force push','drop table','delete production'];
161
- const high = ['migration','deploy production','routing logic','dispatcher','pipeline gate','delete','drop'];
162
- const low = ['readme','doc','comment','explain','list','show','what is','how does'];
163
-
164
- if (critical.some(k => q.includes(k))) return 'critical';
165
- if (high.some(k => q.includes(k))) return 'high';
166
- if (low.some(k => q.includes(k))) return 'low';
167
- return 'medium';
168
- }
169
-
170
- function detectComplexity(question) {
171
- const wordCount = question.trim().split(/\s+/).length;
172
- const hasMultiStep = /and then|then also|first.*then|step \d|multiple|several|across|all/i.test(question);
173
- const hasComparison = /vs|versus|compare|difference|between|trade.?off/i.test(question);
174
-
175
- if (wordCount > 80 || (hasMultiStep && hasComparison)) return 'complex';
176
- if (wordCount > 30 || hasMultiStep || hasComparison) return 'moderate';
177
- return 'simple';
178
- }
179
-
180
- function detectNovelty(preflight) {
181
- if (!preflight || !preflight.found) return 'novel';
182
- if (preflight.recommendation === 'reuse') return 'known';
183
- if (preflight.candidates?.some(c => c.applicability === 'related_precedent' || c.applicability === 'reuse_with_validation')) {
184
- return 'variation';
185
- }
186
- return 'novel';
187
- }
188
-
189
- function hasHardEscalation(question) {
190
- const q = question.toLowerCase();
191
- return HARD_ESCALATION_KEYWORDS.some(k => q.includes(k));
192
- }
193
-
194
- export function triageQuestion(question, projectBrief, preflight) {
195
- const risk = detectRisk(question);
196
- const complexity = detectComplexity(question);
197
- const novelty = detectNovelty(preflight);
198
- const hardEscalation = hasHardEscalation(question);
199
-
200
- let recommendedTier;
201
- let reason;
202
-
203
- if (preflight?.recommendation === 'reuse') {
204
- recommendedTier = 'recall';
205
- reason = 'exact match found in decision log';
206
- } else if (hardEscalation || risk === 'critical') {
207
- recommendedTier = 'ultra';
208
- reason = hardEscalation
209
- ? `hard escalation keyword detected`
210
- : 'critical risk requires maximum deliberation';
211
- } else if (preflight?.candidates?.some(c => c.applicability === 'conflicting')) {
212
- recommendedTier = 'ultra';
213
- reason = 'conflicting prior decisions require reconciliation';
214
- } else if (risk === 'high' && (novelty === 'novel' || complexity === 'complex')) {
215
- recommendedTier = 'deep';
216
- reason = `high risk + ${novelty === 'novel' ? 'novel question' : 'complex scope'}`;
217
- } else if (novelty === 'novel' && (risk === 'medium' || complexity === 'complex')) {
218
- recommendedTier = 'standard';
219
- reason = 'novel question with non-trivial risk or complexity';
220
- } else if (novelty === 'variation' && risk === 'low') {
221
- recommendedTier = 'quick';
222
- reason = 'similar precedent found, low risk variation';
223
- } else if (preflight?.candidates?.length > 0 && novelty !== 'novel') {
224
- recommendedTier = 'quick';
225
- reason = 'related precedent available, minor adaptation needed';
226
- } else if (novelty === 'novel' && risk === 'low' && complexity === 'simple') {
227
- recommendedTier = 'quick';
228
- reason = 'novel but simple and low risk';
229
- } else {
230
- recommendedTier = 'standard';
231
- reason = 'default tier for unclassified novel questions';
232
- }
233
-
234
- const riskRank = { low: 0, medium: 1, high: 2, critical: 3 };
235
- const tierRank = { recall: 0, quick: 1, standard: 2, deep: 3, ultra: 4 };
236
- const minTierForRisk = { low: 'recall', medium: 'quick', high: 'deep', critical: 'ultra' };
237
- const riskFloor = minTierForRisk[risk] ?? 'quick';
238
- if (tierRank[recommendedTier] < tierRank[riskFloor]) {
239
- recommendedTier = riskFloor;
240
- reason += ` (escalated to ${riskFloor} by risk floor)`;
241
- }
242
-
243
- const confidenceBase = novelty === 'known' ? 0.9
244
- : novelty === 'variation' ? 0.75
245
- : 0.6;
246
- const confidence = Math.max(0.3, confidenceBase - (risk === 'critical' ? 0.2 : 0));
247
-
248
- const estimatedTokens = TIER_TOKENS[recommendedTier] ?? 0;
249
-
250
- return {
251
- novelty,
252
- risk,
253
- complexity,
254
- confidence,
255
- recommendedTier,
256
- reason,
257
- estimatedTokens,
258
- hardEscalation,
259
- };
260
- }
261
-
262
- export async function think(question, options = {}, cwd = process.cwd()) {
263
- const result = {
264
- question,
265
- startedAt: Date.now(),
266
- tier: null,
267
- phases: [],
268
- answer: null,
269
- tokensUsed: 0,
270
- cost: 'minimal',
271
- fromCache: false,
272
- decision: null,
273
- };
274
-
275
- if (!options.skipRecall) {
276
- const preflight = lookupDecision(question, options.tags || [], cwd);
277
- result.phases.push({ phase: 'recall', ...preflight });
278
-
279
- if (preflight.recommendation === 'reuse' && preflight.candidates[0]) {
280
- result.tier = 'recall';
281
- result.answer = preflight.candidates[0].decision;
282
- result.fromCache = true;
283
- result.cost = 'zero';
284
- result.tokensUsed = 0;
285
- return result;
286
- }
287
- }
288
-
289
- const recallPhase = result.phases[0] ?? null;
290
- const triage = triageQuestion(question, options.projectBrief, recallPhase);
291
- result.phases.push({ phase: 'triage', ...triage });
292
- result.tier = options.forceLevel || triage.recommendedTier;
293
-
294
- result.tokensUsed = TIER_TOKENS[result.tier] ?? triage.estimatedTokens;
295
- result.cost = TIER_COST[result.tier] ?? 'moderate';
296
-
297
- return result;
298
- }
299
-
300
- export function persistDecision(question, answer, tier, options = {}, cwd = process.cwd()) {
301
- const dir = join(cwd, DOCS_DIR);
302
- if (!existsSync(dir)) {
303
- mkdirSync(dir, { recursive: true });
304
- }
305
-
306
- const kw = normalizeIntent(question);
307
- const normalizedIntent = kw.join(' ');
308
-
309
- const answerText = typeof answer === 'string' ? answer : JSON.stringify(answer);
310
- const sentences = answerText.match(/[^.!?]+[.!?]+/g) ?? [];
311
- const rationale = sentences.slice(0, 3).map(s => s.trim()).filter(Boolean);
312
-
313
- const autoTags = [];
314
- const q = question.toLowerCase();
315
- if (/auth|security|credential|secret|token/.test(q)) autoTags.push('security');
316
- if (/migration|migrate|upgrade/.test(q)) autoTags.push('migration');
317
- if (/architecture|design|structure|pattern/.test(q)) autoTags.push('architecture');
318
- if (/test|spec|coverage/.test(q)) autoTags.push('testing');
319
- if (/deploy|release|publish|production/.test(q)) autoTags.push('deployment');
320
- if (/routing|dispatch|pipeline/.test(q)) autoTags.push('routing');
321
-
322
- const tags = [...new Set([...(options.tags || []), ...autoTags])];
323
-
324
- const contextSpecific = /this session|right now|current branch|today|temporary|one.?off/i.test(answerText);
325
- const reusable = !contextSpecific;
326
-
327
- const tokensUsed = options.tokensUsed ?? TIER_TOKENS[tier] ?? 0;
328
-
329
- const now = new Date();
330
- const expiresAt = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString();
331
-
332
- const confScore = options.confidence ?? (
333
- tier === 'ultra' || tier === 'deep' ? 'high'
334
- : tier === 'standard' ? 'medium'
335
- : 'low'
336
- );
337
-
338
- const entry = {
339
- id: `dec_${Date.now()}`,
340
- timestamp: now.toISOString(),
341
- question,
342
- normalizedIntent,
343
- decision: answerText,
344
- rationale,
345
- tags,
346
- confidence: typeof confScore === 'string' ? confScore : (confScore > 0.7 ? 'high' : confScore > 0.4 ? 'medium' : 'low'),
347
- tier,
348
- tokensUsed,
349
- expiresAt,
350
- reusable,
351
- };
352
-
353
- appendFileSync(join(dir, DECISIONS_FILE), JSON.stringify(entry) + '\n');
354
- return entry;
355
- }
356
-
357
- export function getThinkingStats(cwd = process.cwd()) {
358
- const decisions = readDecisions(cwd);
359
- if (!decisions.length) {
360
- return {
361
- totalDecisions: 0,
362
- cacheHits: 0,
363
- cacheHitRate: 0,
364
- tierDistribution: { recall: 0, quick: 0, standard: 0, deep: 0, ultra: 0 },
365
- totalTokensSaved: 0,
366
- avgTier: 'none',
367
- };
368
- }
369
-
370
- const tierDist = { recall: 0, quick: 0, standard: 0, deep: 0, ultra: 0 };
371
- let cacheHits = 0;
372
- let totalTokensSaved = 0;
373
- const tierCounts = {};
374
-
375
- for (const dec of decisions) {
376
- const t = dec.tier ?? 'standard';
377
- if (tierDist[t] !== undefined) tierDist[t]++;
378
- tierCounts[t] = (tierCounts[t] ?? 0) + 1;
379
-
380
- if (t === 'recall') {
381
- cacheHits++;
382
- totalTokensSaved += TIER_TOKENS.standard;
383
- }
384
- }
385
-
386
- const cacheHitRate = decisions.length > 0 ? cacheHits / decisions.length : 0;
387
-
388
- let maxCount = 0;
389
- let avgTier = 'standard';
390
- for (const [tier, count] of Object.entries(tierCounts)) {
391
- if (count > maxCount) { maxCount = count; avgTier = tier; }
392
- }
393
-
394
- return {
395
- totalDecisions: decisions.length,
396
- cacheHits,
397
- cacheHitRate: Math.round(cacheHitRate * 1000) / 1000,
398
- tierDistribution: tierDist,
399
- totalTokensSaved,
400
- avgTier,
401
- };
402
- }
403
-
404
- export function formatThinkResult(result) {
405
- const { tier, phases, cost, fromCache, tokensUsed } = result;
406
-
407
- const tierLabel = tier ? tier.charAt(0).toUpperCase() + tier.slice(1) : 'Unknown';
408
- const tokenStr = tokensUsed > 0 ? `${(tokensUsed / 1000).toFixed(0)}K tokens estimated` : 'zero tokens';
409
-
410
- const lines = [`THINKING: ${tierLabel} tier (${tokenStr})`];
411
-
412
- for (const phase of phases ?? []) {
413
- if (phase.phase === 'recall') {
414
- const count = phase.candidates?.length ?? 0;
415
- const found = count > 0
416
- ? `${count} related precedent${count === 1 ? '' : 's'} found`
417
- : 'no prior decisions found';
418
- lines.push(` Phase 1: Recall — ${found}`);
419
- } else if (phase.phase === 'triage') {
420
- lines.push(` Phase 2: Triage — ${phase.novelty ?? 'novel'} question, ${phase.risk ?? 'medium'} risk`);
421
- }
422
- }
423
-
424
- lines.push(` Cost: ${cost ?? 'unknown'}`);
425
- if (fromCache) lines.push(' Source: decision cache (no model call needed)');
426
-
427
- return lines.join('\n');
428
- }
package/src/tui.mjs DELETED
@@ -1,276 +0,0 @@
1
- /**
2
- * tui.mjs — Zero-dependency terminal UI renderer for the dual-brain CLI.
3
- * All functions return strings; callers use console.log to print.
4
- */
5
-
6
- import { fileURLToPath } from 'node:url';
7
- import { readFileSync } from 'node:fs';
8
- import { join, dirname } from 'node:path';
9
-
10
- // ─── Unicode / ASCII mode ─────────────────────────────────────────────────────
11
-
12
- export const useUnicode =
13
- process.env.DUALBRAIN_ASCII !== '1' && process.stdout.isTTY !== false;
14
-
15
- const CH = useUnicode
16
- ? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║', ts: '╠', te: '╣', fill: '█', empty: '░' }
17
- : { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|', ts: '+', te: '+', fill: '#', empty: '.' };
18
-
19
- // ─── ANSI / emoji helpers ─────────────────────────────────────────────────────
20
-
21
- /** Strip ANSI escape codes from a string. */
22
- export function stripAnsi(str) {
23
- // eslint-disable-next-line no-control-regex
24
- return String(str).replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
25
- }
26
-
27
- /**
28
- * Visible display length of a string.
29
- * Strips ANSI codes and counts each emoji as 2 columns wide.
30
- */
31
- export function visibleLength(str) {
32
- const plain = stripAnsi(String(str));
33
- let len = 0;
34
- for (const ch of plain) {
35
- const cp = ch.codePointAt(0);
36
- // Emoji / wide symbol ranges (covers most common emoji)
37
- if (
38
- (cp >= 0x1f300 && cp <= 0x1faff) || // Misc symbols, emoji
39
- (cp >= 0x2600 && cp <= 0x27bf) || // Misc symbols
40
- (cp >= 0xfe00 && cp <= 0xfe0f) || // Variation selectors
41
- (cp >= 0x1f1e0 && cp <= 0x1f1ff) || // Flags
42
- cp === 0x20e3 // Combining enclosing keycap
43
- ) {
44
- len += 2;
45
- } else {
46
- len += 1;
47
- }
48
- }
49
- return len;
50
- }
51
-
52
- /**
53
- * Right-pad `str` with spaces so that its visible width equals `width`.
54
- * Accounts for emoji (2-wide) and ANSI codes.
55
- */
56
- export function pad(str, width) {
57
- const vl = visibleLength(str);
58
- const spaces = Math.max(0, width - vl);
59
- return String(str) + ' '.repeat(spaces);
60
- }
61
-
62
- // ─── box ─────────────────────────────────────────────────────────────────────
63
-
64
- /**
65
- * Renders a Unicode (or ASCII) box with a title bar.
66
- * @param {string} title
67
- * @param {string[]} lines
68
- * @param {{ width?: number }} opts
69
- * @returns {string}
70
- */
71
- export function box(title, lines = [], opts = {}) {
72
- const inner = opts.width ?? 56;
73
- const total = inner + 2; // 2 spaces padding on each side counted inside border
74
-
75
- const top = CH.tl + CH.h.repeat(total) + CH.tr;
76
- const divider = CH.ts + CH.h.repeat(total) + CH.te;
77
- const bottom = CH.bl + CH.h.repeat(total) + CH.br;
78
-
79
- // Title row: 2-space left pad
80
- const titleContent = ' ' + title;
81
- const titleRow = CH.v + pad(titleContent, total) + CH.v;
82
-
83
- const bodyRows = lines.map(line => {
84
- const content = ' ' + line;
85
- return CH.v + pad(content, total) + CH.v;
86
- });
87
-
88
- return [top, titleRow, divider, ...bodyRows, bottom].join('\n');
89
- }
90
-
91
- // ─── bar ─────────────────────────────────────────────────────────────────────
92
-
93
- /**
94
- * Renders a percentage bar.
95
- * @param {number} percent 0–100
96
- * @param {number} width bar width in chars (default 20)
97
- * @param {{ label?: string }} opts
98
- * @returns {string}
99
- */
100
- export function bar(percent, width = 20, opts = {}) {
101
- const pct = Math.max(0, Math.min(100, percent));
102
- const filled = Math.round((pct / 100) * width);
103
- const empty = width - filled;
104
-
105
- const track = CH.fill.repeat(filled) + CH.empty.repeat(empty);
106
- const pctStr = String(Math.round(pct)).padStart(3) + '%';
107
- const label = opts.label ? ` ${opts.label}` : '';
108
-
109
- return `${track} ${pctStr}${label}`;
110
- }
111
-
112
- // ─── badge ────────────────────────────────────────────────────────────────────
113
-
114
- /**
115
- * Returns a status badge emoji/symbol.
116
- * @param {string} status
117
- * @returns {string}
118
- */
119
- export function badge(status) {
120
- const map = {
121
- healthy: '🟢',
122
- degraded: '🟡',
123
- hot: '🔴',
124
- probing: '🟠',
125
- connected: '✅',
126
- missing: '❌',
127
- warning: '⚠️',
128
- };
129
- return map[status] ?? '❓';
130
- }
131
-
132
- // ─── separator ───────────────────────────────────────────────────────────────
133
-
134
- /**
135
- * Returns a section separator line.
136
- * @param {string} label
137
- * @returns {string}
138
- */
139
- export function separator(label = '') {
140
- const dash = useUnicode ? '─' : '-';
141
- return label
142
- ? ` ${dash}${dash}${dash} ${label}`
143
- : ` ${dash}${dash}${dash}`;
144
- }
145
-
146
- // ─── menu ────────────────────────────────────────────────────────────────────
147
-
148
- /**
149
- * Renders a numbered/lettered menu grouped by section.
150
- * @param {{ key: string, label: string, section?: string }[]} options
151
- * @param {object} opts (reserved)
152
- * @returns {string}
153
- */
154
- export function menu(options, opts = {}) {
155
- const rows = [];
156
- let lastSection = Symbol('none');
157
-
158
- for (const opt of options) {
159
- const section = opt.section ?? '';
160
- if (section !== lastSection) {
161
- if (section) {
162
- rows.push(separator(section));
163
- } else {
164
- rows.push(separator());
165
- }
166
- lastSection = section;
167
- }
168
- rows.push(` [${opt.key}] ${opt.label}`);
169
- }
170
-
171
- return rows.join('\n');
172
- }
173
-
174
- // ── Modern box rendering with rounded corners ────────────────────────────────
175
-
176
- const ROUNDED = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│', ml: '├', mr: '┤' };
177
-
178
- export function panel(title, content, opts = {}) {
179
- const { width = 70, titleColor = '\x1b[36m', borderColor = '\x1b[2m', reset = '\x1b[0m' } = opts;
180
- const lines = [];
181
- const innerW = width - 2;
182
-
183
- // Top border with title
184
- if (title) {
185
- const titleStr = ` ${title} `;
186
- const remaining = innerW - titleStr.length - 1;
187
- lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h} ${titleColor}${title}${borderColor} ${ROUNDED.h.repeat(Math.max(0, remaining))}${ROUNDED.tr}${reset}`);
188
- } else {
189
- lines.push(`${borderColor}${ROUNDED.tl}${ROUNDED.h.repeat(innerW)}${ROUNDED.tr}${reset}`);
190
- }
191
-
192
- // Content lines
193
- const contentLines = (typeof content === 'string' ? content.split('\n') : content);
194
- for (const line of contentLines) {
195
- const stripped = line.replace(/\x1b\[[0-9;]*m/g, '');
196
- const pad = Math.max(0, innerW - stripped.length);
197
- lines.push(`${borderColor}${ROUNDED.v}${reset} ${line}${' '.repeat(pad)}${borderColor}${ROUNDED.v}${reset}`);
198
- }
199
-
200
- // Bottom border
201
- lines.push(`${borderColor}${ROUNDED.bl}${ROUNDED.h.repeat(innerW)}${ROUNDED.br}${reset}`);
202
-
203
- return lines.join('\n');
204
- }
205
-
206
- export function divider(width = 70) {
207
- const borderColor = '\x1b[2m';
208
- const reset = '\x1b[0m';
209
- return `${borderColor}${ROUNDED.ml}${ROUNDED.h.repeat(width - 2)}${ROUNDED.mr}${reset}`;
210
- }
211
-
212
- export function statusChip(label, healthy, opts = {}) {
213
- const green = '\x1b[32m';
214
- const red = '\x1b[31m';
215
- const dim = '\x1b[2m';
216
- const reset = '\x1b[0m';
217
- const icon = healthy ? `${green}●${reset}` : `${red}●${reset}`;
218
- return `${icon} ${dim}${label}${reset}`;
219
- }
220
-
221
- export function headerBar(left, right, width = 70) {
222
- const leftStripped = left.replace(/\x1b\[[0-9;]*m/g, '');
223
- const rightStripped = right.replace(/\x1b\[[0-9;]*m/g, '');
224
- const gap = Math.max(1, width - leftStripped.length - rightStripped.length);
225
- return `${left}${' '.repeat(gap)}${right}`;
226
- }
227
-
228
- export function prompt(text = '> task or /help') {
229
- const cyan = '\x1b[36m';
230
- const dim = '\x1b[2m';
231
- const reset = '\x1b[0m';
232
- return `${cyan}>${reset} ${dim}${text.replace(/^>\s*/, '')}${reset}`;
233
- }
234
-
235
- export function signalLine(type, text, meta = '') {
236
- const green = '\x1b[32m';
237
- const yellow = '\x1b[33m';
238
- const dim = '\x1b[2m';
239
- const reset = '\x1b[0m';
240
-
241
- let icon;
242
- switch (type) {
243
- case 'success': icon = `${green}✓${reset}`; break;
244
- case 'warning': icon = `${yellow}!${reset}`; break;
245
- case 'info': icon = `${dim}·${reset}`; break;
246
- default: icon = `${dim}·${reset}`;
247
- }
248
-
249
- const metaStr = meta ? `${dim}${meta}${reset}` : '';
250
- return `${icon} ${text}${metaStr ? ' ' + metaStr : ''}`;
251
- }
252
-
253
- // ─── Self-test ────────────────────────────────────────────────────────────────
254
-
255
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
256
- // Read version dynamically from package.json
257
- let selfTestVersion = '0.0.0';
258
- try {
259
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
260
- selfTestVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
261
- } catch { /* fallback to 0.0.0 */ }
262
-
263
- console.log(box(`🧠 Dual-Brain v${selfTestVersion}`, [
264
- '🟢 Claude ✅ 🟢 OpenAI ✅',
265
- '🌀 Replit + replit-tools',
266
- ]));
267
- console.log(bar(75, 20, { label: 'Claude' }));
268
- console.log(bar(25, 20, { label: 'OpenAI' }));
269
- console.log(menu([
270
- { key: 'c', label: 'Continue last session', section: 'Sessions' },
271
- { key: 'n', label: 'New session', section: 'Sessions' },
272
- { key: 'a', label: 'Auth management', section: 'Settings' },
273
- { key: 'p', label: 'Profile settings', section: 'Settings' },
274
- { key: 's', label: 'Exit to shell', section: '' },
275
- ]));
276
- }