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
@@ -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,96 @@ 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
+ }
182
+ /**
183
+ * Neutral post-council continuation. Used by the auto-council path (tool-engine)
184
+ * and the `/council` slash path (runCouncilV2) once they run with
185
+ * `convenePath: true` — the hardcoded post-debate option card is suppressed, so
186
+ * there is no `chosenAction` to branch on. Instead of the CLI deciding the next
187
+ * step, we hand the synthesis back to a normal agent turn with a NON-BINDING
188
+ * nudge and let the agent's own intent drive the follow-up (respond / ask_user /
189
+ * implement). Returns "" for an empty synthesis so the caller skips re-entry.
190
+ */
191
+ export function buildNeutralPostCouncilContinuation(synthesis) {
192
+ if (!synthesis || !synthesis.trim())
193
+ return "";
194
+ return (`Council debate completed. Conclusion:\n\n${synthesis}\n\n` +
195
+ `You now decide the next step based on the user's original request — do not ` +
196
+ `stop without doing one of these:\n` +
197
+ ` • If the conclusion IS the deliverable (analysis/evaluation/decision), ` +
198
+ `respond to the user with it.\n` +
199
+ ` • If a choice genuinely needs the human before proceeding, call ask_user.\n` +
200
+ ` • If the task calls for building and the conclusion is a sufficient spec, ` +
201
+ `implement it now through your normal workflow — do NOT re-litigate the ` +
202
+ `decision or expand scope beyond it.`);
203
+ }
88
204
  export async function* runCouncil(topic, sessionModelId, messages, sessionId, rawLlm, respondToQuestion, respondToPreflight, processMessageFn, options) {
89
205
  const stats = options?.councilStats ?? { calls: 0, startMs: Date.now(), phases: [] };
90
206
  const costAware = isCouncilCostAware();
@@ -103,7 +219,21 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
103
219
  // ── Resolve models ──────────────────────────────────────────────────────────
104
220
  const leaderResolution = await resolveLeaderModelDetailed(sessionModelId);
105
221
  const leaderModelId = leaderResolution.modelId;
106
- const participants = await resolveParticipants(sessionModelId, isCouncilMultiProviderPreferred());
222
+ let participants = await resolveParticipants(sessionModelId, isCouncilMultiProviderPreferred());
223
+ // U3 — task-aware panel: let the leader read the task and pick which reachable
224
+ // models should debate it, instead of the prompt-blind capability roster.
225
+ // Fails open to the default roster on any provider/parse failure.
226
+ if (participants.length >= 2 && isTaskAwarePanelEnabled()) {
227
+ try {
228
+ const pool = await buildCouncilCandidatePool(participants);
229
+ const taskAware = yield* selectTaskAwarePanel({ topic, pool, leaderModelId, llm });
230
+ if (taskAware && taskAware.length >= 2)
231
+ participants = taskAware;
232
+ }
233
+ catch {
234
+ /* fail-open — keep the default roster */
235
+ }
236
+ }
107
237
  if (participants.length < 2) {
108
238
  yield {
109
239
  type: "content",
@@ -112,21 +242,48 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
112
242
  yield { type: "done" };
113
243
  return null;
114
244
  }
115
- if (leaderResolution.promotedFrom) {
245
+ // When the TUI Context Rail is active it renders the leader/panel/cost data as
246
+ // ambient sidebar rows from the council_meta patch below, so emitting the same
247
+ // data inline would both duplicate it AND read as a roster "decided" before any
248
+ // task assessment. Railless sinks (headless, telegram) keep the inline summary.
249
+ const suppressInlineMeta = options?.suppressInlineMeta === true;
250
+ if (!suppressInlineMeta) {
251
+ if (leaderResolution.promotedFrom) {
252
+ yield {
253
+ type: "content",
254
+ content: `\n> Leader auto-promoted within session provider: \`${leaderResolution.promotedFrom.modelId}\`` +
255
+ `${leaderResolution.promotedFrom.tier ? ` (${leaderResolution.promotedFrom.tier})` : ""}` +
256
+ ` → \`${leaderModelId}\`. Synthesis benefits from the highest tier available on the same provider. ` +
257
+ `Set \`roleModels.leader\` to override.\n`,
258
+ };
259
+ }
116
260
  yield {
117
261
  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`,
262
+ // Show models only the `implement/verify/research` roles are internal
263
+ // cost-tier routing slots, NOT debate personas (those are task-adaptive and
264
+ // shown in the Debate Plan card once assigned). Printing the slot names here
265
+ // misleadingly implied implementation intent on analysis/decision topics.
266
+ content: `\n> Leader: \`${leaderModelId}\` · Panel: ${participants.map((p) => `\`${p.model}\``).join(", ")}${costAware ? " · Cost-aware sub-tasks: ON" : ""}\n`,
122
267
  };
123
268
  }
269
+ // P3 — mirror the leader/panel/cost metadata as a structured council_meta patch
270
+ // so the context rail can show it as rows instead of transcript spam. The round
271
+ // budget/ceiling arrive later from inside runDebate (locals unavailable here).
124
272
  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`,
273
+ type: "council_meta",
274
+ councilMeta: {
275
+ topic,
276
+ leader: leaderModelId,
277
+ panel: participants.map((p) => p.model),
278
+ costAware,
279
+ },
127
280
  };
128
281
  const baseContext = buildCouncilContext(messages);
129
- const projectInfo = options?.cwd ? await buildProjectSnapshot(options.cwd) : { snapshot: "", isEmpty: true };
282
+ // Fall back to process.cwd() when the caller omits cwd. The old default of
283
+ // { isEmpty: true } forced internet-first research (and skipped codebase-first
284
+ // analysis) even when the council was invoked inside a real repo.
285
+ const projectCwd = options?.cwd ?? process.cwd();
286
+ const projectInfo = await buildProjectSnapshot(projectCwd);
130
287
  const conversationContext = projectInfo.snapshot
131
288
  ? `## Current Project\n${projectInfo.snapshot}\n\n---\n\n${baseContext}`
132
289
  : baseContext;
@@ -163,7 +320,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
163
320
  content: `\n> Clarification seeded by PIL (${pilSeed.length} gray-area question${pilSeed.length === 1 ? "" : "s"}).\n`,
164
321
  };
165
322
  }
166
- const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, options?.clarifyMaxRounds ?? EXPLICIT_COUNCIL_CLARIFY_ROUNDS, undefined, costAware);
323
+ 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
324
  let clarifyResult;
168
325
  do {
169
326
  clarifyResult = await clarifyGen.next();
@@ -177,13 +334,48 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
177
334
  spec = buildSpecFromTopic(topic, conversationContext);
178
335
  yield { type: "content", content: `\n> Auto-council: skipping clarification (PIL pre-classified).\n` };
179
336
  }
337
+ // Guarantee context continuity on BOTH paths: the explicit `/council`
338
+ // clarifier (synthesizeSpec / inferSpecFromTopicOnly) does not always set
339
+ // parentContext, and the skip path sets it via buildSpecFromTopic. Attach it
340
+ // centrally here so every downstream debate stage sees the ongoing task
341
+ // context regardless of how the council was triggered.
342
+ if (!spec.parentContext) {
343
+ spec.parentContext = conversationContext?.trim() || undefined;
344
+ }
345
+ // B2: pin the outcome criteria into the Context Rail so the user SEES what
346
+ // the debate is graded against (not a leader-improvised per-round criterion).
347
+ // Emitted once here; per-round met/pending arrives via later council_meta
348
+ // patches from debate.ts. Only emit when there is something meaningful (skip
349
+ // the single "Address the topic" auto-fallback).
350
+ if (spec.successCriteria.length > 0) {
351
+ // Emit a count-matched all-false criteriaMet ALONGSIDE successCriteria so
352
+ // the rail's Outcome block starts at 0/N. councilMeta is upsert-merged
353
+ // ({...prev, ...patch}); without this reset a previous council's
354
+ // criteriaMet array bleeds through (e.g. after an Esc-interrupt that
355
+ // skipped clearLiveTurnUi) and paints stale ✓ / a wrong "N/N met" counter
356
+ // before this debate has graded anything. debate.ts overwrites it post-eval.
357
+ yield {
358
+ type: "council_meta",
359
+ councilMeta: {
360
+ successCriteria: spec.successCriteria,
361
+ criteriaMet: spec.successCriteria.map(() => false),
362
+ },
363
+ };
364
+ }
180
365
  // Cancelled during clarification — don't pop the preflight approval card.
181
366
  if (userAborted())
182
367
  break;
183
368
  const researchNeeded = true;
369
+ // ROI: when the clarifier judged the spec ready (high confidence, no gaps),
370
+ // the approve card is a rubber-stamp — auto-approve after showing the brief.
184
371
  const preflightGen = runPreflight(spec, participants, researchNeeded, respondToPreflight, {
185
372
  repoEmpty: internetFirst,
186
373
  researchOverridable: true,
374
+ // convenePath auto-approves the pre-debate plan card too: the agent
375
+ // already decided to convene, so re-gating the discussion plan is a
376
+ // redundant interruption of the autonomous tool call (same rationale as
377
+ // sprintPlanningMode). The brief is still shown; it just isn't blocking.
378
+ autoApprove: spec.ready === true || options?.autoApprovePreflight === true || options?.convenePath === true,
187
379
  });
188
380
  let preflightResult;
189
381
  do {
@@ -204,59 +396,47 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
204
396
  // Leader-LLM decides if research is required. If yes, give the user a chance
205
397
  // to skip — research is the slowest part of council and trivial questions
206
398
  // (e.g. "what did we just decide?") should not pay that cost.
207
- let researchSkipOverride = false;
399
+ // When the caller (sprint-planning) already has product-level research from
400
+ // CB-1, skip the second research pass entirely: force researchSkipOverride so
401
+ // runDebate does not re-run it, and short-circuit leaderNeedsResearch to false.
402
+ const researchSkipOverride = options?.skipResearch === true;
208
403
  // Hoisted so the leader's research decision can be reused by runDebate instead
209
404
  // of re-running the classifier LLM call (see CouncilConfig.leaderNeedsResearch).
210
405
  // Stays undefined if the classifier throws — fail-open: runDebate re-evaluates.
211
406
  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
- }
407
+ if (options?.skipResearch) {
408
+ leaderNeedsResearch = false;
409
+ yield { type: "council_meta", councilMeta: { researchMode: false } };
256
410
  }
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}`);
411
+ else {
412
+ try {
413
+ const needGen = evaluateResearchNeed(spec, leaderModelId, conversationContext, llm, costAware);
414
+ let needStep;
415
+ do {
416
+ needStep = await needGen.next();
417
+ if (!needStep.done && needStep.value)
418
+ yield needStep.value;
419
+ } while (!needStep.done);
420
+ leaderNeedsResearch = needStep.value;
421
+ if (leaderNeedsResearch !== undefined) {
422
+ yield { type: "council_meta", councilMeta: { researchMode: leaderNeedsResearch } };
423
+ }
424
+ // ROI: the leader already decided research is needed and the card's default
425
+ // was always "run research" — asking the user to confirm is a rubber-stamp
426
+ // (measured 0 information at real cost). Auto-proceed with research; the
427
+ // leaderNeedsResearch signal still flows to runDebate. researchSkipOverride
428
+ // stays false. (Deliberately no card — see council-UX ROI pass.)
429
+ if (leaderNeedsResearch) {
430
+ yield {
431
+ type: "content",
432
+ content: `\n ↳ Leader recommends research${internetFirst ? " (internet-first — empty workspace)" : " (codebase-first)"} — running it.\n`,
433
+ };
434
+ }
435
+ }
436
+ catch (err) {
437
+ // fail-open — leaderNeedsResearch stays undefined so runDebate re-evaluates.
438
+ console.error(`[council] research-need pre-check failed (fail-open): ${err?.message}`);
439
+ }
260
440
  }
261
441
  // Await EE pre-fetch (started in parallel with clarifier — latency already hidden)
262
442
  const eeResult = await eePromise;
@@ -341,6 +521,17 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
341
521
  internetFirst,
342
522
  costAware,
343
523
  runId: sessionId,
524
+ // #2 — isolated research bridge; when wired, runDebate runs research in a
525
+ // budget-capped explore sub-agent instead of an in-process 15-step call.
526
+ runIsolatedTask: options?.runIsolatedTask,
527
+ // B4 interactive escalation — same responder the clarifier + post-debate
528
+ // askcards use. When the debate is about to stop with pinned criteria
529
+ // unmet, runDebate asks the user (extend / accept / rescope) instead of
530
+ // silently synthesizing a partial outcome.
531
+ respondToQuestion,
532
+ // convene_council path — auto-accept escalation (no blocking card) since
533
+ // the council runs autonomously mid-agent-turn with no interactive user.
534
+ convenePath: options?.convenePath,
344
535
  }, llm);
345
536
  let debateResult;
346
537
  do {
@@ -393,6 +584,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
393
584
  } while (!planResult.done);
394
585
  let { outcome, plan, synthesisText } = planResult.value;
395
586
  const synthesisFailReason = planResult.value.synthesisFailReason;
587
+ // Post-debate action the user picked (hoisted so the completed-status guard +
588
+ // the caller's auto-continue can both read it). Undefined until the card is
589
+ // answered.
590
+ let postDebateAction;
396
591
  stats.phases.push({ name: "planning", durationMs: Date.now() - planStart });
397
592
  // Log interaction: synthesis
398
593
  logInteraction(sessionId ?? "unknown", "council", {
@@ -402,7 +597,13 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
402
597
  data: { topic, roundCount: debateState.roundCount, participantCount: debateState.active.length },
403
598
  });
404
599
  // ── Post-Debate AskCard: What next? ─────────────────────────────────────────
405
- if (sessionId) {
600
+ // convenePath skips this ENTIRE interactive block (recommendation, option set,
601
+ // card, respondToQuestion, postDebateAction, onPostDebateAction, and the whole
602
+ // routing tree). On that path the agent that called `convene_council` decides
603
+ // what happens next — the CLI must not hardcode a post-council pick. The
604
+ // persistence block below still runs (audit trail, not a decision), and the
605
+ // function returns synthesisText as usual.
606
+ if (sessionId && !options?.convenePath) {
406
607
  try {
407
608
  const { randomUUID } = await import("crypto");
408
609
  const refinementTopics = [];
@@ -422,7 +623,19 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
422
623
  const hasEmptySections = refinementTopics.length > 0;
423
624
  // ── Confidence badge (CQ-6) ──────────────────────────────────────────
424
625
  const evidenceDensity = debateState.finalEvidenceDensity ?? 0;
626
+ const taggedClaims = debateState.finalTaggedClaims ?? 0;
425
627
  const synthesisFailed = !!synthesisFailReason || !outcome || synthesisText.trim().length < 20;
628
+ // "Not measured" ≠ "0%". When the debate emitted zero tagged claims the
629
+ // density formula returns 0 by convention, but that means grounding was
630
+ // never measured — not that every claim was refuted. Surfacing "Low 0%"
631
+ // there reads as a scoring failure on debates that are actually fine
632
+ // (session de4bafe5ecb7). Only applies when synthesis itself succeeded.
633
+ // Also treat a genuine 0 density (tags emitted but none resolved to a
634
+ // citation) as "not measured" rather than a literal "Low 0%" score — a
635
+ // bare 0% reads as a scoring failure on debates that were degraded (e.g.
636
+ // the debate model tripped the tool-verification circuit breaker and ran
637
+ // tool-free, so no claims could be grounded). Session 65b66c99ed36.
638
+ const confidenceNotMeasured = !synthesisFailed && (taggedClaims === 0 || evidenceDensity === 0);
426
639
  const confidenceLevel = synthesisFailed
427
640
  ? "low"
428
641
  : evidenceDensity >= 0.6
@@ -437,18 +650,29 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
437
650
  const confidenceReason = synthesisFailed
438
651
  ? (synthesisFailReason ??
439
652
  "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.`;
653
+ : confidenceNotMeasured
654
+ ? "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."
655
+ : confidenceLevel === "low"
656
+ ? `Only ${(evidenceDensity * 100).toFixed(0)}% of claims in the final round carried citations or were resolved — most positions remained asserted without backing evidence.`
657
+ : confidenceLevel === "medium"
658
+ ? `${(evidenceDensity * 100).toFixed(0)}% of claims carried citations or were resolved — some open points remain.`
659
+ : `${(evidenceDensity * 100).toFixed(0)}% of claims were cited or resolved.`;
445
660
  const confidenceBadge = synthesisFailed
446
661
  ? `❌ 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)})`;
662
+ : confidenceNotMeasured
663
+ ? `◐ Confidence not measured the debate emitted no tagged claims`
664
+ : confidenceLevel === "high"
665
+ ? `✅ High confidence (evidence density ${evidenceDensity.toFixed(2)})`
666
+ : confidenceLevel === "medium"
667
+ ? `⚠ Medium confidence (evidence density ${evidenceDensity.toFixed(2)})`
668
+ : `⚠ Low confidence (evidence density ${evidenceDensity.toFixed(2)})`;
669
+ // F1 — did the debate actually satisfy its PINNED success criteria? This is
670
+ // distinct from evidence density (a confidently-worded synthesis can still
671
+ // leave every criterion open). When criteria remain unmet on a successful
672
+ // synthesis the outcome is provisional, and the card must not recommend
673
+ // committing (implement/plan/save) as if it were settled.
674
+ const critOutcome = summarizeCriteriaOutcome(spec.successCriteria ?? [], debateState.finalCriteriaMet);
675
+ const inconclusive = !synthesisFailed && critOutcome.inconclusive;
452
676
  // Recommendation surfaced to the user as the default action. The
453
677
  // implementation_plan-vs-decision/evaluation split lives in
454
678
  // pickPostDebateRecommendation (issue #3 — see its doc comment).
@@ -459,83 +683,282 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
459
683
  confidenceLevel,
460
684
  hasPlan: !!hasPlan,
461
685
  outputKind: debatePlan.outputShape.kind,
686
+ criteriaUnmet: inconclusive ? critOutcome.unmetLabels.length : 0,
462
687
  });
463
688
  const baseOptions = [];
464
- if (synthesisFailed) {
689
+ // Model-first post-debate options. The leader synthesis picks intent-fit
690
+ // next actions (a bug investigation, evaluation, plan, and pure discussion
691
+ // each warrant different follow-ups — the old fixed "accept / research /
692
+ // apply" menu was wrong regardless of intent). Fall back to the
693
+ // deterministic set on synthesis failure or when the model emitted none.
694
+ const modelActions = !synthesisFailed && outcome?.nextActions && outcome.nextActions.length > 0 ? outcome.nextActions : null;
695
+ if (modelActions) {
696
+ for (const a of modelActions) {
697
+ // "implement" no longer needs a separate plan artifact — an analysis /
698
+ // decision synthesis IS the spec (postDebateContinuation loads it and
699
+ // runs the normal plan→change→verify workflow). The old `!hasPlan` drop
700
+ // is why a decision-to-change-code debate had NO build path and the
701
+ // user's "implement"-labelled pick did nothing (session 8191ecaee149).
702
+ baseOptions.push({
703
+ label: a.label,
704
+ // Description is the model's own `reason` (model-first — no hardcoded
705
+ // per-action prose). If the model was terse and omitted it, repeat
706
+ // the label rather than inventing system copy.
707
+ description: a.reason && a.reason.length > 0 ? a.reason : a.label,
708
+ value: a.action,
709
+ kind: a.action === "ask_followup" ? "freetext" : "choice",
710
+ });
711
+ }
712
+ // Context-only option the model doesn't own — surfaced when the debate
713
+ // left shape sections empty.
714
+ if (hasEmptySections) {
715
+ baseOptions.push({
716
+ label: `Refine: ${refinementTopics.join(", ")}`,
717
+ description: `Answer questions about ${refinementTopics.length} unresolved aspect(s)`,
718
+ value: "refine",
719
+ kind: "choice",
720
+ });
721
+ }
722
+ // Guarantee an escape hatch even if the model omitted one.
723
+ if (!baseOptions.some((o) => o.value === "save_exit" || o.value === "continue_session")) {
724
+ baseOptions.push({
725
+ label: "Save & Exit",
726
+ description: "Save the debate outcome and finish",
727
+ value: "save_exit",
728
+ kind: "choice",
729
+ });
730
+ }
731
+ }
732
+ else {
733
+ // ── Fallback: deterministic option set ──────────────────────────────
734
+ if (synthesisFailed) {
735
+ baseOptions.push({
736
+ label: "Retry Synthesis (compact)",
737
+ description: "Re-synthesize from final positions only (drop full exchange history). Fastest recovery from provider timeouts.",
738
+ value: "retry_synthesis",
739
+ kind: "choice",
740
+ });
741
+ }
465
742
  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",
743
+ label: "Save & Exit",
744
+ description: synthesisFailed
745
+ ? "Save raw debate exchanges as-is; no structured outcome will be persisted"
746
+ : "Save the debate outcome and finish",
747
+ value: "save_exit",
469
748
  kind: "choice",
470
749
  });
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) {
750
+ if (!hasPlan && !synthesisFailed) {
751
+ baseOptions.push({
752
+ label: "Lock plan and execute Sprint 1",
753
+ 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.",
754
+ value: "generate_plan",
755
+ kind: "choice",
756
+ });
757
+ }
758
+ if (hasEmptySections && !synthesisFailed) {
759
+ baseOptions.push({
760
+ label: `Refine: ${refinementTopics.join(", ")}`,
761
+ description: `Answer questions about ${refinementTopics.length} unresolved aspect(s)`,
762
+ value: "refine",
763
+ kind: "choice",
764
+ });
765
+ }
766
+ // CQ-3: free-text follow-up to the council on the same debate context.
481
767
  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",
768
+ label: "Ask Council a follow-up",
769
+ description: "Pose a new question that re-uses this debate's context (no new clarification).",
770
+ value: "ask_followup",
771
+ kind: "freetext",
486
772
  });
773
+ if (!synthesisFailed) {
774
+ baseOptions.push({
775
+ label: "Start Implementation",
776
+ description: "Load the council conclusion as the spec and build it (plan → change → verify)",
777
+ value: "implement",
778
+ kind: "choice",
779
+ });
780
+ }
487
781
  }
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",
782
+ // Canonicalize the post-analysis choices to the user's mental model:
783
+ // IMPLEMENT / CONTINUE / SAVE (session 8191ecaee149 redesign).
784
+ // (a) "ask a follow-up" and "continue with council context" are the same
785
+ // thing to the user (both = keep the session going with the debate as
786
+ // context), so collapse a generic ask_followup into continue_session.
787
+ // A pinned criteria-recovery follow-up is added LATER (inconclusive /
788
+ // lowGrounding) and is intentionally distinct, so this only affects
789
+ // the base set built above.
790
+ // (b) guarantee a CONTINUE option exists.
791
+ // (c) offer IMPLEMENT whenever the synthesis is substantive (grounded &
792
+ // conclusive) — the conclusion IS the spec, no plan artifact needed.
793
+ if (!options?.sprintPlanningMode) {
794
+ const CONTINUE_OPT = {
795
+ label: "Continue with council context",
796
+ description: "Return to the composer — your next message keeps this debate's conclusion as context.",
797
+ value: "continue_session",
493
798
  kind: "choice",
799
+ };
800
+ const hasContinue = baseOptions.some((o) => o.value === "continue_session");
801
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
802
+ if (baseOptions[i].value !== "ask_followup")
803
+ continue;
804
+ if (hasContinue)
805
+ baseOptions.splice(i, 1); // merged away — continue already covers it
806
+ else
807
+ baseOptions[i] = { ...CONTINUE_OPT }; // convert the lone follow-up into continue
808
+ }
809
+ if (!baseOptions.some((o) => o.value === "continue_session"))
810
+ baseOptions.push({ ...CONTINUE_OPT });
811
+ if (!synthesisFailed && !inconclusive && !baseOptions.some((o) => o.value === "implement")) {
812
+ // Insert at index 1, NOT 0 — the model's own best-first pick stays the
813
+ // default (defaultIndex is 0 for model-first). We only GUARANTEE the
814
+ // build path is present + prominent; we don't override the model's
815
+ // judgment that building wasn't the recommended next move.
816
+ baseOptions.splice(1, 0, {
817
+ label: "Start Implementation",
818
+ description: "Load the council conclusion as the spec and build it (plan → change → verify)",
819
+ value: "implement",
820
+ kind: "choice",
821
+ });
822
+ }
823
+ }
824
+ // F1 — when the pinned criteria were not met, the model's best-first action
825
+ // (or the deterministic default) may be a commit/hand-back-the-decision step
826
+ // that treats the outcome as settled. Pin a criteria-aware "keep working"
827
+ // option at the front and make it the default so the recommended next move
828
+ // is honest about the unmet bar. Reuses ask_followup routing (freetext,
829
+ // re-runs on this debate's context) — no new downstream action. Deduped so
830
+ // the list never shows two ask_followup rows.
831
+ if (inconclusive) {
832
+ const openList = critOutcome.unmetLabels.join("; ");
833
+ const n = critOutcome.unmetLabels.length;
834
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
835
+ if (baseOptions[i].value === "ask_followup")
836
+ baseOptions.splice(i, 1);
837
+ }
838
+ baseOptions.unshift({
839
+ label: `Keep working the ${n} unmet criteri${n === 1 ? "on" : "a"}`,
840
+ description: `Still open: ${openList}. Pose a targeted follow-up to close ${n === 1 ? "it" : "them"} before committing.`,
841
+ value: "ask_followup",
842
+ kind: "freetext",
494
843
  });
495
844
  }
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",
845
+ // A2 synthesis succeeded but grounding is weak (density 0 / low /
846
+ // "not measured"). The honest next move is to RAISE confidence, not to
847
+ // commit or to ask a blind clarification — the user reported the askcard
848
+ // asked "clarify more?" without saying WHAT would help. Pin a guided
849
+ // follow-up that names the concrete confidence-raising ask (make the
850
+ // council cite/verify its weakest claims) and make it the default.
851
+ // Reuses ask_followup routing (freetext, re-runs on this debate's
852
+ // context) — no new downstream action. Skipped when `inconclusive`
853
+ // already pinned a criteria-aware follow-up, or when synthesis failed
854
+ // (the retry_synthesis path owns that recovery).
855
+ const lowGrounding = !synthesisFailed && !inconclusive && (confidenceNotMeasured || confidenceLevel === "low");
856
+ if (lowGrounding) {
857
+ for (let i = baseOptions.length - 1; i >= 0; i--) {
858
+ if (baseOptions[i].value === "ask_followup")
859
+ baseOptions.splice(i, 1);
860
+ }
861
+ baseOptions.unshift({
862
+ label: "Raise confidence — have the council cite & verify",
863
+ 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.",
864
+ value: "ask_followup",
865
+ kind: "freetext",
509
866
  });
510
867
  }
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` };
868
+ // Model orders actions best-first (index 0 = recommended default); the
869
+ // fallback set uses the deterministic recommendation. When inconclusive,
870
+ // the pinned criteria option at index 0 is the honest default regardless of
871
+ // path.
872
+ const defaultIndex = inconclusive || lowGrounding
873
+ ? 0
874
+ : modelActions
875
+ ? 0
876
+ : Math.max(0, baseOptions.findIndex((o) => o.value === recommendation.value));
877
+ const recommendReason = inconclusive || lowGrounding
878
+ ? (baseOptions[0]?.description ?? recommendation.reason)
879
+ : modelActions
880
+ ? (baseOptions[0]?.description ?? recommendation.reason)
881
+ : recommendation.reason;
882
+ const heading = synthesisFailed
883
+ ? "## Debate Synthesis Failed"
884
+ : inconclusive
885
+ ? `## Debate Synthesis — Inconclusive (${critOutcome.metCount}/${critOutcome.total} criteria met)`
886
+ : "## Debate Synthesis Complete";
887
+ // F1 — an explicit provisional-outcome line so the user sees the unmet bar
888
+ // even if they skim past the recommendation.
889
+ const outcomeLine = inconclusive
890
+ ? `\n\n⚠ Outcome: ${critOutcome.metCount}/${critOutcome.total} criteria met. Unmet: ${critOutcome.unmetLabels.join("; ")}. Treat the synthesis as provisional — not a settled decision.`
891
+ : "";
892
+ const recommendLine = `**Recommended:** ${baseOptions[defaultIndex]?.label ?? recommendation.value} ${recommendReason}`;
893
+ // B — the live per-round transcript is cleared from the view at turn end
894
+ // (it renders as a bottom block decoupled from the timeline, so keeping it
895
+ // would mis-order later messages). The full exchange IS persisted though —
896
+ // point the user at it so the rounds aren't "lost" (user report: after a
897
+ // debate the rounds vanish with no way to re-read them). `/council inspect`
898
+ // is a registered slash command that replays [Council Round N] / [Council
899
+ // Memory] from the DB.
900
+ const roundsArchivedLine = debateState.roundCount > 0
901
+ ? `\n\n📋 All ${debateState.roundCount} debate round(s) are archived — run \`/council inspect ${sessionId}\` to re-read the full exchange.`
902
+ : "";
903
+ 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?`;
904
+ let answer;
905
+ if (options?.sprintPlanningMode) {
906
+ // Blocker 4/5 fix: no interactive post-debate menu inside automated
907
+ // per-sprint planning. Presenting it stranded the sprint before
908
+ // implementation — picking "Save & Exit" ended the run with no Sprint
909
+ // Implementation, and "Refine" (the default) looped back into more
910
+ // debate. Auto-lock the synthesized plan (== "Lock plan and execute
911
+ // Sprint 1") and hand control back to the sprint runner.
912
+ answer = "generate_plan";
913
+ idealTrace("council.postDebate.autoLock", { sessionId });
914
+ yield {
915
+ type: "content",
916
+ content: "\n> Sprint plan synthesized — auto-locked and handed to the sprint runner " +
917
+ "(the product plan was already approved at the /ideal preflight).\n",
918
+ };
919
+ }
920
+ else {
921
+ yield {
922
+ type: "council_question",
923
+ content: headerBlock,
924
+ councilQuestion: {
925
+ questionId,
926
+ phase: "post-debate",
927
+ question: synthesisFailed
928
+ ? "Synthesis did not produce a structured outcome. How do you want to recover?"
929
+ : inconclusive
930
+ ? `${critOutcome.metCount}/${critOutcome.total} success criteria met — the outcome is provisional. Keep working the unmet criteria, or save it as-is?`
931
+ : hasEmptySections
932
+ ? `The debate left ${refinementTopics.length} area(s) unresolved. Refine them or save the current outcome?`
933
+ : "What would you like to do next?",
934
+ context: `${confidenceBadge}\n${confidenceReason}` +
935
+ (inconclusive ? `\nUnmet criteria: ${critOutcome.unmetLabels.join("; ")}` : "") +
936
+ (hasEmptySections ? `\nUnresolved areas: ${refinementTopics.join(", ")}` : "") +
937
+ `\n→ ${recommendation.reason}`,
938
+ isRequired: false,
939
+ options: baseOptions,
940
+ defaultIndex,
941
+ },
942
+ };
943
+ answer = await respondToQuestion(questionId);
944
+ }
945
+ postDebateAction = answer;
946
+ idealTrace("council.postDebate.answer", { sessionId, answer });
947
+ options?.onPostDebateAction?.(answer);
948
+ // Echo the human-readable option label, never the raw action id
949
+ // (`continue_session`, `save_exit`, …) — the id is an internal routing
950
+ // token users should never see. Free-text follow-ups (no matching option)
951
+ // echo verbatim.
952
+ const answeredLabel = baseOptions.find((o) => o.value === answer)?.label ?? answer;
953
+ // No "↳ choice" echo in sprint-planning mode — there was no user choice to
954
+ // echo (the plan was auto-locked above with its own status line).
955
+ if (!options?.sprintPlanningMode) {
956
+ yield { type: "content", content: `\n ↳ ${answeredLabel}\n` };
957
+ }
536
958
  // Treat any non-empty answer that doesn't match a known choice value as a follow-up question.
537
959
  const knownValues = new Set([
538
960
  "save_exit",
961
+ "continue_session",
539
962
  "generate_plan",
540
963
  "refine",
541
964
  "ask_followup",
@@ -603,6 +1026,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
603
1026
  synthesisText =
604
1027
  `Sprint plan locked (${existingActionItems.length} steps):\n` +
605
1028
  synthesizedPlan.steps.map((s) => `- [${s.priority}] ${s.description}`).join("\n");
1029
+ idealTrace("council.generatePlan.locked.fast", {
1030
+ sessionId,
1031
+ actionItems: existingActionItems.length,
1032
+ });
606
1033
  }
607
1034
  else {
608
1035
  yield { type: "content", content: "\n> Synthesizing sprint plan...\n" };
@@ -621,6 +1048,10 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
621
1048
  type: "content",
622
1049
  content: "\n> Plan locked — sprint runner will execute planning → implementation → verification → judgment.\n",
623
1050
  };
1051
+ idealTrace("council.generatePlan.locked.synth", {
1052
+ sessionId,
1053
+ synthesisLen: synthesisText?.length ?? 0,
1054
+ });
624
1055
  }
625
1056
  // Do NOT call runExecution here. Return synthesisText to the sprint-runner
626
1057
  // caller so it drives the full sprint lifecycle (Step 4–8 in sprint-runner.ts).
@@ -655,7 +1086,13 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
655
1086
  };
656
1087
  const ans = await respondToQuestion(sqId);
657
1088
  refinedAnswers.push({ section: label, answer: ans });
658
- yield { type: "content", content: `\n ↳ ${ans}\n` };
1089
+ // Only echo sections the user actually filled. "Skip leave as-is"
1090
+ // returns an empty value; echoing it emits a blank "↳ " bubble per
1091
+ // section (6 skips = 6 empty rows of transcript garbage). Prefix the
1092
+ // section label so a real answer reads as "↳ <section>: <answer>".
1093
+ if (ans.trim().length > 0) {
1094
+ yield { type: "content", content: `\n ↳ ${label}: ${ans}\n` };
1095
+ }
659
1096
  }
660
1097
  // Build refineContext string from user answers
661
1098
  const refineCtx = refinedAnswers
@@ -678,69 +1115,85 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
678
1115
  }
679
1116
  // "save_exit" and "implement" fall through to normal persistence
680
1117
  }
681
- catch {
682
- /* non-critical */
1118
+ catch (err) {
1119
+ // Post-debate interaction (menu, follow-up re-synthesis, refine) is
1120
+ // non-critical to the persisted outcome, so we swallow — but NEVER
1121
+ // silently: a throw here previously vanished, hiding a "generate_plan
1122
+ // stalled" root cause. Log it and breadcrumb it so blocker-5 forensics
1123
+ // can see whether the tail was reached via an exception.
1124
+ console.error(`[council] post-debate interaction failed: ${err?.message}`);
1125
+ idealTrace("council.postDebate.threw", { sessionId, err: err?.message });
683
1126
  }
684
1127
  }
1128
+ idealTrace("council.persist.start", { sessionId, hasOutcome: !!outcome, postDebateAction });
685
1129
  // ── Persist outcome ─────────────────────────────────────────────────────────
686
1130
  if (sessionId) {
687
1131
  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: {
1132
+ // Skip session-scoped persistence in sprintPlanningMode: messages /
1133
+ // interaction_logs FK-reference sessions(id), but the sprint-planning caller
1134
+ // passes the product-RUN id (no session row) "FOREIGN KEY constraint
1135
+ // failed" on the FIRST write, which under the catch below previously aborted
1136
+ // the whole block — silently taking writeDecisionsLock down with it. The
1137
+ // file-based decisions.lock still writes below (outside this guard).
1138
+ if (!options?.sprintPlanningMode) {
1139
+ if (outcome) {
1140
+ const agreedLine = outcome.agreed?.length ? `\nAgreed: ${outcome.agreed.join("; ")}` : "";
1141
+ const recLine = outcome.recommendation ? `\nRecommendation: ${outcome.recommendation}` : "";
1142
+ appendSystemMessage(sessionId, `[Council Decision]\nTopic: ${topic}\n${outcome.summary}${agreedLine}${recLine}`);
1143
+ appendSystemMessage(sessionId, `[Council Outcome]\n${JSON.stringify(outcome)}`);
1144
+ }
1145
+ const evidenceDensityPersist = debateState.finalEvidenceDensity ?? 0;
1146
+ const confidenceLevelPersist = evidenceDensityPersist >= 0.6 ? "high" : evidenceDensityPersist >= 0.3 ? "medium" : "low";
1147
+ const councilRecord = {
729
1148
  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
- });
1149
+ spec,
1150
+ debatePlan,
1151
+ leaderModel: leaderModelId,
1152
+ participants: debateState.active.map((a) => ({ role: a.role, model: a.model, stance: a.stance })),
1153
+ finalPositions: debateState.active.map((a) => ({ role: a.role, position: a.position })),
1154
+ archive: debateState.archive ?? [],
1155
+ synthesis: synthesisText,
1156
+ confidence: {
1157
+ level: confidenceLevelPersist,
1158
+ evidenceDensity: evidenceDensityPersist,
1159
+ rounds: debateState.roundCount,
1160
+ },
1161
+ stats: { calls: stats.calls, durationMs: Date.now() - stats.startMs, phases: stats.phases },
1162
+ timestamp: new Date().toISOString(),
1163
+ };
1164
+ appendSystemMessage(sessionId, `[Council Memory] ${JSON.stringify(councilRecord)}`);
1165
+ // Forensics-friendly summary row in interaction_logs. The full
1166
+ // [Council Memory] system message above is great for context replay but
1167
+ // can't be queried — `usage forensics` reads interaction_logs only.
1168
+ // Excerpts are capped to keep metadata_json small (~2-4KB per run).
1169
+ const stancesForLog = debateState.active.slice(0, 8).map((a) => ({
1170
+ role: a.role,
1171
+ model: a.model,
1172
+ stanceName: a.stance?.name,
1173
+ finalPositionExcerpt: (a.position ?? "").slice(0, 400),
1174
+ }));
1175
+ logInteraction(sessionId, "council", {
1176
+ eventSubtype: "council_summary",
1177
+ model: leaderModelId,
1178
+ durationMs: Date.now() - stats.startMs,
1179
+ data: {
1180
+ topic,
1181
+ roundCount: debateState.roundCount,
1182
+ participantCount: debateState.active.length,
1183
+ stances: stancesForLog,
1184
+ synthesisExcerpt: synthesisText.slice(0, 1500),
1185
+ evidenceDensity: evidenceDensityPersist,
1186
+ confidenceLevel: confidenceLevelPersist,
1187
+ recommendation: outcome?.recommendation?.slice(0, 400) ?? null,
1188
+ agreedCount: outcome?.agreed?.length ?? 0,
1189
+ },
1190
+ });
1191
+ }
740
1192
  // C2: Persist decisions.lock.md to the run directory so sprint-runner
741
1193
  // can inject locked decisions into the implementation prompt.
742
1194
  if (options?.runDir) {
743
1195
  const rejectedProposals = detectOutOfStackProposals(synthesisText, spec);
1196
+ idealTrace("council.persist.writeDecisionsLock.before", { sessionId, runDir: options.runDir });
744
1197
  await writeDecisionsLock({
745
1198
  runId: sessionId,
746
1199
  runDir: options.runDir,
@@ -758,14 +1211,22 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
758
1211
  // only fires on an unexpected throw — log it (No-Silent-Catch), never break council.
759
1212
  console.error(`[council] decisions.lock write guard caught: ${err?.message}`);
760
1213
  });
1214
+ idealTrace("council.persist.writeDecisionsLock.after", { sessionId });
761
1215
  }
762
1216
  }
763
- catch {
764
- /* non-critical */
1217
+ catch (err) {
1218
+ // Persistence is best-effort (session-message / interaction-log writes),
1219
+ // but log so a storage fault is not mistaken for a hang in blocker-5
1220
+ // forensics.
1221
+ console.error(`[council] outcome persistence failed: ${err?.message}`);
1222
+ idealTrace("council.persist.threw", { sessionId, err: err?.message });
765
1223
  }
766
1224
  }
767
- // Update session status to completed
768
- if (sessionId) {
1225
+ idealTrace("council.persist.done", { sessionId });
1226
+ // Update session status to completed — EXCEPT when the user chose
1227
+ // "continue_session", where the agent keeps working in this session; marking
1228
+ // it completed here is what dropped it from the resume picker.
1229
+ if (sessionId && postDebateAction !== "continue_session") {
769
1230
  try {
770
1231
  new SessionStore(options?.cwd ?? process.cwd()).setStatus(sessionId, "completed");
771
1232
  }
@@ -802,15 +1263,27 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
802
1263
  stats.phases.push({ name: "execution", durationMs: Date.now() - execStart });
803
1264
  }
804
1265
  // ── Stats ───────────────────────────────────────────────────────────────────
1266
+ idealTrace("council.stats", { sessionId });
805
1267
  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" };
1268
+ // Blocker-5 root cause: in sprintPlanningMode this runCouncil is a SUB-STEP of
1269
+ // runSprint, not a standalone turn. The terminal `{type:"done"}` chunk (and the
1270
+ // stats banner) are turn-terminal signals — forwarded verbatim by sprint-runner
1271
+ // they made the app's stream consumer STOP pulling right here, so the generator
1272
+ // suspended at these yields and never returned. sprint-runner's `planGen.next()`
1273
+ // therefore never saw `done`, and the Sprint Implementation stage never ran
1274
+ // (idle at the composer, no error). A sub-step must not emit them: skip both so
1275
+ // the generator returns cleanly and the sprint runner proceeds to implementation.
1276
+ if (!options?.sprintPlanningMode) {
1277
+ yield {
1278
+ type: "content",
1279
+ content: `\n---\n` +
1280
+ `> Council stats: ${stats.calls} API calls, ${(totalMs / 1000).toFixed(1)}s total, ` +
1281
+ `${active.length} participants, ${debateState.roundCount} rounds\n` +
1282
+ `> Phases: ${stats.phases.map((p) => `${p.name}=${(p.durationMs / 1000).toFixed(1)}s`).join(", ")}\n`,
1283
+ };
1284
+ yield { type: "done" };
1285
+ }
1286
+ idealTrace("council.return", { sessionId, synthesisLen: (synthesisText || "").length });
814
1287
  return synthesisText || null;
815
1288
  }
816
1289
  // ── P7: action-item reuse helpers ─────────────────────────────────────────────