muonroi-cli 1.7.2 → 1.8.1

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 (326) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  3. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  4. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  5. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  6. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  7. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  8. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  9. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  10. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  11. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  12. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  13. package/dist/src/agent-harness/mock-model.js +143 -6
  14. package/dist/src/agent-harness/test-spawn.js +3 -1
  15. package/dist/src/chat/chat-keychain.js +9 -1
  16. package/dist/src/cli/cost-forensics.js +7 -4
  17. package/dist/src/cli/keys.d.ts +2 -2
  18. package/dist/src/cli/keys.js +107 -28
  19. package/dist/src/cli/usage-report.js +2 -2
  20. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  21. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  22. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  23. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  24. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  25. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  27. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  28. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  29. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  30. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  31. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  32. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  33. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  34. package/dist/src/council/clarifier.d.ts +29 -1
  35. package/dist/src/council/clarifier.js +64 -1
  36. package/dist/src/council/debate-planner.d.ts +2 -1
  37. package/dist/src/council/debate-planner.js +5 -3
  38. package/dist/src/council/debate.d.ts +9 -0
  39. package/dist/src/council/debate.js +88 -8
  40. package/dist/src/council/index.d.ts +46 -1
  41. package/dist/src/council/index.js +125 -19
  42. package/dist/src/council/llm.js +41 -13
  43. package/dist/src/council/prompts.js +36 -1
  44. package/dist/src/council/types.d.ts +14 -1
  45. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  46. package/dist/src/ee/bridge.d.ts +3 -1
  47. package/dist/src/ee/bridge.js +9 -3
  48. package/dist/src/ee/bridge.test.js +1 -2
  49. package/dist/src/ee/client.js +15 -2
  50. package/dist/src/ee/recall-ledger.d.ts +20 -0
  51. package/dist/src/ee/recall-ledger.js +40 -1
  52. package/dist/src/ee/search.d.ts +25 -0
  53. package/dist/src/ee/search.js +73 -0
  54. package/dist/src/ee/types.d.ts +15 -1
  55. package/dist/src/ee/who-am-i.d.ts +41 -0
  56. package/dist/src/ee/who-am-i.js +143 -0
  57. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  58. package/dist/src/ee/who-am-i.test.js +101 -0
  59. package/dist/src/flow/compaction/index.d.ts +1 -0
  60. package/dist/src/flow/compaction/index.js +4 -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 +59 -62
  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 +13 -5
  76. package/dist/src/lsp/manager.test.js +41 -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/auto-setup.js +0 -8
  86. package/dist/src/mcp/client-pool.d.ts +1 -1
  87. package/dist/src/mcp/client-pool.js +25 -9
  88. package/dist/src/mcp/ee-tools.d.ts +10 -0
  89. package/dist/src/mcp/ee-tools.js +34 -0
  90. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/self-verify-runner.js +1 -1
  93. package/dist/src/models/catalog-gemini.test.js +12 -10
  94. package/dist/src/models/catalog.json +106 -38
  95. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  96. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  97. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +3 -0
  98. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  99. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  100. package/dist/src/orchestrator/__tests__/message-processor.test.js +2 -0
  101. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  102. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  105. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  106. package/dist/src/orchestrator/agent.test.js +5 -2
  107. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  108. package/dist/src/orchestrator/auto-commit.js +366 -0
  109. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  110. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  111. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  112. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  113. package/dist/src/orchestrator/compaction.d.ts +22 -0
  114. package/dist/src/orchestrator/compaction.js +42 -0
  115. package/dist/src/orchestrator/delegations.d.ts +1 -0
  116. package/dist/src/orchestrator/delegations.js +41 -0
  117. package/dist/src/orchestrator/error-utils.js +27 -2
  118. package/dist/src/orchestrator/message-processor.d.ts +23 -5
  119. package/dist/src/orchestrator/message-processor.js +226 -2573
  120. package/dist/src/orchestrator/orchestrator.d.ts +11 -2
  121. package/dist/src/orchestrator/orchestrator.js +530 -43
  122. package/dist/src/orchestrator/pending-calls.js +2 -1
  123. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  124. package/dist/src/orchestrator/preprocessor.js +146 -0
  125. package/dist/src/orchestrator/prompts.d.ts +16 -0
  126. package/dist/src/orchestrator/prompts.js +130 -67
  127. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  128. package/dist/src/orchestrator/safety-askcard.js +74 -0
  129. package/dist/src/orchestrator/sandbox.test.js +8 -5
  130. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  131. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  132. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  133. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  134. package/dist/src/orchestrator/stream-runner.js +52 -30
  135. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  136. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  137. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  138. package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
  139. package/dist/src/orchestrator/subagent-compactor.js +148 -24
  140. package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
  141. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  142. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  143. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  144. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  145. package/dist/src/orchestrator/tool-engine.d.ts +153 -0
  146. package/dist/src/orchestrator/tool-engine.js +3138 -0
  147. package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -0
  148. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  149. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  150. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  151. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  152. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  153. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  154. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  155. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  156. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  157. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  158. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  159. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  160. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  161. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  162. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  163. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  164. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  165. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  166. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  167. package/dist/src/pil/agent-operating-contract.js +5 -3
  168. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  169. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  170. package/dist/src/pil/cheap-model-playbook.js +1 -1
  171. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  172. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  173. package/dist/src/pil/config.d.ts +20 -0
  174. package/dist/src/pil/config.js +35 -0
  175. package/dist/src/pil/discovery-types.d.ts +32 -9
  176. package/dist/src/pil/discovery.d.ts +3 -3
  177. package/dist/src/pil/discovery.js +138 -244
  178. package/dist/src/pil/index.d.ts +1 -1
  179. package/dist/src/pil/index.js +1 -1
  180. package/dist/src/pil/layer1-intent.d.ts +9 -1
  181. package/dist/src/pil/layer1-intent.js +62 -7
  182. package/dist/src/pil/layer1-intent.test.js +112 -2
  183. package/dist/src/pil/layer15-context-scan.js +1 -1
  184. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  185. package/dist/src/pil/layer16-clarity.js +52 -88
  186. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  187. package/dist/src/pil/layer18-acceptance.js +1 -75
  188. package/dist/src/pil/layer2-personality.js +7 -8
  189. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  190. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  191. package/dist/src/pil/layer3-ee-injection.js +180 -14
  192. package/dist/src/pil/layer4-gsd.js +22 -8
  193. package/dist/src/pil/layer5-context.d.ts +1 -0
  194. package/dist/src/pil/layer5-context.js +26 -11
  195. package/dist/src/pil/layer6-output.js +23 -5
  196. package/dist/src/pil/llm-classify.d.ts +11 -0
  197. package/dist/src/pil/llm-classify.js +196 -3
  198. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  199. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  200. package/dist/src/pil/pipeline.js +15 -1
  201. package/dist/src/pil/response-tools.d.ts +16 -0
  202. package/dist/src/pil/response-tools.js +41 -0
  203. package/dist/src/pil/schema.d.ts +8 -0
  204. package/dist/src/pil/schema.js +24 -3
  205. package/dist/src/pil/task-tier-map.js +7 -7
  206. package/dist/src/pil/types.d.ts +9 -3
  207. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  208. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  209. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  210. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  211. package/dist/src/product-loop/discovery-interview.js +121 -7
  212. package/dist/src/product-loop/gather.js +19 -5
  213. package/dist/src/product-loop/loop-driver.js +2 -2
  214. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  215. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  216. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  217. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  218. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  219. package/dist/src/providers/auth/gcloud.js +102 -0
  220. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  221. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  222. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  223. package/dist/src/providers/auth/grok-oauth.js +1 -1
  224. package/dist/src/providers/auth/registry.js +28 -6
  225. package/dist/src/providers/gemini.d.ts +1 -1
  226. package/dist/src/providers/gemini.js +10 -3
  227. package/dist/src/providers/keychain.d.ts +2 -2
  228. package/dist/src/providers/keychain.js +20 -2
  229. package/dist/src/providers/openai.js +5 -1
  230. package/dist/src/providers/runtime.d.ts +1 -1
  231. package/dist/src/providers/runtime.js +11 -2
  232. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  233. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  234. package/dist/src/providers/strategies/google.strategy.js +159 -9
  235. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  236. package/dist/src/providers/types.d.ts +1 -1
  237. package/dist/src/router/decide.test.js +40 -8
  238. package/dist/src/router/step-router.d.ts +20 -0
  239. package/dist/src/router/step-router.js +73 -0
  240. package/dist/src/self-qa/agentic-loop.js +30 -21
  241. package/dist/src/self-qa/orchestrator.js +5 -5
  242. package/dist/src/self-qa/scenario-planner.js +1 -1
  243. package/dist/src/self-qa/spec-emitter.js +4 -4
  244. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  245. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  246. package/dist/src/storage/__tests__/sweep-stale-pending.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/migrations.js +13 -1
  250. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  251. package/dist/src/storage/transcript-view.js +7 -1
  252. package/dist/src/storage/transcript.d.ts +3 -0
  253. package/dist/src/storage/transcript.js +254 -73
  254. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  255. package/dist/src/storage/usage.js +22 -5
  256. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  257. package/dist/src/tools/bash.d.ts +36 -23
  258. package/dist/src/tools/bash.js +59 -301
  259. package/dist/src/tools/bash.test.js +2 -257
  260. package/dist/src/tools/file-tracker.d.ts +5 -0
  261. package/dist/src/tools/file-tracker.js +14 -0
  262. package/dist/src/tools/file.js +2 -0
  263. package/dist/src/tools/git-safety.d.ts +38 -1
  264. package/dist/src/tools/git-safety.js +94 -4
  265. package/dist/src/tools/git-safety.test.js +45 -1
  266. package/dist/src/tools/native-tools.d.ts +1 -1
  267. package/dist/src/tools/native-tools.js +66 -1
  268. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  269. package/dist/src/tools/registry-git-safety.test.js +17 -6
  270. package/dist/src/tools/registry.d.ts +6 -0
  271. package/dist/src/tools/registry.js +215 -13
  272. package/dist/src/types/index.d.ts +8 -2
  273. package/dist/src/ui/app.d.ts +1 -0
  274. package/dist/src/ui/app.js +0 -0
  275. package/dist/src/ui/components/council-question-card.js +1 -0
  276. package/dist/src/ui/components/message-view.js +37 -3
  277. package/dist/src/ui/components/structured-response-view.js +7 -0
  278. package/dist/src/ui/components/task-list-panel.js +3 -12
  279. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  280. package/dist/src/ui/containers/chat-feed.js +66 -0
  281. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  282. package/dist/src/ui/containers/modals-layer.js +18 -0
  283. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  284. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  285. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  286. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  287. package/dist/src/ui/slash/expand.js +14 -1
  288. package/dist/src/ui/slash/export.js +16 -1
  289. package/dist/src/ui/slash/menu-items.js +1 -0
  290. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  291. package/dist/src/ui/slash/ponytail.js +19 -0
  292. package/dist/src/ui/status-bar/store.js +0 -1
  293. package/dist/src/ui/status-bar/store.test.js +2 -2
  294. package/dist/src/ui/use-app-logic.d.ts +26 -0
  295. package/dist/src/ui/use-app-logic.js +0 -0
  296. package/dist/src/ui/utils/relaunch.js +1 -1
  297. package/dist/src/ui/utils/text.d.ts +11 -0
  298. package/dist/src/ui/utils/text.js +21 -1
  299. package/dist/src/ui/utils/text.test.js +25 -1
  300. package/dist/src/ui/utils/tools.js +3 -1
  301. package/dist/src/usage/cost-log.js +2 -2
  302. package/dist/src/usage/estimator.d.ts +14 -0
  303. package/dist/src/usage/estimator.js +22 -0
  304. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  305. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  306. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  307. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  308. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  309. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/logger.test.js +115 -0
  311. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  312. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  313. package/dist/src/utils/install-manager.d.ts +1 -0
  314. package/dist/src/utils/install-manager.js +76 -7
  315. package/dist/src/utils/install-manager.test.js +43 -1
  316. package/dist/src/utils/llm-deadline.d.ts +9 -1
  317. package/dist/src/utils/llm-deadline.js +31 -2
  318. package/dist/src/utils/logger.d.ts +28 -0
  319. package/dist/src/utils/logger.js +171 -0
  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 +36 -32
  324. package/dist/src/utils/settings.js +151 -123
  325. package/dist/src/utils/update-checker.test.js +5 -1
  326. 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.",
@@ -180,6 +236,7 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
180
236
  goal: { type: "string" },
181
237
  llm: { type: "string" },
182
238
  turns: { type: "number" },
239
+ mockLlmDir: { type: "string", description: "Use 'none' to run child TUI against real LLM APIs" },
183
240
  },
184
241
  required: ["mode"],
185
242
  }),
@@ -191,7 +248,15 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
191
248
  const { getModelInfo } = await import("../models/registry.js");
192
249
  if (!getModelInfo(input.llm))
193
250
  return errLine("unknown_model", `llm '${input.llm}' is not in catalog.json`);
194
- return json({ runId: jm.start({ kind: "agentic", goal: input.goal, llm: input.llm, turns: input.turns }) });
251
+ return json({
252
+ runId: jm.start({
253
+ kind: "agentic",
254
+ goal: input.goal,
255
+ llm: input.llm,
256
+ turns: input.turns,
257
+ mockLlmDir: input.mockLlmDir,
258
+ }),
259
+ });
195
260
  }
196
261
  return json({
197
262
  runId: jm.start({ kind: "tier1", since: input?.since, max: input?.max, emit: input?.emit, out: input?.out }),
@@ -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,7 +13,9 @@ 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;
18
+ consultParentSession?: (question: string) => Promise<string>;
17
19
  /**
18
20
  * When false, the 3 vision-proxy tools (analyze_image, ask_vision_proxy,
19
21
  * list_vision_cache) are omitted even for vision-proxy models. Used by the
@@ -51,6 +53,10 @@ interface BashRepeatEntry {
51
53
  }
52
54
  declare global {
53
55
  var __muonroiBashRepeatState: Map<string, BashRepeatEntry> | undefined;
56
+ var __muonroiSafetyApproved: Map<string, {
57
+ kind: "once" | "session";
58
+ command: string;
59
+ }> | undefined;
54
60
  }
55
61
  export declare const MAX_TOOL_OUTPUT_CHARS: number;
56
62
  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,11 +141,17 @@ 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). " +
141
152
  "Do NOT pipe `| tail`, `| head`, `| grep`, or `> file` — that hides output from " +
142
- "the cache. Run unpiped and slice via bash_output_get instead. For collecting system info (OS, versions, cwd layout, git, disk, processes) batch with ; or && in ONE call, e.g. 'uname -a; node -v; bun --version; ls -la | head -15; git status --short; df -h .; ps aux | head -5'. Set background=true for long-running processes (dev servers, watchers).",
153
+ "the cache. Run unpiped and slice via bash_output_get instead. For collecting system info (OS, versions, cwd layout, git, disk, processes) batch with ; or && in ONE call, e.g. 'uname -a; node -v; bun --version; ls -la | head -15; git status --short; df -h .; ps aux | head -5'. Set background=true for long-running processes (dev servers, watchers). " +
154
+ 'Avoid nesting double-quotes inline for database queries/scripts (e.g. executing raw SQL via `sqlite3 db "SELECT..."`) since they can fail parsing in shell environments; write query scripts to a temporary file first and run them.',
143
155
  inputSchema: jsonSchema({
144
156
  type: "object",
145
157
  properties: {
@@ -149,7 +161,10 @@ export function createBuiltinTools(bash, mode, opts) {
149
161
  },
150
162
  required: ["command"],
151
163
  }),
152
- execute: async (input) => {
164
+ // Phase 8 safety-blocked commands queue, keyed by sessionId + toolCallId.
165
+ // When the orchestrator intercepts a blocked result, the approval handler
166
+ // stores the approved command here so bash.execute() can retry it.
167
+ execute: async (input, extra) => {
153
168
  // Corrective guard for malformed calls: a cheap model sometimes emits a
154
169
  // bash call with a missing / empty `command` (live: deepseek sent `{}`
155
170
  // repeatedly until the loop-guard fired). Passing undefined to
@@ -158,9 +173,41 @@ export function createBuiltinTools(bash, mode, opts) {
158
173
  // both look like progress and feed the loop. Return a crisp instruction
159
174
  // so the next step supplies a real command instead of repeating.
160
175
  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"}.';
176
+ // Track empty-bash streak per session; escalate from guidance to hard block.
177
+ const _ebKey = gitSafetyKey ?? "no-session";
178
+ let _eb = _emptyBashStreak.get(_ebKey) ?? 0;
179
+ _eb++;
180
+ _emptyBashStreak.set(_ebKey, _eb);
181
+ if (_eb >= 3) {
182
+ return ('BLOCKED (empty-bash): the `bash` tool has been called with an empty/missing "command" 3+ times in a row. ' +
183
+ "Bash is now DISABLED for the remainder of this session — use read_file, grep, or other tools instead. " +
184
+ "If you need to run a shell command, state the blocker explicitly and the CLI will enable it again on the next turn.");
185
+ }
186
+ if (_eb >= 2) {
187
+ return ('BLOCKED (empty-bash): ERROR (2nd consecutive empty bash call): the `bash` tool requires a non-empty "command" string ' +
188
+ "but this is the 2nd call in a row with empty arguments. One more empty call will BLOCK bash for the session. " +
189
+ 'Provide a real command, e.g. {"command":"ls -la"}.');
190
+ }
191
+ 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
192
  }
193
+ // Reset the empty-bash streak on any successful command.
194
+ _emptyBashStreak.delete(gitSafetyKey ?? "no-session");
163
195
  const cmd = typeof input.command === "string" ? input.command : "";
196
+ // Safety override check: approval is granted after a blocked call, but
197
+ // the model's retry receives a new toolCallId. Match by id first, then
198
+ // by exact command so allow-once survives the retry boundary.
199
+ const _approvedMap = getSafetyApprovedMap();
200
+ const _approvalKey = extra?.toolCallId && _approvedMap.has(extra.toolCallId)
201
+ ? extra.toolCallId
202
+ : [..._approvedMap.entries()].find(([, approval]) => approval.command === cmd)?.[0];
203
+ const _approvalEntry = _approvalKey ? _approvedMap.get(_approvalKey) : undefined;
204
+ if (_approvalEntry) {
205
+ if (_approvalEntry.kind === "once") {
206
+ _approvedMap.delete(_approvalKey);
207
+ }
208
+ const result = await bash.execute(input.command, input.timeout ?? 30000);
209
+ return formatResult(result);
210
+ }
164
211
  // Git safety (pre-execution). Block `git push` while a verification
165
212
  // command failed this session and was not re-run green; warn on broad
166
213
  // `git add -A` / `git commit -a` when sensitive paths exist. Applied to
@@ -169,16 +216,46 @@ export function createBuiltinTools(bash, mode, opts) {
169
216
  // (or the real sessionId) — unlike repeatKey, whose anon fallback changes
170
217
  // on every registry rebuild and would silently drop the gate across turns.
171
218
  const gitShape = analyzeGitCommand(cmd);
172
- const stageWarn = gitShape.isBroadStage ? stagingWarning(bash.getCwd()) : "";
219
+ // Hard-block broad staging when sensitive files are present.
220
+ // This runs PRE-EXECUTION (before bash.execute) regardless of permission mode.
221
+ if (gitShape.isBroadStage) {
222
+ const stagingBlock = checkSensitiveStaging(bash.getCwd());
223
+ if (stagingBlock.blocked) {
224
+ return _prefixBlock("git-safety", stagingBlock.message);
225
+ }
226
+ }
173
227
  if (gitShape.isPush) {
174
228
  const gate = checkPushGate(gitSafetyKey);
175
229
  if (gate.blocked) {
176
- return `${pushBlockedMessage(gate.failed)}${stageWarn}`;
230
+ return _prefixBlock("git-safety", pushBlockedMessage(gate.failed));
231
+ }
232
+ }
233
+ // G1 follow-up: a raw bash `git commit` must not bypass the LSP commit
234
+ // gate that the `git_commit` tool + auto-commit backstop enforce. Derive
235
+ // the to-be-committed paths from git state and block (pre-exec, never run
236
+ // the commit) if any staged source file has a severity-1 LSP error. The
237
+ // gate is fail-OPEN (LSP slow/down → allow) and self-disables under
238
+ // MUONROI_COMMIT_GATE=0 / the unit-test suite. Lazy-import keeps the
239
+ // LSP-heavy module off the hot bash path (mirrors git_commit below).
240
+ if (gitShape.isCommit) {
241
+ const { gateStagedPaths, isCommitGateEnabled, pathsForCommitGate } = await import("../orchestrator/auto-commit.js");
242
+ if (isCommitGateEnabled()) {
243
+ const commitCwd = bash.getCwd();
244
+ const paths = await pathsForCommitGate(commitCwd, {
245
+ broadAdd: gitShape.isBroadAdd,
246
+ commitAll: gitShape.isCommitAll,
247
+ });
248
+ if (paths.length > 0) {
249
+ const gate = await gateStagedPaths(commitCwd, paths);
250
+ if (!gate.ok) {
251
+ return _prefixBlock("git-safety", commitBlockedMessage(gate.summary));
252
+ }
253
+ }
177
254
  }
178
255
  }
179
256
  if (input.background) {
180
257
  const result = await bash.startBackground(input.command);
181
- return `${formatResult(result)}${stageWarn}`;
258
+ return formatResult(result);
182
259
  }
183
260
  // 3-3: compute canonical form BEFORE running so we can attach an
184
261
  // inline reminder if it matches the previous bash call.
@@ -213,9 +290,9 @@ export function createBuiltinTools(bash, mode, opts) {
213
290
  const hint = chars >= 4_000
214
291
  ? ` — ${chars} chars cached; use bash_output_get(run_id, mode=tail|head|grep|lines) to re-query`
215
292
  : "";
216
- return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}${stageWarn}`;
293
+ return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}`;
217
294
  }
218
- return `${formatted}${stageWarn}`;
295
+ return formatted;
219
296
  },
220
297
  });
221
298
  // bash_output_get — re-query the cached full output of a previous bash run.
@@ -344,6 +421,58 @@ export function createBuiltinTools(bash, mode, opts) {
344
421
  };
345
422
  },
346
423
  });
424
+ // git_commit — the agent commits its OWN work with a message IT writes.
425
+ tools.git_commit = dynamicTool({
426
+ description: "Commit the files you have created/edited so far, with a commit message YOU write. Use this to commit each " +
427
+ "cohesive, working chunk the moment it is finished — and after EACH step of a multi-step plan — instead of " +
428
+ "leaving everything for one commit at the end. message: a clear conventional subject describing WHAT changed " +
429
+ "(e.g. 'feat(auth): add token refresh'), optionally followed by a body — NOT a restatement of the request. " +
430
+ "Only files you wrote via write_file/edit_file are staged (secrets + CLI artifacts are excluded, and the " +
431
+ "'Coding by - Muonroi-CLI' attribution line is appended automatically). No-op if none of your written files " +
432
+ "have uncommitted changes.",
433
+ inputSchema: jsonSchema({
434
+ type: "object",
435
+ properties: {
436
+ message: {
437
+ type: "string",
438
+ description: "Conventional commit subject (optionally + body) describing what changed, authored by you.",
439
+ },
440
+ },
441
+ required: ["message"],
442
+ }),
443
+ execute: async (input) => {
444
+ const message = typeof input?.message === "string" ? input.message.trim() : "";
445
+ if (message.length < 3) {
446
+ return { success: false, output: "git_commit requires a non-empty commit message." };
447
+ }
448
+ const written = fileTracker.writtenPaths();
449
+ if (written.length === 0) {
450
+ return {
451
+ success: false,
452
+ output: "Nothing to commit — you have not created or edited any file via write_file/edit_file this session.",
453
+ };
454
+ }
455
+ try {
456
+ const { commitSpecificPaths } = await import("../orchestrator/auto-commit.js");
457
+ const result = await commitSpecificPaths(bash.getCwd(), written, message);
458
+ if (!result.committed) {
459
+ // G1: when the LSP quality gate blocked the commit, surface the
460
+ // per-file errors so the agent can fix them and call git_commit again.
461
+ // Tell the agent to FIX the errors — do NOT advertise the bypass
462
+ // env here (that is a USER escape hatch; surfacing it to the agent
463
+ // just invites it to circumvent the gate instead of fixing).
464
+ const detail = result.reason === "lsp-errors"
465
+ ? `\nStaged files have errors — fix them and call git_commit again:\n${result.detail ?? ""}`
466
+ : "";
467
+ return { success: false, output: `No commit made (${result.reason}).${detail}` };
468
+ }
469
+ return { success: true, output: `Committed ${result.fileCount} file(s) → ${result.sha}` };
470
+ }
471
+ catch (e) {
472
+ return { success: false, output: `git_commit failed: ${e instanceof Error ? e.message : String(e)}` };
473
+ }
474
+ },
475
+ });
347
476
  // task
348
477
  if (opts?.runTask) {
349
478
  const runTask = opts.runTask;
@@ -358,15 +487,45 @@ export function createBuiltinTools(bash, mode, opts) {
358
487
  },
359
488
  description: { type: "string", description: "Short description of the task" },
360
489
  prompt: { type: "string", description: "Detailed instructions for the sub-agent" },
490
+ maxToolRounds: {
491
+ type: "number",
492
+ description: "Optional maximum tool execution rounds (default is 12 for foreground tasks)",
493
+ },
361
494
  },
362
495
  required: ["agent", "description", "prompt"],
363
496
  }),
364
497
  execute: async (input) => {
365
- const result = await runTask({ agent: input.agent, description: input.description, prompt: input.prompt });
498
+ const result = await runTask({
499
+ agent: input.agent,
500
+ description: input.description,
501
+ prompt: input.prompt,
502
+ maxToolRounds: input.maxToolRounds,
503
+ });
366
504
  return formatResult(result);
367
505
  },
368
506
  });
369
507
  }
508
+ // consult_parent_session
509
+ if (opts?.consultParentSession) {
510
+ const consultParentSession = opts.consultParentSession;
511
+ tools.consult_parent_session = dynamicTool({
512
+ description: "Consult the parent session for supervision or guidance when stuck, when needing clarification on the overall goal, or when encountering critical errors. ONLY available in sub-sessions.",
513
+ inputSchema: jsonSchema({
514
+ type: "object",
515
+ properties: {
516
+ question: {
517
+ type: "string",
518
+ description: "The specific question or issue you need the parent session to advise on.",
519
+ },
520
+ },
521
+ required: ["question"],
522
+ }),
523
+ execute: async (input) => {
524
+ const result = await consultParentSession(input.question);
525
+ return result;
526
+ },
527
+ });
528
+ }
370
529
  // delegate
371
530
  if (opts?.runDelegation) {
372
531
  const runDelegation = opts.runDelegation;
@@ -378,6 +537,10 @@ export function createBuiltinTools(bash, mode, opts) {
378
537
  agent: { type: "string", description: "Sub-agent type (usually 'explore')" },
379
538
  description: { type: "string", description: "Short description of the research task" },
380
539
  prompt: { type: "string", description: "Detailed research instructions" },
540
+ maxToolRounds: {
541
+ type: "number",
542
+ description: "Optional maximum tool execution rounds (default is 60 for explore delegations)",
543
+ },
381
544
  },
382
545
  required: ["agent", "description", "prompt"],
383
546
  }),
@@ -386,6 +549,7 @@ export function createBuiltinTools(bash, mode, opts) {
386
549
  agent: input.agent,
387
550
  description: input.description,
388
551
  prompt: input.prompt,
552
+ maxToolRounds: input.maxToolRounds,
389
553
  });
390
554
  return formatResult(result);
391
555
  },
@@ -424,6 +588,24 @@ export function createBuiltinTools(bash, mode, opts) {
424
588
  },
425
589
  });
426
590
  }
591
+ // delegation_kill
592
+ if (opts?.killDelegation) {
593
+ const killDelegation = opts.killDelegation;
594
+ tools.delegation_kill = dynamicTool({
595
+ description: "Terminate a running background delegation/subagent by ID.",
596
+ inputSchema: jsonSchema({
597
+ type: "object",
598
+ properties: {
599
+ id: { type: "string", description: "Delegation ID to terminate" },
600
+ },
601
+ required: ["id"],
602
+ }),
603
+ execute: async (input) => {
604
+ const result = await killDelegation(input.id);
605
+ return formatResult(result);
606
+ },
607
+ });
608
+ }
427
609
  // ee_query — semantic recall over the Experience Engine brain. This is the
428
610
  // in-CLI counterpart of the MCP `ee.query` tool (src/mcp/ee-tools.ts):
429
611
  // without it, the Agent Operating Contract + checkpoint reminders instruct
@@ -493,7 +675,13 @@ export function createBuiltinTools(bash, mode, opts) {
493
675
  return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured) and the artifact is not in this session's local cache. Proceed without EE recall — re-read the source directly if you need the elided content.";
494
676
  }
495
677
  recordRehydration("ee");
496
- return truncateOutput(JSON.stringify(resp));
678
+ const points = resp.points ?? [];
679
+ const bestHit = points[0];
680
+ if (bestHit) {
681
+ const matchedId = query.match(/(?:id\s*=\s*|id\s*\b)([a-zA-Z0-9_:-]+)/i)?.[1] ?? "unknown";
682
+ return truncateOutput(`[tool-artifact id=${matchedId} — rehydrated from Experience Engine]\n${bestHit.text}`);
683
+ }
684
+ return `[tool-artifact — not found in Experience Engine]`;
497
685
  }
498
686
  // General recall → /api/recall (recallMode, [id col] index + surface).
499
687
  const { recallEE, formatRecallForAgent } = await import("../ee/search.js");
@@ -503,6 +691,20 @@ export function createBuiltinTools(bash, mode, opts) {
503
691
  if (resp === null) {
504
692
  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
693
  }
694
+ // Record recalled entries as pending feedback debt in the same in-process
695
+ // ledger the native ee_feedback builtin clears, so an in-CLI active recall
696
+ // accrues a verdict obligation exactly like the external MCP ee.query does
697
+ // (mcp/ee-tools.ts). Layer 3 surfaces this debt as a reminder on the next
698
+ // enriched turn; without it the in-CLI recall arm was write-only and
699
+ // ee_feedback.clear() was a no-op.
700
+ try {
701
+ const { sessionRecallLedger, isRecallLedgerEnabled } = await import("../ee/recall-ledger.js");
702
+ if (isRecallLedgerEnabled())
703
+ sessionRecallLedger.record(resp.entries, query);
704
+ }
705
+ catch (err) {
706
+ console.error(`[tools:ee_query] recall-ledger record failed: ${err?.message}`);
707
+ }
506
708
  // Compact ranked `[id col]` index, not a JSON dump — the recallMode text
507
709
  // is ~30k (wide net), so JSON.stringify wasted the budget on escaping.
508
710
  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;
@@ -413,7 +419,7 @@ export interface ModelInfo {
413
419
  supportsVision?: boolean;
414
420
  }
415
421
  export type AgentMode = "agent" | "plan" | "ask";
416
- export type SessionStatus = "active" | "completed" | "archived";
422
+ export type SessionStatus = "active" | "completed" | "archived" | "abandoned";
417
423
  export type UsageSource = "message" | "title" | "task" | "delegation" | "council" | "other";
418
424
  export interface WorkspaceInfo {
419
425
  id: 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