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,328 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * task-classifier.mjs — Analyze work descriptions and return model + effort + mode config.
4
+ *
5
+ * Uses model-registry capabilities to make informed routing decisions:
6
+ * - Which model (per provider) handles this task best
7
+ * - What effort/reasoning level to use
8
+ * - Whether to enable extended thinking, fast mode, extended context, web search
9
+ * - How to dispatch (Claude Agent vs Codex exec)
10
+ *
11
+ * Exports: classifyTask, selectModelEffort, INTENTS
12
+ * CLI: node hooks/task-classifier.mjs "description" [--files a,b] [--budget-pressure 0.8]
13
+ */
14
+
15
+ import { classifyRisk, extractPaths } from './risk-classifier.mjs';
16
+ import {
17
+ MODEL_CAPABILITIES, getCapabilities, getDispatchConfig,
18
+ recommendEffort, shouldUseExtendedContext, shouldUseFastMode,
19
+ getBestModelFor,
20
+ } from './model-registry.mjs';
21
+
22
+ // ─── Intent definitions ───────────────────────────────────────────────────────
23
+
24
+ const INTENTS = {
25
+ search: /\b(grep|find|locate|where is|where are|list|explore|read|look up|look for|check|what is|show me|display)\b/i,
26
+ explain: /\b(explain|walk me through|what does|how does|describe|summarize|understand|clarify)\b/i,
27
+ compare: /\b(compare|contrast|difference|versus|vs\.?|trade.?off|which is better|pros and cons|benchmark|performance)\b/i,
28
+ document: /\b(document|docs?|readme|jsdoc|typedoc|api docs|write docs|add docs|update docs)\b/i,
29
+ format: /\b(format|lint|prettier|style|indent|whitespace|typo|typos|comment[s]?|reformat)\b/i,
30
+ planning: /\b(plan|roadmap|strategy|prioritize|break down|decompose|prioritise)\b/i,
31
+ architecture: /\b(design|architect|architecture|propose|how should we|system design|system architecture)\b/i,
32
+ security: /\b(auth|credential|secret|token|password|encrypt|permission[s]?|vulnerability|vulnerabilities|CVE|oauth|jwt|api.?key)\b/i,
33
+ review: /\b(review|audit|check for issues|evaluate|assess|inspect code|code review)\b/i,
34
+ debug: /\b(debug|investigate|why (is|does|isn't|doesn't)|trace|diagnose|figure out|broken|not working|failing|regression)\b/i,
35
+ test: /\b(test[s]?|spec[s]?|add test|fix test|test coverage|unit test|e2e|integration test|jest|vitest|mocha)\b/i,
36
+ refactor: /\b(refactor|restructure|reorganize|reorganise|extract|split|consolidate|clean up|cleanup|dedupe|dedup)\b/i,
37
+ edit: /\b(fix|add|update|modify|change|rename|move|replace|write|implement|create|remove|delete|insert)\b/i,
38
+ };
39
+
40
+ const INTENT_PRIORITY = [
41
+ 'security', 'architecture', 'planning', 'compare', 'review',
42
+ 'debug', 'refactor', 'test', 'explain', 'document', 'format', 'search', 'edit',
43
+ ];
44
+
45
+ // ─── Risk keyword detection (description-level) ──────────────────────────────
46
+
47
+ const RISK_KEYWORDS = [
48
+ { level: 'critical', regex: /\b(auth|secret|credential|token|password|encrypt|certificate|oauth|jwt|api.?key|vulnerability|CVE)\b/i },
49
+ { level: 'high', regex: /\b(billing|payment|migration|deploy|ci.?cd|security|permission|policy|schema|openapi|swagger|production|prod)\b/i },
50
+ { level: 'medium', regex: /\b(test|spec|config|shared|util|lib|integration|public.?api)\b/i },
51
+ { level: 'low', regex: /\b(readme|docs?|comment|format|lint|changelog|typo|whitespace)\b/i },
52
+ ];
53
+
54
+ const LEVEL_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
55
+
56
+ function detectKeywordRisk(description) {
57
+ for (const { level, regex } of RISK_KEYWORDS) {
58
+ if (regex.test(description)) return level;
59
+ }
60
+ return 'low';
61
+ }
62
+
63
+ function higherRisk(a, b) {
64
+ return LEVEL_ORDER[a] >= LEVEL_ORDER[b] ? a : b;
65
+ }
66
+
67
+ // ─── classifyTask ─────────────────────────────────────────────────────────────
68
+
69
+ function classifyTask(description, options = {}) {
70
+ const { files = [], priorFailures = 0 } = options;
71
+
72
+ // 1. Intent detection
73
+ let intent = 'edit';
74
+ for (const key of INTENT_PRIORITY) {
75
+ if (INTENTS[key].test(description)) {
76
+ intent = key;
77
+ break;
78
+ }
79
+ }
80
+
81
+ // 2. Risk detection
82
+ const allPaths = [...files, ...extractPaths(description)];
83
+ const pathRisk = allPaths.length > 0 ? classifyRisk(allPaths).level : 'low';
84
+ const keywordRisk = detectKeywordRisk(description);
85
+ const risk = higherRisk(pathRisk, keywordRisk);
86
+
87
+ // 3. File count
88
+ const fileCount = files.length;
89
+
90
+ // 4. Complexity detection
91
+ let complexity;
92
+ const isAmbiguous = description.length > 120 || /\b(and also|as well as|plus|additionally|also)\b/i.test(description);
93
+
94
+ if (priorFailures >= 2 || intent === 'architecture' || risk === 'critical' || fileCount >= 6 || isAmbiguous && risk === 'critical') {
95
+ complexity = 'complex';
96
+ } else if (fileCount >= 3 || intent === 'refactor' || intent === 'debug' || risk === 'high' || isAmbiguous) {
97
+ complexity = 'moderate';
98
+ } else if (fileCount <= 2 && (risk === 'low' || risk === 'medium')) {
99
+ if (intent === 'format' || fileCount <= 1 && risk === 'low') {
100
+ complexity = 'trivial';
101
+ } else {
102
+ complexity = 'simple';
103
+ }
104
+ } else {
105
+ complexity = 'moderate';
106
+ }
107
+
108
+ // 5. Effort selection
109
+ const baseEffort = { trivial: 'low', simple: 'medium', moderate: 'high', complex: 'high' }[complexity];
110
+ const effortOrder = ['low', 'medium', 'high', 'xhigh'];
111
+
112
+ function bumpEffort(e, n = 1) {
113
+ return effortOrder[Math.min(effortOrder.indexOf(e) + n, effortOrder.length - 1)];
114
+ }
115
+
116
+ let effort = baseEffort;
117
+
118
+ if (risk === 'critical' && LEVEL_ORDER[effort] < LEVEL_ORDER['high']) effort = 'high';
119
+
120
+ if (priorFailures >= 2) {
121
+ effort = 'xhigh';
122
+ } else if (priorFailures === 1) {
123
+ effort = bumpEffort(effort, 1);
124
+ }
125
+
126
+ if (intent === 'format' || intent === 'search') {
127
+ if (LEVEL_ORDER[effort] > LEVEL_ORDER['medium']) effort = 'medium';
128
+ }
129
+ if ((intent === 'architecture' || intent === 'security') && LEVEL_ORDER[effort] < LEVEL_ORDER['high']) {
130
+ effort = 'high';
131
+ }
132
+
133
+ // 6. Reason
134
+ const reasons = [];
135
+ if (fileCount > 0) reasons.push(`${fileCount} file(s)`);
136
+ if (risk !== 'low') reasons.push(`${risk} risk`);
137
+ if (priorFailures > 0) reasons.push(`${priorFailures} prior failure(s)`);
138
+ reasons.push(`intent=${intent}, complexity=${complexity}`);
139
+ const reason = reasons.join('; ');
140
+
141
+ return { intent, risk, complexity, fileCount, effort, reason };
142
+ }
143
+
144
+ // ─── selectModelEffort ────────────────────────────────────────────────────────
145
+
146
+ function selectModelEffort(taskProfile, options = {}) {
147
+ const { budgetPressure = 0, userBudgetTier = '$100', isIterating = false, estimatedTokens = 0 } = options;
148
+ const { intent, risk, effort, complexity } = taskProfile;
149
+
150
+ // ── Intent classification for routing ──
151
+ const thinkIntents = ['architecture', 'security', 'review', 'planning', 'compare'];
152
+ const searchIntents = ['search', 'format', 'explain'];
153
+ const lightIntents = ['document', 'explain', 'format', 'search'];
154
+
155
+ const needsOpus = thinkIntents.includes(intent)
156
+ || risk === 'critical'
157
+ || effort === 'xhigh';
158
+
159
+ const needsHaiku = searchIntents.includes(intent) && effort === 'low';
160
+
161
+ let claudeModel = needsOpus ? 'opus' : needsHaiku ? 'haiku' : 'sonnet';
162
+
163
+ // ── Claude effort (from registry, null-safe for haiku) ──
164
+ const caps = getCapabilities(claudeModel);
165
+ let claudeEffort = caps?.reasoning?.effortLevels
166
+ ? (recommendEffort(claudeModel, complexity, risk) || effort)
167
+ : null;
168
+
169
+ // ── Claude modes ──
170
+ const claudeModes = {
171
+ extendedThinking: caps?.reasoning?.extendedThinking
172
+ && (complexity === 'moderate' || complexity === 'complex')
173
+ && !lightIntents.includes(intent),
174
+ fastMode: shouldUseFastMode(claudeModel, isIterating),
175
+ extendedContext: shouldUseExtendedContext(claudeModel, estimatedTokens),
176
+ ultrathink: claudeModel === 'opus'
177
+ && (risk === 'critical' || (complexity === 'complex' && thinkIntents.includes(intent))),
178
+ };
179
+
180
+ // ── OpenAI model selection (all models reachable) ──
181
+ let openaiModel;
182
+ if (needsOpus) {
183
+ openaiModel = 'gpt-5.5';
184
+ } else if (searchIntents.includes(intent) && effort === 'low') {
185
+ openaiModel = 'gpt-4.1-mini';
186
+ } else if (['edit', 'test', 'document'].includes(intent) && ['simple', 'trivial'].includes(complexity)) {
187
+ openaiModel = 'gpt-4.1';
188
+ } else if (intent === 'explain' && complexity !== 'trivial') {
189
+ openaiModel = 'gpt-5.2';
190
+ } else if (['edit', 'document'].includes(intent) && complexity === 'moderate') {
191
+ openaiModel = 'gpt-5.3-codex';
192
+ } else if (intent === 'test' && complexity === 'moderate') {
193
+ openaiModel = 'gpt-5.4-mini';
194
+ } else if (['refactor', 'debug'].includes(intent)) {
195
+ openaiModel = complexity === 'complex' ? 'gpt-5.4' : 'gpt-5.3-codex';
196
+ } else {
197
+ openaiModel = 'gpt-5.4';
198
+ }
199
+
200
+ // ── OpenAI effort (from registry) ──
201
+ let openaiEffort = recommendEffort(openaiModel, complexity, risk) || effort;
202
+
203
+ // ── OpenAI modes ──
204
+ const openaiCaps = getCapabilities(openaiModel);
205
+ const openaiModes = {
206
+ webSearch: openaiCaps?.modes?.webSearch ?? false,
207
+ sandbox: openaiCaps?.modes?.sandbox?.[
208
+ thinkIntents.includes(intent) ? 'think' : searchIntents.includes(intent) ? 'search' : 'execute'
209
+ ] ?? 'danger-full-access',
210
+ };
211
+
212
+ // ── Outcome learning override ──
213
+ // If we have enough empirical data, let it influence model selection
214
+ const empiricalClaude = getBestModelFor(intent, 'claude', { minSamples: 5 });
215
+ if (empiricalClaude && empiricalClaude.successRate !== null && empiricalClaude.successRate > 0.8) {
216
+ const caps = getCapabilities(empiricalClaude.model);
217
+ if (caps && !caps.avoidFor?.includes(intent)) {
218
+ claudeModel = empiricalClaude.model;
219
+ }
220
+ }
221
+
222
+ const empiricalOpenai = getBestModelFor(intent, 'openai', { minSamples: 5 });
223
+ if (empiricalOpenai && empiricalOpenai.successRate !== null && empiricalOpenai.successRate > 0.8) {
224
+ const caps = getCapabilities(empiricalOpenai.model);
225
+ if (caps && !caps.avoidFor?.includes(intent)) {
226
+ openaiModel = empiricalOpenai.model;
227
+ }
228
+ }
229
+
230
+ // ── Budget pressure adjustments ──
231
+ const reasons = [];
232
+ const isHighStakes = risk === 'critical' || risk === 'high';
233
+ const openaiModelRank = [
234
+ 'gpt-4.1-mini', 'gpt-4.1', 'gpt-5.2', 'gpt-5.4-mini',
235
+ 'gpt-5.3-codex', 'gpt-5.3-codex-spark', 'gpt-5.4', 'gpt-5.5',
236
+ ];
237
+
238
+ if (budgetPressure > 0.9 && !isHighStakes) {
239
+ claudeModel = claudeModel === 'opus' ? 'sonnet' : 'haiku';
240
+ const oaiIdx = openaiModelRank.indexOf(openaiModel);
241
+ openaiModel = openaiModelRank[Math.max(0, oaiIdx - 2)] || 'gpt-4.1-mini';
242
+ claudeModes.fastMode = false;
243
+ claudeModes.extendedContext = false;
244
+ claudeModes.extendedThinking = false;
245
+ reasons.push('near limit, aggressive downgrade for non-critical task');
246
+ } else if (budgetPressure > 0.7 && !isHighStakes) {
247
+ claudeModel = claudeModel === 'opus' ? 'sonnet' : claudeModel === 'sonnet' ? 'haiku' : 'haiku';
248
+ const oaiIdx = openaiModelRank.indexOf(openaiModel);
249
+ openaiModel = openaiModelRank[Math.max(0, oaiIdx - 1)] || 'gpt-4.1-mini';
250
+ claudeModes.fastMode = false;
251
+ reasons.push('downgraded due to budget pressure');
252
+ }
253
+
254
+ // Recalculate efforts after potential model change
255
+ const newCaps = getCapabilities(claudeModel);
256
+ claudeEffort = newCaps?.reasoning?.effortLevels
257
+ ? (recommendEffort(claudeModel, complexity, risk) || effort)
258
+ : null;
259
+ openaiEffort = recommendEffort(openaiModel, complexity, risk) || effort;
260
+
261
+ // ── Preferred provider (think→claude, isolated execute→openai) ──
262
+ const preferred = thinkIntents.includes(intent) ? 'claude' : 'openai';
263
+
264
+ // ── Dual-brain recommendation ──
265
+ const dualBrain = risk === 'critical'
266
+ || (thinkIntents.includes(intent) && (complexity === 'complex' || complexity === 'moderate'))
267
+ || intent === 'security'
268
+ || (intent === 'review' && risk !== 'low')
269
+ || (intent === 'refactor' && risk === 'critical');
270
+
271
+ if (reasons.length === 0) {
272
+ reasons.push(`${claudeModel}/${openaiModel} matched to ${intent} @ ${complexity} complexity`);
273
+ }
274
+ if (empiricalClaude?.successRate !== null) reasons.push(`claude empirical: ${empiricalClaude.model} ${Math.round(empiricalClaude.successRate * 100)}%`);
275
+ if (empiricalOpenai?.successRate !== null) reasons.push(`openai empirical: ${empiricalOpenai.model} ${Math.round(empiricalOpenai.successRate * 100)}%`);
276
+
277
+ return {
278
+ claude: {
279
+ model: claudeModel,
280
+ effort: claudeEffort,
281
+ modes: claudeModes,
282
+ dispatch: getDispatchConfig(claudeModel),
283
+ },
284
+ openai: {
285
+ model: openaiModel,
286
+ effort: openaiEffort,
287
+ modes: openaiModes,
288
+ dispatch: getDispatchConfig(openaiModel),
289
+ },
290
+ preferred,
291
+ dualBrain,
292
+ reason: reasons.join('; '),
293
+ };
294
+ }
295
+
296
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
297
+
298
+ if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
299
+ const args = process.argv.slice(2);
300
+ const description = args.find(a => !a.startsWith('--')) || '';
301
+ const filesArg = args.find(a => a.startsWith('--files=')) || args[args.indexOf('--files') + 1];
302
+ const budgetArg = args.find(a => a.startsWith('--budget-pressure=')) || args[args.indexOf('--budget-pressure') + 1];
303
+ const failuresArg = args.find(a => a.startsWith('--failures=')) || args[args.indexOf('--failures') + 1];
304
+
305
+ const files = (filesArg && !filesArg.startsWith('--'))
306
+ ? filesArg.replace(/^--files=/, '').split(',').map(f => f.trim())
307
+ : [];
308
+
309
+ const budgetPressure = budgetArg
310
+ ? parseFloat(budgetArg.replace(/^--budget-pressure=/, ''))
311
+ : 0;
312
+
313
+ const priorFailures = failuresArg
314
+ ? parseInt(failuresArg.replace(/^--failures=/, ''), 10)
315
+ : 0;
316
+
317
+ if (!description) {
318
+ console.error('Usage: node hooks/task-classifier.mjs "task description" [--files a,b] [--budget-pressure 0.8] [--failures 1]');
319
+ process.exit(1);
320
+ }
321
+
322
+ const profile = classifyTask(description, { files, priorFailures });
323
+ const selection = selectModelEffort(profile, { budgetPressure });
324
+
325
+ console.log(JSON.stringify({ profile, selection }, null, 2));
326
+ }
327
+
328
+ export { classifyTask, selectModelEffort, INTENTS };