muonroi-cli 1.7.2 → 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 (322) 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 +55 -60
  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.js +43 -43
  253. package/dist/src/storage/transcript-view.js +7 -1
  254. package/dist/src/storage/transcript.d.ts +1 -0
  255. package/dist/src/storage/transcript.js +55 -0
  256. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  257. package/dist/src/storage/usage.js +14 -14
  258. package/dist/src/storage/workspaces.js +12 -12
  259. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  260. package/dist/src/tools/bash.d.ts +36 -23
  261. package/dist/src/tools/bash.js +59 -301
  262. package/dist/src/tools/bash.test.js +2 -257
  263. package/dist/src/tools/file-tracker.d.ts +5 -0
  264. package/dist/src/tools/file-tracker.js +14 -0
  265. package/dist/src/tools/file.js +2 -0
  266. package/dist/src/tools/git-safety.d.ts +38 -1
  267. package/dist/src/tools/git-safety.js +94 -4
  268. package/dist/src/tools/git-safety.test.js +45 -1
  269. package/dist/src/tools/native-tools.d.ts +1 -1
  270. package/dist/src/tools/native-tools.js +56 -0
  271. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  272. package/dist/src/tools/registry-git-safety.test.js +17 -6
  273. package/dist/src/tools/registry.d.ts +5 -0
  274. package/dist/src/tools/registry.js +185 -11
  275. package/dist/src/types/index.d.ts +7 -1
  276. package/dist/src/ui/app.d.ts +1 -0
  277. package/dist/src/ui/app.js +0 -0
  278. package/dist/src/ui/components/council-question-card.js +1 -0
  279. package/dist/src/ui/components/message-view.js +37 -3
  280. package/dist/src/ui/components/structured-response-view.js +7 -0
  281. package/dist/src/ui/components/task-list-panel.js +3 -12
  282. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  283. package/dist/src/ui/containers/chat-feed.js +66 -0
  284. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  285. package/dist/src/ui/containers/modals-layer.js +18 -0
  286. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  287. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  288. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  289. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  290. package/dist/src/ui/slash/council-inspect.js +4 -4
  291. package/dist/src/ui/slash/export.js +16 -1
  292. package/dist/src/ui/slash/menu-items.js +1 -0
  293. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  294. package/dist/src/ui/slash/ponytail.js +19 -0
  295. package/dist/src/ui/status-bar/store.js +0 -1
  296. package/dist/src/ui/status-bar/store.test.js +2 -2
  297. package/dist/src/ui/use-app-logic.d.ts +26 -0
  298. package/dist/src/ui/use-app-logic.js +0 -0
  299. package/dist/src/ui/utils/relaunch.js +1 -1
  300. package/dist/src/ui/utils/text.d.ts +11 -0
  301. package/dist/src/ui/utils/text.js +21 -1
  302. package/dist/src/ui/utils/text.test.js +25 -1
  303. package/dist/src/ui/utils/tools.js +3 -1
  304. package/dist/src/usage/cost-log.js +2 -2
  305. package/dist/src/usage/estimator.d.ts +14 -0
  306. package/dist/src/usage/estimator.js +22 -0
  307. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  308. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  309. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  311. package/dist/src/utils/clipboard-image.js +23 -23
  312. package/dist/src/utils/llm-deadline.d.ts +9 -1
  313. package/dist/src/utils/llm-deadline.js +31 -2
  314. package/dist/src/utils/permission-mode.d.ts +33 -0
  315. package/dist/src/utils/permission-mode.js +164 -3
  316. package/dist/src/utils/permission-mode.test.js +92 -1
  317. package/dist/src/utils/settings.d.ts +27 -31
  318. package/dist/src/utils/settings.js +100 -109
  319. package/dist/src/utils/side-question.js +2 -2
  320. package/dist/src/utils/skills.js +3 -3
  321. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  322. package/package.json +1 -1
@@ -26,6 +26,7 @@ import { SETUP_GUIDE_TEXT } from "../mcp/setup-guide-text.js";
26
26
  export const NATIVE_MUONROI_TOOL_NAMES = [
27
27
  "ee_health",
28
28
  "ee_feedback",
29
+ "ee_write",
29
30
  "usage_forensics",
30
31
  "lsp_query",
31
32
  "setup_guide",
@@ -103,6 +104,61 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
103
104
  }
104
105
  },
105
106
  });
107
+ tools.ee_write = dynamicTool({
108
+ description: "Save a NEW lesson to the Experience Engine brain so you and future sessions recall it. Call this the MOMENT " +
109
+ "you hit a mistake / error / dead-end and find the working fix: record the pitfall AND the fix in one concise, " +
110
+ "generalizable lesson (1-3 sentences — what to do or avoid next time, NOT a play-by-play of this turn). The " +
111
+ "lesson is embedded immediately and becomes recallable via ee_query in THIS and future sessions. Use ee_query " +
112
+ "first when unsure if a lesson already exists. collection defaults to experience-behavioral (use " +
113
+ "experience-principles only for a broad, project-independent principle).",
114
+ inputSchema: jsonSchema({
115
+ type: "object",
116
+ properties: {
117
+ lesson: {
118
+ type: "string",
119
+ description: "The lesson: the mistake/pitfall AND the fix, concise and generalizable.",
120
+ },
121
+ title: { type: "string", description: "Optional short title (<=120 chars)." },
122
+ collection: {
123
+ type: "string",
124
+ enum: ["experience-behavioral", "experience-principles"],
125
+ description: "Target tier; default experience-behavioral.",
126
+ },
127
+ },
128
+ required: ["lesson"],
129
+ additionalProperties: false,
130
+ }),
131
+ execute: async (input) => {
132
+ const lesson = typeof input?.lesson === "string" ? input.lesson.trim() : "";
133
+ if (lesson.length < 12) {
134
+ return errLine("invalid_args", "ee_write requires a non-empty, substantive lesson (>=12 chars)");
135
+ }
136
+ // Cap oversized bodies (EE import-quality rule: bare points >1500 chars are
137
+ // status-dumps, not lessons).
138
+ const text = lesson.length > 1500 ? `${lesson.slice(0, 1497)}...` : lesson;
139
+ const collection = input?.collection === "experience-principles" ? "experience-principles" : "experience-behavioral";
140
+ const title = typeof input?.title === "string" && input.title.trim() ? input.title.trim().slice(0, 120) : undefined;
141
+ try {
142
+ const { writeExperienceEE } = await import("../ee/search.js");
143
+ const path = await import("node:path");
144
+ const cwd = opts.cwd ?? process.cwd();
145
+ // confidence 0.65 (above the import default 0.6): an agent-asserted lesson
146
+ // is deliberate but unproven — the ee_feedback loop then tunes it.
147
+ const result = await writeExperienceEE(text, {
148
+ collection,
149
+ title,
150
+ projectSlug: path.basename(cwd),
151
+ confidence: 0.65,
152
+ });
153
+ if (!result.ok)
154
+ return errLine("write_failed", result.error ?? "import-memory POST failed");
155
+ return json({ ok: true, id: result.id, collection, recallable: "now — same session via ee_query" });
156
+ }
157
+ catch (e) {
158
+ return errLine("write_failed", e instanceof Error ? e.message : String(e));
159
+ }
160
+ },
161
+ });
106
162
  // ── Self-diagnostics: usage_forensics ─────────────────────────────────────
107
163
  tools.usage_forensics = dynamicTool({
108
164
  description: "Per-session token-cost forensics by session-id prefix: peak input, cache-hit ratio, per-event breakdown.",
@@ -26,7 +26,8 @@ describe("bash tool — empty/missing command corrective guard", () => {
26
26
  const bash = new BashTool(os.tmpdir());
27
27
  const tools = createBuiltinTools(bash, "agent");
28
28
  const out = await runBash(tools, {});
29
- expect(out).toMatch(/ERROR/);
29
+ // Guard uses "BLOCKED (empty-bash):" prefix — more semantic than bare "ERROR".
30
+ expect(out).toMatch(/BLOCKED/);
30
31
  expect(out).toMatch(/non-empty/i);
31
32
  expect(out).toMatch(/command/i);
32
33
  });
@@ -34,14 +35,14 @@ describe("bash tool — empty/missing command corrective guard", () => {
34
35
  const bash = new BashTool(os.tmpdir());
35
36
  const tools = createBuiltinTools(bash, "agent");
36
37
  const out = await runBash(tools, { command: "" });
37
- expect(out).toMatch(/ERROR/);
38
+ expect(out).toMatch(/BLOCKED/);
38
39
  expect(out).toMatch(/non-empty/i);
39
40
  });
40
41
  it("returns the corrective error for a whitespace-only command", async () => {
41
42
  const bash = new BashTool(os.tmpdir());
42
43
  const tools = createBuiltinTools(bash, "agent");
43
44
  const out = await runBash(tools, { command: " " });
44
- expect(out).toMatch(/ERROR/);
45
+ expect(out).toMatch(/BLOCKED/);
45
46
  expect(out).toMatch(/non-empty/i);
46
47
  });
47
48
  it("does NOT block a real command", async () => {
@@ -12,11 +12,11 @@ import { BashTool } from "./bash.js";
12
12
  import { clearBashOutputCache } from "./bash-output-cache.js";
13
13
  import { __resetGitSafetyState, recordCommandOutcome } from "./git-safety.js";
14
14
  import { createBuiltinTools } from "./registry.js";
15
- async function runBash(tools, args) {
15
+ async function runBash(tools, args, extra) {
16
16
  const t = tools.bash;
17
17
  if (!t?.execute)
18
18
  throw new Error("bash tool has no execute");
19
- const out = await t.execute(args);
19
+ const out = await t.execute(args, extra);
20
20
  return typeof out === "string" ? out : JSON.stringify(out);
21
21
  }
22
22
  describe("git-safety wiring in bash tool", () => {
@@ -28,6 +28,7 @@ describe("git-safety wiring in bash tool", () => {
28
28
  });
29
29
  afterEach(() => {
30
30
  delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
31
+ delete globalThis.__muonroiSafetyApproved;
31
32
  });
32
33
  it("BLOCKS git push (without executing) after a verification failed this session", async () => {
33
34
  const bash = new BashTool(os.tmpdir());
@@ -35,7 +36,7 @@ describe("git-safety wiring in bash tool", () => {
35
36
  // Simulate a failed test earlier in the session.
36
37
  recordCommandOutcome("GS1", "npm test", false);
37
38
  const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 });
38
- expect(out).toMatch(/^BLOCKED:/);
39
+ expect(out).toMatch(/^BLOCKED \(git-safety\):/);
39
40
  expect(out).toMatch(/npm test/);
40
41
  // The distinctive block message proves git push never ran (a real push in
41
42
  // tmpdir would fail with a git error like "not a git repository", not this).
@@ -72,16 +73,26 @@ describe("git-safety wiring in bash tool", () => {
72
73
  // Fresh anon registry (simulates the per-turn rebuild).
73
74
  const toolsB = createBuiltinTools(bash, "agent"); // no sessionId
74
75
  const pushOut = await runBash(toolsB, { command: "git push origin main", timeout: 10_000 });
75
- expect(pushOut).toMatch(/^BLOCKED:/);
76
+ expect(pushOut).toMatch(/^BLOCKED \(git-safety\):/);
76
77
  }, 30_000);
77
- it("appends a sensitive-path WARNING on a broad git add when secrets exist", async () => {
78
+ it("uses command-matched safety approval when the retry has a new toolCallId", async () => {
79
+ const bash = new BashTool(os.tmpdir());
80
+ const tools = createBuiltinTools(bash, "agent", { sessionId: "GS-APPROVE" });
81
+ recordCommandOutcome("GS-APPROVE", "npm test", false);
82
+ globalThis.__muonroiSafetyApproved = new Map([
83
+ ["blocked-tool-call-id", { kind: "once", command: "git push origin main" }],
84
+ ]);
85
+ const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 }, { toolCallId: "retry-tool-call-id" });
86
+ expect(out).not.toMatch(/^BLOCKED/);
87
+ }, 20_000);
88
+ it("blocks broad git add when sensitive paths exist", async () => {
78
89
  const dir = mkdtempSync(join(os.tmpdir(), "gs-stage-"));
79
90
  writeFileSync(join(dir, ".env"), "API_KEY=secret");
80
91
  try {
81
92
  const bash = new BashTool(dir);
82
93
  const tools = createBuiltinTools(bash, "agent", { sessionId: "GS4" });
83
94
  const out = await runBash(tools, { command: "git add -A", timeout: 10_000 });
84
- expect(out).toMatch(/\[WARNING:/);
95
+ expect(out).toMatch(/^BLOCKED \(git-safety\):/);
85
96
  expect(out).toMatch(/\.env/);
86
97
  }
87
98
  finally {
@@ -13,6 +13,7 @@ interface ToolRegistryOpts {
13
13
  runDelegation?: (request: TaskRequest, abortSignal?: AbortSignal) => Promise<ToolResult>;
14
14
  readDelegation?: (id: string) => Promise<ToolResult>;
15
15
  listDelegations?: () => Promise<ToolResult>;
16
+ killDelegation?: (id: string) => Promise<ToolResult>;
16
17
  modelId?: string;
17
18
  /**
18
19
  * When false, the 3 vision-proxy tools (analyze_image, ask_vision_proxy,
@@ -51,6 +52,10 @@ interface BashRepeatEntry {
51
52
  }
52
53
  declare global {
53
54
  var __muonroiBashRepeatState: Map<string, BashRepeatEntry> | undefined;
55
+ var __muonroiSafetyApproved: Map<string, {
56
+ kind: "once" | "session";
57
+ command: string;
58
+ }> | undefined;
54
59
  }
55
60
  export declare const MAX_TOOL_OUTPUT_CHARS: number;
56
61
  export declare function truncateOutput(text: string, maxChars?: number): string;
@@ -12,10 +12,16 @@ import { needsVisionProxy } from "../providers/vision-proxy.js";
12
12
  import { getBashRun, sliceBashOutput } from "./bash-output-cache.js";
13
13
  import { editFile, readFile, writeFile } from "./file.js";
14
14
  import { FileTracker } from "./file-tracker.js";
15
- import { analyzeGitCommand, checkPushGate, pushBlockedMessage, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
15
+ import { analyzeGitCommand, checkPushGate, checkSensitiveStaging, commitBlockedMessage, pushBlockedMessage, recordCommandOutcome, } from "./git-safety.js";
16
16
  import { executeGrep } from "./grep.js";
17
17
  import { registerNativeMuonroiTools } from "./native-tools.js";
18
18
  import { VISION_TOOL_NAMES } from "./vision-gate.js";
19
+ function getSafetyApprovedMap() {
20
+ if (!globalThis.__muonroiSafetyApproved) {
21
+ globalThis.__muonroiSafetyApproved = new Map();
22
+ }
23
+ return globalThis.__muonroiSafetyApproved;
24
+ }
19
25
  function getBashRepeatState() {
20
26
  if (!globalThis.__muonroiBashRepeatState) {
21
27
  globalThis.__muonroiBashRepeatState = new Map();
@@ -80,7 +86,7 @@ export function createBuiltinTools(bash, mode, opts) {
80
86
  const fileTracker = new FileTracker();
81
87
  // read_file
82
88
  tools.read_file = dynamicTool({
83
- description: "Read file contents with optional start_line/end_line for iterative reading.",
89
+ description: "Read file contents. For large files, you MUST use start_line and end_line to extract only the needed sections (e.g. specific functions). Reading full large files will quickly exhaust your context budget. Use grep or lsp first to find line numbers.",
84
90
  inputSchema: jsonSchema({
85
91
  type: "object",
86
92
  properties: {
@@ -97,7 +103,7 @@ export function createBuiltinTools(bash, mode, opts) {
97
103
  });
98
104
  // grep
99
105
  tools.grep = dynamicTool({
100
- description: "Fast regex content search across the codebase using ripgrep. Returns matching lines with file paths and line numbers.",
106
+ description: "Fast regex content search across the codebase using ripgrep. Returns matching lines with file paths and line numbers. Use this to find precise line numbers before calling read_file with start_line/end_line.",
101
107
  inputSchema: jsonSchema({
102
108
  type: "object",
103
109
  properties: {
@@ -135,6 +141,11 @@ export function createBuiltinTools(bash, mode, opts) {
135
141
  // else a single process-stable key. Over-sharing here is the safe direction
136
142
  // (it can only over-block a push, never wrongly allow one).
137
143
  const gitSafetyKey = opts?.sessionId && opts.sessionId.length > 0 ? opts.sessionId : `__proc_default__:${process.pid}`;
144
+ // Per-session empty-bash streak counter: escalates from guidance (strike 1-2)
145
+ // to hard block (strike 3+) so a cheap model that repeatedly emits `bash: {}`
146
+ // cannot loop indefinitely (live: deepseek session bf58d0f46b51 — 8+ empty calls).
147
+ const _emptyBashStreak = new Map();
148
+ const _prefixBlock = (kind, msg) => `BLOCKED (${kind}): ${msg}`;
138
149
  tools.bash = dynamicTool({
139
150
  description: "Execute a shell command. Output is automatically cached — every call returns a " +
140
151
  "run_id you can re-query via bash_output_get(run_id, mode=tail|head|grep|lines). " +
@@ -149,7 +160,10 @@ export function createBuiltinTools(bash, mode, opts) {
149
160
  },
150
161
  required: ["command"],
151
162
  }),
152
- execute: async (input) => {
163
+ // Phase 8 safety-blocked commands queue, keyed by sessionId + toolCallId.
164
+ // When the orchestrator intercepts a blocked result, the approval handler
165
+ // stores the approved command here so bash.execute() can retry it.
166
+ execute: async (input, extra) => {
153
167
  // Corrective guard for malformed calls: a cheap model sometimes emits a
154
168
  // bash call with a missing / empty `command` (live: deepseek sent `{}`
155
169
  // repeatedly until the loop-guard fired). Passing undefined to
@@ -158,9 +172,41 @@ export function createBuiltinTools(bash, mode, opts) {
158
172
  // both look like progress and feed the loop. Return a crisp instruction
159
173
  // so the next step supplies a real command instead of repeating.
160
174
  if (typeof input.command !== "string" || input.command.trim() === "") {
161
- return 'ERROR: the `bash` tool requires a non-empty "command" string, but the call had empty arguments. Provide the shell command to run, e.g. {"command":"ls -la"}.';
175
+ // Track empty-bash streak per session; escalate from guidance to hard block.
176
+ const _ebKey = gitSafetyKey ?? "no-session";
177
+ let _eb = _emptyBashStreak.get(_ebKey) ?? 0;
178
+ _eb++;
179
+ _emptyBashStreak.set(_ebKey, _eb);
180
+ if (_eb >= 3) {
181
+ return ('BLOCKED (empty-bash): the `bash` tool has been called with an empty/missing "command" 3+ times in a row. ' +
182
+ "Bash is now DISABLED for the remainder of this session — use read_file, grep, or other tools instead. " +
183
+ "If you need to run a shell command, state the blocker explicitly and the CLI will enable it again on the next turn.");
184
+ }
185
+ if (_eb >= 2) {
186
+ return ('BLOCKED (empty-bash): ERROR (2nd consecutive empty bash call): the `bash` tool requires a non-empty "command" string ' +
187
+ "but this is the 2nd call in a row with empty arguments. One more empty call will BLOCK bash for the session. " +
188
+ 'Provide a real command, e.g. {"command":"ls -la"}.');
189
+ }
190
+ return 'BLOCKED (empty-bash): the `bash` tool requires a non-empty "command" string, but the call had empty arguments. Provide the shell command to run, e.g. {"command":"ls -la"}.';
162
191
  }
192
+ // Reset the empty-bash streak on any successful command.
193
+ _emptyBashStreak.delete(gitSafetyKey ?? "no-session");
163
194
  const cmd = typeof input.command === "string" ? input.command : "";
195
+ // Safety override check: approval is granted after a blocked call, but
196
+ // the model's retry receives a new toolCallId. Match by id first, then
197
+ // by exact command so allow-once survives the retry boundary.
198
+ const _approvedMap = getSafetyApprovedMap();
199
+ const _approvalKey = extra?.toolCallId && _approvedMap.has(extra.toolCallId)
200
+ ? extra.toolCallId
201
+ : [..._approvedMap.entries()].find(([, approval]) => approval.command === cmd)?.[0];
202
+ const _approvalEntry = _approvalKey ? _approvedMap.get(_approvalKey) : undefined;
203
+ if (_approvalEntry) {
204
+ if (_approvalEntry.kind === "once") {
205
+ _approvedMap.delete(_approvalKey);
206
+ }
207
+ const result = await bash.execute(input.command, input.timeout ?? 30000);
208
+ return formatResult(result);
209
+ }
164
210
  // Git safety (pre-execution). Block `git push` while a verification
165
211
  // command failed this session and was not re-run green; warn on broad
166
212
  // `git add -A` / `git commit -a` when sensitive paths exist. Applied to
@@ -169,16 +215,46 @@ export function createBuiltinTools(bash, mode, opts) {
169
215
  // (or the real sessionId) — unlike repeatKey, whose anon fallback changes
170
216
  // on every registry rebuild and would silently drop the gate across turns.
171
217
  const gitShape = analyzeGitCommand(cmd);
172
- const stageWarn = gitShape.isBroadStage ? stagingWarning(bash.getCwd()) : "";
218
+ // Hard-block broad staging when sensitive files are present.
219
+ // This runs PRE-EXECUTION (before bash.execute) regardless of permission mode.
220
+ if (gitShape.isBroadStage) {
221
+ const stagingBlock = checkSensitiveStaging(bash.getCwd());
222
+ if (stagingBlock.blocked) {
223
+ return _prefixBlock("git-safety", stagingBlock.message);
224
+ }
225
+ }
173
226
  if (gitShape.isPush) {
174
227
  const gate = checkPushGate(gitSafetyKey);
175
228
  if (gate.blocked) {
176
- return `${pushBlockedMessage(gate.failed)}${stageWarn}`;
229
+ return _prefixBlock("git-safety", pushBlockedMessage(gate.failed));
230
+ }
231
+ }
232
+ // G1 follow-up: a raw bash `git commit` must not bypass the LSP commit
233
+ // gate that the `git_commit` tool + auto-commit backstop enforce. Derive
234
+ // the to-be-committed paths from git state and block (pre-exec, never run
235
+ // the commit) if any staged source file has a severity-1 LSP error. The
236
+ // gate is fail-OPEN (LSP slow/down → allow) and self-disables under
237
+ // MUONROI_COMMIT_GATE=0 / the unit-test suite. Lazy-import keeps the
238
+ // LSP-heavy module off the hot bash path (mirrors git_commit below).
239
+ if (gitShape.isCommit) {
240
+ const { gateStagedPaths, isCommitGateEnabled, pathsForCommitGate } = await import("../orchestrator/auto-commit.js");
241
+ if (isCommitGateEnabled()) {
242
+ const commitCwd = bash.getCwd();
243
+ const paths = await pathsForCommitGate(commitCwd, {
244
+ broadAdd: gitShape.isBroadAdd,
245
+ commitAll: gitShape.isCommitAll,
246
+ });
247
+ if (paths.length > 0) {
248
+ const gate = await gateStagedPaths(commitCwd, paths);
249
+ if (!gate.ok) {
250
+ return _prefixBlock("git-safety", commitBlockedMessage(gate.summary));
251
+ }
252
+ }
177
253
  }
178
254
  }
179
255
  if (input.background) {
180
256
  const result = await bash.startBackground(input.command);
181
- return `${formatResult(result)}${stageWarn}`;
257
+ return formatResult(result);
182
258
  }
183
259
  // 3-3: compute canonical form BEFORE running so we can attach an
184
260
  // inline reminder if it matches the previous bash call.
@@ -213,9 +289,9 @@ export function createBuiltinTools(bash, mode, opts) {
213
289
  const hint = chars >= 4_000
214
290
  ? ` — ${chars} chars cached; use bash_output_get(run_id, mode=tail|head|grep|lines) to re-query`
215
291
  : "";
216
- return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}${stageWarn}`;
292
+ return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}`;
217
293
  }
218
- return `${formatted}${stageWarn}`;
294
+ return formatted;
219
295
  },
220
296
  });
221
297
  // bash_output_get — re-query the cached full output of a previous bash run.
@@ -344,6 +420,58 @@ export function createBuiltinTools(bash, mode, opts) {
344
420
  };
345
421
  },
346
422
  });
423
+ // git_commit — the agent commits its OWN work with a message IT writes.
424
+ tools.git_commit = dynamicTool({
425
+ description: "Commit the files you have created/edited so far, with a commit message YOU write. Use this to commit each " +
426
+ "cohesive, working chunk the moment it is finished — and after EACH step of a multi-step plan — instead of " +
427
+ "leaving everything for one commit at the end. message: a clear conventional subject describing WHAT changed " +
428
+ "(e.g. 'feat(auth): add token refresh'), optionally followed by a body — NOT a restatement of the request. " +
429
+ "Only files you wrote via write_file/edit_file are staged (secrets + CLI artifacts are excluded, and the " +
430
+ "'Coding by - Muonroi-CLI' attribution line is appended automatically). No-op if none of your written files " +
431
+ "have uncommitted changes.",
432
+ inputSchema: jsonSchema({
433
+ type: "object",
434
+ properties: {
435
+ message: {
436
+ type: "string",
437
+ description: "Conventional commit subject (optionally + body) describing what changed, authored by you.",
438
+ },
439
+ },
440
+ required: ["message"],
441
+ }),
442
+ execute: async (input) => {
443
+ const message = typeof input?.message === "string" ? input.message.trim() : "";
444
+ if (message.length < 3) {
445
+ return { success: false, output: "git_commit requires a non-empty commit message." };
446
+ }
447
+ const written = fileTracker.writtenPaths();
448
+ if (written.length === 0) {
449
+ return {
450
+ success: false,
451
+ output: "Nothing to commit — you have not created or edited any file via write_file/edit_file this session.",
452
+ };
453
+ }
454
+ try {
455
+ const { commitSpecificPaths } = await import("../orchestrator/auto-commit.js");
456
+ const result = await commitSpecificPaths(bash.getCwd(), written, message);
457
+ if (!result.committed) {
458
+ // G1: when the LSP quality gate blocked the commit, surface the
459
+ // per-file errors so the agent can fix them and call git_commit again.
460
+ // Tell the agent to FIX the errors — do NOT advertise the bypass
461
+ // env here (that is a USER escape hatch; surfacing it to the agent
462
+ // just invites it to circumvent the gate instead of fixing).
463
+ const detail = result.reason === "lsp-errors"
464
+ ? `\nStaged files have errors — fix them and call git_commit again:\n${result.detail ?? ""}`
465
+ : "";
466
+ return { success: false, output: `No commit made (${result.reason}).${detail}` };
467
+ }
468
+ return { success: true, output: `Committed ${result.fileCount} file(s) → ${result.sha}` };
469
+ }
470
+ catch (e) {
471
+ return { success: false, output: `git_commit failed: ${e instanceof Error ? e.message : String(e)}` };
472
+ }
473
+ },
474
+ });
347
475
  // task
348
476
  if (opts?.runTask) {
349
477
  const runTask = opts.runTask;
@@ -358,11 +486,20 @@ export function createBuiltinTools(bash, mode, opts) {
358
486
  },
359
487
  description: { type: "string", description: "Short description of the task" },
360
488
  prompt: { type: "string", description: "Detailed instructions for the sub-agent" },
489
+ maxToolRounds: {
490
+ type: "number",
491
+ description: "Optional maximum tool execution rounds (default is 12 for foreground tasks)",
492
+ },
361
493
  },
362
494
  required: ["agent", "description", "prompt"],
363
495
  }),
364
496
  execute: async (input) => {
365
- const result = await runTask({ agent: input.agent, description: input.description, prompt: input.prompt });
497
+ const result = await runTask({
498
+ agent: input.agent,
499
+ description: input.description,
500
+ prompt: input.prompt,
501
+ maxToolRounds: input.maxToolRounds,
502
+ });
366
503
  return formatResult(result);
367
504
  },
368
505
  });
@@ -378,6 +515,10 @@ export function createBuiltinTools(bash, mode, opts) {
378
515
  agent: { type: "string", description: "Sub-agent type (usually 'explore')" },
379
516
  description: { type: "string", description: "Short description of the research task" },
380
517
  prompt: { type: "string", description: "Detailed research instructions" },
518
+ maxToolRounds: {
519
+ type: "number",
520
+ description: "Optional maximum tool execution rounds (default is 60 for explore delegations)",
521
+ },
381
522
  },
382
523
  required: ["agent", "description", "prompt"],
383
524
  }),
@@ -386,6 +527,7 @@ export function createBuiltinTools(bash, mode, opts) {
386
527
  agent: input.agent,
387
528
  description: input.description,
388
529
  prompt: input.prompt,
530
+ maxToolRounds: input.maxToolRounds,
389
531
  });
390
532
  return formatResult(result);
391
533
  },
@@ -424,6 +566,24 @@ export function createBuiltinTools(bash, mode, opts) {
424
566
  },
425
567
  });
426
568
  }
569
+ // delegation_kill
570
+ if (opts?.killDelegation) {
571
+ const killDelegation = opts.killDelegation;
572
+ tools.delegation_kill = dynamicTool({
573
+ description: "Terminate a running background delegation/subagent by ID.",
574
+ inputSchema: jsonSchema({
575
+ type: "object",
576
+ properties: {
577
+ id: { type: "string", description: "Delegation ID to terminate" },
578
+ },
579
+ required: ["id"],
580
+ }),
581
+ execute: async (input) => {
582
+ const result = await killDelegation(input.id);
583
+ return formatResult(result);
584
+ },
585
+ });
586
+ }
427
587
  // ee_query — semantic recall over the Experience Engine brain. This is the
428
588
  // in-CLI counterpart of the MCP `ee.query` tool (src/mcp/ee-tools.ts):
429
589
  // without it, the Agent Operating Contract + checkpoint reminders instruct
@@ -503,6 +663,20 @@ export function createBuiltinTools(bash, mode, opts) {
503
663
  if (resp === null) {
504
664
  return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured). Proceed without EE recall — re-read the source directly if you need the elided content.";
505
665
  }
666
+ // Record recalled entries as pending feedback debt in the same in-process
667
+ // ledger the native ee_feedback builtin clears, so an in-CLI active recall
668
+ // accrues a verdict obligation exactly like the external MCP ee.query does
669
+ // (mcp/ee-tools.ts). Layer 3 surfaces this debt as a reminder on the next
670
+ // enriched turn; without it the in-CLI recall arm was write-only and
671
+ // ee_feedback.clear() was a no-op.
672
+ try {
673
+ const { sessionRecallLedger, isRecallLedgerEnabled } = await import("../ee/recall-ledger.js");
674
+ if (isRecallLedgerEnabled())
675
+ sessionRecallLedger.record(resp.entries, query);
676
+ }
677
+ catch (err) {
678
+ console.error(`[tools:ee_query] recall-ledger record failed: ${err?.message}`);
679
+ }
506
680
  // Compact ranked `[id col]` index, not a JSON dump — the recallMode text
507
681
  // is ~30k (wide net), so JSON.stringify wasted the budget on escaping.
508
682
  return truncateOutput(formatRecallForAgent(resp, {
@@ -33,6 +33,7 @@ export interface TaskRequest {
33
33
  agent: BuiltinSubagentId | string;
34
34
  description: string;
35
35
  prompt: string;
36
+ maxToolRounds?: number;
36
37
  }
37
38
  export interface TaskRun {
38
39
  agent: string;
@@ -207,6 +208,7 @@ export interface ChatEntry {
207
208
  toolResult?: ToolResult;
208
209
  toolGroup?: ToolGroup;
209
210
  structuredResponse?: StructuredResponse;
211
+ reasoning?: string;
210
212
  }
211
213
  export interface PaymentPrecheck {
212
214
  security?: string;
@@ -221,13 +223,17 @@ export interface StructuredResponse {
221
223
  taskType: string;
222
224
  data: Record<string, unknown>;
223
225
  }
224
- export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap";
226
+ export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap" | "safety-override";
225
227
  export type CouncilOptionKind = "choice" | "freetext" | "chat";
226
228
  export interface CouncilQuestionOption {
227
229
  label: string;
228
230
  description?: string;
229
231
  value: string;
230
232
  kind: CouncilOptionKind;
233
+ /** Model-driven: if true, picking this cancels the whole interaction (discovery) */
234
+ isCancel?: boolean;
235
+ /** Model-driven: if true, picking this triggers a re-interview round (discovery) */
236
+ isAdjust?: boolean;
231
237
  }
232
238
  export interface CouncilQuestionData {
233
239
  questionId: string;
@@ -14,6 +14,7 @@ import "./slash/council.js";
14
14
  import "./slash/ideal.js";
15
15
  import "./slash/export.js";
16
16
  import "./slash/status.js";
17
+ import "./slash/ponytail.js";
17
18
  import type { AppProps } from "./types.js";
18
19
  export type { AppStartupConfig } from "./types.js";
19
20
  export declare function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }: AppProps): import("react").ReactNode;
Binary file
@@ -8,6 +8,7 @@ const PHASE_LABEL = {
8
8
  "pil-interview": "Understanding",
9
9
  "pil-acceptance": "Confirm Intent",
10
10
  "tool-loop-cap": "Tool-loop guard",
11
+ "safety-override": "Safety override",
11
12
  };
12
13
  /**
13
14
  * AskUserQuestion-style card. Controlled component — parent owns the state
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
2
2
  import { memo } from "react";
3
3
  import { Markdown } from "../markdown.js";
4
4
  import { PlanView } from "../plan.js";
5
- import { stripStrayModelMacros, trunc } from "../utils/text.js";
5
+ import { stripInvisibleChars, stripStrayModelMacros, trunc } from "../utils/text.js";
6
6
  import { describeMcpFsTool, toolArgs, toolLabel, tryParseArg } from "../utils/tools.js";
7
7
  import { DiffView, ReadFilePreviewView } from "./diff-view.js";
8
8
  import { LspDiagnosticsView, LspResultView } from "./lsp-views.js";
@@ -32,6 +32,8 @@ export function AssistantMessageContent({ content, t, expanded, isFinal, }) {
32
32
  // Strip stray model self-annotation macros (e.g. grok's trailing
33
33
  // `\confidence{85}`) that leak into the answer — not instructed in the prompt.
34
34
  content = stripStrayModelMacros(content);
35
+ // Strip invisible/control Unicode characters that mess up terminal rendering.
36
+ content = stripInvisibleChars(content);
35
37
  const lines = content.split("\n");
36
38
  const isLong = lines.length > ASSISTANT_MSG_COLLAPSED_LINES;
37
39
  // Phase 5 F7 — the FINAL assistant message in a turn IS the answer the
@@ -114,8 +116,34 @@ function MessageViewImpl({ entry, index, t, modeColor, expandedMessages, mcpRun,
114
116
  switch (entry.type) {
115
117
  case "user":
116
118
  return (_jsx("box", { border: ["left"], customBorderChars: SPLIT, borderColor: entry.modeColor || modeColor, marginTop: index === 0 ? 0 : 1, marginBottom: 1, children: _jsxs("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: t.backgroundPanel, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, _jsx(UserMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false })] }) }));
117
- case "assistant":
118
- return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, _jsx(AssistantMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false, isFinal: isFinalAssistant === true })] }));
119
+ case "assistant": {
120
+ let fallbackSr = null;
121
+ const rawContent = entry.content.trim();
122
+ if (rawContent.startsWith("{") && rawContent.endsWith("}")) {
123
+ try {
124
+ const parsed = JSON.parse(rawContent);
125
+ if (Array.isArray(parsed.steps) && parsed.steps.length > 0 && parsed.steps[0].action) {
126
+ fallbackSr = { taskType: "plan", data: parsed };
127
+ }
128
+ else if (Array.isArray(parsed.changes) && parsed.changes.length > 0) {
129
+ fallbackSr = { taskType: "refactor", data: parsed };
130
+ }
131
+ else if (parsed.hypothesis && parsed.root_cause) {
132
+ fallbackSr = { taskType: "debug", data: parsed };
133
+ }
134
+ else if (Array.isArray(parsed.findings)) {
135
+ fallbackSr = { taskType: "analyze", data: parsed };
136
+ }
137
+ else if (Array.isArray(parsed.files) && parsed.files.length > 0 && parsed.files[0].path) {
138
+ fallbackSr = { taskType: "generate", data: parsed };
139
+ }
140
+ }
141
+ catch {
142
+ // ignore
143
+ }
144
+ }
145
+ return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, entry.reasoning ? (_jsxs("box", { paddingLeft: 0, marginBottom: 1, flexShrink: 0, flexDirection: "column", children: [_jsx("text", { fg: t.textMuted, children: "[Thought] Thought" }), _jsx("box", { border: ["left"], borderColor: t.textMuted, paddingLeft: 2, marginTop: 1, flexDirection: "column", children: _jsx(Markdown, { content: entry.reasoning, t: t }) })] })) : null, fallbackSr ? (_jsx(StructuredResponseView, { t: t, sr: fallbackSr, modeColor: entry.modeColor || modeColor })) : (_jsx(AssistantMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false, isFinal: isFinalAssistant === true }))] }));
146
+ }
119
147
  case "tool_call":
120
148
  return (_jsx("box", { paddingLeft: 3, marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: entry.modeColor || modeColor }, children: "▣ " }), _jsx("span", { style: { fg: t.textMuted }, children: entry.content.replace("▣ ", "") })] }) }));
121
149
  case "tool_group":
@@ -234,6 +262,12 @@ export const MessageView = memo(MessageViewImpl, (prev, next) => {
234
262
  return false;
235
263
  if ((prevMcp?.count ?? 0) !== (nextMcp?.count ?? 0))
236
264
  return false;
265
+ // Phase 5 F7 — isFinalAssistant changes when a new assistant message is
266
+ // appended (the previously-final message is no longer final). Without this
267
+ // check, the memo skips the re-render and the old final assistant stays
268
+ // incorrectly expanded for the rest of the session.
269
+ if ((prev.isFinalAssistant ?? false) !== (next.isFinalAssistant ?? false))
270
+ return false;
237
271
  return true;
238
272
  });
239
273
  //# sourceMappingURL=message-view.js.map
@@ -23,6 +23,13 @@ export function StructuredResponseView({ t, sr, modeColor }) {
23
23
  }
24
24
  case "analyze": {
25
25
  const r = d;
26
+ // Graceful fallback: model may have called respond_analyze but sent a
27
+ // free-form { response: "..." } payload (schema mismatch due to tool being
28
+ // unavailable in the current turn). Render as plain markdown rather than
29
+ // an empty findings list (session 48d22fe436f6 swallowed-answer bug).
30
+ if ((!r.findings || r.findings.length === 0) && typeof r.response === "string" && r.response.trim()) {
31
+ return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: renderMarkdown(r.response, t) }));
32
+ }
26
33
  const sevColor = (s) => (s === "high" ? t.diffRemovedFg : s === "medium" ? t.planStepNum : t.textMuted);
27
34
  return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: (r.findings ?? []).map((f, i) => (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: sevColor(f.severity) }, children: `[${f.severity.toUpperCase()}] ` }), _jsx("span", { children: f.text })] }), _jsx("text", { fg: t.textMuted, children: ` evidence: ${f.evidence}` })] }, `af${i}`))) }));
28
35
  }