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