muonroi-cli 1.7.1 → 1.8.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 (328) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +143 -104
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.d.ts +6 -0
  253. package/dist/src/storage/sessions.js +49 -42
  254. package/dist/src/storage/transcript-view.js +7 -1
  255. package/dist/src/storage/transcript.d.ts +1 -0
  256. package/dist/src/storage/transcript.js +55 -0
  257. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  258. package/dist/src/storage/usage.js +14 -14
  259. package/dist/src/storage/workspaces.js +12 -12
  260. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  261. package/dist/src/tools/bash.d.ts +36 -23
  262. package/dist/src/tools/bash.js +59 -301
  263. package/dist/src/tools/bash.test.js +2 -257
  264. package/dist/src/tools/file-tracker.d.ts +5 -0
  265. package/dist/src/tools/file-tracker.js +14 -0
  266. package/dist/src/tools/file.js +2 -0
  267. package/dist/src/tools/git-safety.d.ts +38 -1
  268. package/dist/src/tools/git-safety.js +94 -4
  269. package/dist/src/tools/git-safety.test.js +45 -1
  270. package/dist/src/tools/native-tools.d.ts +1 -1
  271. package/dist/src/tools/native-tools.js +56 -0
  272. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  273. package/dist/src/tools/registry-git-safety.test.js +17 -6
  274. package/dist/src/tools/registry.d.ts +5 -0
  275. package/dist/src/tools/registry.js +185 -11
  276. package/dist/src/types/index.d.ts +7 -1
  277. package/dist/src/ui/app.d.ts +2 -1
  278. package/dist/src/ui/app.js +0 -0
  279. package/dist/src/ui/components/council-question-card.js +1 -0
  280. package/dist/src/ui/components/message-view.js +37 -3
  281. package/dist/src/ui/components/structured-response-view.js +7 -0
  282. package/dist/src/ui/components/task-list-panel.js +3 -12
  283. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  284. package/dist/src/ui/containers/chat-feed.js +66 -0
  285. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  286. package/dist/src/ui/containers/modals-layer.js +18 -0
  287. package/dist/src/ui/modals/session-picker-modal.js +14 -9
  288. package/dist/src/ui/modals/update-modal.js +2 -1
  289. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  290. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  291. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  292. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  293. package/dist/src/ui/slash/council-inspect.js +4 -4
  294. package/dist/src/ui/slash/export.js +16 -1
  295. package/dist/src/ui/slash/menu-items.js +1 -0
  296. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  297. package/dist/src/ui/slash/ponytail.js +19 -0
  298. package/dist/src/ui/status-bar/store.js +0 -1
  299. package/dist/src/ui/status-bar/store.test.js +2 -2
  300. package/dist/src/ui/types.d.ts +7 -0
  301. package/dist/src/ui/use-app-logic.d.ts +26 -0
  302. package/dist/src/ui/use-app-logic.js +0 -0
  303. package/dist/src/ui/utils/relaunch.d.ts +17 -0
  304. package/dist/src/ui/utils/relaunch.js +28 -4
  305. package/dist/src/ui/utils/relaunch.test.js +50 -0
  306. package/dist/src/ui/utils/text.d.ts +11 -0
  307. package/dist/src/ui/utils/text.js +21 -1
  308. package/dist/src/ui/utils/text.test.js +25 -1
  309. package/dist/src/ui/utils/tools.js +3 -1
  310. package/dist/src/usage/cost-log.js +2 -2
  311. package/dist/src/usage/estimator.d.ts +14 -0
  312. package/dist/src/usage/estimator.js +22 -0
  313. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  314. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  315. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  316. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  317. package/dist/src/utils/clipboard-image.js +23 -23
  318. package/dist/src/utils/llm-deadline.d.ts +9 -1
  319. package/dist/src/utils/llm-deadline.js +31 -2
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +27 -31
  324. package/dist/src/utils/settings.js +100 -109
  325. package/dist/src/utils/side-question.js +2 -2
  326. package/dist/src/utils/skills.js +3 -3
  327. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  328. package/package.json +1 -1
@@ -0,0 +1,143 @@
1
+ // src/ee/who-am-i.ts
2
+ //
3
+ // "Who Am I" v4.0 profile provider for the PIL. The personality/work-style profile
4
+ // is DEVICE-LOCAL (~/.experience/profile.yaml) and never leaves the machine, so it
5
+ // cannot come from the EE HTTP API — we read it on-device, reusing the EE install's
6
+ // parser (profile-model.js loadProfile) + privacy gate (config.js getPrivacyLevel)
7
+ // via createRequire (the same in-process pattern as src/ee/bridge.ts).
8
+ //
9
+ // Privacy is enforced HERE at read time via a positive per-dimension-NAME allowlist
10
+ // (defense in depth): the EE writer keeps committed values regardless of privacyLevel,
11
+ // so a standard→minimal downgrade leaves stale Tang-2 values physically in the file —
12
+ // trusting the file would leak opted-out data. Mirrors experience-engine
13
+ // src/profile-render.js. Fail-open: any error / missing EE install returns null and the
14
+ // PIL keeps its own per-turn defaults.
15
+ import { createRequire } from "node:module";
16
+ import * as os from "node:os";
17
+ import * as path from "node:path";
18
+ import { classifyEeError, logEeFailure } from "../utils/ee-logger.js";
19
+ // Tang 1 (work patterns + activity-derived decision_speed — namespaced personality.*
20
+ // but its SOURCE is activity, so it belongs to the minimal tier; allowlist by NAME).
21
+ const TIER_MINIMAL = [
22
+ "work_patterns.energy",
23
+ "work_patterns.multitasking",
24
+ "work_patterns.session_length",
25
+ "personality.decision_speed",
26
+ ];
27
+ // Tang 2 (decision + communication style) added at standard. `full` == standard today
28
+ // (no Tang-3 emotional dims exist); the allowlist guarantees emotional.* can never leak.
29
+ const TIER_STANDARD = [
30
+ ...TIER_MINIMAL,
31
+ "communication.question_style",
32
+ "communication.feedback_style",
33
+ "communication.brevity",
34
+ "personality.conflict_style",
35
+ "personality.risk_tolerance",
36
+ // delegation_style is `work_patterns.*` by name but transcript-derived, so it commits
37
+ // only at standard+ (the EE writer skips the transcript at `minimal`). Standard-only.
38
+ "work_patterns.delegation_style",
39
+ ];
40
+ const ALLOWLIST = {
41
+ minimal: TIER_MINIMAL,
42
+ standard: TIER_STANDARD,
43
+ full: TIER_STANDARD,
44
+ };
45
+ const WORK_DIMS = new Set(TIER_MINIMAL);
46
+ // Coarse work dims commit over 2-3-way splits (confidence ~0.45-0.6); the lower floor
47
+ // keeps the minimal tier usable. Mirrors experience-engine src/profile-render.js.
48
+ const WORK_MIN_CONFIDENCE = 0.45;
49
+ const MIN_CONFIDENCE = 0.6;
50
+ /**
51
+ * PURE: project a raw EE profile to the privacy-gated dims for `level`, applying the
52
+ * positive name allowlist + value!=null commit gate + per-tier confidence floor.
53
+ */
54
+ export function selectWhoAmIDims(raw, level) {
55
+ const out = {};
56
+ const allow = ALLOWLIST[level];
57
+ if (!allow || !raw?.dimensions)
58
+ return out;
59
+ for (const name of allow) {
60
+ const d = raw.dimensions[name];
61
+ if (!d || d.value == null)
62
+ continue;
63
+ const confidence = Number(d.confidence) || 0;
64
+ const floor = WORK_DIMS.has(name) ? WORK_MIN_CONFIDENCE : MIN_CONFIDENCE;
65
+ if (confidence < floor)
66
+ continue;
67
+ out[name] = {
68
+ value: String(d.value),
69
+ confidence,
70
+ samples: Math.round(Number(d.sampleCount ?? d.samples) || 0),
71
+ };
72
+ }
73
+ return out;
74
+ }
75
+ /**
76
+ * PURE: derive an output-style baseline from the profile. brevity is the primary
77
+ * signal; decision_speed is a weaker fallback. Returns null when no usable signal
78
+ * exists so the caller keeps its own per-turn default.
79
+ */
80
+ export function outputStyleFromProfile(profile) {
81
+ if (!profile)
82
+ return null;
83
+ const brevity = profile.dims["communication.brevity"]?.value;
84
+ if (brevity === "concise")
85
+ return "concise";
86
+ if (brevity === "verbose")
87
+ return "detailed";
88
+ if (brevity === "moderate")
89
+ return "balanced";
90
+ const speed = profile.dims["personality.decision_speed"]?.value;
91
+ if (speed === "fast-intuitive")
92
+ return "concise";
93
+ if (speed === "deliberate")
94
+ return "detailed";
95
+ return null;
96
+ }
97
+ let _cache;
98
+ /**
99
+ * Load + cache the privacy-gated profile for this process. The profile changes
100
+ * slowly (rebuilt by the EE Stop hook), so one read per process is enough.
101
+ * Returns null when EE is not installed, privacy is off, or nothing is committed.
102
+ */
103
+ export function getWhoAmIProfile() {
104
+ if (_cache !== undefined)
105
+ return _cache;
106
+ _cache = loadWhoAmIProfile();
107
+ return _cache;
108
+ }
109
+ /** Clear the cache — test-only / after a known profile change. */
110
+ export function resetWhoAmICache() {
111
+ _cache = undefined;
112
+ }
113
+ function loadWhoAmIProfile() {
114
+ try {
115
+ const req = createRequire(import.meta.url);
116
+ const srcDir = path.join(os.homedir(), ".experience", "src");
117
+ const config = req(path.join(srcDir, "config.js"));
118
+ const model = req(path.join(srcDir, "profile-model.js"));
119
+ if (typeof config.getPrivacyLevel !== "function" || typeof config.getProfilePath !== "function")
120
+ return null;
121
+ if (typeof model.loadProfile !== "function")
122
+ return null;
123
+ const level = String(config.getPrivacyLevel() ?? "off");
124
+ if (level !== "minimal" && level !== "standard" && level !== "full")
125
+ return null;
126
+ const raw = model.loadProfile(config.getProfilePath());
127
+ const dims = selectWhoAmIDims(raw, level);
128
+ if (Object.keys(dims).length === 0)
129
+ return null;
130
+ return { level, dims };
131
+ }
132
+ catch (err) {
133
+ // EE not installed (module absent) is an expected feature-off condition, not a
134
+ // failure — stay quiet. Any other error (corrupt profile, throwing module) is a
135
+ // real fault: log it (No-Silent-Catch) and degrade.
136
+ const code = err?.code;
137
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_MODULE_NOT_FOUND") {
138
+ logEeFailure("who-am-i.load", classifyEeError(err), err);
139
+ }
140
+ return null;
141
+ }
142
+ }
143
+ //# sourceMappingURL=who-am-i.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,101 @@
1
+ import * as os from "node:os";
2
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
3
+ import { getWhoAmIProfile, outputStyleFromProfile, resetWhoAmICache, selectWhoAmIDims, } from "./who-am-i.js";
4
+ // Build a raw EE profile (the shape ~/.experience/src/profile-model.js loadProfile() returns).
5
+ function rawDim(value, confidence, sampleCount = 20) {
6
+ return { value, confidence, sampleCount, distribution: {}, evidence: null };
7
+ }
8
+ function rawProfile(dims) {
9
+ return { version: 1, updatedAt: null, dimensions: dims };
10
+ }
11
+ describe("selectWhoAmIDims — privacy allowlist + commit/confidence gates", () => {
12
+ const all = () => rawProfile({
13
+ "communication.question_style": rawDim("directive", 0.8),
14
+ "communication.feedback_style": rawDim("implicit", 0.75),
15
+ "communication.brevity": rawDim("concise", 0.83),
16
+ "personality.conflict_style": rawDim("direct-constructive", 0.7),
17
+ "personality.risk_tolerance": rawDim("experimental", 0.72),
18
+ "personality.decision_speed": rawDim("fast-intuitive", 0.7),
19
+ "work_patterns.energy": rawDim("night-owl", 0.6),
20
+ "work_patterns.multitasking": rawDim("sequential-deep", 0.65),
21
+ "work_patterns.session_length": rawDim("long", 0.55),
22
+ "work_patterns.delegation_style": rawDim("autonomous", 0.68),
23
+ });
24
+ test("minimal exposes only the 4 Tang-1 work dims", () => {
25
+ const dims = selectWhoAmIDims(all(), "minimal");
26
+ expect(Object.keys(dims).sort()).toEqual([
27
+ "personality.decision_speed",
28
+ "work_patterns.energy",
29
+ "work_patterns.multitasking",
30
+ "work_patterns.session_length",
31
+ ].sort());
32
+ });
33
+ test("standard exposes all 10 committed dims", () => {
34
+ const dims = selectWhoAmIDims(all(), "standard");
35
+ expect(Object.keys(dims).length).toBe(10);
36
+ });
37
+ test("delegation_style is standard-only — stripped at minimal (transcript-derived, not Tang-1)", () => {
38
+ expect(selectWhoAmIDims(all(), "minimal")["work_patterns.delegation_style"]).toBeUndefined();
39
+ expect(selectWhoAmIDims(all(), "standard")["work_patterns.delegation_style"]).toBeDefined();
40
+ });
41
+ test("standard-built profile rendered at minimal strips Tang-2 dims (no stale leak)", () => {
42
+ const dims = selectWhoAmIDims(all(), "minimal");
43
+ expect(dims["communication.brevity"]).toBeUndefined();
44
+ expect(dims["personality.conflict_style"]).toBeUndefined();
45
+ });
46
+ test("pending (value=null) dims are skipped", () => {
47
+ const dims = selectWhoAmIDims(rawProfile({ "communication.brevity": rawDim(null, 0.9, 5) }), "standard");
48
+ expect(Object.keys(dims).length).toBe(0);
49
+ });
50
+ test("per-tier confidence floors: 0.55 Tang-2 drops at 0.6, 0.50 work survives at 0.45", () => {
51
+ const dims = selectWhoAmIDims(rawProfile({
52
+ "communication.brevity": rawDim("concise", 0.55),
53
+ "work_patterns.energy": rawDim("night-owl", 0.5),
54
+ }), "standard");
55
+ expect(dims["communication.brevity"]).toBeUndefined();
56
+ expect(dims["work_patterns.energy"]).toBeDefined();
57
+ });
58
+ test("Tang-3 emotional.* is never exposed (not in any allowlist)", () => {
59
+ const dims = selectWhoAmIDims(rawProfile({ "emotional.mood": rawDim("calm", 0.99) }), "full");
60
+ expect(Object.keys(dims).length).toBe(0);
61
+ });
62
+ test("decision_speed (personality.* but Tang-1) is allowed at minimal — namespace trap", () => {
63
+ const dims = selectWhoAmIDims(rawProfile({ "personality.decision_speed": rawDim("measured", 0.7) }), "minimal");
64
+ expect(dims["personality.decision_speed"]).toBeDefined();
65
+ });
66
+ });
67
+ describe("outputStyleFromProfile — brevity/decision_speed → OutputStyle", () => {
68
+ const p = (dims) => ({ level: "standard", dims });
69
+ test("brevity concise → concise, verbose → detailed, moderate → balanced", () => {
70
+ expect(outputStyleFromProfile(p({ "communication.brevity": { value: "concise", confidence: 0.8, samples: 20 } }))).toBe("concise");
71
+ expect(outputStyleFromProfile(p({ "communication.brevity": { value: "verbose", confidence: 0.8, samples: 20 } }))).toBe("detailed");
72
+ expect(outputStyleFromProfile(p({ "communication.brevity": { value: "moderate", confidence: 0.8, samples: 20 } }))).toBe("balanced");
73
+ });
74
+ test("no brevity → decision_speed fast-intuitive → concise; deliberate → detailed", () => {
75
+ expect(outputStyleFromProfile(p({ "personality.decision_speed": { value: "fast-intuitive", confidence: 0.7, samples: 20 } }))).toBe("concise");
76
+ expect(outputStyleFromProfile(p({ "personality.decision_speed": { value: "deliberate", confidence: 0.7, samples: 20 } }))).toBe("detailed");
77
+ });
78
+ test("null profile or no usable dim → null (caller keeps its own default)", () => {
79
+ expect(outputStyleFromProfile(null)).toBeNull();
80
+ expect(outputStyleFromProfile(p({ "personality.decision_speed": { value: "measured", confidence: 0.7, samples: 20 } }))).toBeNull();
81
+ });
82
+ });
83
+ describe("getWhoAmIProfile — fail-open + cache", () => {
84
+ const origHome = process.env.HOME;
85
+ const origUser = process.env.USERPROFILE;
86
+ beforeEach(() => {
87
+ resetWhoAmICache();
88
+ });
89
+ afterEach(() => {
90
+ process.env.HOME = origHome;
91
+ process.env.USERPROFILE = origUser;
92
+ resetWhoAmICache();
93
+ });
94
+ test("returns null when no EE install is present (no ~/.experience modules)", () => {
95
+ const empty = os.tmpdir(); // no .experience/src here
96
+ process.env.HOME = empty;
97
+ process.env.USERPROFILE = empty;
98
+ expect(getWhoAmIProfile()).toBeNull();
99
+ });
100
+ });
101
+ //# sourceMappingURL=who-am-i.test.js.map
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.7.1";
1
+ export declare const PACKAGE_VERSION = "1.8.0";
2
2
  export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
2
2
  // Sourced from package.json at build time so it survives bun --compile bundling.
3
- export const PACKAGE_VERSION = "1.7.1";
3
+ export const PACKAGE_VERSION = "1.8.0";
4
4
  export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
5
5
  //# sourceMappingURL=version.js.map
@@ -9,6 +9,7 @@ export function createCouncilAutoAnswerer(opts) {
9
9
  "pil-interview": [],
10
10
  "pil-acceptance": [],
11
11
  "tool-loop-cap": [],
12
+ "safety-override": [],
12
13
  };
13
14
  const preflightApprove = opts.file?.preflightApprove ?? true;
14
15
  return {
@@ -1,5 +1,5 @@
1
1
  import type { ProcessMessageObserver } from "../orchestrator/agent-options.js";
2
- import type { StreamChunk, ToolCall, ToolResult } from "../types/index.js";
2
+ import type { StreamChunk, StructuredResponse, ToolCall, ToolResult } from "../types/index.js";
3
3
  export type HeadlessOutputFormat = "text" | "json";
4
4
  export interface HeadlessWrites {
5
5
  stdout?: string;
@@ -42,6 +42,13 @@ export type HeadlessJsonEvent = {
42
42
  totalTokens?: number;
43
43
  costUsdTicks?: number;
44
44
  };
45
+ } | {
46
+ type: "structured_response";
47
+ sessionID?: string;
48
+ stepNumber: number;
49
+ timestamp: number;
50
+ taskType: string;
51
+ data: Record<string, unknown>;
45
52
  } | {
46
53
  type: "error";
47
54
  sessionID?: string;
@@ -54,6 +61,28 @@ export declare function renderHeadlessPrelude(format: HeadlessOutputFormat, sess
54
61
  * Headless text output only. JSON streaming uses {@link createHeadlessJsonlEmitter} + `Agent.processMessage` observer.
55
62
  */
56
63
  export declare function renderHeadlessChunk(chunk: StreamChunk): HeadlessWrites;
64
+ /**
65
+ * Plain-text rendering of a respond_* terminal answer for headless `--format
66
+ * text`. Mirrors the per-taskType layout of {@link StructuredResponseView}
67
+ * (src/ui/components/structured-response-view.tsx) but emits flat text (no ANSI
68
+ * box-drawing) so the answer pipes cleanly. Falls back to the primary text
69
+ * field, then raw JSON, for taskTypes without a dedicated layout.
70
+ */
71
+ export declare function formatStructuredResponseText(sr: StructuredResponse): string;
72
+ /**
73
+ * Stateful headless TEXT consumer. Streams tool/error progress to stderr (via
74
+ * {@link renderHeadlessChunk}) but BUFFERS assistant `content` so that a
75
+ * terminal `respond_*` answer ({@link StructuredResponse}) supersedes any
76
+ * preamble the model leaked before calling the response tool — otherwise the
77
+ * answer would print twice (once as raw leaked content, once formatted). For a
78
+ * normal chat turn with no structured answer, the buffered content is flushed
79
+ * verbatim at the end. Mirrors the buffer-and-supersede design of
80
+ * {@link createHeadlessJsonlEmitter}.
81
+ */
82
+ export declare function createHeadlessTextEmitter(): {
83
+ consumeChunk(chunk: StreamChunk): HeadlessWrites;
84
+ flush(): HeadlessWrites;
85
+ };
57
86
  /**
58
87
  * Buffers assistant `content` per step and emits JSONL: step_start, text, tool_use, step_finish, error.
59
88
  * Pair with `agent.processMessage(prompt, emitter.observer)` in headless JSON mode only.
@@ -39,6 +39,11 @@ export function renderHeadlessChunk(chunk) {
39
39
  const stderr = [`${color}${icon} ${label}\x1b[0m`, ...mediaLines].join("\n");
40
40
  return { stderr: `${stderr}\n` };
41
41
  }
42
+ case "structured_response":
43
+ // A respond_* terminal answer arrives ONLY as this chunk (never as
44
+ // `content`). Without this case it hit the no-op default below and the
45
+ // answer was silently dropped from `--format text` stdout.
46
+ return chunk.structuredResponse ? { stdout: `${formatStructuredResponseText(chunk.structuredResponse)}\n` } : {};
42
47
  case "error":
43
48
  return chunk.content ? { stderr: `\x1b[31m${chunk.content}\x1b[0m\n` } : {};
44
49
  case "done":
@@ -52,6 +57,119 @@ export function renderHeadlessChunk(chunk) {
52
57
  function truncate(text, max) {
53
58
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
54
59
  }
60
+ /**
61
+ * Plain-text rendering of a respond_* terminal answer for headless `--format
62
+ * text`. Mirrors the per-taskType layout of {@link StructuredResponseView}
63
+ * (src/ui/components/structured-response-view.tsx) but emits flat text (no ANSI
64
+ * box-drawing) so the answer pipes cleanly. Falls back to the primary text
65
+ * field, then raw JSON, for taskTypes without a dedicated layout.
66
+ */
67
+ export function formatStructuredResponseText(sr) {
68
+ const d = (sr.data ?? {});
69
+ const str = (v) => (typeof v === "string" ? v : "");
70
+ const arr = (v) => (Array.isArray(v) ? v : []);
71
+ switch (sr.taskType) {
72
+ case "general":
73
+ return str(d.response) || JSON.stringify(d, null, 2);
74
+ case "documentation": {
75
+ const examples = arr(d.examples);
76
+ const parts = [str(d.content)];
77
+ for (const ex of examples) {
78
+ if (ex.description)
79
+ parts.push(`\n${ex.description}`);
80
+ if (ex.code)
81
+ parts.push(ex.code);
82
+ }
83
+ const out = parts.filter(Boolean).join("\n");
84
+ return out || JSON.stringify(d, null, 2);
85
+ }
86
+ case "analyze": {
87
+ const findings = arr(d.findings);
88
+ if (findings.length === 0)
89
+ return JSON.stringify(d, null, 2);
90
+ return findings
91
+ .map((f) => {
92
+ const sev = (f.severity ?? "").toUpperCase();
93
+ const head = sev ? `[${sev}] ${f.text ?? ""}` : (f.text ?? "");
94
+ return f.evidence ? `${head}\n evidence: ${f.evidence}` : head;
95
+ })
96
+ .join("\n");
97
+ }
98
+ case "plan": {
99
+ const steps = arr(d.steps);
100
+ const lines = [];
101
+ steps.forEach((s, i) => {
102
+ lines.push(`${i + 1}. ${s.action ?? ""}`);
103
+ if (s.criterion)
104
+ lines.push(` done when: ${s.criterion}`);
105
+ if (s.rationale)
106
+ lines.push(` why: ${s.rationale}`);
107
+ });
108
+ const assumptions = arr(d.assumptions);
109
+ const risks = arr(d.risks);
110
+ if (assumptions.length > 0) {
111
+ lines.push("", "assumptions:");
112
+ for (const a of assumptions)
113
+ lines.push(` - ${a}`);
114
+ }
115
+ if (risks.length > 0) {
116
+ lines.push("", "risks:");
117
+ for (const rk of risks)
118
+ lines.push(` - ${rk}`);
119
+ }
120
+ return lines.length > 0 ? lines.join("\n") : JSON.stringify(d, null, 2);
121
+ }
122
+ case "debug": {
123
+ const r = d;
124
+ const lines = [];
125
+ if (r.hypothesis)
126
+ lines.push(`hypothesis: ${r.hypothesis}`);
127
+ if (r.root_cause)
128
+ lines.push(`root cause: ${r.root_cause}`);
129
+ if (r.fix?.file)
130
+ lines.push(`fix: ${r.fix.file}`);
131
+ if (r.fix?.diff)
132
+ lines.push(r.fix.diff);
133
+ if (r.verify_command)
134
+ lines.push(`verify: ${r.verify_command}`);
135
+ return lines.length > 0 ? lines.join("\n") : JSON.stringify(d, null, 2);
136
+ }
137
+ case "refactor": {
138
+ const r = d;
139
+ const lines = [];
140
+ if (r.summary)
141
+ lines.push(r.summary);
142
+ for (const c of r.changes ?? []) {
143
+ if (c.file)
144
+ lines.push(`\n── ${c.file} ──`);
145
+ if (c.diff)
146
+ lines.push(c.diff);
147
+ }
148
+ if (r.verify_command)
149
+ lines.push(`verify: ${r.verify_command}`);
150
+ return lines.length > 0 ? lines.join("\n") : JSON.stringify(d, null, 2);
151
+ }
152
+ case "generate": {
153
+ const r = d;
154
+ const lines = [];
155
+ if (r.explanation)
156
+ lines.push(r.explanation);
157
+ for (const f of r.files ?? []) {
158
+ const lang = f.language ? ` (${f.language})` : "";
159
+ if (f.path)
160
+ lines.push(`\n── ${f.path}${lang} ──`);
161
+ if (f.content)
162
+ lines.push(f.content);
163
+ }
164
+ return lines.length > 0 ? lines.join("\n") : JSON.stringify(d, null, 2);
165
+ }
166
+ default: {
167
+ // Renderer-missing taskType: probe common text-bearing fields, then JSON.
168
+ const primary = str(d.response) || str(d.summary) || str(d.content) || str(d.text);
169
+ return primary || JSON.stringify(d, null, 2);
170
+ }
171
+ }
172
+ }
55
173
  function formatToolCallLabel(tc) {
56
174
  const name = tc.function.name;
57
175
  try {
@@ -74,6 +192,45 @@ function formatToolCallLabel(tc) {
74
192
  catch { }
75
193
  return name;
76
194
  }
195
+ /**
196
+ * Stateful headless TEXT consumer. Streams tool/error progress to stderr (via
197
+ * {@link renderHeadlessChunk}) but BUFFERS assistant `content` so that a
198
+ * terminal `respond_*` answer ({@link StructuredResponse}) supersedes any
199
+ * preamble the model leaked before calling the response tool — otherwise the
200
+ * answer would print twice (once as raw leaked content, once formatted). For a
201
+ * normal chat turn with no structured answer, the buffered content is flushed
202
+ * verbatim at the end. Mirrors the buffer-and-supersede design of
203
+ * {@link createHeadlessJsonlEmitter}.
204
+ */
205
+ export function createHeadlessTextEmitter() {
206
+ let pendingContent = "";
207
+ let structuredEmitted = false;
208
+ function consumeChunk(chunk) {
209
+ switch (chunk.type) {
210
+ case "content":
211
+ pendingContent += chunk.content ?? "";
212
+ return {};
213
+ case "structured_response":
214
+ if (!chunk.structuredResponse)
215
+ return {};
216
+ // Terminal answer is authoritative — drop any buffered preamble.
217
+ pendingContent = "";
218
+ structuredEmitted = true;
219
+ return { stdout: `${formatStructuredResponseText(chunk.structuredResponse)}\n` };
220
+ case "done":
221
+ // Trailing newline is emitted by flush() alongside the final answer.
222
+ return {};
223
+ default:
224
+ return renderHeadlessChunk(chunk);
225
+ }
226
+ }
227
+ function flush() {
228
+ if (structuredEmitted || pendingContent.length === 0)
229
+ return {};
230
+ return { stdout: `${pendingContent}\n` };
231
+ }
232
+ return { consumeChunk, flush };
233
+ }
77
234
  function jsonLine(event) {
78
235
  return `${JSON.stringify(event)}\n`;
79
236
  }
@@ -188,6 +345,29 @@ export function createHeadlessJsonlEmitter(sessionId) {
188
345
  }
189
346
  break;
190
347
  }
348
+ case "structured_response": {
349
+ if (chunk.structuredResponse) {
350
+ // Flush any buffered preamble text first so ordering is preserved,
351
+ // then emit the typed terminal answer (previously dropped entirely).
352
+ if (textBuffer.length > 0) {
353
+ stdout += jsonLine(withSession({
354
+ type: "text",
355
+ stepNumber: currentStep,
356
+ text: textBuffer,
357
+ timestamp: Date.now(),
358
+ }));
359
+ textBuffer = "";
360
+ }
361
+ stdout += jsonLine(withSession({
362
+ type: "structured_response",
363
+ stepNumber: currentStep,
364
+ timestamp: Date.now(),
365
+ taskType: chunk.structuredResponse.taskType,
366
+ data: chunk.structuredResponse.data,
367
+ }));
368
+ }
369
+ break;
370
+ }
191
371
  case "error":
192
372
  stdout += jsonLine(withSession({
193
373
  type: "error",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { createHeadlessJsonlEmitter, isHeadlessOutputFormat, renderHeadlessChunk, renderHeadlessPrelude, } from "./output.js";
2
+ import { createHeadlessJsonlEmitter, createHeadlessTextEmitter, formatStructuredResponseText, isHeadlessOutputFormat, renderHeadlessChunk, renderHeadlessPrelude, } from "./output.js";
3
3
  function toolCall(name) {
4
4
  return {
5
5
  id: "tc-1",
@@ -175,5 +175,65 @@ describe("headless output helpers", () => {
175
175
  });
176
176
  expectSessionAndTimestamp(parsed, sessionId);
177
177
  });
178
+ // Regression backstop for G5: a respond_* terminal answer arrives ONLY as a
179
+ // `structured_response` chunk. Both headless renderers used to drop it
180
+ // (no-op default branch), so `--format text` produced empty stdout and
181
+ // `--format json` emitted no answer payload on a preamble-free turn.
182
+ describe("structured_response (respond_* terminal answer)", () => {
183
+ const generalChunk = {
184
+ type: "structured_response",
185
+ structuredResponse: { taskType: "general", data: { response: "The answer." } },
186
+ };
187
+ it("renderHeadlessChunk renders the answer to stdout (no longer dropped)", () => {
188
+ const w = renderHeadlessChunk(generalChunk);
189
+ expect(w.stdout).toBe("The answer.\n");
190
+ expect(w.stderr).toBeUndefined();
191
+ });
192
+ it("formatStructuredResponseText lays out each taskType as flat text", () => {
193
+ expect(formatStructuredResponseText({
194
+ taskType: "analyze",
195
+ data: { findings: [{ text: "Bug X", evidence: "file:1", severity: "high" }] },
196
+ })).toBe("[HIGH] Bug X\n evidence: file:1");
197
+ expect(formatStructuredResponseText({
198
+ taskType: "plan",
199
+ data: { steps: [{ action: "Do A", criterion: "A done", rationale: "because" }] },
200
+ })).toBe("1. Do A\n done when: A done\n why: because");
201
+ // Unknown taskType falls back to a primary text field, then JSON.
202
+ expect(formatStructuredResponseText({ taskType: "mystery", data: { summary: "hi" } })).toBe("hi");
203
+ expect(formatStructuredResponseText({ taskType: "mystery", data: { n: 1 } })).toBe('{\n "n": 1\n}');
204
+ });
205
+ it("text emitter: a structured answer SUPERSEDES leaked preamble content (no duplicate)", () => {
206
+ const e = createHeadlessTextEmitter();
207
+ // Model leaks the raw answer as content, then delivers it via respond_*.
208
+ expect(e.consumeChunk({ type: "content", content: '```json\n{"x":1}\n```' }).stdout).toBeUndefined();
209
+ const out = e.consumeChunk(generalChunk).stdout;
210
+ expect(out).toBe("The answer.\n");
211
+ // Nothing buffered remains to flush — the answer is not printed twice.
212
+ expect(e.flush().stdout).toBeUndefined();
213
+ });
214
+ it("text emitter: a normal content-only turn flushes its buffered answer", () => {
215
+ const e = createHeadlessTextEmitter();
216
+ expect(e.consumeChunk({ type: "content", content: "PONG" }).stdout).toBeUndefined();
217
+ expect(e.consumeChunk({ type: "done" }).stdout).toBeUndefined();
218
+ expect(e.flush().stdout).toBe("PONG\n");
219
+ });
220
+ it("text emitter: tool/error progress still streams to stderr immediately", () => {
221
+ const e = createHeadlessTextEmitter();
222
+ const w = e.consumeChunk({ type: "tool_calls", toolCalls: [toolCall("bash")] });
223
+ expect(w.stderr).toContain("bash");
224
+ expect(w.stdout).toBeUndefined();
225
+ });
226
+ it("json emitter: emits a typed structured_response event carrying the payload", () => {
227
+ const { consumeChunk } = createHeadlessJsonlEmitter("sess-sr");
228
+ const out = consumeChunk(generalChunk).stdout ?? "";
229
+ const events = out
230
+ .trim()
231
+ .split("\n")
232
+ .map((l) => JSON.parse(l));
233
+ const sr = events.find((ev) => ev.type === "structured_response");
234
+ expect(sr).toMatchObject({ type: "structured_response", taskType: "general", data: { response: "The answer." } });
235
+ expectSessionAndTimestamp(sr, "sess-sr");
236
+ });
237
+ });
178
238
  });
179
239
  //# sourceMappingURL=output.test.js.map