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
@@ -0,0 +1,744 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * decide.ts — Routing decision module for the Dual-Brain Orchestrator.
4
+ *
5
+ * Given a task detection + user profile, decides which provider/model/effort/mode
6
+ * to use and explains why in one sentence.
7
+ *
8
+ * Exports: decideRoute, getModelCapabilities, getAvailableModels,
9
+ * WORK_STYLES, getWorkStyle, estimateBudgetPressure,
10
+ * shouldDualBrain, explainDecision, getFailoverOrder
11
+ */
12
+ import { readFileSync } from 'fs';
13
+ import { join, dirname } from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ // @ts-ignore — health.mjs not yet migrated
16
+ import { getProviderScore, checkCooldown } from './health.js';
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const WORKSPACE = join(__dirname, '..');
19
+ // ─── Model Registry (optional, lazy-loaded) ───────────────────────────────────
20
+ let modelRegistry = null;
21
+ let _registryLoadAttempted = false;
22
+ function _loadModelRegistry() {
23
+ if (_registryLoadAttempted)
24
+ return;
25
+ _registryLoadAttempted = true;
26
+ import('./models.js').then(mod => {
27
+ modelRegistry = mod;
28
+ }).catch(() => { });
29
+ }
30
+ _loadModelRegistry();
31
+ let routingAdvisor = null;
32
+ let _advisorLoadAttempted = false;
33
+ function _loadRoutingAdvisor() {
34
+ if (_advisorLoadAttempted)
35
+ return;
36
+ _advisorLoadAttempted = true;
37
+ // @ts-ignore — routing-advisor.mjs not yet migrated
38
+ import('./routing-advisor.js').then(mod => {
39
+ routingAdvisor = mod;
40
+ }).catch(() => { });
41
+ }
42
+ _loadRoutingAdvisor();
43
+ export const WORK_STYLES = {
44
+ fast: {
45
+ label: 'Fast',
46
+ defaultWorker: 'claude-sonnet-4-6',
47
+ complexWorker: 'claude-sonnet-4-6',
48
+ challengerPolicy: 'never',
49
+ checkpointPolicy: 'never',
50
+ reviewPolicy: 'skip',
51
+ description: 'Quick answers, single model, minimal reviews',
52
+ },
53
+ balanced: {
54
+ label: 'Balanced',
55
+ defaultWorker: 'claude-sonnet-4-6',
56
+ complexWorker: 'claude-opus-4-6',
57
+ challengerPolicy: 'high-risk',
58
+ checkpointPolicy: 'risky-ops',
59
+ reviewPolicy: 'important',
60
+ description: 'Smart routing, reviews on important changes',
61
+ },
62
+ fullpower: {
63
+ label: 'Full Power',
64
+ defaultWorker: 'claude-sonnet-4-6',
65
+ complexWorker: 'claude-opus-4-6',
66
+ challengerPolicy: 'medium-risk',
67
+ checkpointPolicy: 'all-edits',
68
+ reviewPolicy: 'non-trivial',
69
+ description: 'Deep reasoning, dual-brain on everything that matters',
70
+ },
71
+ };
72
+ export function getWorkStyle(profile) {
73
+ const key = profile?.workStyle || profile?.work_style || 'balanced';
74
+ const style = WORK_STYLES[key] ?? WORK_STYLES.balanced;
75
+ return { ...style, key: WORK_STYLES[key] ? key : 'balanced' };
76
+ }
77
+ const MODEL_CAPABILITIES = {
78
+ haiku: {
79
+ provider: 'claude', tierFit: ['search'], contextWindow: 200_000,
80
+ strengths: ['search', 'format', 'lookup', 'classification', 'grep-analysis'],
81
+ weaknesses: ['complex-edits', 'architecture', 'security', 'multi-file-refactor'],
82
+ effortLevels: null, costTier: 'cheap',
83
+ },
84
+ sonnet: {
85
+ provider: 'claude', tierFit: ['execute', 'search'], contextWindow: 200_000,
86
+ strengths: ['edit', 'refactor', 'test', 'debug', 'code-generation', 'tool-use'],
87
+ weaknesses: ['deep-architecture', 'ambiguous-requirements', 'frontier-reasoning'],
88
+ effortLevels: ['low', 'medium', 'high', 'xhigh'], costTier: 'medium',
89
+ },
90
+ opus: {
91
+ provider: 'claude', tierFit: ['think', 'execute'], contextWindow: 200_000,
92
+ strengths: ['architecture', 'security', 'complex-debug', 'review', 'planning', 'threat-modeling'],
93
+ weaknesses: ['cost', 'overkill-for-simple-tasks'],
94
+ effortLevels: ['low', 'medium', 'high', 'xhigh'], costTier: 'expensive',
95
+ },
96
+ 'gpt-4.1-mini': {
97
+ provider: 'openai', tierFit: ['search'], contextWindow: 1_047_576,
98
+ strengths: ['search', 'format', 'classification', 'fast-lookups'],
99
+ weaknesses: ['complex-refactors', 'architecture', 'multi-file-edits'],
100
+ effortLevels: ['low', 'medium', 'high'], costTier: 'cheap',
101
+ },
102
+ 'gpt-4.1': {
103
+ provider: 'openai', tierFit: ['execute', 'search'], contextWindow: 1_047_576,
104
+ strengths: ['edit', 'code-generation', 'simple-refactor'],
105
+ weaknesses: ['architecture', 'security', 'complex-debug'],
106
+ effortLevels: ['low', 'medium', 'high'], costTier: 'medium',
107
+ },
108
+ 'gpt-4o': {
109
+ provider: 'openai', tierFit: ['execute', 'think'], contextWindow: 128_000,
110
+ strengths: ['refactor', 'debug', 'code-generation', 'test', 'multimodal'],
111
+ weaknesses: ['cost vs mini'],
112
+ effortLevels: ['low', 'medium', 'high'], costTier: 'medium',
113
+ },
114
+ 'gpt-4o-mini': {
115
+ provider: 'openai', tierFit: ['search'], contextWindow: 128_000, costTier: 'cheap',
116
+ strengths: ['quick-tasks', 'search', 'classification'],
117
+ weaknesses: ['complex-edits', 'architecture'],
118
+ effortLevels: null,
119
+ },
120
+ 'o3': {
121
+ provider: 'openai', tierFit: ['think'], contextWindow: 200_000,
122
+ strengths: ['architecture', 'security', 'review', 'planning', 'complex-debug', 'deep-reasoning'],
123
+ weaknesses: ['cost', 'latency'],
124
+ effortLevels: ['low', 'medium', 'high'], costTier: 'expensive',
125
+ },
126
+ };
127
+ const WORK_MODELS = {
128
+ execute: 'claude-sonnet-4-6',
129
+ think: 'claude-opus-4-6',
130
+ search: 'claude-haiku-4-5-20251001',
131
+ challengerGpt: 'o3',
132
+ challengerGptFallback: 'gpt-4o',
133
+ searchGpt: 'gpt-4o-mini',
134
+ };
135
+ export function getModelCapabilities(model) {
136
+ return MODEL_CAPABILITIES[model] ?? null;
137
+ }
138
+ export function getAvailableModels(profile) {
139
+ const ALL_CLAUDE = ['haiku', 'sonnet', 'opus'];
140
+ const ALL_OPENAI = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
141
+ const claudeModels = profile?.providers?.claude?.models;
142
+ const openaiModels = profile?.providers?.openai?.models;
143
+ return {
144
+ claude: Array.isArray(claudeModels) ? claudeModels : ALL_CLAUDE,
145
+ openai: Array.isArray(openaiModels) ? openaiModels : ALL_OPENAI,
146
+ };
147
+ }
148
+ // ─── Internal helpers ────────────────────────────────────────────────────────
149
+ function pickChallengerModel(primaryProvider, available) {
150
+ if (primaryProvider === 'claude') {
151
+ if (available.openai.includes(WORK_MODELS.challengerGpt))
152
+ return WORK_MODELS.challengerGpt;
153
+ if (available.openai.includes(WORK_MODELS.challengerGptFallback))
154
+ return WORK_MODELS.challengerGptFallback;
155
+ return null;
156
+ }
157
+ else {
158
+ if (available.claude.includes('opus'))
159
+ return WORK_MODELS.think;
160
+ if (available.claude.includes('sonnet'))
161
+ return WORK_MODELS.execute;
162
+ return null;
163
+ }
164
+ }
165
+ function shouldTriggerChallenger(challengerPolicy, risk, hasBothProviders) {
166
+ if (challengerPolicy === 'never' || !hasBothProviders)
167
+ return false;
168
+ if (challengerPolicy === 'high-risk')
169
+ return ['high', 'critical'].includes(risk);
170
+ if (challengerPolicy === 'medium-risk')
171
+ return ['medium', 'high', 'critical'].includes(risk);
172
+ return false;
173
+ }
174
+ export function estimateBudgetPressure(_profile, _cwd) {
175
+ return { claude: 0, openai: 0 };
176
+ }
177
+ function getHealthScores(tier, cwd) {
178
+ const claudeClass = tier === 'search' ? 'haiku' : tier === 'think' ? 'opus' : 'sonnet';
179
+ const openaiClass = tier === 'search' ? 'gpt-4o-mini' : tier === 'think' ? 'o3' : 'gpt-4o';
180
+ checkCooldown('claude', claudeClass, cwd);
181
+ checkCooldown('openai', openaiClass, cwd);
182
+ return {
183
+ claude: getProviderScore('claude', claudeClass, cwd),
184
+ openai: getProviderScore('openai', openaiClass, cwd),
185
+ };
186
+ }
187
+ export function shouldDualBrain(detection, profile) {
188
+ const { intent = '', risk = 'low', complexity = 'simple', designImpact = false } = detection;
189
+ const dualEnabled = profile?.dual_brain_enabled !== false;
190
+ if (!dualEnabled)
191
+ return false;
192
+ const hasBothProviders = !!(profile?.providers?.claude?.enabled &&
193
+ profile?.providers?.claude?.plan &&
194
+ profile?.providers?.openai?.enabled &&
195
+ profile?.providers?.openai?.plan);
196
+ if (designImpact)
197
+ return true;
198
+ if (!hasBothProviders)
199
+ return false;
200
+ const criticalRisk = risk === 'critical';
201
+ const archOrSecurity = ['architecture', 'security'].includes(intent);
202
+ const complexHighRisk = complexity === 'complex' && risk === 'high';
203
+ return criticalRisk || archOrSecurity || complexHighRisk;
204
+ }
205
+ const THINK_INTENTS = ['architecture', 'security', 'review', 'planning', 'compare'];
206
+ const SEARCH_INTENTS = ['search', 'format', 'explain', 'lookup'];
207
+ function pickClaudeModel(detection, available) {
208
+ const { intent = '', risk = 'low', effort = 'medium' } = detection;
209
+ const needsOpus = THINK_INTENTS.includes(intent) || risk === 'critical' || effort === 'xhigh';
210
+ const needsHaiku = SEARCH_INTENTS.includes(intent) && !['high', 'critical'].includes(risk);
211
+ if (needsOpus && available.includes('opus'))
212
+ return 'opus';
213
+ if (needsHaiku && available.includes('haiku'))
214
+ return 'haiku';
215
+ return available.includes('sonnet') ? 'sonnet' : available[available.length - 1];
216
+ }
217
+ function pickOpenAIModel(detection, available) {
218
+ const { intent = '', risk = 'low', complexity = 'simple', effort = 'medium' } = detection;
219
+ const needsTop = THINK_INTENTS.includes(intent) || risk === 'critical' || effort === 'xhigh';
220
+ const needsMini = SEARCH_INTENTS.includes(intent) && effort === 'low';
221
+ const needsCodex = ['refactor', 'debug'].includes(intent) && complexity !== 'trivial';
222
+ const pref = needsTop ? 'o3' : needsMini ? 'gpt-4o-mini' : needsCodex ? 'gpt-4o' : 'gpt-4o';
223
+ const rank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
224
+ const idx = rank.indexOf(pref);
225
+ for (let i = idx; i >= 0; i--) {
226
+ if (available.includes(rank[i]))
227
+ return rank[i];
228
+ }
229
+ return available[0] ?? 'gpt-4o-mini';
230
+ }
231
+ function toShortName(model, provider) {
232
+ if (!model)
233
+ return model;
234
+ const m = model.toLowerCase();
235
+ if (provider === 'claude') {
236
+ if (m.includes('haiku'))
237
+ return 'haiku';
238
+ if (m.includes('opus'))
239
+ return 'opus';
240
+ if (m.includes('sonnet'))
241
+ return 'sonnet';
242
+ }
243
+ return model;
244
+ }
245
+ function toFullModelId(shortName, provider, tier) {
246
+ if (!modelRegistry)
247
+ return shortName;
248
+ const registryProvider = provider === 'claude' ? 'anthropic' : 'openai';
249
+ const taskType = tier === 'search' ? 'search' : tier === 'think' ? 'think' : 'execute';
250
+ const candidates = modelRegistry.getModelsForTask(taskType, registryProvider);
251
+ const match = candidates.find(m => m.id.toLowerCase().includes(shortName.toLowerCase()));
252
+ return match ? match.id : shortName;
253
+ }
254
+ function applyHealthDowngrade(model, score, provider, available, isHighStakes) {
255
+ if (score >= 50 || isHighStakes)
256
+ return model;
257
+ if (provider === 'claude') {
258
+ const claudeRank = ['haiku', 'sonnet', 'opus'];
259
+ const idx = claudeRank.indexOf(model);
260
+ const steps = score === 0 ? 2 : 1;
261
+ const downIdx = Math.max(0, idx - steps);
262
+ for (let i = downIdx; i <= idx; i++) {
263
+ if (available.includes(claudeRank[i]))
264
+ return claudeRank[i];
265
+ }
266
+ return available[0] ?? 'haiku';
267
+ }
268
+ else {
269
+ const oaiRank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
270
+ const idx = oaiRank.indexOf(model);
271
+ const steps = score === 0 ? 2 : 1;
272
+ const downIdx = Math.max(0, idx - steps);
273
+ for (let i = downIdx; i <= idx; i++) {
274
+ if (available.includes(oaiRank[i]))
275
+ return oaiRank[i];
276
+ }
277
+ return available[0] ?? 'gpt-4o-mini';
278
+ }
279
+ }
280
+ function applyProfileBias(model, profile, provider, available, tier) {
281
+ const mode = (profile?.mode || profile?.profile || 'auto');
282
+ if (mode === 'cost-saver') {
283
+ const ranks = {
284
+ claude: ['haiku', 'sonnet', 'opus'],
285
+ openai: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'],
286
+ };
287
+ for (const m of ranks[provider]) {
288
+ if (!available.includes(m))
289
+ continue;
290
+ const caps = MODEL_CAPABILITIES[m];
291
+ if (tier && caps && !caps.tierFit.includes(tier))
292
+ continue;
293
+ return m;
294
+ }
295
+ }
296
+ if (mode === 'quality-first') {
297
+ const ranks = {
298
+ claude: ['opus', 'sonnet', 'haiku'],
299
+ openai: ['o3', 'o4-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o-mini'],
300
+ };
301
+ for (const m of ranks[provider]) {
302
+ if (available.includes(m))
303
+ return m;
304
+ }
305
+ }
306
+ const prefs = profile?.preferences || [];
307
+ for (const pref of prefs) {
308
+ if (pref.model && available.includes(pref.model) &&
309
+ pref.for && MODEL_CAPABILITIES[pref.model]?.strengths?.includes(pref.for)) {
310
+ return pref.model;
311
+ }
312
+ }
313
+ return model;
314
+ }
315
+ function pickEffort(model, detection) {
316
+ const caps = MODEL_CAPABILITIES[model];
317
+ if (!caps?.effortLevels)
318
+ return null;
319
+ const { risk = 'low', complexity = 'simple', effort } = detection;
320
+ if (effort && caps.effortLevels.includes(effort))
321
+ return effort;
322
+ if (risk === 'critical' || complexity === 'complex')
323
+ return 'xhigh';
324
+ if (risk === 'high' || complexity === 'moderate')
325
+ return 'high';
326
+ if (risk === 'low' && complexity === 'trivial')
327
+ return 'low';
328
+ return 'medium';
329
+ }
330
+ function pickModes(model, detection) {
331
+ const { intent = '', complexity = 'simple' } = detection;
332
+ const thinkingModels = ['sonnet', 'opus', 'o3', 'gpt-4o'];
333
+ const lightIntents = ['search', 'format', 'explain', 'lookup'];
334
+ return {
335
+ extendedThinking: thinkingModels.includes(model)
336
+ && ['moderate', 'complex'].includes(complexity)
337
+ && !lightIntents.includes(intent),
338
+ fastMode: model === 'opus',
339
+ extendedContext: ['sonnet', 'opus'].includes(model),
340
+ webSearch: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o'].includes(model),
341
+ };
342
+ }
343
+ function pickSandbox(model, detection) {
344
+ const { tier = 'execute' } = detection;
345
+ if (tier === 'search')
346
+ return 'read-only';
347
+ if (MODEL_CAPABILITIES[model]?.provider === 'openai')
348
+ return 'danger-full-access';
349
+ return 'workspace-write';
350
+ }
351
+ function chooseProvider(detection, profile, healthScores) {
352
+ const { tier = 'execute', intent = '' } = detection;
353
+ const claudeScore = healthScores.claude;
354
+ const openaiScore = healthScores.openai;
355
+ const providers = profile?.providers;
356
+ if (!providers?.openai?.enabled)
357
+ return 'claude';
358
+ if (claudeScore === 0 && openaiScore === 0) {
359
+ return claudeScore >= openaiScore ? 'claude' : 'openai';
360
+ }
361
+ if (THINK_INTENTS.includes(intent) && claudeScore > 0)
362
+ return 'claude';
363
+ if (claudeScore === 0 && openaiScore > 0)
364
+ return 'openai';
365
+ if (tier === 'execute' && !THINK_INTENTS.includes(intent)) {
366
+ if (claudeScore < 100 && openaiScore > claudeScore)
367
+ return 'openai';
368
+ }
369
+ return claudeScore >= openaiScore ? 'claude' : 'openai';
370
+ }
371
+ export function explainDecision(decision, detection, profile) {
372
+ const { provider, model, effort, dualBrain, workStyle, challengerModel } = decision;
373
+ const { intent = 'task', risk = 'low', complexity = 'simple', tier = 'execute' } = detection;
374
+ const healthScores = (decision._healthScores || {});
375
+ const mode = (profile?.mode || profile?.profile || 'auto');
376
+ const ws = (decision._workStyle ?? getWorkStyle(profile));
377
+ const wsLabel = ws.label ?? workStyle ?? 'Balanced';
378
+ const modelLabel = effort ? `${model} ${effort}` : model;
379
+ if (dualBrain && challengerModel) {
380
+ return `${wsLabel} mode: ${modelLabel} for ${intent}, ${challengerModel} challenger on ${risk}-risk changes.`;
381
+ }
382
+ if (dualBrain) {
383
+ return `${wsLabel} mode: ${modelLabel} with dual-brain review because this ${intent} change is ${risk} risk.`;
384
+ }
385
+ const claudeScore = healthScores.claude ?? 100;
386
+ const providerScore = healthScores[provider] ?? 100;
387
+ if (claudeScore === 0 && provider === 'openai') {
388
+ return `${wsLabel} mode: using ${modelLabel} because Claude is rate-limited and this is an isolated ${tier} task.`;
389
+ }
390
+ if (providerScore < 50) {
391
+ return `${wsLabel} mode: using ${modelLabel} (downgraded due to rate-limit cooldown) for this ${complexity} ${intent}.`;
392
+ }
393
+ if (mode === 'cost-saver') {
394
+ return `${wsLabel} mode: using ${modelLabel} (cost-saver bias) for ${risk}-risk ${intent}.`;
395
+ }
396
+ if (mode === 'quality-first') {
397
+ return `${wsLabel} mode: using ${modelLabel} (quality-first bias) for ${intent}.`;
398
+ }
399
+ if (THINK_INTENTS.includes(intent)) {
400
+ return `${wsLabel} mode: ${modelLabel} for ${intent} — deep reasoning needed.`;
401
+ }
402
+ if (tier === 'search' || SEARCH_INTENTS.includes(intent)) {
403
+ return `${wsLabel} mode: ${modelLabel} for lightweight ${intent} lookup.`;
404
+ }
405
+ return `${wsLabel} mode: ${modelLabel} for ${intent} (${risk} risk, ${provider} healthy).`;
406
+ }
407
+ export function parsePreferences(preferences) {
408
+ const active = (preferences || []).filter(p => p.enabled);
409
+ const signals = {
410
+ biasOverride: null, preferProvider: null, avoidProvider: null,
411
+ alwaysDualBrain: false, neverDualBrain: false, preferModel: null,
412
+ };
413
+ for (const pref of active) {
414
+ const t = pref.text.toLowerCase();
415
+ if (/cheap|save|budget|frugal|economical|cost/i.test(t))
416
+ signals.biasOverride = 'cost-saver';
417
+ if (/quality|best|thorough|careful|premium/i.test(t))
418
+ signals.biasOverride = 'quality-first';
419
+ if (/prefer claude|use claude|claude first/i.test(t))
420
+ signals.preferProvider = 'claude';
421
+ if (/prefer (openai|gpt|chatgpt)|use (openai|gpt)/i.test(t))
422
+ signals.preferProvider = 'openai';
423
+ if (/avoid claude|no claude/i.test(t))
424
+ signals.avoidProvider = 'claude';
425
+ if (/avoid (openai|gpt)|no (openai|gpt)/i.test(t))
426
+ signals.avoidProvider = 'openai';
427
+ if (/always/.test(t) && /(consensus|dual.brain|two.brain|dual)/i.test(t))
428
+ signals.alwaysDualBrain = true;
429
+ if (/never (consensus|dual)|skip (review|consensus)|solo/i.test(t))
430
+ signals.neverDualBrain = true;
431
+ if (/prefer opus|use opus/i.test(t))
432
+ signals.preferModel = 'opus';
433
+ if (/prefer sonnet|use sonnet/i.test(t))
434
+ signals.preferModel = 'sonnet';
435
+ if (/prefer haiku|use haiku/i.test(t))
436
+ signals.preferModel = 'haiku';
437
+ }
438
+ return signals;
439
+ }
440
+ function applyCriticalRiskFloor(model, provider, available, risk) {
441
+ if (risk !== 'critical')
442
+ return model;
443
+ const cheapModels = { claude: 'haiku', openai: 'gpt-4.1-mini' };
444
+ const floorModels = { claude: 'sonnet', openai: 'gpt-4.1' };
445
+ if (model === cheapModels[provider]) {
446
+ const floor = floorModels[provider];
447
+ const escalated = available.includes(floor) ? floor : available[available.length - 1] ?? model;
448
+ process.stderr.write(`[dual-brain] Warning: cost-saver selected ${model} for a critical-risk task. Escalating to ${escalated} (safety floor).\n`);
449
+ return escalated;
450
+ }
451
+ return model;
452
+ }
453
+ // ─── Exported: decideRoute ────────────────────────────────────────────────────
454
+ export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult, sessionContext = null } = {}) {
455
+ const available = getAvailableModels(profile);
456
+ const workStyle = getWorkStyle(profile);
457
+ const prefSignals = parsePreferences(profile.preferences);
458
+ const profileWithEffectiveBias = prefSignals.biasOverride
459
+ ? { ...profile, mode: prefSignals.biasOverride }
460
+ : profile;
461
+ const { tier = 'execute', risk = 'low', complexity = 'simple', effort: detectionEffort } = detection;
462
+ const isHighStakes = ['critical', 'high'].includes(risk);
463
+ const needsDeepReasoning = THINK_INTENTS.includes(detection.intent || '') ||
464
+ risk === 'critical' ||
465
+ (complexity === 'complex' && ['high', 'critical'].includes(risk)) ||
466
+ detectionEffort === 'xhigh';
467
+ const healthScores = getHealthScores(tier, cwd);
468
+ let provider = chooseProvider(detection, profileWithEffectiveBias, healthScores);
469
+ if (prefSignals.preferProvider) {
470
+ const preferred = prefSignals.preferProvider;
471
+ const prefEnabled = profile?.providers?.[preferred]?.enabled;
472
+ const prefScore = healthScores[preferred] ?? 0;
473
+ if (prefEnabled && prefScore > 0)
474
+ provider = preferred;
475
+ }
476
+ if (prefSignals.avoidProvider && provider === prefSignals.avoidProvider) {
477
+ const other = prefSignals.avoidProvider === 'claude' ? 'openai' : 'claude';
478
+ const otherEnabled = profile?.providers?.[other]?.enabled;
479
+ const otherScore = healthScores[other] ?? 0;
480
+ if (otherEnabled && otherScore > 0)
481
+ provider = other;
482
+ }
483
+ const _fallbackClaude = (() => {
484
+ const wantOpus = needsDeepReasoning && workStyle.key !== 'fast';
485
+ const fb = wantOpus && available.claude.includes('opus') ? 'opus' : 'sonnet';
486
+ return available.claude.includes(fb) ? fb : (available.claude[available.claude.length - 1] ?? 'sonnet');
487
+ })();
488
+ const _fallbackOpenAI = (() => {
489
+ const wantO3 = needsDeepReasoning && workStyle.key === 'fullpower';
490
+ const fb = wantO3 && available.openai.includes('o3') ? 'o3' : 'gpt-4o';
491
+ return available.openai.includes(fb) ? fb : (available.openai[available.openai.length - 1] ?? 'gpt-4o');
492
+ })();
493
+ let model;
494
+ if (modelRegistry) {
495
+ const registryProvider = provider === 'claude' ? 'anthropic' : 'openai';
496
+ const taskType = tier === 'search' ? 'search' : tier === 'think' ? 'think' : 'execute';
497
+ const constraints = {
498
+ provider: registryProvider,
499
+ ...(tier === 'search' && { preferSpeed: true }),
500
+ ...(tier === 'think' && { requireReasoning: true }),
501
+ ...(!needsDeepReasoning && workStyle.key === 'fast' && { maxCost: 'medium' }),
502
+ };
503
+ const registryResult = modelRegistry.getBestModel(taskType, constraints);
504
+ if (registryResult) {
505
+ model = registryResult.id;
506
+ }
507
+ else {
508
+ model = provider === 'claude' ? _fallbackClaude : _fallbackOpenAI;
509
+ }
510
+ }
511
+ else {
512
+ model = provider === 'claude' ? _fallbackClaude : _fallbackOpenAI;
513
+ }
514
+ model = toShortName(model, provider);
515
+ model = applyHealthDowngrade(model, healthScores[provider], provider, available[provider], isHighStakes);
516
+ model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider], detection.tier);
517
+ let thinkTier = null;
518
+ try {
519
+ if (thinkResult?.tier)
520
+ thinkTier = thinkResult.tier;
521
+ }
522
+ catch { }
523
+ if (thinkTier && !isHighStakes) {
524
+ const claudeRankAsc = ['haiku', 'sonnet', 'opus'];
525
+ const openaiRankAsc = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
526
+ if (thinkTier === 'recall' && provider === 'claude') {
527
+ const target = 'haiku';
528
+ const currentIdx = claudeRankAsc.indexOf(model);
529
+ const targetIdx = claudeRankAsc.indexOf(target);
530
+ if (targetIdx !== -1 && targetIdx < currentIdx && available.claude.includes(target))
531
+ model = target;
532
+ }
533
+ else if (thinkTier === 'recall' && provider === 'openai') {
534
+ const target = 'gpt-4o-mini';
535
+ const currentIdx = openaiRankAsc.indexOf(model);
536
+ const targetIdx = openaiRankAsc.indexOf(target);
537
+ if (targetIdx !== -1 && targetIdx < currentIdx && available.openai.includes(target))
538
+ model = target;
539
+ }
540
+ else if (thinkTier === 'quick' && provider === 'claude') {
541
+ const target = 'sonnet';
542
+ const currentIdx = claudeRankAsc.indexOf(model);
543
+ const targetIdx = claudeRankAsc.indexOf(target);
544
+ if (targetIdx !== -1 && targetIdx < currentIdx && available.claude.includes(target))
545
+ model = target;
546
+ }
547
+ else if (thinkTier === 'quick' && provider === 'openai') {
548
+ const target = 'gpt-4o';
549
+ const currentIdx = openaiRankAsc.indexOf(model);
550
+ const targetIdx = openaiRankAsc.indexOf(target);
551
+ if (targetIdx !== -1 && targetIdx < currentIdx && available.openai.includes(target))
552
+ model = target;
553
+ }
554
+ }
555
+ // Session context escalation (abbreviated for brevity — same logic as .mjs)
556
+ if (sessionContext) {
557
+ const sessionAttempts = Array.isArray(sessionContext.priorAttempts) ? sessionContext.priorAttempts : [];
558
+ const sessionFailures = sessionAttempts.filter(a => a && (a.failed || a.status === 'failed'));
559
+ const sessionSuccesses = sessionAttempts.filter(a => a && !a.failed && a.status !== 'failed');
560
+ if (sessionFailures.length >= 2 && !isHighStakes) {
561
+ if (provider === 'claude') {
562
+ const claudeRank = ['haiku', 'sonnet', 'opus'];
563
+ const currentIdx = claudeRank.indexOf(toShortName(model, 'claude'));
564
+ if (currentIdx !== -1 && currentIdx < claudeRank.length - 1) {
565
+ const escalated = claudeRank[currentIdx + 1];
566
+ if (available.claude.includes(escalated))
567
+ model = escalated;
568
+ }
569
+ }
570
+ else {
571
+ const oaiRank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
572
+ const currentIdx = oaiRank.indexOf(model);
573
+ if (currentIdx !== -1 && currentIdx < oaiRank.length - 1) {
574
+ const escalated = oaiRank[currentIdx + 1];
575
+ if (available.openai.includes(escalated))
576
+ model = escalated;
577
+ }
578
+ }
579
+ }
580
+ if (sessionSuccesses.length > 0) {
581
+ const lastSuccess = sessionSuccesses[sessionSuccesses.length - 1];
582
+ if (lastSuccess.provider && lastSuccess.model && !isHighStakes) {
583
+ const successProvider = lastSuccess.provider;
584
+ const successModel = lastSuccess.model;
585
+ const providerEnabled = profile?.providers?.[successProvider]?.enabled;
586
+ const providerHealthy = (healthScores[successProvider] ?? 0) > 0;
587
+ if (providerEnabled && providerHealthy) {
588
+ const shortSuccess = toShortName(successModel, successProvider);
589
+ if (available[successProvider]?.includes(shortSuccess)) {
590
+ provider = successProvider;
591
+ model = shortSuccess;
592
+ }
593
+ }
594
+ }
595
+ }
596
+ }
597
+ model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk || 'low');
598
+ if (prefSignals.preferModel) {
599
+ const wantedModel = prefSignals.preferModel;
600
+ if ((available[provider])?.includes(wantedModel)) {
601
+ model = wantedModel;
602
+ }
603
+ }
604
+ model = toFullModelId(model, provider, tier);
605
+ let _advisorOverride = null;
606
+ if (routingAdvisor && provider === 'claude') {
607
+ try {
608
+ const advice = routingAdvisor.adviseModel({ intent: detection.intent || '', tier, risk: detection.risk || 'low' }, cwd);
609
+ if (advice.confidence > 0.3 && advice.model) {
610
+ const advisorShort = advice.model;
611
+ const previousModel = toShortName(model, 'claude');
612
+ if (advisorShort !== previousModel && available.claude.includes(advisorShort)) {
613
+ const overrideFullId = toFullModelId(advisorShort, 'claude', tier);
614
+ _advisorOverride = { from: model, to: overrideFullId, reason: advice.reason, explored: advice.explored };
615
+ model = overrideFullId;
616
+ }
617
+ }
618
+ }
619
+ catch { /* non-blocking */ }
620
+ }
621
+ const hasBothProviders = !!(profile?.providers?.claude?.enabled &&
622
+ profile?.providers?.openai?.enabled);
623
+ const challengerTriggered = shouldTriggerChallenger(workStyle.challengerPolicy, risk, hasBothProviders);
624
+ const legacyDualBrain = !!(detection.designImpact && profile?.dual_brain_enabled !== false);
625
+ let dual = challengerTriggered || legacyDualBrain || shouldDualBrain(detection, profile);
626
+ if (prefSignals.alwaysDualBrain)
627
+ dual = true;
628
+ if (prefSignals.neverDualBrain)
629
+ dual = false;
630
+ if (dual && !hasBothProviders && !legacyDualBrain)
631
+ dual = false;
632
+ const degradedDualBrain = !!(legacyDualBrain && !hasBothProviders);
633
+ const challengerModel = dual ? pickChallengerModel(provider, available) : null;
634
+ const effort = pickEffort(model, detection);
635
+ const modes = pickModes(model, detection);
636
+ const sandbox = pickSandbox(model, detection);
637
+ const decision = {
638
+ provider,
639
+ model,
640
+ effort,
641
+ tier,
642
+ dualBrain: dual,
643
+ ...(degradedDualBrain && { degradedDualBrain: true }),
644
+ ...(challengerModel && { challengerModel }),
645
+ workStyle: workStyle.key,
646
+ modes,
647
+ sandbox,
648
+ explanation: '',
649
+ _healthScores: healthScores,
650
+ _workStyle: workStyle,
651
+ ...(_advisorOverride && { _advisorOverride }),
652
+ };
653
+ decision.explanation = explainDecision(decision, detection, profileWithEffectiveBias);
654
+ const { _healthScores, _workStyle, ...result } = decision;
655
+ return result;
656
+ }
657
+ // ─── Exported: getFailoverOrder ──────────────────────────────────────────────
658
+ export function getFailoverOrder(decision, profile) {
659
+ const { provider: failedProvider, model: failedModel, tier = 'execute' } = decision;
660
+ const available = getAvailableModels(profile);
661
+ const claudeRankByTier = {
662
+ think: ['opus', 'sonnet', 'haiku'],
663
+ execute: ['sonnet', 'opus', 'haiku'],
664
+ search: ['haiku', 'sonnet', 'opus'],
665
+ };
666
+ const openaiRankByTier = {
667
+ think: ['o3', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o-mini'],
668
+ execute: ['gpt-4o', 'gpt-4.1', 'o3', 'gpt-4.1-mini', 'gpt-4o-mini'],
669
+ search: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o3'],
670
+ };
671
+ const claudeRank = claudeRankByTier[tier] ?? claudeRankByTier.execute;
672
+ const openaiRank = openaiRankByTier[tier] ?? openaiRankByTier.execute;
673
+ const claudeEnabled = !!(profile?.providers?.claude?.enabled);
674
+ const openaiEnabled = !!(profile?.providers?.openai?.enabled);
675
+ const fallbacks = [];
676
+ if (failedProvider === 'claude') {
677
+ for (const m of claudeRank) {
678
+ if (m === failedModel || !available.claude.includes(m))
679
+ continue;
680
+ fallbacks.push({ provider: 'claude', model: m, label: `Claude ${m}` });
681
+ }
682
+ if (openaiEnabled) {
683
+ for (const m of openaiRank) {
684
+ if (!available.openai.includes(m))
685
+ continue;
686
+ fallbacks.push({ provider: 'openai', model: m, label: `OpenAI ${m}` });
687
+ }
688
+ }
689
+ }
690
+ else {
691
+ for (const m of openaiRank) {
692
+ if (m === failedModel || !available.openai.includes(m))
693
+ continue;
694
+ fallbacks.push({ provider: 'openai', model: m, label: `OpenAI ${m}` });
695
+ }
696
+ if (claudeEnabled) {
697
+ for (const m of claudeRank) {
698
+ if (!available.claude.includes(m))
699
+ continue;
700
+ fallbacks.push({ provider: 'claude', model: m, label: `Claude ${m}` });
701
+ }
702
+ }
703
+ }
704
+ return fallbacks;
705
+ }
706
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
707
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
708
+ const args = process.argv.slice(2);
709
+ let profilePath, detectionJson, cwd;
710
+ for (let i = 0; i < args.length; i++) {
711
+ if (args[i] === '--profile' && args[i + 1]) {
712
+ profilePath = args[++i];
713
+ }
714
+ if (args[i] === '--detection' && args[i + 1]) {
715
+ detectionJson = args[++i];
716
+ }
717
+ if (args[i] === '--cwd' && args[i + 1]) {
718
+ cwd = args[++i];
719
+ }
720
+ }
721
+ let profile = {};
722
+ let detection = {};
723
+ if (profilePath) {
724
+ try {
725
+ profile = JSON.parse(readFileSync(profilePath, 'utf8'));
726
+ }
727
+ catch (e) {
728
+ console.error(`Failed to load profile: ${e.message}`);
729
+ process.exit(1);
730
+ }
731
+ }
732
+ if (detectionJson) {
733
+ try {
734
+ detection = JSON.parse(detectionJson);
735
+ }
736
+ catch (e) {
737
+ console.error(`Failed to parse detection JSON: ${e.message}`);
738
+ process.exit(1);
739
+ }
740
+ }
741
+ const result = decideRoute({ profile, detection, cwd });
742
+ console.log(JSON.stringify(result, null, 2));
743
+ }
744
+ //# sourceMappingURL=decide.js.map