muonroi-cli 1.8.4 → 1.9.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 (562) hide show
  1. package/LICENSE +17 -5
  2. package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
  3. package/dist/packages/agent-harness-core/src/driver.js +46 -0
  4. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  5. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  6. package/dist/packages/agent-harness-core/src/event-tee.d.ts +64 -0
  7. package/dist/packages/agent-harness-core/src/event-tee.js +104 -0
  8. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +25 -0
  9. package/dist/packages/agent-harness-core/src/mcp-server.js +169 -21
  10. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  11. package/dist/packages/agent-harness-core/src/protocol.d.ts +90 -4
  12. package/dist/packages/agent-harness-core/src/protocol.js +15 -0
  13. package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
  14. package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
  15. package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
  16. package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
  17. package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
  18. package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
  19. package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
  20. package/dist/packages/agent-harness-opentui/src/install.js +10 -0
  21. package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
  22. package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
  23. package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
  24. package/dist/src/agent-harness/mock-model.d.ts +38 -0
  25. package/dist/src/agent-harness/mock-model.js +69 -3
  26. package/dist/src/agent-harness/test-spawn.js +31 -0
  27. package/dist/src/chat/chat-keychain.d.ts +7 -12
  28. package/dist/src/chat/chat-keychain.js +19 -86
  29. package/dist/src/cli/config/screen-providers.js +1 -1
  30. package/dist/src/cli/cost-forensics.d.ts +10 -0
  31. package/dist/src/cli/cost-forensics.js +18 -3
  32. package/dist/src/cli/keys-bundle.d.ts +1 -1
  33. package/dist/src/cli/keys-bundle.js +1 -1
  34. package/dist/src/cli/keys.d.ts +10 -47
  35. package/dist/src/cli/keys.js +31 -399
  36. package/dist/src/council/clarifier.d.ts +31 -3
  37. package/dist/src/council/clarifier.js +220 -32
  38. package/dist/src/council/context.js +49 -15
  39. package/dist/src/council/debate-checkpoint.d.ts +129 -0
  40. package/dist/src/council/debate-checkpoint.js +176 -0
  41. package/dist/src/council/debate-planner.js +54 -5
  42. package/dist/src/council/debate-summary.d.ts +25 -0
  43. package/dist/src/council/debate-summary.js +85 -0
  44. package/dist/src/council/debate.d.ts +169 -2
  45. package/dist/src/council/debate.js +1265 -135
  46. package/dist/src/council/index.d.ts +108 -1
  47. package/dist/src/council/index.js +670 -197
  48. package/dist/src/council/leader.d.ts +26 -0
  49. package/dist/src/council/leader.js +150 -9
  50. package/dist/src/council/llm.d.ts +94 -0
  51. package/dist/src/council/llm.js +348 -55
  52. package/dist/src/council/panel-select.d.ts +30 -0
  53. package/dist/src/council/panel-select.js +82 -0
  54. package/dist/src/council/planner.js +40 -0
  55. package/dist/src/council/preflight.d.ts +17 -0
  56. package/dist/src/council/preflight.js +50 -2
  57. package/dist/src/council/prompts.d.ts +39 -4
  58. package/dist/src/council/prompts.js +256 -69
  59. package/dist/src/council/stance-recall.d.ts +42 -0
  60. package/dist/src/council/stance-recall.js +57 -0
  61. package/dist/src/council/strip-think.d.ts +17 -0
  62. package/dist/src/council/strip-think.js +33 -0
  63. package/dist/src/council/types.d.ts +138 -0
  64. package/dist/src/ee/artifact-cache.d.ts +16 -0
  65. package/dist/src/ee/artifact-cache.js +32 -0
  66. package/dist/src/ee/auth.d.ts +20 -0
  67. package/dist/src/ee/auth.js +54 -2
  68. package/dist/src/ee/bridge.d.ts +10 -0
  69. package/dist/src/ee/bridge.js +58 -0
  70. package/dist/src/ee/client.js +109 -21
  71. package/dist/src/ee/ee-onboarding.js +6 -26
  72. package/dist/src/ee/export-transcripts.d.ts +1 -0
  73. package/dist/src/ee/export-transcripts.js +8 -10
  74. package/dist/src/ee/extract-session.js +29 -0
  75. package/dist/src/ee/extract-style.d.ts +58 -0
  76. package/dist/src/ee/extract-style.js +270 -0
  77. package/dist/src/ee/recall-ledger.d.ts +9 -0
  78. package/dist/src/ee/recall-ledger.js +3 -0
  79. package/dist/src/ee/scope.d.ts +1 -0
  80. package/dist/src/ee/scope.js +26 -1
  81. package/dist/src/ee/search.d.ts +7 -0
  82. package/dist/src/ee/search.js +24 -0
  83. package/dist/src/ee/transcript-emit.js +2 -0
  84. package/dist/src/ee/types.d.ts +22 -0
  85. package/dist/src/ee/who-am-i-brain.d.ts +35 -0
  86. package/dist/src/ee/who-am-i-brain.js +220 -0
  87. package/dist/src/ee/who-am-i.d.ts +10 -3
  88. package/dist/src/ee/who-am-i.js +12 -0
  89. package/dist/src/ee/workflow-event.d.ts +48 -0
  90. package/dist/src/ee/workflow-event.js +81 -0
  91. package/dist/src/flow/compaction/compress.d.ts +3 -3
  92. package/dist/src/flow/compaction/compress.js +58 -8
  93. package/dist/src/flow/compaction/extract.d.ts +4 -7
  94. package/dist/src/flow/compaction/extract.js +50 -10
  95. package/dist/src/flow/compaction/index.d.ts +14 -1
  96. package/dist/src/flow/compaction/index.js +96 -3
  97. package/dist/src/flow/compaction/input-guard.d.ts +24 -0
  98. package/dist/src/flow/compaction/input-guard.js +43 -0
  99. package/dist/src/flow/compaction/progress.d.ts +35 -0
  100. package/dist/src/flow/compaction/progress.js +35 -0
  101. package/dist/src/flow/fold-planning.d.ts +36 -0
  102. package/dist/src/flow/fold-planning.js +83 -0
  103. package/dist/src/flow/hierarchy.d.ts +146 -0
  104. package/dist/src/flow/hierarchy.js +427 -0
  105. package/dist/src/flow/index.d.ts +1 -0
  106. package/dist/src/flow/index.js +2 -0
  107. package/dist/src/flow/run-artifacts.d.ts +102 -0
  108. package/dist/src/flow/run-artifacts.js +208 -0
  109. package/dist/src/generated/version.d.ts +1 -1
  110. package/dist/src/generated/version.js +1 -1
  111. package/dist/src/gsd/assessment-schema.d.ts +44 -0
  112. package/dist/src/gsd/assessment-schema.js +134 -0
  113. package/dist/src/gsd/capability-registry.d.ts +45 -0
  114. package/dist/src/gsd/capability-registry.js +337 -0
  115. package/dist/src/gsd/complexity-assessor.d.ts +39 -0
  116. package/dist/src/gsd/complexity-assessor.js +152 -0
  117. package/dist/src/gsd/config-bridge.d.ts +7 -0
  118. package/dist/src/gsd/config-bridge.js +114 -0
  119. package/dist/src/gsd/config-loader.d.ts +27 -0
  120. package/dist/src/gsd/config-loader.js +50 -0
  121. package/dist/src/gsd/council-context.d.ts +44 -0
  122. package/dist/src/gsd/council-context.js +114 -0
  123. package/dist/src/gsd/ee-closure.d.ts +28 -0
  124. package/dist/src/gsd/ee-closure.js +49 -0
  125. package/dist/src/gsd/flags.d.ts +66 -0
  126. package/dist/src/gsd/flags.js +102 -0
  127. package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
  128. package/dist/src/gsd/gsd-dispatch.js +131 -0
  129. package/dist/src/gsd/gsd-runtime.d.ts +22 -0
  130. package/dist/src/gsd/gsd-runtime.js +37 -0
  131. package/dist/src/gsd/host-adapter.d.ts +11 -0
  132. package/dist/src/gsd/host-adapter.js +29 -0
  133. package/dist/src/gsd/index.d.ts +24 -1
  134. package/dist/src/gsd/index.js +27 -0
  135. package/dist/src/gsd/loop-host-contract.d.ts +21 -0
  136. package/dist/src/gsd/loop-host-contract.js +39 -0
  137. package/dist/src/gsd/loop-host.d.ts +69 -0
  138. package/dist/src/gsd/loop-host.js +245 -0
  139. package/dist/src/gsd/loop-resolver.d.ts +36 -0
  140. package/dist/src/gsd/loop-resolver.js +79 -0
  141. package/dist/src/gsd/model-tier.d.ts +13 -0
  142. package/dist/src/gsd/model-tier.js +45 -0
  143. package/dist/src/gsd/mutation-gate.d.ts +16 -0
  144. package/dist/src/gsd/mutation-gate.js +41 -0
  145. package/dist/src/gsd/native-roadmap.d.ts +89 -0
  146. package/dist/src/gsd/native-roadmap.js +343 -0
  147. package/dist/src/gsd/native-state.d.ts +47 -0
  148. package/dist/src/gsd/native-state.js +220 -0
  149. package/dist/src/gsd/paths.d.ts +23 -0
  150. package/dist/src/gsd/paths.js +66 -0
  151. package/dist/src/gsd/phase-dag.d.ts +12 -0
  152. package/dist/src/gsd/phase-dag.js +94 -0
  153. package/dist/src/gsd/phase-sync.d.ts +42 -0
  154. package/dist/src/gsd/phase-sync.js +321 -0
  155. package/dist/src/gsd/pil-gate-context.d.ts +13 -0
  156. package/dist/src/gsd/pil-gate-context.js +64 -0
  157. package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
  158. package/dist/src/gsd/pil-gate-critic.js +74 -0
  159. package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
  160. package/dist/src/gsd/plan-council-prompts.js +79 -0
  161. package/dist/src/gsd/plan-council.d.ts +44 -0
  162. package/dist/src/gsd/plan-council.js +283 -0
  163. package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
  164. package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
  165. package/dist/src/gsd/product-workspace.d.ts +13 -0
  166. package/dist/src/gsd/product-workspace.js +124 -0
  167. package/dist/src/gsd/ship-bridge.d.ts +25 -0
  168. package/dist/src/gsd/ship-bridge.js +65 -0
  169. package/dist/src/gsd/state-document.d.ts +40 -0
  170. package/dist/src/gsd/state-document.js +163 -0
  171. package/dist/src/gsd/verdict-schema.d.ts +39 -0
  172. package/dist/src/gsd/verdict-schema.js +144 -0
  173. package/dist/src/gsd/verify-context.d.ts +22 -0
  174. package/dist/src/gsd/verify-context.js +27 -0
  175. package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
  176. package/dist/src/gsd/verify-council-prompts.js +85 -0
  177. package/dist/src/gsd/verify-council.d.ts +25 -0
  178. package/dist/src/gsd/verify-council.js +119 -0
  179. package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
  180. package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
  181. package/dist/src/gsd/workflow-engine.d.ts +60 -0
  182. package/dist/src/gsd/workflow-engine.js +207 -0
  183. package/dist/src/gsd/workflow-tools.d.ts +13 -0
  184. package/dist/src/gsd/workflow-tools.js +277 -0
  185. package/dist/src/headless/council-answers.js +4 -0
  186. package/dist/src/hooks/index.js +1 -1
  187. package/dist/src/index.js +172 -270
  188. package/dist/src/lsp/builtins.js +3 -1
  189. package/dist/src/lsp/manager.d.ts +5 -1
  190. package/dist/src/lsp/manager.js +249 -3
  191. package/dist/src/lsp/npm-cache.d.ts +11 -1
  192. package/dist/src/lsp/npm-cache.js +17 -1
  193. package/dist/src/lsp/runtime.d.ts +6 -1
  194. package/dist/src/lsp/runtime.js +17 -1
  195. package/dist/src/lsp/types.d.ts +83 -1
  196. package/dist/src/lsp/types.js +10 -0
  197. package/dist/src/maintain/pr-builder.js +23 -13
  198. package/dist/src/mcp/auto-setup.js +57 -32
  199. package/dist/src/mcp/client-pool.js +44 -16
  200. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  201. package/dist/src/mcp/lsp-tools.js +93 -2
  202. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  203. package/dist/src/mcp/mcp-keychain.js +9 -49
  204. package/dist/src/mcp/research-onboarding.js +8 -7
  205. package/dist/src/mcp/runtime.js +34 -2
  206. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  207. package/dist/src/mcp/setup-guide-text.js +22 -2
  208. package/dist/src/mcp/tools-server.d.ts +10 -0
  209. package/dist/src/mcp/tools-server.js +10 -2
  210. package/dist/src/models/catalog-client.d.ts +87 -0
  211. package/dist/src/models/catalog-client.js +105 -38
  212. package/dist/src/models/catalog.json +528 -265
  213. package/dist/src/models/registry.d.ts +22 -7
  214. package/dist/src/models/registry.js +73 -10
  215. package/dist/src/ops/doctor.js +1 -1
  216. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  217. package/dist/src/orchestrator/ask-user.js +65 -0
  218. package/dist/src/orchestrator/auto-commit.js +1 -1
  219. package/dist/src/orchestrator/batch-turn-runner.js +2 -2
  220. package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
  221. package/dist/src/orchestrator/cache-prefix.js +83 -0
  222. package/dist/src/orchestrator/compact-request.d.ts +32 -0
  223. package/dist/src/orchestrator/compact-request.js +41 -0
  224. package/dist/src/orchestrator/compaction.d.ts +12 -3
  225. package/dist/src/orchestrator/compaction.js +35 -15
  226. package/dist/src/orchestrator/council-manager.d.ts +12 -3
  227. package/dist/src/orchestrator/council-manager.js +74 -32
  228. package/dist/src/orchestrator/council-request.d.ts +49 -0
  229. package/dist/src/orchestrator/council-request.js +62 -0
  230. package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
  231. package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
  232. package/dist/src/orchestrator/error-utils.d.ts +29 -0
  233. package/dist/src/orchestrator/error-utils.js +132 -24
  234. package/dist/src/orchestrator/grounding-check.js +39 -1
  235. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  236. package/dist/src/orchestrator/interactive-pause.js +36 -0
  237. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  238. package/dist/src/orchestrator/message-processor.js +268 -41
  239. package/dist/src/orchestrator/orchestrator.d.ts +64 -3
  240. package/dist/src/orchestrator/orchestrator.js +823 -120
  241. package/dist/src/orchestrator/preprocessor.js +3 -3
  242. package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
  243. package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
  244. package/dist/src/orchestrator/prompts.js +17 -17
  245. package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
  246. package/dist/src/orchestrator/reactive-delegation.js +59 -0
  247. package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
  248. package/dist/src/orchestrator/retry-classifier.js +46 -2
  249. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  250. package/dist/src/orchestrator/safety-askcard.js +5 -2
  251. package/dist/src/orchestrator/safety-intercept.d.ts +50 -0
  252. package/dist/src/orchestrator/safety-intercept.js +62 -0
  253. package/dist/src/orchestrator/scope-reminder.js +1 -1
  254. package/dist/src/orchestrator/session-experience.d.ts +2 -1
  255. package/dist/src/orchestrator/session-experience.js +2 -1
  256. package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
  257. package/dist/src/orchestrator/should-run-gate.js +18 -0
  258. package/dist/src/orchestrator/stall-watchdog.d.ts +31 -3
  259. package/dist/src/orchestrator/stall-watchdog.js +65 -10
  260. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  261. package/dist/src/orchestrator/stream-runner.js +115 -49
  262. package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
  263. package/dist/src/orchestrator/sub-agent-cap.js +16 -1
  264. package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
  265. package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
  266. package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
  267. package/dist/src/orchestrator/subagent-compactor.js +126 -15
  268. package/dist/src/orchestrator/tool-engine.d.ts +41 -0
  269. package/dist/src/orchestrator/tool-engine.js +846 -66
  270. package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
  271. package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
  272. package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
  273. package/dist/src/orchestrator/turn-watchdog.d.ts +44 -0
  274. package/dist/src/orchestrator/turn-watchdog.js +84 -0
  275. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  276. package/dist/src/pil/agent-operating-contract.js +6 -4
  277. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  278. package/dist/src/pil/cheap-model-playbook.js +5 -1
  279. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  280. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  281. package/dist/src/pil/discovery-types.d.ts +1 -0
  282. package/dist/src/pil/discovery.d.ts +1 -1
  283. package/dist/src/pil/discovery.js +18 -13
  284. package/dist/src/pil/layer1-intent.d.ts +18 -6
  285. package/dist/src/pil/layer1-intent.js +66 -757
  286. package/dist/src/pil/layer15-context-scan.js +15 -1
  287. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  288. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  289. package/dist/src/pil/layer3-ee-injection.js +23 -8
  290. package/dist/src/pil/layer4-gsd.js +69 -16
  291. package/dist/src/pil/layer5-context.js +7 -3
  292. package/dist/src/pil/layer6-output.d.ts +23 -0
  293. package/dist/src/pil/layer6-output.js +5 -1
  294. package/dist/src/pil/llm-classify.d.ts +111 -5
  295. package/dist/src/pil/llm-classify.js +421 -189
  296. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  297. package/dist/src/pil/native-capabilities-workbook.js +8 -0
  298. package/dist/src/pil/pipeline.js +36 -2
  299. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  300. package/dist/src/pil/repo-grounding-probe.js +136 -0
  301. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  302. package/dist/src/pil/repo-structure-hints.js +45 -0
  303. package/dist/src/pil/response-tools.js +5 -3
  304. package/dist/src/pil/schema.d.ts +1 -0
  305. package/dist/src/pil/schema.js +2 -0
  306. package/dist/src/pil/types.d.ts +18 -0
  307. package/dist/src/playbook/directives.d.ts +4 -0
  308. package/dist/src/playbook/directives.js +17 -5
  309. package/dist/src/product-loop/artifact-io.js +4 -0
  310. package/dist/src/product-loop/backlog-builder.d.ts +14 -1
  311. package/dist/src/product-loop/backlog-builder.js +30 -6
  312. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  313. package/dist/src/product-loop/criteria-seed.js +200 -0
  314. package/dist/src/product-loop/discovery-context-format.js +3 -1
  315. package/dist/src/product-loop/discovery-ecosystem.js +4 -1
  316. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  317. package/dist/src/product-loop/discovery-interview.js +60 -12
  318. package/dist/src/product-loop/discovery-recommender.js +2 -1
  319. package/dist/src/product-loop/discovery-schema.js +19 -2
  320. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  321. package/dist/src/product-loop/discovery-triage.js +109 -0
  322. package/dist/src/product-loop/gather.js +150 -2
  323. package/dist/src/product-loop/ideal-trace.d.ts +7 -0
  324. package/dist/src/product-loop/ideal-trace.js +64 -0
  325. package/dist/src/product-loop/index.d.ts +13 -1
  326. package/dist/src/product-loop/index.js +340 -52
  327. package/dist/src/product-loop/loop-driver.d.ts +7 -0
  328. package/dist/src/product-loop/loop-driver.js +330 -106
  329. package/dist/src/product-loop/phase-plan.d.ts +21 -0
  330. package/dist/src/product-loop/phase-plan.js +81 -6
  331. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  332. package/dist/src/product-loop/phase-rituals.js +8 -3
  333. package/dist/src/product-loop/phase-runner.js +39 -12
  334. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  335. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  336. package/dist/src/product-loop/sprint-runner.d.ts +173 -0
  337. package/dist/src/product-loop/sprint-runner.js +863 -19
  338. package/dist/src/product-loop/types.d.ts +61 -5
  339. package/dist/src/providers/adapter.d.ts +1 -1
  340. package/dist/src/providers/adapter.js +3 -4
  341. package/dist/src/providers/anthropic.d.ts +9 -8
  342. package/dist/src/providers/anthropic.js +13 -47
  343. package/dist/src/providers/auth/browser-flow.d.ts +1 -1
  344. package/dist/src/providers/auth/browser-flow.js +1 -1
  345. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  346. package/dist/src/providers/auth/grok-oauth.js +30 -5
  347. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  348. package/dist/src/providers/auth/openai-oauth.js +15 -1
  349. package/dist/src/providers/auth/registry.js +0 -34
  350. package/dist/src/providers/auth/token-store.d.ts +9 -9
  351. package/dist/src/providers/auth/token-store.js +8 -67
  352. package/dist/src/providers/auth/types.d.ts +9 -1
  353. package/dist/src/providers/auth/types.js +1 -1
  354. package/dist/src/providers/capabilities.d.ts +24 -5
  355. package/dist/src/providers/capabilities.js +42 -24
  356. package/dist/src/providers/endpoints.d.ts +2 -2
  357. package/dist/src/providers/endpoints.js +11 -10
  358. package/dist/src/providers/env-store.d.ts +17 -0
  359. package/dist/src/providers/env-store.js +228 -0
  360. package/dist/src/providers/keychain.d.ts +22 -18
  361. package/dist/src/providers/keychain.js +127 -140
  362. package/dist/src/providers/mcp-vision-bridge.js +56 -146
  363. package/dist/src/providers/openai-compatible.js +8 -1
  364. package/dist/src/providers/pricing.d.ts +2 -2
  365. package/dist/src/providers/pricing.js +3 -13
  366. package/dist/src/providers/runtime.d.ts +43 -3
  367. package/dist/src/providers/runtime.js +88 -14
  368. package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
  369. package/dist/src/providers/strategies/base.strategy.js +24 -1
  370. package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
  371. package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
  372. package/dist/src/providers/strategies/registry.js +4 -4
  373. package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
  374. package/dist/src/providers/strategies/thinking-mode.js +288 -1
  375. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  376. package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
  377. package/dist/src/providers/strategies/zai.strategy.js +44 -0
  378. package/dist/src/providers/types.d.ts +5 -6
  379. package/dist/src/providers/types.js +2 -2
  380. package/dist/src/providers/vision-backend.d.ts +47 -0
  381. package/dist/src/providers/vision-backend.js +258 -0
  382. package/dist/src/providers/vision-proxy.d.ts +22 -9
  383. package/dist/src/providers/vision-proxy.js +63 -132
  384. package/dist/src/providers/warm.d.ts +65 -0
  385. package/dist/src/providers/warm.js +145 -0
  386. package/dist/src/providers/wire-debug.js +95 -0
  387. package/dist/src/router/decide.d.ts +13 -0
  388. package/dist/src/router/decide.js +138 -36
  389. package/dist/src/router/peak-hour.d.ts +38 -0
  390. package/dist/src/router/peak-hour.js +107 -0
  391. package/dist/src/router/step-router.js +3 -2
  392. package/dist/src/router/warm.js +4 -5
  393. package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
  394. package/dist/src/scaffold/continuation-prompt.js +26 -0
  395. package/dist/src/scaffold/point-to-existing.d.ts +21 -0
  396. package/dist/src/scaffold/point-to-existing.js +25 -0
  397. package/dist/src/self-qa/agentic-loop.js +6 -5
  398. package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
  399. package/dist/src/{ui/state → state}/active-run.js +21 -0
  400. package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
  401. package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
  402. package/dist/src/state/turn-trace.d.ts +43 -0
  403. package/dist/src/state/turn-trace.js +32 -0
  404. package/dist/src/storage/db.js +2 -1
  405. package/dist/src/storage/index.d.ts +1 -1
  406. package/dist/src/storage/index.js +1 -1
  407. package/dist/src/storage/interaction-log.d.ts +1 -1
  408. package/dist/src/storage/migrations.js +71 -1
  409. package/dist/src/storage/sessions.d.ts +28 -10
  410. package/dist/src/storage/sessions.js +78 -21
  411. package/dist/src/storage/transcript-view.js +1 -1
  412. package/dist/src/storage/transcript.d.ts +51 -0
  413. package/dist/src/storage/transcript.js +340 -15
  414. package/dist/src/tools/file.d.ts +15 -0
  415. package/dist/src/tools/file.js +32 -0
  416. package/dist/src/tools/git-safety.d.ts +19 -0
  417. package/dist/src/tools/git-safety.js +168 -0
  418. package/dist/src/tools/native-tools.d.ts +1 -1
  419. package/dist/src/tools/native-tools.js +81 -1
  420. package/dist/src/tools/registry.d.ts +20 -0
  421. package/dist/src/tools/registry.js +576 -23
  422. package/dist/src/tools/research.d.ts +29 -0
  423. package/dist/src/tools/research.js +233 -0
  424. package/dist/src/types/index.d.ts +147 -4
  425. package/dist/src/ui/app.js +0 -0
  426. package/dist/src/ui/cards/product-status-card.js +1 -1
  427. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  428. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  429. package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
  430. package/dist/src/ui/components/bubble-body-guard.js +50 -0
  431. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  432. package/dist/src/ui/components/compact-progress-card.js +42 -0
  433. package/dist/src/ui/components/context-rail.d.ts +26 -0
  434. package/dist/src/ui/components/context-rail.js +33 -0
  435. package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
  436. package/dist/src/ui/components/council-conclusion-card.js +420 -0
  437. package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
  438. package/dist/src/ui/components/council-debate-pill.js +34 -0
  439. package/dist/src/ui/components/council-info-card.js +2 -2
  440. package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
  441. package/dist/src/ui/components/council-leader-bubble.js +21 -11
  442. package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
  443. package/dist/src/ui/components/council-message-bubble.js +16 -15
  444. package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
  445. package/dist/src/ui/components/council-phase-timeline.js +66 -17
  446. package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
  447. package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
  448. package/dist/src/ui/components/council-question-card.js +13 -12
  449. package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
  450. package/dist/src/ui/components/council-rail-rounds.js +57 -0
  451. package/dist/src/ui/components/council-round-group.d.ts +38 -0
  452. package/dist/src/ui/components/council-round-group.js +88 -0
  453. package/dist/src/ui/components/council-status-list.d.ts +3 -1
  454. package/dist/src/ui/components/council-status-list.js +36 -24
  455. package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
  456. package/dist/src/ui/components/council-synthesis-banner.js +20 -5
  457. package/dist/src/ui/components/halt-recovery-card.js +9 -5
  458. package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
  459. package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
  460. package/dist/src/ui/components/message-view.d.ts +15 -0
  461. package/dist/src/ui/components/message-view.js +50 -1
  462. package/dist/src/ui/components/prompt-box.js +18 -16
  463. package/dist/src/ui/components/session-tree-card.d.ts +14 -0
  464. package/dist/src/ui/components/session-tree-card.js +46 -0
  465. package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
  466. package/dist/src/ui/components/slash-inline-menu.js +26 -5
  467. package/dist/src/ui/components/task-list-panel.d.ts +14 -1
  468. package/dist/src/ui/components/task-list-panel.js +22 -2
  469. package/dist/src/ui/components/tool-group.d.ts +15 -3
  470. package/dist/src/ui/components/tool-group.js +69 -11
  471. package/dist/src/ui/containers/modals-layer.d.ts +4 -2
  472. package/dist/src/ui/containers/modals-layer.js +2 -2
  473. package/dist/src/ui/council-harness-event.d.ts +57 -0
  474. package/dist/src/ui/council-harness-event.js +46 -0
  475. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  476. package/dist/src/ui/heartbeat-debug.js +45 -0
  477. package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
  478. package/dist/src/ui/mcp-modal.js +2 -4
  479. package/dist/src/ui/modals/api-key-modal.js +1 -1
  480. package/dist/src/ui/modals/connect-modal.js +4 -3
  481. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  482. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  483. package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
  484. package/dist/src/ui/modals/session-picker-modal.js +3 -5
  485. package/dist/src/ui/picker-providers.d.ts +1 -1
  486. package/dist/src/ui/picker-providers.js +1 -1
  487. package/dist/src/ui/primitives/index.d.ts +1 -0
  488. package/dist/src/ui/primitives/index.js +2 -0
  489. package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
  490. package/dist/src/ui/primitives/semantic-primitives.js +81 -0
  491. package/dist/src/ui/slash/compact.js +5 -7
  492. package/dist/src/ui/slash/cost.js +1 -1
  493. package/dist/src/ui/slash/council.js +19 -1
  494. package/dist/src/ui/slash/debug.d.ts +3 -31
  495. package/dist/src/ui/slash/debug.js +9 -20
  496. package/dist/src/ui/slash/ee.js +81 -0
  497. package/dist/src/ui/slash/ideal.d.ts +6 -2
  498. package/dist/src/ui/slash/ideal.js +97 -7
  499. package/dist/src/ui/slash/menu-items.d.ts +7 -0
  500. package/dist/src/ui/slash/menu-items.js +23 -20
  501. package/dist/src/ui/slash/registry.d.ts +2 -0
  502. package/dist/src/ui/slash/registry.js +4 -0
  503. package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
  504. package/dist/src/ui/status-bar/cache-hit.js +9 -0
  505. package/dist/src/ui/status-bar/index.d.ts +1 -1
  506. package/dist/src/ui/status-bar/index.js +7 -3
  507. package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
  508. package/dist/src/ui/status-bar/usd-meter.js +6 -4
  509. package/dist/src/ui/theme.d.ts +1 -0
  510. package/dist/src/ui/theme.js +2 -0
  511. package/dist/src/ui/types.d.ts +7 -0
  512. package/dist/src/ui/use-app-logic.js +0 -0
  513. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  514. package/dist/src/ui/utils/agent-activities.js +96 -0
  515. package/dist/src/ui/utils/format.d.ts +14 -0
  516. package/dist/src/ui/utils/format.js +23 -3
  517. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  518. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  519. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  520. package/dist/src/ui/utils/tool-summary.js +91 -0
  521. package/dist/src/usage/downgrade.js +2 -2
  522. package/dist/src/usage/product-ledger.js +2 -2
  523. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  524. package/dist/src/utils/event-loop-monitor.js +107 -0
  525. package/dist/src/utils/install-manager.js +2 -1
  526. package/dist/src/utils/llm-deadline.d.ts +14 -0
  527. package/dist/src/utils/llm-deadline.js +19 -0
  528. package/dist/src/utils/logger.js +2 -2
  529. package/dist/src/utils/loop-profiler.d.ts +102 -0
  530. package/dist/src/utils/loop-profiler.js +202 -0
  531. package/dist/src/utils/permission-mode.js +5 -3
  532. package/dist/src/utils/redactor.js +1 -1
  533. package/dist/src/utils/settings.d.ts +180 -5
  534. package/dist/src/utils/settings.js +271 -31
  535. package/dist/src/utils/side-question.d.ts +1 -2
  536. package/dist/src/utils/side-question.js +2 -2
  537. package/dist/src/utils/visible-retry.d.ts +11 -0
  538. package/dist/src/utils/visible-retry.js +10 -1
  539. package/dist/src/verify/entrypoint.d.ts +1 -1
  540. package/dist/src/verify/entrypoint.js +52 -17
  541. package/dist/src/verify/orchestrator.d.ts +1 -1
  542. package/dist/src/verify/orchestrator.js +20 -3
  543. package/dist/src/verify/recipes.d.ts +13 -0
  544. package/dist/src/verify/recipes.js +15 -0
  545. package/package.json +134 -132
  546. package/dist/src/cli/bw-vault.d.ts +0 -55
  547. package/dist/src/cli/bw-vault.js +0 -133
  548. package/dist/src/mcp/ee-tools.d.ts +0 -46
  549. package/dist/src/mcp/ee-tools.js +0 -193
  550. package/dist/src/providers/auth/gcloud.d.ts +0 -28
  551. package/dist/src/providers/auth/gcloud.js +0 -102
  552. package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
  553. package/dist/src/providers/auth/gemini-oauth.js +0 -472
  554. package/dist/src/providers/gemini.d.ts +0 -11
  555. package/dist/src/providers/gemini.js +0 -45
  556. package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
  557. package/dist/src/providers/siliconflow-sse-repair.js +0 -177
  558. package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
  559. package/dist/src/providers/strategies/google.strategy.js +0 -174
  560. package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
  561. package/dist/src/ui/containers/chat-feed.d.ts +0 -40
  562. package/dist/src/ui/containers/chat-feed.js +0 -66
@@ -12,25 +12,140 @@
12
12
  * Cost target: <200 input tokens, <10 output tokens per call (~$0.0001 on
13
13
  * DeepSeek Flash). Timeout 2500ms — bails fast if the model stalls.
14
14
  */
15
+ import { appendFileSync } from "node:fs";
15
16
  import { streamText } from "ai";
16
- import { getModelByTier, getModelInfo } from "../models/registry.js";
17
+ import { getModelInfo, SWITCH_PROVIDER_ORDER } from "../models/registry.js";
17
18
  import { getProviderCapabilities } from "../providers/capabilities.js";
18
- import { resolveModelRuntime } from "../providers/runtime.js";
19
- const LLM_CLASSIFY_TIMEOUT_MS = 2500;
20
- // Reasoning models (grok-4.3, deepseek-v4-flash, gpt-5.x) spend their output
21
- // budget on reasoning tokens BEFORE any visible text. The legacy 16-token cap
22
- // was consumed entirely by reasoning zero text-delta → parseResponse("") →
23
- // null `llm=fail` on every borderline turn (observed 5/5 live grok sessions).
24
- // Give reasoning models a real ceiling so the 2-word answer streams back, and a
25
- // longer timeout because reasoning round-trips take seconds, not ~200ms.
26
- // The ceiling is a cap, not padding: the model still stops after two words, so a
27
- // generous headroom costs nothing when reasoning is short.
28
- const REASONING_CLASSIFY_TIMEOUT_MS = 8000;
29
- // Seven comma-separated words now (added <scope>,<lang>) ~18-26 tokens worst
30
- // case ("documentation,balanced,task,report,standard,ecosystem,vietnamese").
31
- // 48 keeps headroom without padding (the model still stops after seven words).
32
- const NONREASONING_MAX_OUTPUT_TOKENS = 48;
19
+ import { getConfiguredProviders, loadKeyForProvider, ProviderKeyMissingError } from "../providers/keychain.js";
20
+ import { createProviderFactoryAsync, resolveModelRuntime } from "../providers/runtime.js";
21
+ import { getRoutedModelByTier } from "../router/peak-hour.js";
22
+ import { isProviderDisabled } from "../utils/settings.js";
23
+ // Single flat classify ceiling for EVERY model. Harness-measured latency
24
+ // (2026-07-15, grok-composer via OAuth, 7 samples) is 1045–1277ms no tested
25
+ // model (agentic balanced OR fast flash) approaches even the legacy 2.5s cap,
26
+ // so a tier-scaled timeout was solving a non-existent latency problem: the
27
+ // /ideal over-engineering root cause was NOT a timeout abort but grok-composer
28
+ // ignoring the terse contract (it emits task-planning prose, never the 8-word
29
+ // line null → fail-open). The ceiling is a pure safety net: a healthy model
30
+ // returns in <1.5s, so the headroom only bites a genuinely stuck call. Data-
31
+ // driven off measurement, not an identity/tier proxy string.
32
+ const CLASSIFY_TIMEOUT_MS = 8000;
33
+ // Absolute wall-clock cap for the WHOLE classify (every candidate AND its
34
+ // self-repair). Enforced via a shared deadline passed into attemptClassify, so
35
+ // a run of dead/hanging keys — or one legitimately slow reasoning model —
36
+ // degrades to fail-open in bounded time. Sized to fit a healthy reasoning fast
37
+ // model twice over: deepseek-v4-flash measured 2026-07-15 answers a classify in
38
+ // ~2–5s (with a valid key), so 10s leaves room for one candidate + a self-repair
39
+ // before the deadline. A dead key ahead of it (auth-fails in ~0.2s) barely eats
40
+ // the budget; only a genuinely hanging candidate consumes it.
41
+ const CLASSIFY_TOTAL_BUDGET_MS = 10_000;
42
+ // Floor for any single streamText attempt's own timeout, so a nearly-exhausted
43
+ // deadline still gives a final candidate a real (if short) chance rather than an
44
+ // instant abort.
45
+ const CLASSIFY_MIN_ATTEMPT_MS = 1200;
46
+ // Eight comma-separated words now (added <clarity>) — ~20-30 tokens worst case
47
+ // ("documentation,balanced,task,report,standard,ecosystem,vietnamese,underspecified").
48
+ // 56 keeps headroom without padding (the model still stops after eight words).
49
+ const NONREASONING_MAX_OUTPUT_TOKENS = 56;
33
50
  const REASONING_MAX_OUTPUT_TOKENS = 2048;
51
+ /**
52
+ * Compute the classify call budget from the resolved model's catalog metadata.
53
+ *
54
+ * TIMEOUT is a flat safety-net ceiling for all models — see CLASSIFY_TIMEOUT_MS.
55
+ * Measurement showed classify latency is provider-independent and well under
56
+ * the ceiling, so keying the timeout off `tier`/`reasoning` added no value and
57
+ * amounted to an identity-proxy soft-hardcode.
58
+ *
59
+ * MAX-OUTPUT scales with REASONING only: a reasoning model burns its output
60
+ * budget on reasoning tokens before any visible text, so it needs the room in
61
+ * tokens; a non-reasoning model emits the 8 words directly. This knob is real —
62
+ * it is the fix for reasoning models routing the verdict into the reasoning
63
+ * channel — and stays decoupled from the (now flat) timeout.
64
+ */
65
+ export function classifierBudget(modelInfo) {
66
+ const isReasoning = modelInfo?.reasoning === true;
67
+ return {
68
+ isReasoning,
69
+ timeoutMs: CLASSIFY_TIMEOUT_MS,
70
+ maxOutputTokens: isReasoning ? REASONING_MAX_OUTPUT_TOKENS : NONREASONING_MAX_OUTPUT_TOKENS,
71
+ };
72
+ }
73
+ /**
74
+ * Session-scoped cache of cross-provider factories built for the throwaway
75
+ * classify. Keyed by provider so the OAuth-aware build (keychain read + token
76
+ * refresh) happens at most once per provider per process, not per turn.
77
+ */
78
+ const crossFactoryCache = new Map();
79
+ /** Test seam — clear the cross-provider factory cache between specs. */
80
+ export function __resetClassifyFactoryCache() {
81
+ crossFactoryCache.clear();
82
+ }
83
+ /**
84
+ * Build (or reuse) a real factory for a DIFFERENT provider than the session's,
85
+ * so the throwaway classify can run on a keyed instruction-following model when
86
+ * the session provider has none. Mirrors the council's `resolveCouncilFactory`:
87
+ * `loadKeyForProvider` for API-key providers, falling back to
88
+ * `createProviderFactoryAsync` for OAuth-only providers (injects the bearer
89
+ * token). Failures degrade gracefully (logged, returns undefined → caller keeps
90
+ * the session model). Never throws.
91
+ */
92
+ async function resolveCrossProviderClassifyFactory(providerId) {
93
+ const cached = crossFactoryCache.get(providerId);
94
+ if (cached)
95
+ return cached;
96
+ try {
97
+ let apiKey;
98
+ try {
99
+ apiKey = await loadKeyForProvider(providerId);
100
+ }
101
+ catch (err) {
102
+ if (!(err instanceof ProviderKeyMissingError))
103
+ throw err;
104
+ // OAuth-only provider — createProviderFactoryAsync injects the bearer token.
105
+ }
106
+ const { factory } = await createProviderFactoryAsync(providerId, apiKey ? { apiKey } : {});
107
+ crossFactoryCache.set(providerId, factory);
108
+ return factory;
109
+ }
110
+ catch (err) {
111
+ console.error(`[pil.llm-classify] cross-provider classify factory build failed for ${providerId}: ${err?.message}`);
112
+ return undefined;
113
+ }
114
+ }
115
+ /**
116
+ * Ordered list of keyed fast-tier models from providers OTHER than the session's,
117
+ * for the throwaway classify. Candidate order is the catalog's vendor-defined
118
+ * `switch_provider_order` (zero-hardcode), gated by `getConfiguredProviders`
119
+ * (the authoritative credential check — unifies API key / env / OAuth) and the
120
+ * user's disabled-provider setting. The caller tries them in order and falls
121
+ * through to the next on an auth/stream failure — so a configured-but-DEAD key
122
+ * (e.g. an expired deepseek key) doesn't strand the classify at fail-open.
123
+ * Empty when no other provider is configured with a fast tier → caller keeps the
124
+ * session model (status quo).
125
+ */
126
+ async function pickCrossProviderClassifyModels(excludeProvider) {
127
+ let configured;
128
+ try {
129
+ configured = new Set(await getConfiguredProviders());
130
+ }
131
+ catch (err) {
132
+ console.error(`[pil.llm-classify] getConfiguredProviders failed for classify route: ${err?.message}`);
133
+ return [];
134
+ }
135
+ const out = [];
136
+ for (const p of SWITCH_PROVIDER_ORDER) {
137
+ if (p === excludeProvider)
138
+ continue;
139
+ if (isProviderDisabled(p))
140
+ continue;
141
+ if (!configured.has(p))
142
+ continue;
143
+ const m = getRoutedModelByTier("fast", p);
144
+ if (m && m.provider === p)
145
+ out.push({ modelId: m.id, providerId: p });
146
+ }
147
+ return out;
148
+ }
34
149
  /**
35
150
  * Per-namespace shallow merge of providerOptions. The base already carries
36
151
  * factory-level defaults folded into the provider namespace (e.g. OAuth
@@ -85,8 +200,11 @@ const KNOWN_CLASSIFY_WORDS = new Set([
85
200
  "heavy",
86
201
  "ecosystem",
87
202
  "local",
203
+ "clear",
204
+ "underspecified",
88
205
  ]);
89
- const SYSTEM_PROMPT = "You classify user prompts for a coding assistant. Reply with ONE line of SEVEN lowercase words separated by commas: <taskType>,<style>,<intent>,<deliverable>,<depth>,<scope>,<lang>\n\n" +
206
+ const SYSTEM_PROMPT = "You classify user prompts for a coding assistant. Reply with ONE line of EIGHT lowercase words separated by commas: <taskType>,<style>,<intent>,<deliverable>,<depth>,<scope>,<lang>,<clarity>\n\n" +
207
+ "The message may be preceded by a '[RECENT CONVERSATION]' block. Use it ONLY to resolve what a terse follow-up refers to (e.g. 'từ các phần đó', 'làm tiếp', 'debate mode đi', 'this one'); then classify the NEW message. Crucially, if the new message points back at heavy prior work, its depth is the depth of THAT work — a short sentence like 'ok debate these parts and plan improvements' is NOT quick just because it is short. Never classify the conversation block itself.\n\n" +
90
208
  "taskType ∈ { refactor | debug | plan | analyze | documentation | generate | general }\n" +
91
209
  "style ∈ { concise | balanced | detailed }\n" +
92
210
  "intent ∈ { task | chat } — 'chat' ONLY for a pure greeting, thanks, or acknowledgement with NO work request (e.g. 'hi', 'cảm ơn nhé', 'ok great'). EVERYTHING else is 'task', including questions about code or the CLI, 'are you done?', and requests to call a tool. When unsure, choose 'task'.\n" +
@@ -99,8 +217,12 @@ const SYSTEM_PROMPT = "You classify user prompts for a coding assistant. Reply w
99
217
  "- quick — a trivial single-shot change or a small direct answer: typo, rename one symbol, one-line edit, a quick lookup, 'what does X do'. No plan needed.\n" +
100
218
  "- standard — ordinary feature or bugfix touching a handful of files/functions; needs a short plan + a verify step, but no upfront research or user discussion.\n" +
101
219
  "- heavy — architectural, cross-cutting, multi-file/multi-module, a migration, 'redo/rebuild', a vague 'make it better', or a request with real unresolved design choices. Needs discussion + research + a checked plan before any code.\n" +
220
+ " BREADTH decides heavy, NOT how clearly the steps are spelled out. A migration, vendoring an external dependency's code in-tree, or a rename/restructure that spans MANY files or modules is ALWAYS heavy — even when the plan is fully specified and it 'just' has to keep tests green. Do not downgrade a wide change to standard because it sounds mechanical.\n" +
102
221
  " For a pure question/answer (deliverable=answer), depth reflects how much investigation the answer needs: 'quick' for a simple fact, 'standard' for a normal explanation, 'heavy' for a deep architectural review.\n" +
103
222
  " When unsure between quick and standard, choose standard. When the task is genuinely wide or ambiguous, choose heavy.\n" +
223
+ "clarity ∈ { clear | underspecified } — whether the request gives enough to proceed WITHOUT guessing:\n" +
224
+ "- underspecified — missing information the agent would need: an unstated target/scope ('add auth' — which flow?), a vague 'make it better' with no direction, competing interpretations, or an unresolved design choice. Such a task should be clarified with the user before code.\n" +
225
+ "- clear — well-specified enough to plan and execute directly, even if large. A fully-spelled-out migration is 'clear'. When unsure, choose 'clear' (do NOT over-ask on ordinary work).\n" +
104
226
  "scope ∈ { ecosystem | local }:\n" +
105
227
  "- ecosystem — the turn is about the Muonroi PLATFORM as a whole: the building-block / .NET packages, open-core boundary, the rule engine / decision tables, NuGet packages, or platform setup/install. These are documented in an authoritative docs source.\n" +
106
228
  "- local — EVERYTHING else, including questions about this CLI's own internals (even when they mention the word 'muonroi'). When unsure, choose local.\n" +
@@ -129,21 +251,29 @@ const SYSTEM_PROMPT = "You classify user prompts for a coding assistant. Reply w
129
251
  "- documentation → balanced (examples + explanation)\n" +
130
252
  "- general → concise\n" +
131
253
  "Only output 'detailed' if the user prompt LITERALLY contains words like 'explain in detail', 'thorough analysis', 'walk me through', 'giải thích chi tiết', 'phân tích kỹ'.\n\n" +
132
- "Full examples (taskType,style,intent,deliverable,depth,scope,lang):\n" +
133
- "- 'hi' → general,concise,chat,answer,quick,local,english\n" +
134
- "- 'cảm ơn bạn nhé' → general,concise,chat,answer,quick,local,vietnamese\n" +
135
- "- 'bạn xong chưa' → general,concise,task,answer,quick,local,vietnamese (a question — NOT chat)\n" +
136
- "- 'fix the typo in the README title' → generate,concise,task,code,quick,local,english\n" +
137
- "- 'fix CI failing on Windows' → debug,concise,task,code,standard,local,english\n" +
138
- "- 'rename function shouldInject to needsReminder' → refactor,concise,task,code,quick,local,english\n" +
139
- "- 'thêm caching cho provider layer và update tests' → generate,concise,task,code,standard,local,vietnamese\n" +
140
- "- 'tại sao bash_output_get trả empty' → analyze,concise,task,answer,standard,local,vietnamese\n" +
141
- "- 'liệt kê tất cả env var CLI đọc' → analyze,concise,task,report,standard,local,vietnamese\n" +
142
- "- 'refactor the entire auth system to use OAuth' → refactor,concise,task,code,heavy,local,english\n" +
143
- "- 'how does the building-block rule engine work' → analyze,concise,task,answer,standard,ecosystem,english\n" +
144
- "- 'hệ sinh thái muonroi gồm những gì' → analyze,balanced,task,answer,standard,ecosystem,vietnamese\n" +
145
- "- 'plan the migration to hooks' → plan,balanced,task,report,heavy,local,english\n\n" +
146
- "Prompts may be Vietnamese, English, or mixed. Reply with exactly seven words separated by commas. No other text.";
254
+ "Full examples (taskType,style,intent,deliverable,depth,scope,lang,clarity):\n" +
255
+ "- 'hi' → general,concise,chat,answer,quick,local,english,clear\n" +
256
+ "- 'cảm ơn bạn nhé' → general,concise,chat,answer,quick,local,vietnamese,clear\n" +
257
+ "- 'bạn xong chưa' → general,concise,task,answer,quick,local,vietnamese,clear (a question — NOT chat)\n" +
258
+ "- 'fix the typo in the README title' → generate,concise,task,code,quick,local,english,clear\n" +
259
+ "- 'fix CI failing on Windows' → debug,concise,task,code,standard,local,english,clear\n" +
260
+ "- 'rename function shouldInject to needsReminder' → refactor,concise,task,code,quick,local,english,clear\n" +
261
+ "- 'thêm caching cho provider layer và update tests' → generate,concise,task,code,standard,local,vietnamese,clear\n" +
262
+ "- 'tại sao bash_output_get trả empty' → analyze,concise,task,answer,standard,local,vietnamese,clear\n" +
263
+ "- 'liệt kê tất cả env var CLI đọc' → analyze,concise,task,report,standard,local,vietnamese,clear\n" +
264
+ "- 'refactor the entire auth system to use OAuth' → refactor,concise,task,code,heavy,local,english,clear\n" +
265
+ "- 'vendor the used subset of the gsd package natively into src and rename gsd to workflow, keep tests green' → refactor,concise,task,code,heavy,local,english,clear (a migration spanning many files — heavy even though fully specified)\n" +
266
+ "- 'add auth' → generate,concise,task,code,standard,local,english,underspecified (which flow/provider? unstated)\n" +
267
+ "- 'làm cho CLI tốt hơn' → generate,concise,task,code,heavy,local,vietnamese,underspecified (vague 'make it better', no target)\n" +
268
+ "- 'how does the building-block rule engine work' analyze,concise,task,answer,standard,ecosystem,english,clear\n" +
269
+ "- 'hệ sinh thái muonroi gồm những gì' → analyze,balanced,task,answer,standard,ecosystem,vietnamese,clear\n" +
270
+ "- 'plan the migration to hooks' → plan,balanced,task,report,heavy,local,english,clear\n\n" +
271
+ "Prompts may be Vietnamese, English, or mixed. Reply with exactly eight words separated by commas. No other text.";
272
+ // Appended to SYSTEM_PROMPT on the self-repair retry (see createLlmClassifier).
273
+ // The first attempt produced an unparseable reply; this reminder + the full
274
+ // (untrimmed) prompt is the agent-first recovery the design mandates INSTEAD of
275
+ // a keyword-regex fallback.
276
+ const CLASSIFY_REPAIR_INSTRUCTION = "REPAIR MODE: your previous reply could NOT be parsed. Output NOTHING except the single line of eight lowercase words separated by commas — no prose, no explanation, no code fences, no quotes. If you are unsure of a field, pick the safe default (task, standard, clear, local).";
147
277
  function parseResponse(raw) {
148
278
  const cleaned = raw.trim().toLowerCase().replace(/[`*"]/g, "");
149
279
  const firstLine = cleaned.split(/\r?\n/)[0] ?? "";
@@ -177,6 +307,12 @@ function parseResponse(raw) {
177
307
  // anything else (incl. absent) → not ecosystem. Position-independent.
178
308
  const scopeWord = parts.find((p) => p === "ecosystem" || p === "local");
179
309
  const ecosystemScope = scopeWord ? scopeWord === "ecosystem" : null;
310
+ // Eighth word is the clarity signal. "underspecified" → the request is missing
311
+ // information the agent needs → earn a clarify/council pass. Anything else
312
+ // (incl. absent) → not underspecified (don't-over-ask safe direction).
313
+ // Position-independent so a reordered/garbled reply still recovers it.
314
+ const clarityWord = parts.find((p) => p === "clear" || p === "underspecified");
315
+ const needsClarification = clarityWord ? clarityWord === "underspecified" : null;
180
316
  // Seventh word is the user's language. It is the one alphabetic token that is
181
317
  // NOT a known enum value (open vocabulary). null when English / absent so
182
318
  // Layer 4 skips the language re-anchor for English turns.
@@ -191,6 +327,7 @@ function parseResponse(raw) {
191
327
  intentKind,
192
328
  deliverableKind,
193
329
  depthTier,
330
+ needsClarification,
194
331
  ecosystemScope,
195
332
  replyLanguage,
196
333
  };
@@ -202,26 +339,64 @@ function parseResponse(raw) {
202
339
  * Returns null if the call fails / times out / parses to garbage. Callers must
203
340
  * fail-open (keep prior taskType, do not block the turn).
204
341
  */
205
- export function createLlmClassifier(factory, modelId) {
206
- return async function classify(prompt, signal) {
207
- const controller = new AbortController();
208
- let timer;
209
- try {
210
- const runtime = resolveModelRuntime(factory, modelId);
211
- const isReasoning = runtime.modelInfo?.reasoning === true;
212
- // Budget + timeout scale with reasoning: a reasoning model needs room and
213
- // time to emit reasoning THEN the answer; a plain model answers in <16
214
- // tokens almost instantly.
215
- timer = setTimeout(() => controller.abort(), isReasoning ? REASONING_CLASSIFY_TIMEOUT_MS : LLM_CLASSIFY_TIMEOUT_MS);
216
- const combinedSignal = signal
217
- ? (AbortSignal.any?.([signal, controller.signal]) ?? controller.signal)
218
- : controller.signal;
342
+ /**
343
+ * Order the classify candidate list.
344
+ *
345
+ * - `hasSameFast` (e.g. openai → gpt-5.4-mini): the same-provider fast tier is a
346
+ * real fast model, so try it FIRST, then keyed cross-provider models as a
347
+ * FALLBACK. This is the fix for the failure-not-just-absence case: an
348
+ * openai-OAuth session's fast tier can 401 on the api-key path — previously
349
+ * the chain stopped there and fail-opened to "standard" (so /ideal
350
+ * over-councilled trivial tasks). Appending the cross-provider models lets a
351
+ * working alternative (a keyed deepseek/opencode fast model) rescue it.
352
+ * - No same-provider fast tier (e.g. xai, whose session model is agentic and
353
+ * ignores the terse contract): try keyed cross-provider fast models FIRST and
354
+ * keep the session model as the last-resort candidate.
355
+ */
356
+ export function orderClassifyCandidates(args) {
357
+ const candidates = [];
358
+ if (args.hasSameFast) {
359
+ candidates.push({ modelId: args.primaryModelId, providerId: null });
360
+ for (const m of args.crossModels)
361
+ candidates.push({ modelId: m.modelId, providerId: m.providerId });
362
+ }
363
+ else {
364
+ for (const m of args.crossModels)
365
+ candidates.push({ modelId: m.modelId, providerId: m.providerId });
366
+ candidates.push({ modelId: args.primaryModelId, providerId: null });
367
+ }
368
+ return candidates;
369
+ }
370
+ export function createLlmClassifier(modelId, classifyOpts) {
371
+ return async function classify(prompt, opts) {
372
+ const signal = opts?.signal;
373
+ const recentTurns = opts?.recentTurns;
374
+ const debug = process.env.MUONROI_DEBUG_PIL_CLASSIFY === "1";
375
+ // Bounded recent-conversation block so the classifier can resolve back-
376
+ // references in a terse follow-up ("từ các phần đó", "làm tiếp", "this")
377
+ // instead of scoring the isolated sentence. Same for every candidate.
378
+ const trimmedRecent = recentTurns?.trim();
379
+ const promptWithContext = trimmedRecent
380
+ ? `[RECENT CONVERSATION — reference only, do NOT classify this]\n${trimmedRecent.slice(0, 800)}\n\n` +
381
+ `[NEW USER MESSAGE — classify THIS; if it refers back to the conversation above, judge the depth of the work it actually entails]\n${prompt.slice(0, 600)}`
382
+ : prompt.slice(0, 600);
383
+ const fullRecent = recentTurns?.trim();
384
+ const repairPrompt = (fullRecent
385
+ ? `[RECENT CONVERSATION — reference only, do NOT classify this]\n${fullRecent.slice(0, 1500)}\n\n`
386
+ : "") + `[NEW USER MESSAGE — classify THIS]\n${prompt.slice(0, 1500)}`;
387
+ // One classify attempt against a single resolved model, including the
388
+ // self-repair retry on that same model. Returns the parsed verdict, or null
389
+ // (unparseable OR provider/stream error) so the caller can fall through to
390
+ // the next candidate. Each attempt owns its AbortController/timer.
391
+ const attemptClassify = async (runtime, cmId, deadlineMs) => {
392
+ // Max-output scales with reasoning; each streamText timeout is bounded by
393
+ // the shared deadline (min of the flat ceiling and the time left), so the
394
+ // whole classify — this attempt AND its repair — never overruns the budget.
395
+ const { isReasoning } = classifierBudget(runtime.modelInfo);
396
+ const timeoutMs = Math.max(CLASSIFY_MIN_ATTEMPT_MS, Math.min(CLASSIFY_TIMEOUT_MS, deadlineMs - Date.now()));
219
397
  const dropMaxTokens = runtime.unsupportedParams?.includes("maxOutputTokens") === true;
220
398
  const maxOut = isReasoning ? REASONING_MAX_OUTPUT_TOKENS : NONREASONING_MAX_OUTPUT_TOKENS;
221
- // Minimize reasoning cost: force the lowest effort the provider exposes for
222
- // this throwaway 2-word classification. Only providers with
223
- // `supportsReasoningEffort` (openai, xai) honor it; deepseek has no per-call
224
- // knob (disable via MUONROI_DEEPSEEK_DISABLE_THINKING at the factory).
399
+ // Minimize reasoning cost: force the lowest effort the provider exposes.
225
400
  let providerOptions = runtime.providerOptions;
226
401
  if (isReasoning && runtime.modelInfo?.supportsReasoningEffort && runtime.modelInfo.provider) {
227
402
  const lowEffort = getProviderCapabilities(runtime.modelInfo.provider).buildProviderOptions({
@@ -230,34 +405,186 @@ export function createLlmClassifier(factory, modelId) {
230
405
  });
231
406
  providerOptions = mergeProviderOptions(runtime.providerOptions, lowEffort);
232
407
  }
233
- const result = streamText({
234
- model: runtime.model,
235
- abortSignal: combinedSignal,
236
- system: SYSTEM_PROMPT,
237
- prompt: prompt.slice(0, 600),
238
- ...(dropMaxTokens ? {} : { maxOutputTokens: maxOut }),
239
- ...(providerOptions ? { providerOptions } : {}),
240
- });
241
- let text = "";
242
- let reasoningText = "";
243
- const partCounts = {};
244
- const debug = process.env.MUONROI_DEBUG_PIL_CLASSIFY === "1";
245
- for await (const part of result.fullStream) {
246
- if (debug)
247
- partCounts[part.type] = (partCounts[part.type] ?? 0) + 1;
248
- if (part.type === "text-delta")
249
- text += part.textDelta ?? part.text ?? "";
250
- else if (part.type === "reasoning-delta")
251
- reasoningText += part.textDelta ?? part.text ?? "";
408
+ const controller = new AbortController();
409
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
410
+ const combinedSignal = signal
411
+ ? (AbortSignal.any?.([signal, controller.signal]) ?? controller.signal)
412
+ : controller.signal;
413
+ try {
414
+ const t0 = Date.now();
415
+ const result = streamText({
416
+ model: runtime.model,
417
+ abortSignal: combinedSignal,
418
+ system: SYSTEM_PROMPT,
419
+ prompt: promptWithContext,
420
+ ...(dropMaxTokens ? {} : { maxOutputTokens: maxOut }),
421
+ ...(providerOptions ? { providerOptions } : {}),
422
+ });
423
+ let text = "";
424
+ let reasoningText = "";
425
+ let streamError = "";
426
+ const partCounts = {};
427
+ for await (const part of result.fullStream) {
428
+ if (debug)
429
+ partCounts[part.type] = (partCounts[part.type] ?? 0) + 1;
430
+ if (part.type === "text-delta")
431
+ text += part.textDelta ?? part.text ?? "";
432
+ else if (part.type === "reasoning-delta")
433
+ reasoningText += part.textDelta ?? part.text ?? "";
434
+ else if (part.type === "error") {
435
+ const e = part.error;
436
+ streamError = e instanceof Error ? e.message : String(e ?? "unknown");
437
+ }
438
+ }
439
+ const elapsedMs = Date.now() - t0;
440
+ if (debug) {
441
+ console.error(`[pil.llm-classify] raw(${cmId}${cmId !== modelId ? `←${modelId}` : ""}) ` +
442
+ `maxOut=${dropMaxTokens ? "dropped" : maxOut} streamError=${JSON.stringify(streamError)} ` +
443
+ `parts=${JSON.stringify(partCounts)} text<<<${text}>>> reasoning<<<${reasoningText.slice(0, 200)}>>>`);
444
+ }
445
+ // Reasoning models occasionally route the entire answer into reasoning
446
+ // parts (no committed text). Fall back to the reasoning channel.
447
+ let parsed = parseResponse(text) ?? (reasoningText ? parseResponse(reasoningText) : null);
448
+ // Metrics-only probe sink (env-gated): latency + parsed depth + any
449
+ // stream error. NO prompt/response content — safe to leave wired.
450
+ const probeLog = process.env.MUONROI_CLASSIFY_LATENCY_LOG;
451
+ if (probeLog) {
452
+ try {
453
+ appendFileSync(probeLog, `${JSON.stringify({
454
+ model: cmId,
455
+ from: modelId === cmId ? undefined : modelId,
456
+ tier: runtime.modelInfo?.tier,
457
+ reasoning: isReasoning,
458
+ timeoutMs,
459
+ elapsedMs,
460
+ textLen: text.length,
461
+ reasoningLen: reasoningText.length,
462
+ parsed: parsed != null,
463
+ depth: parsed?.depthTier ?? null,
464
+ streamError: streamError || undefined,
465
+ })}\n`);
466
+ }
467
+ catch (e) {
468
+ console.error(`[pil.llm-classify] probe-log write failed: ${e?.message}`);
469
+ }
470
+ }
471
+ if (parsed)
472
+ return parsed;
473
+ // Surface a swallowed provider/transport error (No-Silent-Catch): a
474
+ // stream `error` part means the call FAILED (auth/key/rate-limit), which
475
+ // is categorically different from unparseable text. Before this fix such
476
+ // errors vanished into a silent null → fail-open "standard". On error we
477
+ // skip the same-model self-repair (it would fail identically) and let the
478
+ // caller fall through to the next provider candidate.
479
+ if (streamError) {
480
+ console.error(`[pil.llm-classify] stream error on ${cmId} (from ${modelId}): ${streamError} — trying next candidate`);
481
+ return null;
482
+ }
483
+ // Self-repair (agent-first recovery — NOT a regex fallback): the reply
484
+ // did not parse. Call the SAME model once more with the full prompt + an
485
+ // explicit format-repair instruction on a doubled budget. Skip it when
486
+ // too little of the shared deadline remains (the caller then falls
487
+ // through to the next candidate / fails open).
488
+ clearTimeout(timer);
489
+ const repairBudget = deadlineMs - Date.now();
490
+ if (repairBudget < 1500)
491
+ return null;
492
+ const repairTimeout = Math.max(CLASSIFY_MIN_ATTEMPT_MS, Math.min(CLASSIFY_TIMEOUT_MS, repairBudget));
493
+ const repairController = new AbortController();
494
+ const repairTimer = setTimeout(() => repairController.abort(), repairTimeout);
495
+ const repairSignal = signal
496
+ ? (AbortSignal.any?.([signal, repairController.signal]) ?? repairController.signal)
497
+ : repairController.signal;
498
+ try {
499
+ const repairRun = streamText({
500
+ model: runtime.model,
501
+ abortSignal: repairSignal,
502
+ system: `${SYSTEM_PROMPT}\n\n${CLASSIFY_REPAIR_INSTRUCTION}`,
503
+ prompt: repairPrompt,
504
+ ...(dropMaxTokens ? {} : { maxOutputTokens: maxOut * 2 }),
505
+ ...(providerOptions ? { providerOptions } : {}),
506
+ });
507
+ let rt = "";
508
+ let rr = "";
509
+ for await (const part of repairRun.fullStream) {
510
+ if (part.type === "text-delta")
511
+ rt += part.textDelta ?? part.text ?? "";
512
+ else if (part.type === "reasoning-delta")
513
+ rr += part.textDelta ?? part.text ?? "";
514
+ }
515
+ parsed = parseResponse(rt) ?? (rr ? parseResponse(rr) : null);
516
+ if (parsed)
517
+ console.error(`[pil.llm-classify] self-repair recovered classification (${cmId})`);
518
+ return parsed;
519
+ }
520
+ finally {
521
+ clearTimeout(repairTimer);
522
+ }
523
+ }
524
+ catch (err) {
525
+ console.error(`[pil.llm-classify] classify attempt failed on ${cmId}: ${err?.message}`, {
526
+ stack: err?.stack?.split("\n").slice(0, 3),
527
+ });
528
+ return null;
529
+ }
530
+ finally {
531
+ clearTimeout(timer);
532
+ }
533
+ };
534
+ try {
535
+ // Build the ordered candidate list. Primary = same-provider fast tier (or
536
+ // the session model); on NO same-provider fast tier (e.g. xai) append keyed
537
+ // cross-provider fast models. Measured 2026-07-15: an agentic session model
538
+ // (grok-composer) ignores the terse contract and emits task-planning prose
539
+ // → null → fail-open "standard" (the /ideal over-engineering root cause);
540
+ // and a configured-but-dead key (expired deepseek) errors. Trying
541
+ // candidates in order until one parses fixes both.
542
+ let candidates = [];
543
+ const provider = getModelInfo(modelId)?.provider;
544
+ let primaryModelId = modelId;
545
+ if (classifyOpts?.routeFastTier) {
546
+ const sameFast = provider ? getRoutedModelByTier("fast", provider) : undefined;
547
+ if (sameFast && sameFast.id !== modelId)
548
+ primaryModelId = sameFast.id;
549
+ // Cross-provider models are a FALLBACK appended whenever crossProviderFallback
550
+ // is set — NOT only when the session provider lacks a fast tier. Measured
551
+ // 2026-07-16: an openai-OAuth session's fast tier (gpt-5.4-mini) 401s on the
552
+ // api-key path; with the old absence-only gate the chain had no fallback and
553
+ // fail-opened to "standard", so /ideal over-councilled trivial tasks. Ordering
554
+ // (same-fast-first vs cross-first) is decided by orderClassifyCandidates.
555
+ const crossModels = classifyOpts?.crossProviderFallback && provider ? await pickCrossProviderClassifyModels(provider) : [];
556
+ candidates = orderClassifyCandidates({ primaryModelId, hasSameFast: !!sameFast, crossModels });
252
557
  }
253
- if (debug) {
254
- console.error(`[pil.llm-classify] raw(${modelId}) maxOut=${dropMaxTokens ? "dropped" : maxOut} ` +
255
- `parts=${JSON.stringify(partCounts)} text<<<${text}>>> reasoning<<<${reasoningText.slice(0, 200)}>>>`);
558
+ else {
559
+ candidates.push({ modelId: primaryModelId, providerId: null });
256
560
  }
257
- // Reasoning models occasionally route the entire answer into reasoning
258
- // parts (no committed text). Fall back to the reasoning channel so the
259
- // 2-word verdict is still recoverable.
260
- return parseResponse(text) ?? (reasoningText ? parseResponse(reasoningText) : null);
561
+ // Shared absolute deadline bounds the whole chain (each attempt + its
562
+ // repair). A lone same-provider candidate (the common case) simply gets
563
+ // the full flat ceiling within it.
564
+ const chainDeadline = Date.now() + CLASSIFY_TOTAL_BUDGET_MS;
565
+ for (const cand of candidates) {
566
+ if (chainDeadline - Date.now() < 750)
567
+ break; // too little left → fail-open
568
+ // A cross-provider candidate's factory must exist in the registry before
569
+ // resolveModelRuntime can derive it — the session only ever warmed its
570
+ // own. A provider we cannot build (no key, no OAuth) simply drops out.
571
+ if (cand.providerId && !(await resolveCrossProviderClassifyFactory(cand.providerId)))
572
+ continue;
573
+ let runtime;
574
+ try {
575
+ runtime = resolveModelRuntime(cand.modelId);
576
+ }
577
+ catch (e) {
578
+ console.error(`[pil.llm-classify] resolveModelRuntime failed for ${cand.modelId}: ${e?.message}`);
579
+ continue;
580
+ }
581
+ const res = await attemptClassify(runtime, cand.modelId, chainDeadline);
582
+ if (res)
583
+ return res;
584
+ }
585
+ console.error(`[pil.llm-classify] all ${candidates.length} candidate(s) failed — surfacing UNKNOWN, NO regex fallback. ` +
586
+ `rawPreview=${JSON.stringify(prompt.slice(0, 120))}`);
587
+ return null;
261
588
  }
262
589
  catch (err) {
263
590
  console.error(`[pil.llm-classify] classify failed: ${err?.message}`, {
@@ -266,112 +593,8 @@ export function createLlmClassifier(factory, modelId) {
266
593
  });
267
594
  return null;
268
595
  }
269
- finally {
270
- if (timer)
271
- clearTimeout(timer);
272
- }
273
596
  };
274
597
  }
275
- export function classifySubSessionActionHeuristic(prompt) {
276
- const trimmed = prompt.trim().toLowerCase();
277
- if (!trimmed)
278
- return null;
279
- // Strip trailing punctuation for list-based matching so "hello!" and "cảm ơn!"
280
- // still hit the static lists instead of falling through to the LLM classifier.
281
- const stripped = trimmed.replace(/[!?.…,;:]+$/g, "").trim();
282
- // 1. Simple math equations (exact matches like "2+2", "1 + 1")
283
- if (/^\d+\s*[+\-*/]\s*\d+$/.test(trimmed)) {
284
- return {
285
- action: "DIRECT_ANSWER",
286
- confidence: 0.99,
287
- reason: "Obvious input classified via heuristic (simple math)",
288
- };
289
- }
290
- // 2. Greetings (exact matches only, trailing punctuation stripped)
291
- const greetings = [
292
- "hi",
293
- "hello",
294
- "hey",
295
- "chào",
296
- "xin chào",
297
- "hi there",
298
- "hello there",
299
- "chào bạn",
300
- "halo",
301
- "hola",
302
- "bạn ơi",
303
- ];
304
- if (greetings.includes(stripped)) {
305
- return {
306
- action: "DIRECT_ANSWER",
307
- confidence: 0.99,
308
- reason: "Obvious input classified via heuristic (greeting)",
309
- };
310
- }
311
- // 3. Thanks (exact matches only, trailing punctuation stripped)
312
- const thanks = [
313
- "thanks",
314
- "thank you",
315
- "cảm ơn",
316
- "cám ơn",
317
- "thank",
318
- "thx",
319
- "ty",
320
- "cảm ơn bạn",
321
- "cám ơn bạn",
322
- "cảm ơn nhé",
323
- "cám ơn nhé",
324
- ];
325
- if (thanks.includes(stripped)) {
326
- return {
327
- action: "DIRECT_ANSWER",
328
- confidence: 0.99,
329
- reason: "Obvious input classified via heuristic (thanks)",
330
- };
331
- }
332
- // 4. Help (exact matches only, trailing punctuation stripped)
333
- const help = ["help", "hướng dẫn", "cứu", "help me"];
334
- if (help.includes(stripped)) {
335
- return {
336
- action: "DIRECT_ANSWER",
337
- confidence: 0.99,
338
- reason: "Obvious input classified via heuristic (help)",
339
- };
340
- }
341
- // 5. Short conversational words / acknowledgements (exact matches only, trailing punctuation stripped)
342
- const conversation = [
343
- "ok",
344
- "okay",
345
- "yes",
346
- "no",
347
- "vâng",
348
- "dạ",
349
- "ừ",
350
- "chắc thế",
351
- "ừm",
352
- "umm",
353
- "cool",
354
- "nice",
355
- "perfect",
356
- "done",
357
- "xong",
358
- "yep",
359
- "yup",
360
- "nah",
361
- "fine",
362
- "tốt",
363
- "được",
364
- "okie",
365
- ];
366
- if (conversation.includes(stripped)) {
367
- return {
368
- action: "DIRECT_ANSWER",
369
- confidence: 0.99,
370
- reason: "Obvious input classified via heuristic (acknowledgement)",
371
- };
372
- }
373
- return null;
374
- }
375
598
  const ROUTER_SYSTEM_PROMPT = "You are a routing controller for an AI coding agent. Your goal is to decide the execution strategy for the user's prompt based on the conversation history and metadata.\n\n" +
376
599
  "Analyze the user's prompt and select one of the following ACTIONS:\n" +
377
600
  '- "DIRECT_ANSWER": The prompt is informational, a quick question, a code review, an explanation, greeting, or thanks. No file creation/modification, test execution, or multi-turn tool runs are needed.\n' +
@@ -385,23 +608,27 @@ const ROUTER_SYSTEM_PROMPT = "You are a routing controller for an AI coding agen
385
608
  '- "ROTATE_SESSION,0.95,Session size exceeds threshold and current request starts a new task."\n' +
386
609
  '- "SPAWN_SUB_SESSION,0.98,Requires writing a test suite and fixing multiple files to get it green."\n' +
387
610
  "No other text, only the comma-separated line.";
388
- export async function classifySubSessionAction(factory, modelId, prompt, contextInfo, signal) {
389
- if (process.env.MUONROI_DISABLE_HEURISTIC_ROUTING !== "1") {
390
- const heuristic = classifySubSessionActionHeuristic(prompt);
391
- if (heuristic)
392
- return heuristic;
393
- }
611
+ export async function classifySubSessionAction(modelId, prompt, contextInfo, signal) {
612
+ // No regex pre-filter: the model decides the route for EVERY prompt, including
613
+ // greetings/acks (which it routes to DIRECT_ANSWER). The old keyword/list
614
+ // heuristic was removed (2026-07-07, no-regex rule) — a hardcoded whitelist
615
+ // mis-handles the long tail of natural-language inputs the whole design moved
616
+ // off of. On a null/failed model result the caller keeps the conservative
617
+ // DIRECT_ANSWER default (a semantic default, not a regex guess).
394
618
  const controller = new AbortController();
395
619
  let timer;
396
620
  try {
397
621
  // Zero-hardcode: query models catalog for a cheap fast-tier model under the same provider.
398
622
  const info = getModelInfo(modelId);
399
623
  const provider = info?.provider;
400
- const fastModel = provider ? getModelByTier("fast", provider) || getModelByTier("balanced", provider) : undefined;
624
+ const fastModel = provider
625
+ ? getRoutedModelByTier("fast", provider) || getRoutedModelByTier("balanced", provider)
626
+ : undefined;
401
627
  const classificationModelId = fastModel?.id ?? modelId;
402
- const runtime = resolveModelRuntime(factory, classificationModelId);
628
+ const runtime = resolveModelRuntime(classificationModelId);
403
629
  const isReasoning = runtime.modelInfo?.reasoning === true;
404
- timer = setTimeout(() => controller.abort(), isReasoning ? REASONING_CLASSIFY_TIMEOUT_MS : LLM_CLASSIFY_TIMEOUT_MS);
630
+ // Same flat safety-net ceiling as the main classifier (see CLASSIFY_TIMEOUT_MS).
631
+ timer = setTimeout(() => controller.abort(), CLASSIFY_TIMEOUT_MS);
405
632
  const combinedSignal = signal
406
633
  ? (AbortSignal.any?.([signal, controller.signal]) ?? controller.signal)
407
634
  : controller.signal;
@@ -417,10 +644,15 @@ export async function classifySubSessionAction(factory, modelId, prompt, context
417
644
  }
418
645
  let promptWithContext = prompt.slice(0, 1000);
419
646
  if (contextInfo) {
647
+ const recent = contextInfo.recentTurns?.trim();
648
+ const historyBlock = recent
649
+ ? `[CONVERSATION HISTORY — for context; the prompt may continue or reference it]\n${recent.slice(0, 800)}\n\n`
650
+ : "";
420
651
  promptWithContext =
421
652
  `[SESSION METADATA]\n` +
422
653
  `Current session size: ${contextInfo.currentChars} characters.\n` +
423
654
  `Rotation threshold: ${contextInfo.threshold} characters.\n\n` +
655
+ `${historyBlock}` +
424
656
  `[USER PROMPT]\n${promptWithContext}`;
425
657
  }
426
658
  const result = streamText({