muonroi-cli 1.8.3 → 1.8.5

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 (477) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +133 -122
  3. package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
  4. package/dist/packages/agent-harness-core/src/driver.js +46 -0
  5. package/dist/packages/agent-harness-core/src/event-tee.d.ts +48 -0
  6. package/dist/packages/agent-harness-core/src/event-tee.js +77 -0
  7. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +11 -0
  8. package/dist/packages/agent-harness-core/src/mcp-server.js +87 -15
  9. package/dist/packages/agent-harness-core/src/protocol.d.ts +66 -2
  10. package/dist/packages/agent-harness-core/src/protocol.js +15 -0
  11. package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
  12. package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
  13. package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
  14. package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
  15. package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
  16. package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
  17. package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
  18. package/dist/packages/agent-harness-opentui/src/install.js +10 -0
  19. package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
  20. package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
  21. package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
  22. package/dist/src/agent-harness/mock-model.d.ts +28 -0
  23. package/dist/src/agent-harness/mock-model.js +63 -1
  24. package/dist/src/agent-harness/test-spawn.js +31 -0
  25. package/dist/src/cli/config/screen-providers.js +1 -1
  26. package/dist/src/cli/cost-forensics.d.ts +10 -0
  27. package/dist/src/cli/cost-forensics.js +30 -15
  28. package/dist/src/cli/keys-bundle.d.ts +1 -1
  29. package/dist/src/cli/keys-bundle.js +1 -1
  30. package/dist/src/cli/keys.d.ts +2 -2
  31. package/dist/src/cli/keys.js +19 -81
  32. package/dist/src/council/clarifier.d.ts +28 -2
  33. package/dist/src/council/clarifier.js +81 -15
  34. package/dist/src/council/context.js +49 -15
  35. package/dist/src/council/debate-checkpoint.d.ts +129 -0
  36. package/dist/src/council/debate-checkpoint.js +176 -0
  37. package/dist/src/council/debate-planner.js +51 -3
  38. package/dist/src/council/debate-summary.d.ts +25 -0
  39. package/dist/src/council/debate-summary.js +85 -0
  40. package/dist/src/council/debate.d.ts +169 -2
  41. package/dist/src/council/debate.js +1210 -134
  42. package/dist/src/council/index.d.ts +85 -1
  43. package/dist/src/council/index.js +634 -196
  44. package/dist/src/council/leader.d.ts +26 -0
  45. package/dist/src/council/leader.js +150 -9
  46. package/dist/src/council/llm.d.ts +32 -0
  47. package/dist/src/council/llm.js +231 -38
  48. package/dist/src/council/panel-select.d.ts +30 -0
  49. package/dist/src/council/panel-select.js +72 -0
  50. package/dist/src/council/planner.js +23 -0
  51. package/dist/src/council/preflight.d.ts +7 -0
  52. package/dist/src/council/preflight.js +14 -2
  53. package/dist/src/council/prompts.d.ts +30 -3
  54. package/dist/src/council/prompts.js +254 -84
  55. package/dist/src/council/stance-recall.d.ts +42 -0
  56. package/dist/src/council/stance-recall.js +57 -0
  57. package/dist/src/council/strip-think.d.ts +17 -0
  58. package/dist/src/council/strip-think.js +33 -0
  59. package/dist/src/council/types.d.ts +128 -0
  60. package/dist/src/ee/artifact-cache.d.ts +16 -0
  61. package/dist/src/ee/artifact-cache.js +32 -0
  62. package/dist/src/ee/auth.d.ts +1 -0
  63. package/dist/src/ee/auth.js +15 -2
  64. package/dist/src/ee/bridge.d.ts +10 -0
  65. package/dist/src/ee/bridge.js +58 -0
  66. package/dist/src/ee/client.js +81 -18
  67. package/dist/src/ee/export-transcripts.d.ts +1 -0
  68. package/dist/src/ee/export-transcripts.js +8 -10
  69. package/dist/src/ee/extract-session.js +29 -0
  70. package/dist/src/ee/extract-style.d.ts +58 -0
  71. package/dist/src/ee/extract-style.js +270 -0
  72. package/dist/src/ee/recall-ledger.d.ts +9 -0
  73. package/dist/src/ee/recall-ledger.js +3 -0
  74. package/dist/src/ee/scope.d.ts +1 -0
  75. package/dist/src/ee/scope.js +26 -1
  76. package/dist/src/ee/search.d.ts +7 -0
  77. package/dist/src/ee/search.js +24 -0
  78. package/dist/src/ee/transcript-emit.js +2 -0
  79. package/dist/src/ee/types.d.ts +22 -0
  80. package/dist/src/ee/who-am-i-brain.d.ts +35 -0
  81. package/dist/src/ee/who-am-i-brain.js +220 -0
  82. package/dist/src/ee/who-am-i.d.ts +10 -3
  83. package/dist/src/ee/who-am-i.js +12 -0
  84. package/dist/src/ee/workflow-event.d.ts +48 -0
  85. package/dist/src/ee/workflow-event.js +81 -0
  86. package/dist/src/flow/compaction/compress.d.ts +3 -3
  87. package/dist/src/flow/compaction/compress.js +45 -8
  88. package/dist/src/flow/compaction/extract.d.ts +4 -7
  89. package/dist/src/flow/compaction/extract.js +50 -10
  90. package/dist/src/flow/compaction/index.d.ts +13 -1
  91. package/dist/src/flow/compaction/index.js +70 -3
  92. package/dist/src/flow/compaction/input-guard.d.ts +24 -0
  93. package/dist/src/flow/compaction/input-guard.js +43 -0
  94. package/dist/src/flow/fold-planning.d.ts +36 -0
  95. package/dist/src/flow/fold-planning.js +83 -0
  96. package/dist/src/flow/hierarchy.d.ts +146 -0
  97. package/dist/src/flow/hierarchy.js +427 -0
  98. package/dist/src/flow/index.d.ts +1 -0
  99. package/dist/src/flow/index.js +2 -0
  100. package/dist/src/flow/run-artifacts.d.ts +102 -0
  101. package/dist/src/flow/run-artifacts.js +208 -0
  102. package/dist/src/generated/version.d.ts +1 -1
  103. package/dist/src/generated/version.js +1 -1
  104. package/dist/src/gsd/assessment-schema.d.ts +44 -0
  105. package/dist/src/gsd/assessment-schema.js +134 -0
  106. package/dist/src/gsd/capability-registry.d.ts +45 -0
  107. package/dist/src/gsd/capability-registry.js +337 -0
  108. package/dist/src/gsd/complexity-assessor.d.ts +39 -0
  109. package/dist/src/gsd/complexity-assessor.js +152 -0
  110. package/dist/src/gsd/config-bridge.d.ts +7 -0
  111. package/dist/src/gsd/config-bridge.js +114 -0
  112. package/dist/src/gsd/config-loader.d.ts +27 -0
  113. package/dist/src/gsd/config-loader.js +50 -0
  114. package/dist/src/gsd/council-context.d.ts +44 -0
  115. package/dist/src/gsd/council-context.js +114 -0
  116. package/dist/src/gsd/ee-closure.d.ts +28 -0
  117. package/dist/src/gsd/ee-closure.js +49 -0
  118. package/dist/src/gsd/flags.d.ts +55 -0
  119. package/dist/src/gsd/flags.js +83 -0
  120. package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
  121. package/dist/src/gsd/gsd-dispatch.js +131 -0
  122. package/dist/src/gsd/gsd-runtime.d.ts +22 -0
  123. package/dist/src/gsd/gsd-runtime.js +37 -0
  124. package/dist/src/gsd/host-adapter.d.ts +11 -0
  125. package/dist/src/gsd/host-adapter.js +29 -0
  126. package/dist/src/gsd/index.d.ts +24 -1
  127. package/dist/src/gsd/index.js +27 -0
  128. package/dist/src/gsd/loop-host-contract.d.ts +21 -0
  129. package/dist/src/gsd/loop-host-contract.js +39 -0
  130. package/dist/src/gsd/loop-host.d.ts +69 -0
  131. package/dist/src/gsd/loop-host.js +245 -0
  132. package/dist/src/gsd/loop-resolver.d.ts +36 -0
  133. package/dist/src/gsd/loop-resolver.js +79 -0
  134. package/dist/src/gsd/model-tier.d.ts +13 -0
  135. package/dist/src/gsd/model-tier.js +45 -0
  136. package/dist/src/gsd/mutation-gate.d.ts +16 -0
  137. package/dist/src/gsd/mutation-gate.js +41 -0
  138. package/dist/src/gsd/native-roadmap.d.ts +89 -0
  139. package/dist/src/gsd/native-roadmap.js +343 -0
  140. package/dist/src/gsd/native-state.d.ts +47 -0
  141. package/dist/src/gsd/native-state.js +220 -0
  142. package/dist/src/gsd/paths.d.ts +23 -0
  143. package/dist/src/gsd/paths.js +66 -0
  144. package/dist/src/gsd/phase-dag.d.ts +12 -0
  145. package/dist/src/gsd/phase-dag.js +94 -0
  146. package/dist/src/gsd/phase-sync.d.ts +42 -0
  147. package/dist/src/gsd/phase-sync.js +321 -0
  148. package/dist/src/gsd/pil-gate-context.d.ts +13 -0
  149. package/dist/src/gsd/pil-gate-context.js +64 -0
  150. package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
  151. package/dist/src/gsd/pil-gate-critic.js +74 -0
  152. package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
  153. package/dist/src/gsd/plan-council-prompts.js +79 -0
  154. package/dist/src/gsd/plan-council.d.ts +44 -0
  155. package/dist/src/gsd/plan-council.js +251 -0
  156. package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
  157. package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
  158. package/dist/src/gsd/product-workspace.d.ts +13 -0
  159. package/dist/src/gsd/product-workspace.js +124 -0
  160. package/dist/src/gsd/ship-bridge.d.ts +25 -0
  161. package/dist/src/gsd/ship-bridge.js +65 -0
  162. package/dist/src/gsd/state-document.d.ts +40 -0
  163. package/dist/src/gsd/state-document.js +163 -0
  164. package/dist/src/gsd/verdict-schema.d.ts +39 -0
  165. package/dist/src/gsd/verdict-schema.js +144 -0
  166. package/dist/src/gsd/verify-context.d.ts +22 -0
  167. package/dist/src/gsd/verify-context.js +27 -0
  168. package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
  169. package/dist/src/gsd/verify-council-prompts.js +85 -0
  170. package/dist/src/gsd/verify-council.d.ts +25 -0
  171. package/dist/src/gsd/verify-council.js +119 -0
  172. package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
  173. package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
  174. package/dist/src/gsd/workflow-engine.d.ts +60 -0
  175. package/dist/src/gsd/workflow-engine.js +207 -0
  176. package/dist/src/gsd/workflow-tools.d.ts +13 -0
  177. package/dist/src/gsd/workflow-tools.js +277 -0
  178. package/dist/src/hooks/index.js +1 -1
  179. package/dist/src/index.js +44 -11
  180. package/dist/src/maintain/pr-builder.js +23 -13
  181. package/dist/src/mcp/auto-setup.js +57 -32
  182. package/dist/src/mcp/client-pool.js +1 -1
  183. package/dist/src/mcp/ee-tools.js +1 -0
  184. package/dist/src/mcp/oauth-callback.js +2 -2
  185. package/dist/src/mcp/research-onboarding.js +8 -7
  186. package/dist/src/mcp/runtime.js +34 -2
  187. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  188. package/dist/src/mcp/setup-guide-text.js +77 -76
  189. package/dist/src/models/catalog-client.d.ts +87 -0
  190. package/dist/src/models/catalog-client.js +105 -38
  191. package/dist/src/models/catalog.json +528 -265
  192. package/dist/src/models/registry.d.ts +22 -7
  193. package/dist/src/models/registry.js +73 -10
  194. package/dist/src/ops/doctor.js +8 -8
  195. package/dist/src/orchestrator/auto-commit.js +1 -1
  196. package/dist/src/orchestrator/batch-turn-runner.js +2 -2
  197. package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
  198. package/dist/src/orchestrator/cache-prefix.js +83 -0
  199. package/dist/src/orchestrator/compact-request.d.ts +32 -0
  200. package/dist/src/orchestrator/compact-request.js +41 -0
  201. package/dist/src/orchestrator/compaction.d.ts +10 -0
  202. package/dist/src/orchestrator/compaction.js +27 -7
  203. package/dist/src/orchestrator/council-manager.d.ts +12 -3
  204. package/dist/src/orchestrator/council-manager.js +65 -24
  205. package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
  206. package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
  207. package/dist/src/orchestrator/error-utils.d.ts +29 -0
  208. package/dist/src/orchestrator/error-utils.js +132 -24
  209. package/dist/src/orchestrator/grounding-check.js +39 -1
  210. package/dist/src/orchestrator/message-processor.js +242 -33
  211. package/dist/src/orchestrator/orchestrator.d.ts +39 -3
  212. package/dist/src/orchestrator/orchestrator.js +651 -102
  213. package/dist/src/orchestrator/preprocessor.js +1 -1
  214. package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
  215. package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
  216. package/dist/src/orchestrator/prompts.js +159 -159
  217. package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
  218. package/dist/src/orchestrator/reactive-delegation.js +59 -0
  219. package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
  220. package/dist/src/orchestrator/retry-classifier.js +46 -2
  221. package/dist/src/orchestrator/safety-intercept.d.ts +45 -0
  222. package/dist/src/orchestrator/safety-intercept.js +55 -0
  223. package/dist/src/orchestrator/scope-reminder.js +1 -1
  224. package/dist/src/orchestrator/session-experience.d.ts +2 -1
  225. package/dist/src/orchestrator/session-experience.js +2 -1
  226. package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
  227. package/dist/src/orchestrator/should-run-gate.js +18 -0
  228. package/dist/src/orchestrator/stall-watchdog.d.ts +24 -3
  229. package/dist/src/orchestrator/stall-watchdog.js +47 -13
  230. package/dist/src/orchestrator/stream-runner.js +62 -29
  231. package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
  232. package/dist/src/orchestrator/sub-agent-cap.js +16 -1
  233. package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
  234. package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
  235. package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
  236. package/dist/src/orchestrator/subagent-compactor.js +126 -15
  237. package/dist/src/orchestrator/tool-engine.d.ts +26 -0
  238. package/dist/src/orchestrator/tool-engine.js +669 -56
  239. package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
  240. package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
  241. package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
  242. package/dist/src/orchestrator/turn-watchdog.d.ts +37 -0
  243. package/dist/src/orchestrator/turn-watchdog.js +55 -0
  244. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  245. package/dist/src/pil/agent-operating-contract.js +1 -1
  246. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  247. package/dist/src/pil/cheap-model-playbook.js +5 -1
  248. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  249. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  250. package/dist/src/pil/discovery-types.d.ts +1 -0
  251. package/dist/src/pil/discovery.js +16 -11
  252. package/dist/src/pil/layer1-intent.d.ts +18 -6
  253. package/dist/src/pil/layer1-intent.js +66 -757
  254. package/dist/src/pil/layer15-context-scan.js +15 -1
  255. package/dist/src/pil/layer2_5-ponytail.js +8 -8
  256. package/dist/src/pil/layer3-ee-injection.js +23 -8
  257. package/dist/src/pil/layer4-gsd.js +69 -16
  258. package/dist/src/pil/layer5-context.js +7 -3
  259. package/dist/src/pil/layer6-output.d.ts +23 -0
  260. package/dist/src/pil/layer6-output.js +5 -1
  261. package/dist/src/pil/llm-classify.d.ts +33 -2
  262. package/dist/src/pil/llm-classify.js +123 -131
  263. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  264. package/dist/src/pil/native-capabilities-workbook.js +1 -0
  265. package/dist/src/pil/pipeline.js +34 -2
  266. package/dist/src/pil/response-tools.js +5 -3
  267. package/dist/src/pil/schema.d.ts +1 -0
  268. package/dist/src/pil/schema.js +2 -0
  269. package/dist/src/pil/types.d.ts +18 -0
  270. package/dist/src/playbook/directives.d.ts +4 -0
  271. package/dist/src/playbook/directives.js +17 -5
  272. package/dist/src/product-loop/backlog-builder.d.ts +14 -1
  273. package/dist/src/product-loop/backlog-builder.js +30 -6
  274. package/dist/src/product-loop/discovery-context-format.js +3 -1
  275. package/dist/src/product-loop/discovery-ecosystem.js +4 -1
  276. package/dist/src/product-loop/discovery-interview.js +32 -3
  277. package/dist/src/product-loop/discovery-schema.js +5 -1
  278. package/dist/src/product-loop/done-gate.js +3 -3
  279. package/dist/src/product-loop/ideal-trace.d.ts +7 -0
  280. package/dist/src/product-loop/ideal-trace.js +64 -0
  281. package/dist/src/product-loop/index.d.ts +13 -1
  282. package/dist/src/product-loop/index.js +333 -52
  283. package/dist/src/product-loop/loop-driver.d.ts +7 -0
  284. package/dist/src/product-loop/loop-driver.js +327 -116
  285. package/dist/src/product-loop/phase-plan.d.ts +5 -0
  286. package/dist/src/product-loop/phase-plan.js +39 -2
  287. package/dist/src/product-loop/phase-runner.js +9 -1
  288. package/dist/src/product-loop/progress-snapshot.js +4 -4
  289. package/dist/src/product-loop/sprint-runner.d.ts +111 -0
  290. package/dist/src/product-loop/sprint-runner.js +559 -16
  291. package/dist/src/product-loop/types.d.ts +36 -5
  292. package/dist/src/providers/adapter.d.ts +1 -1
  293. package/dist/src/providers/adapter.js +3 -4
  294. package/dist/src/providers/auth/browser-flow.d.ts +1 -1
  295. package/dist/src/providers/auth/browser-flow.js +1 -1
  296. package/dist/src/providers/auth/openai-oauth.js +1 -1
  297. package/dist/src/providers/auth/registry.js +0 -34
  298. package/dist/src/providers/auth/token-store.js +4 -1
  299. package/dist/src/providers/auth/types.d.ts +1 -1
  300. package/dist/src/providers/auth/types.js +1 -1
  301. package/dist/src/providers/capabilities.d.ts +24 -5
  302. package/dist/src/providers/capabilities.js +42 -24
  303. package/dist/src/providers/endpoints.d.ts +2 -2
  304. package/dist/src/providers/endpoints.js +11 -10
  305. package/dist/src/providers/keychain.d.ts +1 -1
  306. package/dist/src/providers/keychain.js +7 -9
  307. package/dist/src/providers/mcp-vision-bridge.js +82 -172
  308. package/dist/src/providers/openai-compatible.js +8 -1
  309. package/dist/src/providers/pricing.d.ts +2 -2
  310. package/dist/src/providers/pricing.js +3 -13
  311. package/dist/src/providers/runtime.d.ts +27 -2
  312. package/dist/src/providers/runtime.js +78 -15
  313. package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
  314. package/dist/src/providers/strategies/base.strategy.js +24 -1
  315. package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
  316. package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
  317. package/dist/src/providers/strategies/registry.js +4 -4
  318. package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
  319. package/dist/src/providers/strategies/thinking-mode.js +280 -1
  320. package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
  321. package/dist/src/providers/strategies/zai.strategy.js +44 -0
  322. package/dist/src/providers/types.d.ts +5 -6
  323. package/dist/src/providers/types.js +2 -2
  324. package/dist/src/providers/vision-backend.d.ts +47 -0
  325. package/dist/src/providers/vision-backend.js +258 -0
  326. package/dist/src/providers/vision-proxy.d.ts +22 -9
  327. package/dist/src/providers/vision-proxy.js +63 -132
  328. package/dist/src/providers/wire-debug.js +95 -0
  329. package/dist/src/reporter/index.js +1 -1
  330. package/dist/src/router/decide.d.ts +13 -0
  331. package/dist/src/router/decide.js +138 -36
  332. package/dist/src/router/peak-hour.d.ts +38 -0
  333. package/dist/src/router/peak-hour.js +107 -0
  334. package/dist/src/router/step-router.js +3 -2
  335. package/dist/src/router/warm.js +4 -5
  336. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  337. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  338. package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
  339. package/dist/src/scaffold/continuation-prompt.js +86 -60
  340. package/dist/src/scaffold/init-new.js +453 -453
  341. package/dist/src/scaffold/point-to-existing.d.ts +21 -0
  342. package/dist/src/scaffold/point-to-existing.js +25 -0
  343. package/dist/src/self-qa/agentic-loop.js +22 -22
  344. package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
  345. package/dist/src/{ui/state → state}/active-run.js +21 -0
  346. package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
  347. package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
  348. package/dist/src/state/turn-trace.d.ts +43 -0
  349. package/dist/src/state/turn-trace.js +32 -0
  350. package/dist/src/storage/db.js +2 -1
  351. package/dist/src/storage/index.d.ts +1 -1
  352. package/dist/src/storage/index.js +1 -1
  353. package/dist/src/storage/interaction-log.d.ts +1 -1
  354. package/dist/src/storage/interaction-log.js +5 -5
  355. package/dist/src/storage/migrations.js +196 -126
  356. package/dist/src/storage/session-experience-store.js +4 -4
  357. package/dist/src/storage/sessions.d.ts +28 -10
  358. package/dist/src/storage/sessions.js +112 -55
  359. package/dist/src/storage/transcript-view.js +1 -1
  360. package/dist/src/storage/transcript.d.ts +51 -0
  361. package/dist/src/storage/transcript.js +383 -112
  362. package/dist/src/storage/usage.js +14 -14
  363. package/dist/src/storage/workspaces.js +12 -12
  364. package/dist/src/tools/file.d.ts +15 -0
  365. package/dist/src/tools/file.js +32 -0
  366. package/dist/src/tools/native-tools.js +5 -0
  367. package/dist/src/tools/registry.d.ts +3 -0
  368. package/dist/src/tools/registry.js +460 -22
  369. package/dist/src/tools/research.d.ts +29 -0
  370. package/dist/src/tools/research.js +233 -0
  371. package/dist/src/types/index.d.ts +118 -3
  372. package/dist/src/ui/app.js +0 -0
  373. package/dist/src/ui/cards/product-status-card.js +1 -1
  374. package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
  375. package/dist/src/ui/components/bubble-body-guard.js +50 -0
  376. package/dist/src/ui/components/context-rail.d.ts +26 -0
  377. package/dist/src/ui/components/context-rail.js +33 -0
  378. package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
  379. package/dist/src/ui/components/council-conclusion-card.js +420 -0
  380. package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
  381. package/dist/src/ui/components/council-debate-pill.js +34 -0
  382. package/dist/src/ui/components/council-info-card.js +2 -2
  383. package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
  384. package/dist/src/ui/components/council-leader-bubble.js +21 -11
  385. package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
  386. package/dist/src/ui/components/council-message-bubble.js +16 -15
  387. package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
  388. package/dist/src/ui/components/council-phase-timeline.js +49 -15
  389. package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
  390. package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
  391. package/dist/src/ui/components/council-question-card.js +12 -12
  392. package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
  393. package/dist/src/ui/components/council-rail-rounds.js +57 -0
  394. package/dist/src/ui/components/council-round-group.d.ts +38 -0
  395. package/dist/src/ui/components/council-round-group.js +88 -0
  396. package/dist/src/ui/components/council-status-list.d.ts +3 -1
  397. package/dist/src/ui/components/council-status-list.js +36 -24
  398. package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
  399. package/dist/src/ui/components/council-synthesis-banner.js +20 -5
  400. package/dist/src/ui/components/halt-recovery-card.js +9 -5
  401. package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
  402. package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
  403. package/dist/src/ui/components/prompt-box.js +18 -16
  404. package/dist/src/ui/components/session-tree-card.d.ts +14 -0
  405. package/dist/src/ui/components/session-tree-card.js +46 -0
  406. package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
  407. package/dist/src/ui/components/slash-inline-menu.js +26 -5
  408. package/dist/src/ui/components/task-list-panel.d.ts +14 -1
  409. package/dist/src/ui/components/task-list-panel.js +22 -2
  410. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  411. package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
  412. package/dist/src/ui/mcp-modal.js +2 -4
  413. package/dist/src/ui/modals/api-key-modal.js +1 -1
  414. package/dist/src/ui/modals/connect-modal.js +4 -3
  415. package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
  416. package/dist/src/ui/modals/session-picker-modal.js +3 -5
  417. package/dist/src/ui/picker-providers.d.ts +1 -1
  418. package/dist/src/ui/picker-providers.js +1 -1
  419. package/dist/src/ui/primitives/index.d.ts +1 -0
  420. package/dist/src/ui/primitives/index.js +2 -0
  421. package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
  422. package/dist/src/ui/primitives/semantic-primitives.js +81 -0
  423. package/dist/src/ui/slash/compact.js +5 -7
  424. package/dist/src/ui/slash/cost.js +1 -1
  425. package/dist/src/ui/slash/council-inspect.js +4 -4
  426. package/dist/src/ui/slash/council.js +19 -1
  427. package/dist/src/ui/slash/debug.d.ts +3 -31
  428. package/dist/src/ui/slash/debug.js +9 -20
  429. package/dist/src/ui/slash/ideal.d.ts +6 -2
  430. package/dist/src/ui/slash/ideal.js +97 -7
  431. package/dist/src/ui/slash/menu-items.d.ts +7 -0
  432. package/dist/src/ui/slash/menu-items.js +12 -18
  433. package/dist/src/ui/slash/registry.d.ts +2 -0
  434. package/dist/src/ui/slash/registry.js +4 -0
  435. package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
  436. package/dist/src/ui/status-bar/cache-hit.js +9 -0
  437. package/dist/src/ui/status-bar/index.d.ts +1 -1
  438. package/dist/src/ui/status-bar/index.js +7 -3
  439. package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
  440. package/dist/src/ui/status-bar/usd-meter.js +6 -4
  441. package/dist/src/ui/theme.d.ts +1 -0
  442. package/dist/src/ui/theme.js +2 -0
  443. package/dist/src/ui/types.d.ts +7 -0
  444. package/dist/src/ui/use-app-logic.js +0 -0
  445. package/dist/src/ui/utils/format.d.ts +14 -0
  446. package/dist/src/ui/utils/format.js +23 -3
  447. package/dist/src/usage/downgrade.js +2 -2
  448. package/dist/src/usage/product-ledger.js +2 -2
  449. package/dist/src/utils/clipboard-image.js +23 -23
  450. package/dist/src/utils/install-manager.js +14 -11
  451. package/dist/src/utils/logger.js +2 -2
  452. package/dist/src/utils/permission-mode.js +5 -3
  453. package/dist/src/utils/redactor.js +1 -1
  454. package/dist/src/utils/settings.d.ts +153 -5
  455. package/dist/src/utils/settings.js +233 -29
  456. package/dist/src/utils/side-question.js +2 -2
  457. package/dist/src/utils/skills.js +3 -3
  458. package/dist/src/utils/visible-retry.d.ts +11 -0
  459. package/dist/src/utils/visible-retry.js +10 -1
  460. package/dist/src/verify/entrypoint.d.ts +1 -1
  461. package/dist/src/verify/entrypoint.js +1 -1
  462. package/dist/src/verify/recipes.d.ts +13 -0
  463. package/dist/src/verify/recipes.js +15 -0
  464. package/package.json +135 -132
  465. package/dist/src/providers/auth/gcloud.d.ts +0 -28
  466. package/dist/src/providers/auth/gcloud.js +0 -102
  467. package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
  468. package/dist/src/providers/auth/gemini-oauth.js +0 -472
  469. package/dist/src/providers/gemini.d.ts +0 -11
  470. package/dist/src/providers/gemini.js +0 -45
  471. package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
  472. package/dist/src/providers/siliconflow-sse-repair.js +0 -177
  473. package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
  474. package/dist/src/providers/strategies/google.strategy.js +0 -174
  475. package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
  476. package/dist/src/ui/containers/chat-feed.d.ts +0 -40
  477. package/dist/src/ui/containers/chat-feed.js +0 -66
@@ -1,7 +1,9 @@
1
1
  import { queryExperience } from "../ee/council-bridge.js";
2
2
  import { judgeCouncilOutcome } from "../ee/judge.js";
3
3
  import { recordCouncilOutcome } from "../ee/phase-outcome.js";
4
+ import { isTaskAwarePanelEnabled } from "../gsd/flags.js";
4
5
  import { runPipeline } from "../pil/pipeline.js";
6
+ import { idealTrace } from "../product-loop/ideal-trace.js";
5
7
  import { appendSystemMessage, logInteraction } from "../storage/index.js";
6
8
  import { SessionStore } from "../storage/sessions.js";
7
9
  import { getCouncilExperienceMode, isCouncilCostAware, isCouncilMultiProviderPreferred } from "../utils/settings.js";
@@ -11,7 +13,8 @@ import { evaluateResearchNeed, runDebate } from "./debate.js";
11
13
  import { planDebate } from "./debate-planner.js";
12
14
  import { detectOutOfStackProposals, writeDecisionsLock } from "./decisions-lock.js";
13
15
  import { runExecution } from "./executor.js";
14
- import { resolveLeaderModelDetailed, resolveParticipants } from "./leader.js";
16
+ import { buildCouncilCandidatePool, resolveLeaderModelDetailed, resolveParticipants } from "./leader.js";
17
+ import { selectTaskAwarePanel } from "./panel-select.js";
15
18
  import { phaseDone, phaseStart } from "./phase-events.js";
16
19
  import { runPlanning } from "./planner.js";
17
20
  import { runPreflight } from "./preflight.js";
@@ -59,6 +62,22 @@ const EXPLICIT_COUNCIL_CLARIFY_ROUNDS = 1;
59
62
  * generate_plan OPTION is still offered downstream; it's just no longer the
60
63
  * pre-selected default for non-build topics.
61
64
  */
65
+ /**
66
+ * F1 — summarize how the debate did against its PINNED success criteria, so the
67
+ * post-debate card can distinguish "the criteria were actually met" from "the
68
+ * synthesis reads confidently" (evidence density). `metFlags` is index-aligned
69
+ * to `pinned` (from DebateState.finalCriteriaMet); a missing/short array treats
70
+ * the unmapped criteria as not-met. `inconclusive` is true when the spec had
71
+ * pinned criteria and at least one is still open — the caller ANDs this with
72
+ * `!synthesisFailed` before reframing the card.
73
+ */
74
+ export function summarizeCriteriaOutcome(pinned, metFlags) {
75
+ const flags = metFlags ?? [];
76
+ const total = pinned.length;
77
+ const metCount = pinned.filter((_, i) => flags[i] === true).length;
78
+ const unmetLabels = pinned.filter((_, i) => flags[i] !== true);
79
+ return { total, metCount, unmetLabels, inconclusive: total > 0 && unmetLabels.length > 0 };
80
+ }
62
81
  export function pickPostDebateRecommendation(input) {
63
82
  if (input.synthesisFailed) {
64
83
  return {
@@ -66,6 +85,13 @@ export function pickPostDebateRecommendation(input) {
66
85
  reason: "Re-run synthesis with a compact prompt — usually clears provider-timeout failures.",
67
86
  };
68
87
  }
88
+ if (input.criteriaUnmet && input.criteriaUnmet > 0) {
89
+ const n = input.criteriaUnmet;
90
+ return {
91
+ value: "ask_followup",
92
+ reason: `${n} success criteri${n === 1 ? "on" : "a"} still unmet — press the council to close ${n === 1 ? "it" : "them"} before treating this as settled.`,
93
+ };
94
+ }
69
95
  if (input.hasEmptySections) {
70
96
  return { value: "refine", reason: `Fill in ${input.refinementTopics.length} section(s) the debate left empty.` };
71
97
  }
@@ -85,6 +111,74 @@ export function pickPostDebateRecommendation(input) {
85
111
  }
86
112
  return { value: "save_exit", reason: "Outcome looks solid — save and move on." };
87
113
  }
114
+ /**
115
+ * Decide whether — and with what prompt — the agent session should keep working
116
+ * after the post-debate askcard, given the action the user chose.
117
+ *
118
+ * Single source of truth for BOTH continuation callers (the `/council` slash path
119
+ * in orchestrator.runCouncilV2 and the auto-council path in tool-engine), which
120
+ * previously diverged: the slash path only continued on `continue_session`, while
121
+ * auto-council continued UNCONDITIONALLY with a fixed "Proceed with the recommended
122
+ * action items" prompt — meaningless for an evaluation/decision debate that has no
123
+ * action items, so the chosen action was effectively ignored.
124
+ *
125
+ * Returns the re-entry prompt to feed back into processMessage, or `null` to stop
126
+ * at the composer (the synthesis IS the deliverable).
127
+ * - continue_session → carry the conclusion forward on the ORIGINAL task, but
128
+ * ONLY for an implementation-shaped debate. For an analysis/evaluation debate
129
+ * the conclusion IS the deliverable, so re-enter WITHOUT an implementation
130
+ * mandate (session 578b2eae7099: "Continue the original task using this
131
+ * conclusion" on an evaluation made the model invent phantom Phase-1..7 todos
132
+ * and start editing files, then the rogue turn wedged the UI).
133
+ * - generate_plan / implement → execute the recommended action items.
134
+ * - save_exit / refine / retry_synthesis / follow-up / undefined → stop (those
135
+ * either already re-synthesized inside runCouncil or are terminal by intent).
136
+ */
137
+ const IMPLEMENTATION_OUTPUT_KINDS = new Set(["implementation_plan"]);
138
+ /** Recover the output-shape kind the synthesis was produced under (```json { "type": … }). */
139
+ function synthesisOutputKind(synthesis) {
140
+ const m = synthesis.match(/"type"\s*:\s*"([^"]+)"/);
141
+ return m?.[1];
142
+ }
143
+ export function postDebateContinuation(action, synthesis, outputKind) {
144
+ if (!synthesis || !action)
145
+ return null;
146
+ // IMPLEMENT — the user decided there is enough to build. Load the council
147
+ // conclusion back as the approved spec and carry it out through the normal
148
+ // workflow (the native GSD depth pipeline plans → executes → verifies). Works
149
+ // for ANY output kind: an analysis/decision synthesis is itself a sufficient
150
+ // spec, so this no longer needs a separate plan artifact. Scoped so the agent
151
+ // builds exactly what was decided and cannot balloon into phantom phases.
152
+ if (action === "generate_plan" || action === "implement") {
153
+ return (`Council debate completed. Approved conclusion:\n\n${synthesis}\n\n` +
154
+ `Implement this now. Treat the council conclusion above as the approved spec ` +
155
+ `— load it as your working context and carry it out through your normal ` +
156
+ `workflow: plan the concrete steps, make the changes in the smallest correct ` +
157
+ `increments, and verify (build/tests) as you go. Do NOT re-litigate the ` +
158
+ `decision or expand scope beyond it. If a required detail is genuinely ` +
159
+ `ambiguous, ask ONE focused question before editing.`);
160
+ }
161
+ if (action === "continue_session") {
162
+ const kind = outputKind ?? synthesisOutputKind(synthesis);
163
+ // Only an implementation-shaped debate has an "original task" left to build
164
+ // (the /ideal build flow relies on this carry-forward — do NOT null it out).
165
+ if (kind && IMPLEMENTATION_OUTPUT_KINDS.has(kind)) {
166
+ return `Council debate completed. Conclusion:\n\n${synthesis}\n\nContinue the original task using this conclusion.`;
167
+ }
168
+ // Analysis/evaluation/decision/investigation (or unknown → analysis): the
169
+ // user chose to KEEP THE SESSION GOING without implementing. Stop at the
170
+ // composer — the synthesis was already shown on the debate card and is
171
+ // persisted as [Council Decision]/[Council Memory] system messages, so the
172
+ // user's NEXT message inherits the full council context automatically
173
+ // (buildCouncilContextBundle surfaces it under "Key Decisions"). Returning
174
+ // null avoids the wasteful re-present turn AND the old forbid lecture, while
175
+ // still preventing the phantom-implementation drift (nothing runs). To
176
+ // actually build, the user picks Implement above; to keep discussing, they
177
+ // just type — that turn inherits the council context.
178
+ return null;
179
+ }
180
+ return null;
181
+ }
88
182
  export async function* runCouncil(topic, sessionModelId, messages, sessionId, rawLlm, respondToQuestion, respondToPreflight, processMessageFn, options) {
89
183
  const stats = options?.councilStats ?? { calls: 0, startMs: Date.now(), phases: [] };
90
184
  const costAware = isCouncilCostAware();
@@ -103,7 +197,21 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
103
197
  // ── Resolve models ──────────────────────────────────────────────────────────
104
198
  const leaderResolution = await resolveLeaderModelDetailed(sessionModelId);
105
199
  const leaderModelId = leaderResolution.modelId;
106
- const participants = await resolveParticipants(sessionModelId, isCouncilMultiProviderPreferred());
200
+ let participants = await resolveParticipants(sessionModelId, isCouncilMultiProviderPreferred());
201
+ // U3 — task-aware panel: let the leader read the task and pick which reachable
202
+ // models should debate it, instead of the prompt-blind capability roster.
203
+ // Fails open to the default roster on any provider/parse failure.
204
+ if (participants.length >= 2 && isTaskAwarePanelEnabled()) {
205
+ try {
206
+ const pool = await buildCouncilCandidatePool(participants);
207
+ const taskAware = yield* selectTaskAwarePanel({ topic, pool, leaderModelId, llm });
208
+ if (taskAware && taskAware.length >= 2)
209
+ participants = taskAware;
210
+ }
211
+ catch {
212
+ /* fail-open — keep the default roster */
213
+ }
214
+ }
107
215
  if (participants.length < 2) {
108
216
  yield {
109
217
  type: "content",
@@ -112,21 +220,48 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
112
220
  yield { type: "done" };
113
221
  return null;
114
222
  }
115
- if (leaderResolution.promotedFrom) {
223
+ // When the TUI Context Rail is active it renders the leader/panel/cost data as
224
+ // ambient sidebar rows from the council_meta patch below, so emitting the same
225
+ // data inline would both duplicate it AND read as a roster "decided" before any
226
+ // task assessment. Railless sinks (headless, telegram) keep the inline summary.
227
+ const suppressInlineMeta = options?.suppressInlineMeta === true;
228
+ if (!suppressInlineMeta) {
229
+ if (leaderResolution.promotedFrom) {
230
+ yield {
231
+ type: "content",
232
+ content: `\n> Leader auto-promoted within session provider: \`${leaderResolution.promotedFrom.modelId}\`` +
233
+ `${leaderResolution.promotedFrom.tier ? ` (${leaderResolution.promotedFrom.tier})` : ""}` +
234
+ ` → \`${leaderModelId}\`. Synthesis benefits from the highest tier available on the same provider. ` +
235
+ `Set \`roleModels.leader\` to override.\n`,
236
+ };
237
+ }
116
238
  yield {
117
239
  type: "content",
118
- content: `\n> Leader auto-promoted within session provider: \`${leaderResolution.promotedFrom.modelId}\`` +
119
- `${leaderResolution.promotedFrom.tier ? ` (${leaderResolution.promotedFrom.tier})` : ""}` +
120
- ` \`${leaderModelId}\`. Synthesis benefits from the highest tier available on the same provider. ` +
121
- `Set \`roleModels.leader\` to override.\n`,
240
+ // Show models only the `implement/verify/research` roles are internal
241
+ // cost-tier routing slots, NOT debate personas (those are task-adaptive and
242
+ // shown in the Debate Plan card once assigned). Printing the slot names here
243
+ // misleadingly implied implementation intent on analysis/decision topics.
244
+ content: `\n> Leader: \`${leaderModelId}\` · Panel: ${participants.map((p) => `\`${p.model}\``).join(", ")}${costAware ? " · Cost-aware sub-tasks: ON" : ""}\n`,
122
245
  };
123
246
  }
247
+ // P3 — mirror the leader/panel/cost metadata as a structured council_meta patch
248
+ // so the context rail can show it as rows instead of transcript spam. The round
249
+ // budget/ceiling arrive later from inside runDebate (locals unavailable here).
124
250
  yield {
125
- type: "content",
126
- content: `\n> Leader: \`${leaderModelId}\` · Participants: ${participants.map((p) => `\`${p.role}:${p.model}\``).join(", ")}${costAware ? " · Cost-aware sub-tasks: ON" : ""}\n`,
251
+ type: "council_meta",
252
+ councilMeta: {
253
+ topic,
254
+ leader: leaderModelId,
255
+ panel: participants.map((p) => p.model),
256
+ costAware,
257
+ },
127
258
  };
128
259
  const baseContext = buildCouncilContext(messages);
129
- const projectInfo = options?.cwd ? await buildProjectSnapshot(options.cwd) : { snapshot: "", isEmpty: true };
260
+ // Fall back to process.cwd() when the caller omits cwd. The old default of
261
+ // { isEmpty: true } forced internet-first research (and skipped codebase-first
262
+ // analysis) even when the council was invoked inside a real repo.
263
+ const projectCwd = options?.cwd ?? process.cwd();
264
+ const projectInfo = await buildProjectSnapshot(projectCwd);
130
265
  const conversationContext = projectInfo.snapshot
131
266
  ? `## Current Project\n${projectInfo.snapshot}\n\n---\n\n${baseContext}`
132
267
  : baseContext;
@@ -163,7 +298,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
163
298
  content: `\n> Clarification seeded by PIL (${pilSeed.length} gray-area question${pilSeed.length === 1 ? "" : "s"}).\n`,
164
299
  };
165
300
  }
166
- const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, options?.clarifyMaxRounds ?? EXPLICIT_COUNCIL_CLARIFY_ROUNDS, undefined, costAware);
301
+ const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, options?.clarifyMaxRounds ?? EXPLICIT_COUNCIL_CLARIFY_ROUNDS, undefined, costAware, participants.map((p) => p.model));
167
302
  let clarifyResult;
168
303
  do {
169
304
  clarifyResult = await clarifyGen.next();
@@ -177,13 +312,44 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
177
312
  spec = buildSpecFromTopic(topic, conversationContext);
178
313
  yield { type: "content", content: `\n> Auto-council: skipping clarification (PIL pre-classified).\n` };
179
314
  }
315
+ // Guarantee context continuity on BOTH paths: the explicit `/council`
316
+ // clarifier (synthesizeSpec / inferSpecFromTopicOnly) does not always set
317
+ // parentContext, and the skip path sets it via buildSpecFromTopic. Attach it
318
+ // centrally here so every downstream debate stage sees the ongoing task
319
+ // context regardless of how the council was triggered.
320
+ if (!spec.parentContext) {
321
+ spec.parentContext = conversationContext?.trim() || undefined;
322
+ }
323
+ // B2: pin the outcome criteria into the Context Rail so the user SEES what
324
+ // the debate is graded against (not a leader-improvised per-round criterion).
325
+ // Emitted once here; per-round met/pending arrives via later council_meta
326
+ // patches from debate.ts. Only emit when there is something meaningful (skip
327
+ // the single "Address the topic" auto-fallback).
328
+ if (spec.successCriteria.length > 0) {
329
+ // Emit a count-matched all-false criteriaMet ALONGSIDE successCriteria so
330
+ // the rail's Outcome block starts at 0/N. councilMeta is upsert-merged
331
+ // ({...prev, ...patch}); without this reset a previous council's
332
+ // criteriaMet array bleeds through (e.g. after an Esc-interrupt that
333
+ // skipped clearLiveTurnUi) and paints stale ✓ / a wrong "N/N met" counter
334
+ // before this debate has graded anything. debate.ts overwrites it post-eval.
335
+ yield {
336
+ type: "council_meta",
337
+ councilMeta: {
338
+ successCriteria: spec.successCriteria,
339
+ criteriaMet: spec.successCriteria.map(() => false),
340
+ },
341
+ };
342
+ }
180
343
  // Cancelled during clarification — don't pop the preflight approval card.
181
344
  if (userAborted())
182
345
  break;
183
346
  const researchNeeded = true;
347
+ // ROI: when the clarifier judged the spec ready (high confidence, no gaps),
348
+ // the approve card is a rubber-stamp — auto-approve after showing the brief.
184
349
  const preflightGen = runPreflight(spec, participants, researchNeeded, respondToPreflight, {
185
350
  repoEmpty: internetFirst,
186
351
  researchOverridable: true,
352
+ autoApprove: spec.ready === true || options?.autoApprovePreflight === true,
187
353
  });
188
354
  let preflightResult;
189
355
  do {
@@ -204,59 +370,47 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
204
370
  // Leader-LLM decides if research is required. If yes, give the user a chance
205
371
  // to skip — research is the slowest part of council and trivial questions
206
372
  // (e.g. "what did we just decide?") should not pay that cost.
207
- let researchSkipOverride = false;
373
+ // When the caller (sprint-planning) already has product-level research from
374
+ // CB-1, skip the second research pass entirely: force researchSkipOverride so
375
+ // runDebate does not re-run it, and short-circuit leaderNeedsResearch to false.
376
+ const researchSkipOverride = options?.skipResearch === true;
208
377
  // Hoisted so the leader's research decision can be reused by runDebate instead
209
378
  // of re-running the classifier LLM call (see CouncilConfig.leaderNeedsResearch).
210
379
  // Stays undefined if the classifier throws — fail-open: runDebate re-evaluates.
211
380
  let leaderNeedsResearch;
212
- try {
213
- const needGen = evaluateResearchNeed(spec, leaderModelId, conversationContext, llm, costAware);
214
- let needStep;
215
- do {
216
- needStep = await needGen.next();
217
- if (!needStep.done && needStep.value)
218
- yield needStep.value;
219
- } while (!needStep.done);
220
- leaderNeedsResearch = needStep.value;
221
- if (leaderNeedsResearch) {
222
- const { randomUUID } = await import("crypto");
223
- const overrideId = randomUUID();
224
- yield {
225
- type: "council_question",
226
- content: `\n## Research decision\nLeader recommends a research phase before debate` +
227
- (internetFirst ? " (internet-first — empty workspace)" : " (codebase-first)") +
228
- `. Want to skip it?`,
229
- councilQuestion: {
230
- questionId: overrideId,
231
- phase: "post-debate",
232
- question: "Skip the research phase?",
233
- context: internetFirst
234
- ? "Workspace is empty — research will search the internet. Skip if you already have the answer."
235
- : "Research will grep/read the codebase. Skip for trivial topics that don't need code evidence.",
236
- isRequired: false,
237
- options: [
238
- {
239
- label: "No — run research (recommended)",
240
- description: "Leader thinks evidence is needed.",
241
- value: "no",
242
- kind: "choice",
243
- },
244
- { label: "Yes — skip research", description: "Go straight to debate.", value: "yes", kind: "choice" },
245
- ],
246
- defaultIndex: 0,
247
- },
248
- };
249
- const overrideAnswer = await respondToQuestion(overrideId);
250
- researchSkipOverride = overrideAnswer === "yes";
251
- yield {
252
- type: "content",
253
- content: `\n ↳ ${researchSkipOverride ? "Skipping research per user override." : "Running research."}\n`,
254
- };
255
- }
381
+ if (options?.skipResearch) {
382
+ leaderNeedsResearch = false;
383
+ yield { type: "council_meta", councilMeta: { researchMode: false } };
256
384
  }
257
- catch (err) {
258
- // fail-open — leaderNeedsResearch stays undefined so runDebate re-evaluates.
259
- console.error(`[council] research-need pre-check failed (fail-open): ${err?.message}`);
385
+ else {
386
+ try {
387
+ const needGen = evaluateResearchNeed(spec, leaderModelId, conversationContext, llm, costAware);
388
+ let needStep;
389
+ do {
390
+ needStep = await needGen.next();
391
+ if (!needStep.done && needStep.value)
392
+ yield needStep.value;
393
+ } while (!needStep.done);
394
+ leaderNeedsResearch = needStep.value;
395
+ if (leaderNeedsResearch !== undefined) {
396
+ yield { type: "council_meta", councilMeta: { researchMode: leaderNeedsResearch } };
397
+ }
398
+ // ROI: the leader already decided research is needed and the card's default
399
+ // was always "run research" — asking the user to confirm is a rubber-stamp
400
+ // (measured 0 information at real cost). Auto-proceed with research; the
401
+ // leaderNeedsResearch signal still flows to runDebate. researchSkipOverride
402
+ // stays false. (Deliberately no card — see council-UX ROI pass.)
403
+ if (leaderNeedsResearch) {
404
+ yield {
405
+ type: "content",
406
+ content: `\n ↳ Leader recommends research${internetFirst ? " (internet-first — empty workspace)" : " (codebase-first)"} — running it.\n`,
407
+ };
408
+ }
409
+ }
410
+ catch (err) {
411
+ // fail-open — leaderNeedsResearch stays undefined so runDebate re-evaluates.
412
+ console.error(`[council] research-need pre-check failed (fail-open): ${err?.message}`);
413
+ }
260
414
  }
261
415
  // Await EE pre-fetch (started in parallel with clarifier — latency already hidden)
262
416
  const eeResult = await eePromise;
@@ -341,6 +495,14 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
341
495
  internetFirst,
342
496
  costAware,
343
497
  runId: sessionId,
498
+ // #2 — isolated research bridge; when wired, runDebate runs research in a
499
+ // budget-capped explore sub-agent instead of an in-process 15-step call.
500
+ runIsolatedTask: options?.runIsolatedTask,
501
+ // B4 interactive escalation — same responder the clarifier + post-debate
502
+ // askcards use. When the debate is about to stop with pinned criteria
503
+ // unmet, runDebate asks the user (extend / accept / rescope) instead of
504
+ // silently synthesizing a partial outcome.
505
+ respondToQuestion,
344
506
  }, llm);
345
507
  let debateResult;
346
508
  do {
@@ -393,6 +555,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
393
555
  } while (!planResult.done);
394
556
  let { outcome, plan, synthesisText } = planResult.value;
395
557
  const synthesisFailReason = planResult.value.synthesisFailReason;
558
+ // Post-debate action the user picked (hoisted so the completed-status guard +
559
+ // the caller's auto-continue can both read it). Undefined until the card is
560
+ // answered.
561
+ let postDebateAction;
396
562
  stats.phases.push({ name: "planning", durationMs: Date.now() - planStart });
397
563
  // Log interaction: synthesis
398
564
  logInteraction(sessionId ?? "unknown", "council", {
@@ -422,7 +588,19 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
422
588
  const hasEmptySections = refinementTopics.length > 0;
423
589
  // ── Confidence badge (CQ-6) ──────────────────────────────────────────
424
590
  const evidenceDensity = debateState.finalEvidenceDensity ?? 0;
591
+ const taggedClaims = debateState.finalTaggedClaims ?? 0;
425
592
  const synthesisFailed = !!synthesisFailReason || !outcome || synthesisText.trim().length < 20;
593
+ // "Not measured" ≠ "0%". When the debate emitted zero tagged claims the
594
+ // density formula returns 0 by convention, but that means grounding was
595
+ // never measured — not that every claim was refuted. Surfacing "Low 0%"
596
+ // there reads as a scoring failure on debates that are actually fine
597
+ // (session de4bafe5ecb7). Only applies when synthesis itself succeeded.
598
+ // Also treat a genuine 0 density (tags emitted but none resolved to a
599
+ // citation) as "not measured" rather than a literal "Low 0%" score — a
600
+ // bare 0% reads as a scoring failure on debates that were degraded (e.g.
601
+ // the debate model tripped the tool-verification circuit breaker and ran
602
+ // tool-free, so no claims could be grounded). Session 65b66c99ed36.
603
+ const confidenceNotMeasured = !synthesisFailed && (taggedClaims === 0 || evidenceDensity === 0);
426
604
  const confidenceLevel = synthesisFailed
427
605
  ? "low"
428
606
  : evidenceDensity >= 0.6
@@ -437,18 +615,29 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
437
615
  const confidenceReason = synthesisFailed
438
616
  ? (synthesisFailReason ??
439
617
  "The synthesizer produced no usable output. The debate exchanges above are still readable, but no structured outcome could be extracted.")
440
- : confidenceLevel === "low"
441
- ? `Only ${(evidenceDensity * 100).toFixed(0)}% of claims in the final round carried citations or were resolved most positions remained asserted without backing evidence.`
442
- : confidenceLevel === "medium"
443
- ? `${(evidenceDensity * 100).toFixed(0)}% of claims carried citations or were resolved — some open points remain.`
444
- : `${(evidenceDensity * 100).toFixed(0)}% of claims were cited or resolved.`;
618
+ : confidenceNotMeasured
619
+ ? "The debate produced no explicitly tagged claims ([CONFIRMED]/[REFUTED]/[UNVERIFIED]), so evidence grounding could not be measured — this is NOT a 0% score. The exchanges above may still be substantive; read them directly, or re-run with research enabled to force citations."
620
+ : confidenceLevel === "low"
621
+ ? `Only ${(evidenceDensity * 100).toFixed(0)}% of claims in the final round carried citations or were resolved — most positions remained asserted without backing evidence.`
622
+ : confidenceLevel === "medium"
623
+ ? `${(evidenceDensity * 100).toFixed(0)}% of claims carried citations or were resolved — some open points remain.`
624
+ : `${(evidenceDensity * 100).toFixed(0)}% of claims were cited or resolved.`;
445
625
  const confidenceBadge = synthesisFailed
446
626
  ? `❌ Synthesis failed — confidence cannot be computed`
447
- : confidenceLevel === "high"
448
- ? `✅ High confidence (evidence density ${evidenceDensity.toFixed(2)})`
449
- : confidenceLevel === "medium"
450
- ? `⚠ Medium confidence (evidence density ${evidenceDensity.toFixed(2)})`
451
- : `⚠ Low confidence (evidence density ${evidenceDensity.toFixed(2)})`;
627
+ : confidenceNotMeasured
628
+ ? `◐ Confidence not measured the debate emitted no tagged claims`
629
+ : confidenceLevel === "high"
630
+ ? `✅ High confidence (evidence density ${evidenceDensity.toFixed(2)})`
631
+ : confidenceLevel === "medium"
632
+ ? `⚠ Medium confidence (evidence density ${evidenceDensity.toFixed(2)})`
633
+ : `⚠ Low confidence (evidence density ${evidenceDensity.toFixed(2)})`;
634
+ // F1 — did the debate actually satisfy its PINNED success criteria? This is
635
+ // distinct from evidence density (a confidently-worded synthesis can still
636
+ // leave every criterion open). When criteria remain unmet on a successful
637
+ // synthesis the outcome is provisional, and the card must not recommend
638
+ // committing (implement/plan/save) as if it were settled.
639
+ const critOutcome = summarizeCriteriaOutcome(spec.successCriteria ?? [], debateState.finalCriteriaMet);
640
+ const inconclusive = !synthesisFailed && critOutcome.inconclusive;
452
641
  // Recommendation surfaced to the user as the default action. The
453
642
  // implementation_plan-vs-decision/evaluation split lives in
454
643
  // pickPostDebateRecommendation (issue #3 — see its doc comment).
@@ -459,83 +648,282 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
459
648
  confidenceLevel,
460
649
  hasPlan: !!hasPlan,
461
650
  outputKind: debatePlan.outputShape.kind,
651
+ criteriaUnmet: inconclusive ? critOutcome.unmetLabels.length : 0,
462
652
  });
463
653
  const baseOptions = [];
464
- if (synthesisFailed) {
654
+ // Model-first post-debate options. The leader synthesis picks intent-fit
655
+ // next actions (a bug investigation, evaluation, plan, and pure discussion
656
+ // each warrant different follow-ups — the old fixed "accept / research /
657
+ // apply" menu was wrong regardless of intent). Fall back to the
658
+ // deterministic set on synthesis failure or when the model emitted none.
659
+ const modelActions = !synthesisFailed && outcome?.nextActions && outcome.nextActions.length > 0 ? outcome.nextActions : null;
660
+ if (modelActions) {
661
+ for (const a of modelActions) {
662
+ // "implement" no longer needs a separate plan artifact — an analysis /
663
+ // decision synthesis IS the spec (postDebateContinuation loads it and
664
+ // runs the normal plan→change→verify workflow). The old `!hasPlan` drop
665
+ // is why a decision-to-change-code debate had NO build path and the
666
+ // user's "implement"-labelled pick did nothing (session 8191ecaee149).
667
+ baseOptions.push({
668
+ label: a.label,
669
+ // Description is the model's own `reason` (model-first — no hardcoded
670
+ // per-action prose). If the model was terse and omitted it, repeat
671
+ // the label rather than inventing system copy.
672
+ description: a.reason && a.reason.length > 0 ? a.reason : a.label,
673
+ value: a.action,
674
+ kind: a.action === "ask_followup" ? "freetext" : "choice",
675
+ });
676
+ }
677
+ // Context-only option the model doesn't own — surfaced when the debate
678
+ // left shape sections empty.
679
+ if (hasEmptySections) {
680
+ baseOptions.push({
681
+ label: `Refine: ${refinementTopics.join(", ")}`,
682
+ description: `Answer questions about ${refinementTopics.length} unresolved aspect(s)`,
683
+ value: "refine",
684
+ kind: "choice",
685
+ });
686
+ }
687
+ // Guarantee an escape hatch even if the model omitted one.
688
+ if (!baseOptions.some((o) => o.value === "save_exit" || o.value === "continue_session")) {
689
+ baseOptions.push({
690
+ label: "Save & Exit",
691
+ description: "Save the debate outcome and finish",
692
+ value: "save_exit",
693
+ kind: "choice",
694
+ });
695
+ }
696
+ }
697
+ else {
698
+ // ── Fallback: deterministic option set ──────────────────────────────
699
+ if (synthesisFailed) {
700
+ baseOptions.push({
701
+ label: "Retry Synthesis (compact)",
702
+ description: "Re-synthesize from final positions only (drop full exchange history). Fastest recovery from provider timeouts.",
703
+ value: "retry_synthesis",
704
+ kind: "choice",
705
+ });
706
+ }
465
707
  baseOptions.push({
466
- label: "Retry Synthesis (compact)",
467
- description: "Re-synthesize from final positions only (drop full exchange history). Fastest recovery from provider timeouts.",
468
- value: "retry_synthesis",
708
+ label: "Save & Exit",
709
+ description: synthesisFailed
710
+ ? "Save raw debate exchanges as-is; no structured outcome will be persisted"
711
+ : "Save the debate outcome and finish",
712
+ value: "save_exit",
469
713
  kind: "choice",
470
714
  });
471
- }
472
- baseOptions.push({
473
- label: "Save & Exit",
474
- description: synthesisFailed
475
- ? "Save raw debate exchanges as-is; no structured outcome will be persisted"
476
- : "Save the debate outcome and finish",
477
- value: "save_exit",
478
- kind: "choice",
479
- });
480
- if (!hasPlan && !synthesisFailed) {
715
+ if (!hasPlan && !synthesisFailed) {
716
+ baseOptions.push({
717
+ label: "Lock plan and execute Sprint 1",
718
+ description: "Commit the council outcome as the sprint plan and hand control to the sprint runner (planning → implementation → verification → judgment). Does NOT exit to /gsd.",
719
+ value: "generate_plan",
720
+ kind: "choice",
721
+ });
722
+ }
723
+ if (hasEmptySections && !synthesisFailed) {
724
+ baseOptions.push({
725
+ label: `Refine: ${refinementTopics.join(", ")}`,
726
+ description: `Answer questions about ${refinementTopics.length} unresolved aspect(s)`,
727
+ value: "refine",
728
+ kind: "choice",
729
+ });
730
+ }
731
+ // CQ-3: free-text follow-up to the council on the same debate context.
481
732
  baseOptions.push({
482
- label: "Lock plan and execute Sprint 1",
483
- description: "Commit the council outcome as the sprint plan and hand control to the sprint runner (planning implementation → verification → judgment). Does NOT exit to /gsd.",
484
- value: "generate_plan",
485
- kind: "choice",
733
+ label: "Ask Council a follow-up",
734
+ description: "Pose a new question that re-uses this debate's context (no new clarification).",
735
+ value: "ask_followup",
736
+ kind: "freetext",
486
737
  });
738
+ if (!synthesisFailed) {
739
+ baseOptions.push({
740
+ label: "Start Implementation",
741
+ description: "Load the council conclusion as the spec and build it (plan → change → verify)",
742
+ value: "implement",
743
+ kind: "choice",
744
+ });
745
+ }
487
746
  }
488
- if (hasEmptySections && !synthesisFailed) {
489
- baseOptions.push({
490
- label: `Refine: ${refinementTopics.join(", ")}`,
491
- description: `Answer questions about ${refinementTopics.length} unresolved aspect(s)`,
492
- value: "refine",
747
+ // Canonicalize the post-analysis choices to the user's mental model:
748
+ // IMPLEMENT / CONTINUE / SAVE (session 8191ecaee149 redesign).
749
+ // (a) "ask a follow-up" and "continue with council context" are the same
750
+ // thing to the user (both = keep the session going with the debate as
751
+ // context), so collapse a generic ask_followup into continue_session.
752
+ // A pinned criteria-recovery follow-up is added LATER (inconclusive /
753
+ // lowGrounding) and is intentionally distinct, so this only affects
754
+ // the base set built above.
755
+ // (b) guarantee a CONTINUE option exists.
756
+ // (c) offer IMPLEMENT whenever the synthesis is substantive (grounded &
757
+ // conclusive) — the conclusion IS the spec, no plan artifact needed.
758
+ if (!options?.sprintPlanningMode) {
759
+ const CONTINUE_OPT = {
760
+ label: "Continue with council context",
761
+ description: "Return to the composer — your next message keeps this debate's conclusion as context.",
762
+ value: "continue_session",
493
763
  kind: "choice",
764
+ };
765
+ const hasContinue = baseOptions.some((o) => o.value === "continue_session");
766
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
767
+ if (baseOptions[i].value !== "ask_followup")
768
+ continue;
769
+ if (hasContinue)
770
+ baseOptions.splice(i, 1); // merged away — continue already covers it
771
+ else
772
+ baseOptions[i] = { ...CONTINUE_OPT }; // convert the lone follow-up into continue
773
+ }
774
+ if (!baseOptions.some((o) => o.value === "continue_session"))
775
+ baseOptions.push({ ...CONTINUE_OPT });
776
+ if (!synthesisFailed && !inconclusive && !baseOptions.some((o) => o.value === "implement")) {
777
+ // Insert at index 1, NOT 0 — the model's own best-first pick stays the
778
+ // default (defaultIndex is 0 for model-first). We only GUARANTEE the
779
+ // build path is present + prominent; we don't override the model's
780
+ // judgment that building wasn't the recommended next move.
781
+ baseOptions.splice(1, 0, {
782
+ label: "Start Implementation",
783
+ description: "Load the council conclusion as the spec and build it (plan → change → verify)",
784
+ value: "implement",
785
+ kind: "choice",
786
+ });
787
+ }
788
+ }
789
+ // F1 — when the pinned criteria were not met, the model's best-first action
790
+ // (or the deterministic default) may be a commit/hand-back-the-decision step
791
+ // that treats the outcome as settled. Pin a criteria-aware "keep working"
792
+ // option at the front and make it the default so the recommended next move
793
+ // is honest about the unmet bar. Reuses ask_followup routing (freetext,
794
+ // re-runs on this debate's context) — no new downstream action. Deduped so
795
+ // the list never shows two ask_followup rows.
796
+ if (inconclusive) {
797
+ const openList = critOutcome.unmetLabels.join("; ");
798
+ const n = critOutcome.unmetLabels.length;
799
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
800
+ if (baseOptions[i].value === "ask_followup")
801
+ baseOptions.splice(i, 1);
802
+ }
803
+ baseOptions.unshift({
804
+ label: `Keep working the ${n} unmet criteri${n === 1 ? "on" : "a"}`,
805
+ description: `Still open: ${openList}. Pose a targeted follow-up to close ${n === 1 ? "it" : "them"} before committing.`,
806
+ value: "ask_followup",
807
+ kind: "freetext",
494
808
  });
495
809
  }
496
- // CQ-3: free-text follow-up to the council on the same debate context.
497
- baseOptions.push({
498
- label: "Ask Council a follow-up",
499
- description: "Pose a new question that re-uses this debate's context (no new clarification).",
500
- value: "ask_followup",
501
- kind: "freetext",
502
- });
503
- if (hasPlan) {
504
- baseOptions.push({
505
- label: "Start Implementation",
506
- description: "Execute the action plan now",
507
- value: "implement",
508
- kind: "choice",
810
+ // A2 synthesis succeeded but grounding is weak (density 0 / low /
811
+ // "not measured"). The honest next move is to RAISE confidence, not to
812
+ // commit or to ask a blind clarification — the user reported the askcard
813
+ // asked "clarify more?" without saying WHAT would help. Pin a guided
814
+ // follow-up that names the concrete confidence-raising ask (make the
815
+ // council cite/verify its weakest claims) and make it the default.
816
+ // Reuses ask_followup routing (freetext, re-runs on this debate's
817
+ // context) — no new downstream action. Skipped when `inconclusive`
818
+ // already pinned a criteria-aware follow-up, or when synthesis failed
819
+ // (the retry_synthesis path owns that recovery).
820
+ const lowGrounding = !synthesisFailed && !inconclusive && (confidenceNotMeasured || confidenceLevel === "low");
821
+ if (lowGrounding) {
822
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
823
+ if (baseOptions[i].value === "ask_followup")
824
+ baseOptions.splice(i, 1);
825
+ }
826
+ baseOptions.unshift({
827
+ label: "Raise confidence — have the council cite & verify",
828
+ description: "Grounding is weak: no claims were cited or resolved, so evidence density stayed at 0. Pose a follow-up that forces the council to back its weakest claims against the codebase or sources — that lifts confidence instead of committing on thin evidence.",
829
+ value: "ask_followup",
830
+ kind: "freetext",
509
831
  });
510
832
  }
511
- const defaultIndex = Math.max(0, baseOptions.findIndex((o) => o.value === recommendation.value));
512
- const heading = synthesisFailed ? "## Debate Synthesis Failed" : "## Debate Synthesis Complete";
513
- const recommendLine = `**Recommended:** ${baseOptions[defaultIndex]?.label ?? recommendation.value} ${recommendation.reason}`;
514
- const headerBlock = `${heading}\n\n> ${confidenceBadge}\n>\n> **Why:** ${confidenceReason}\n\n${recommendLine}\n\nLeader: \`${leaderModelId}\`. What would you like to do next?`;
515
- yield {
516
- type: "council_question",
517
- content: headerBlock,
518
- councilQuestion: {
519
- questionId,
520
- phase: "post-debate",
521
- question: synthesisFailed
522
- ? "Synthesis did not produce a structured outcome. How do you want to recover?"
523
- : hasEmptySections
524
- ? `The debate left ${refinementTopics.length} area(s) unresolved. Refine them or save the current outcome?`
525
- : "What would you like to do next?",
526
- context: `${confidenceBadge}\n${confidenceReason}` +
527
- (hasEmptySections ? `\nUnresolved areas: ${refinementTopics.join(", ")}` : "") +
528
- `\n→ ${recommendation.reason}`,
529
- isRequired: false,
530
- options: baseOptions,
531
- defaultIndex,
532
- },
533
- };
534
- const answer = await respondToQuestion(questionId);
535
- yield { type: "content", content: `\n ↳ ${answer}\n` };
833
+ // Model orders actions best-first (index 0 = recommended default); the
834
+ // fallback set uses the deterministic recommendation. When inconclusive,
835
+ // the pinned criteria option at index 0 is the honest default regardless of
836
+ // path.
837
+ const defaultIndex = inconclusive || lowGrounding
838
+ ? 0
839
+ : modelActions
840
+ ? 0
841
+ : Math.max(0, baseOptions.findIndex((o) => o.value === recommendation.value));
842
+ const recommendReason = inconclusive || lowGrounding
843
+ ? (baseOptions[0]?.description ?? recommendation.reason)
844
+ : modelActions
845
+ ? (baseOptions[0]?.description ?? recommendation.reason)
846
+ : recommendation.reason;
847
+ const heading = synthesisFailed
848
+ ? "## Debate Synthesis Failed"
849
+ : inconclusive
850
+ ? `## Debate Synthesis — Inconclusive (${critOutcome.metCount}/${critOutcome.total} criteria met)`
851
+ : "## Debate Synthesis Complete";
852
+ // F1 — an explicit provisional-outcome line so the user sees the unmet bar
853
+ // even if they skim past the recommendation.
854
+ const outcomeLine = inconclusive
855
+ ? `\n\n⚠ Outcome: ${critOutcome.metCount}/${critOutcome.total} criteria met. Unmet: ${critOutcome.unmetLabels.join("; ")}. Treat the synthesis as provisional — not a settled decision.`
856
+ : "";
857
+ const recommendLine = `**Recommended:** ${baseOptions[defaultIndex]?.label ?? recommendation.value} ${recommendReason}`;
858
+ // B — the live per-round transcript is cleared from the view at turn end
859
+ // (it renders as a bottom block decoupled from the timeline, so keeping it
860
+ // would mis-order later messages). The full exchange IS persisted though —
861
+ // point the user at it so the rounds aren't "lost" (user report: after a
862
+ // debate the rounds vanish with no way to re-read them). `/council inspect`
863
+ // is a registered slash command that replays [Council Round N] / [Council
864
+ // Memory] from the DB.
865
+ const roundsArchivedLine = debateState.roundCount > 0
866
+ ? `\n\n📋 All ${debateState.roundCount} debate round(s) are archived — run \`/council inspect ${sessionId}\` to re-read the full exchange.`
867
+ : "";
868
+ const headerBlock = `${heading}\n\n> ${confidenceBadge}\n>\n> **Why:** ${confidenceReason}${outcomeLine}\n\n${recommendLine}${roundsArchivedLine}\n\nLeader: \`${leaderModelId}\`. What would you like to do next?`;
869
+ let answer;
870
+ if (options?.sprintPlanningMode) {
871
+ // Blocker 4/5 fix: no interactive post-debate menu inside automated
872
+ // per-sprint planning. Presenting it stranded the sprint before
873
+ // implementation — picking "Save & Exit" ended the run with no Sprint
874
+ // Implementation, and "Refine" (the default) looped back into more
875
+ // debate. Auto-lock the synthesized plan (== "Lock plan and execute
876
+ // Sprint 1") and hand control back to the sprint runner.
877
+ answer = "generate_plan";
878
+ idealTrace("council.postDebate.autoLock", { sessionId });
879
+ yield {
880
+ type: "content",
881
+ content: "\n> Sprint plan synthesized — auto-locked and handed to the sprint runner " +
882
+ "(the product plan was already approved at the /ideal preflight).\n",
883
+ };
884
+ }
885
+ else {
886
+ yield {
887
+ type: "council_question",
888
+ content: headerBlock,
889
+ councilQuestion: {
890
+ questionId,
891
+ phase: "post-debate",
892
+ question: synthesisFailed
893
+ ? "Synthesis did not produce a structured outcome. How do you want to recover?"
894
+ : inconclusive
895
+ ? `${critOutcome.metCount}/${critOutcome.total} success criteria met — the outcome is provisional. Keep working the unmet criteria, or save it as-is?`
896
+ : hasEmptySections
897
+ ? `The debate left ${refinementTopics.length} area(s) unresolved. Refine them or save the current outcome?`
898
+ : "What would you like to do next?",
899
+ context: `${confidenceBadge}\n${confidenceReason}` +
900
+ (inconclusive ? `\nUnmet criteria: ${critOutcome.unmetLabels.join("; ")}` : "") +
901
+ (hasEmptySections ? `\nUnresolved areas: ${refinementTopics.join(", ")}` : "") +
902
+ `\n→ ${recommendation.reason}`,
903
+ isRequired: false,
904
+ options: baseOptions,
905
+ defaultIndex,
906
+ },
907
+ };
908
+ answer = await respondToQuestion(questionId);
909
+ }
910
+ postDebateAction = answer;
911
+ idealTrace("council.postDebate.answer", { sessionId, answer });
912
+ options?.onPostDebateAction?.(answer);
913
+ // Echo the human-readable option label, never the raw action id
914
+ // (`continue_session`, `save_exit`, …) — the id is an internal routing
915
+ // token users should never see. Free-text follow-ups (no matching option)
916
+ // echo verbatim.
917
+ const answeredLabel = baseOptions.find((o) => o.value === answer)?.label ?? answer;
918
+ // No "↳ choice" echo in sprint-planning mode — there was no user choice to
919
+ // echo (the plan was auto-locked above with its own status line).
920
+ if (!options?.sprintPlanningMode) {
921
+ yield { type: "content", content: `\n ↳ ${answeredLabel}\n` };
922
+ }
536
923
  // Treat any non-empty answer that doesn't match a known choice value as a follow-up question.
537
924
  const knownValues = new Set([
538
925
  "save_exit",
926
+ "continue_session",
539
927
  "generate_plan",
540
928
  "refine",
541
929
  "ask_followup",
@@ -603,6 +991,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
603
991
  synthesisText =
604
992
  `Sprint plan locked (${existingActionItems.length} steps):\n` +
605
993
  synthesizedPlan.steps.map((s) => `- [${s.priority}] ${s.description}`).join("\n");
994
+ idealTrace("council.generatePlan.locked.fast", {
995
+ sessionId,
996
+ actionItems: existingActionItems.length,
997
+ });
606
998
  }
607
999
  else {
608
1000
  yield { type: "content", content: "\n> Synthesizing sprint plan...\n" };
@@ -621,6 +1013,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
621
1013
  type: "content",
622
1014
  content: "\n> Plan locked — sprint runner will execute planning → implementation → verification → judgment.\n",
623
1015
  };
1016
+ idealTrace("council.generatePlan.locked.synth", {
1017
+ sessionId,
1018
+ synthesisLen: synthesisText?.length ?? 0,
1019
+ });
624
1020
  }
625
1021
  // Do NOT call runExecution here. Return synthesisText to the sprint-runner
626
1022
  // caller so it drives the full sprint lifecycle (Step 4–8 in sprint-runner.ts).
@@ -655,7 +1051,13 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
655
1051
  };
656
1052
  const ans = await respondToQuestion(sqId);
657
1053
  refinedAnswers.push({ section: label, answer: ans });
658
- yield { type: "content", content: `\n ↳ ${ans}\n` };
1054
+ // Only echo sections the user actually filled. "Skip leave as-is"
1055
+ // returns an empty value; echoing it emits a blank "↳ " bubble per
1056
+ // section (6 skips = 6 empty rows of transcript garbage). Prefix the
1057
+ // section label so a real answer reads as "↳ <section>: <answer>".
1058
+ if (ans.trim().length > 0) {
1059
+ yield { type: "content", content: `\n ↳ ${label}: ${ans}\n` };
1060
+ }
659
1061
  }
660
1062
  // Build refineContext string from user answers
661
1063
  const refineCtx = refinedAnswers
@@ -678,69 +1080,85 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
678
1080
  }
679
1081
  // "save_exit" and "implement" fall through to normal persistence
680
1082
  }
681
- catch {
682
- /* non-critical */
1083
+ catch (err) {
1084
+ // Post-debate interaction (menu, follow-up re-synthesis, refine) is
1085
+ // non-critical to the persisted outcome, so we swallow — but NEVER
1086
+ // silently: a throw here previously vanished, hiding a "generate_plan
1087
+ // stalled" root cause. Log it and breadcrumb it so blocker-5 forensics
1088
+ // can see whether the tail was reached via an exception.
1089
+ console.error(`[council] post-debate interaction failed: ${err?.message}`);
1090
+ idealTrace("council.postDebate.threw", { sessionId, err: err?.message });
683
1091
  }
684
1092
  }
1093
+ idealTrace("council.persist.start", { sessionId, hasOutcome: !!outcome, postDebateAction });
685
1094
  // ── Persist outcome ─────────────────────────────────────────────────────────
686
1095
  if (sessionId) {
687
1096
  try {
688
- if (outcome) {
689
- const agreedLine = outcome.agreed?.length ? `\nAgreed: ${outcome.agreed.join("; ")}` : "";
690
- const recLine = outcome.recommendation ? `\nRecommendation: ${outcome.recommendation}` : "";
691
- appendSystemMessage(sessionId, `[Council Decision]\nTopic: ${topic}\n${outcome.summary}${agreedLine}${recLine}`);
692
- appendSystemMessage(sessionId, `[Council Outcome]\n${JSON.stringify(outcome)}`);
693
- }
694
- const evidenceDensityPersist = debateState.finalEvidenceDensity ?? 0;
695
- const confidenceLevelPersist = evidenceDensityPersist >= 0.6 ? "high" : evidenceDensityPersist >= 0.3 ? "medium" : "low";
696
- const councilRecord = {
697
- topic,
698
- spec,
699
- debatePlan,
700
- leaderModel: leaderModelId,
701
- participants: debateState.active.map((a) => ({ role: a.role, model: a.model, stance: a.stance })),
702
- finalPositions: debateState.active.map((a) => ({ role: a.role, position: a.position })),
703
- archive: debateState.archive ?? [],
704
- synthesis: synthesisText,
705
- confidence: {
706
- level: confidenceLevelPersist,
707
- evidenceDensity: evidenceDensityPersist,
708
- rounds: debateState.roundCount,
709
- },
710
- stats: { calls: stats.calls, durationMs: Date.now() - stats.startMs, phases: stats.phases },
711
- timestamp: new Date().toISOString(),
712
- };
713
- appendSystemMessage(sessionId, `[Council Memory] ${JSON.stringify(councilRecord)}`);
714
- // Forensics-friendly summary row in interaction_logs. The full
715
- // [Council Memory] system message above is great for context replay but
716
- // can't be queried — `usage forensics` reads interaction_logs only.
717
- // Excerpts are capped to keep metadata_json small (~2-4KB per run).
718
- const stancesForLog = debateState.active.slice(0, 8).map((a) => ({
719
- role: a.role,
720
- model: a.model,
721
- stanceName: a.stance?.name,
722
- finalPositionExcerpt: (a.position ?? "").slice(0, 400),
723
- }));
724
- logInteraction(sessionId, "council", {
725
- eventSubtype: "council_summary",
726
- model: leaderModelId,
727
- durationMs: Date.now() - stats.startMs,
728
- data: {
1097
+ // Skip session-scoped persistence in sprintPlanningMode: messages /
1098
+ // interaction_logs FK-reference sessions(id), but the sprint-planning caller
1099
+ // passes the product-RUN id (no session row) "FOREIGN KEY constraint
1100
+ // failed" on the FIRST write, which under the catch below previously aborted
1101
+ // the whole block — silently taking writeDecisionsLock down with it. The
1102
+ // file-based decisions.lock still writes below (outside this guard).
1103
+ if (!options?.sprintPlanningMode) {
1104
+ if (outcome) {
1105
+ const agreedLine = outcome.agreed?.length ? `\nAgreed: ${outcome.agreed.join("; ")}` : "";
1106
+ const recLine = outcome.recommendation ? `\nRecommendation: ${outcome.recommendation}` : "";
1107
+ appendSystemMessage(sessionId, `[Council Decision]\nTopic: ${topic}\n${outcome.summary}${agreedLine}${recLine}`);
1108
+ appendSystemMessage(sessionId, `[Council Outcome]\n${JSON.stringify(outcome)}`);
1109
+ }
1110
+ const evidenceDensityPersist = debateState.finalEvidenceDensity ?? 0;
1111
+ const confidenceLevelPersist = evidenceDensityPersist >= 0.6 ? "high" : evidenceDensityPersist >= 0.3 ? "medium" : "low";
1112
+ const councilRecord = {
729
1113
  topic,
730
- roundCount: debateState.roundCount,
731
- participantCount: debateState.active.length,
732
- stances: stancesForLog,
733
- synthesisExcerpt: synthesisText.slice(0, 1500),
734
- evidenceDensity: evidenceDensityPersist,
735
- confidenceLevel: confidenceLevelPersist,
736
- recommendation: outcome?.recommendation?.slice(0, 400) ?? null,
737
- agreedCount: outcome?.agreed?.length ?? 0,
738
- },
739
- });
1114
+ spec,
1115
+ debatePlan,
1116
+ leaderModel: leaderModelId,
1117
+ participants: debateState.active.map((a) => ({ role: a.role, model: a.model, stance: a.stance })),
1118
+ finalPositions: debateState.active.map((a) => ({ role: a.role, position: a.position })),
1119
+ archive: debateState.archive ?? [],
1120
+ synthesis: synthesisText,
1121
+ confidence: {
1122
+ level: confidenceLevelPersist,
1123
+ evidenceDensity: evidenceDensityPersist,
1124
+ rounds: debateState.roundCount,
1125
+ },
1126
+ stats: { calls: stats.calls, durationMs: Date.now() - stats.startMs, phases: stats.phases },
1127
+ timestamp: new Date().toISOString(),
1128
+ };
1129
+ appendSystemMessage(sessionId, `[Council Memory] ${JSON.stringify(councilRecord)}`);
1130
+ // Forensics-friendly summary row in interaction_logs. The full
1131
+ // [Council Memory] system message above is great for context replay but
1132
+ // can't be queried — `usage forensics` reads interaction_logs only.
1133
+ // Excerpts are capped to keep metadata_json small (~2-4KB per run).
1134
+ const stancesForLog = debateState.active.slice(0, 8).map((a) => ({
1135
+ role: a.role,
1136
+ model: a.model,
1137
+ stanceName: a.stance?.name,
1138
+ finalPositionExcerpt: (a.position ?? "").slice(0, 400),
1139
+ }));
1140
+ logInteraction(sessionId, "council", {
1141
+ eventSubtype: "council_summary",
1142
+ model: leaderModelId,
1143
+ durationMs: Date.now() - stats.startMs,
1144
+ data: {
1145
+ topic,
1146
+ roundCount: debateState.roundCount,
1147
+ participantCount: debateState.active.length,
1148
+ stances: stancesForLog,
1149
+ synthesisExcerpt: synthesisText.slice(0, 1500),
1150
+ evidenceDensity: evidenceDensityPersist,
1151
+ confidenceLevel: confidenceLevelPersist,
1152
+ recommendation: outcome?.recommendation?.slice(0, 400) ?? null,
1153
+ agreedCount: outcome?.agreed?.length ?? 0,
1154
+ },
1155
+ });
1156
+ }
740
1157
  // C2: Persist decisions.lock.md to the run directory so sprint-runner
741
1158
  // can inject locked decisions into the implementation prompt.
742
1159
  if (options?.runDir) {
743
1160
  const rejectedProposals = detectOutOfStackProposals(synthesisText, spec);
1161
+ idealTrace("council.persist.writeDecisionsLock.before", { sessionId, runDir: options.runDir });
744
1162
  await writeDecisionsLock({
745
1163
  runId: sessionId,
746
1164
  runDir: options.runDir,
@@ -758,14 +1176,22 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
758
1176
  // only fires on an unexpected throw — log it (No-Silent-Catch), never break council.
759
1177
  console.error(`[council] decisions.lock write guard caught: ${err?.message}`);
760
1178
  });
1179
+ idealTrace("council.persist.writeDecisionsLock.after", { sessionId });
761
1180
  }
762
1181
  }
763
- catch {
764
- /* non-critical */
1182
+ catch (err) {
1183
+ // Persistence is best-effort (session-message / interaction-log writes),
1184
+ // but log so a storage fault is not mistaken for a hang in blocker-5
1185
+ // forensics.
1186
+ console.error(`[council] outcome persistence failed: ${err?.message}`);
1187
+ idealTrace("council.persist.threw", { sessionId, err: err?.message });
765
1188
  }
766
1189
  }
767
- // Update session status to completed
768
- if (sessionId) {
1190
+ idealTrace("council.persist.done", { sessionId });
1191
+ // Update session status to completed — EXCEPT when the user chose
1192
+ // "continue_session", where the agent keeps working in this session; marking
1193
+ // it completed here is what dropped it from the resume picker.
1194
+ if (sessionId && postDebateAction !== "continue_session") {
769
1195
  try {
770
1196
  new SessionStore(options?.cwd ?? process.cwd()).setStatus(sessionId, "completed");
771
1197
  }
@@ -802,15 +1228,27 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
802
1228
  stats.phases.push({ name: "execution", durationMs: Date.now() - execStart });
803
1229
  }
804
1230
  // ── Stats ───────────────────────────────────────────────────────────────────
1231
+ idealTrace("council.stats", { sessionId });
805
1232
  const totalMs = Date.now() - stats.startMs;
806
- yield {
807
- type: "content",
808
- content: `\n---\n` +
809
- `> Council stats: ${stats.calls} API calls, ${(totalMs / 1000).toFixed(1)}s total, ` +
810
- `${active.length} participants, ${debateState.roundCount} rounds\n` +
811
- `> Phases: ${stats.phases.map((p) => `${p.name}=${(p.durationMs / 1000).toFixed(1)}s`).join(", ")}\n`,
812
- };
813
- yield { type: "done" };
1233
+ // Blocker-5 root cause: in sprintPlanningMode this runCouncil is a SUB-STEP of
1234
+ // runSprint, not a standalone turn. The terminal `{type:"done"}` chunk (and the
1235
+ // stats banner) are turn-terminal signals — forwarded verbatim by sprint-runner
1236
+ // they made the app's stream consumer STOP pulling right here, so the generator
1237
+ // suspended at these yields and never returned. sprint-runner's `planGen.next()`
1238
+ // therefore never saw `done`, and the Sprint Implementation stage never ran
1239
+ // (idle at the composer, no error). A sub-step must not emit them: skip both so
1240
+ // the generator returns cleanly and the sprint runner proceeds to implementation.
1241
+ if (!options?.sprintPlanningMode) {
1242
+ yield {
1243
+ type: "content",
1244
+ content: `\n---\n` +
1245
+ `> Council stats: ${stats.calls} API calls, ${(totalMs / 1000).toFixed(1)}s total, ` +
1246
+ `${active.length} participants, ${debateState.roundCount} rounds\n` +
1247
+ `> Phases: ${stats.phases.map((p) => `${p.name}=${(p.durationMs / 1000).toFixed(1)}s`).join(", ")}\n`,
1248
+ };
1249
+ yield { type: "done" };
1250
+ }
1251
+ idealTrace("council.return", { sessionId, synthesisLen: (synthesisText || "").length });
814
1252
  return synthesisText || null;
815
1253
  }
816
1254
  // ── P7: action-item reuse helpers ─────────────────────────────────────────────