clawmini 0.0.8 → 0.0.9

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 (367) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.github/workflows/release.yml +49 -0
  4. package/CHANGELOG.md +36 -0
  5. package/README.md +5 -4
  6. package/dist/adapter-discord/index.d.mts.map +1 -1
  7. package/dist/adapter-discord/index.mjs +465 -282
  8. package/dist/adapter-discord/index.mjs.map +1 -1
  9. package/dist/adapter-google-chat/index.mjs +367 -243
  10. package/dist/adapter-google-chat/index.mjs.map +1 -1
  11. package/dist/cli/index.mjs +684 -24
  12. package/dist/cli/index.mjs.map +1 -1
  13. package/dist/cli/lite.mjs +43 -13
  14. package/dist/cli/lite.mjs.map +1 -1
  15. package/dist/cli/{propose-policy.mjs → manage-policies.mjs} +270 -47
  16. package/dist/cli/manage-policies.mjs.map +1 -0
  17. package/dist/cli/run-host.d.mts +1 -0
  18. package/dist/cli/run-host.mjs +3090 -0
  19. package/dist/cli/run-host.mjs.map +1 -0
  20. package/dist/config-CPFQIGdG.mjs +57 -0
  21. package/dist/config-CPFQIGdG.mjs.map +1 -0
  22. package/dist/config-Dvl-Pov4.mjs +76 -0
  23. package/dist/config-Dvl-Pov4.mjs.map +1 -0
  24. package/dist/daemon/index.d.mts.map +1 -1
  25. package/dist/daemon/index.mjs +970 -332
  26. package/dist/daemon/index.mjs.map +1 -1
  27. package/dist/supervisor-actions-CiW56eLi.mjs +843 -0
  28. package/dist/supervisor-actions-CiW56eLi.mjs.map +1 -0
  29. package/dist/turn-log-buffer-DRgW53gl.mjs +767 -0
  30. package/dist/turn-log-buffer-DRgW53gl.mjs.map +1 -0
  31. package/dist/web/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  32. package/dist/web/_app/immutable/chunks/BhRSsUCh.js +2 -0
  33. package/dist/web/_app/immutable/chunks/BiLeM2i1.js +1 -0
  34. package/{web/.svelte-kit/output/client/_app/immutable/chunks/CME08kGM.js → dist/web/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  35. package/dist/web/_app/immutable/chunks/BrERcKAH.js +1 -0
  36. package/dist/web/_app/immutable/chunks/Bv9252RM.js +1 -0
  37. package/dist/web/_app/immutable/chunks/CIXNBPKi.js +1 -0
  38. package/dist/web/_app/immutable/chunks/DISKL3GN.js +2 -0
  39. package/dist/web/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  40. package/dist/web/_app/immutable/chunks/DnQ3vS13.js +1 -0
  41. package/dist/web/_app/immutable/chunks/KsloHTKS.js +1 -0
  42. package/{web/.svelte-kit/output/client/_app/immutable/chunks/Ck-be5J2.js → dist/web/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  43. package/dist/web/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  44. package/dist/web/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  45. package/dist/web/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  46. package/dist/web/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  47. package/dist/web/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  48. package/dist/web/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  49. package/{web/.svelte-kit/output/client/_app/immutable/nodes/3.0arZe_Uf.js → dist/web/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  50. package/dist/web/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  51. package/dist/web/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  52. package/dist/web/_app/version.json +1 -1
  53. package/dist/web/index.html +12 -12
  54. package/dist/workspace-oWmVh5mi.mjs +1001 -0
  55. package/dist/workspace-oWmVh5mi.mjs.map +1 -0
  56. package/docs/23_adapter_slash_autocomplete/development_log.md +19 -0
  57. package/docs/23_adapter_slash_autocomplete/notes.md +18 -0
  58. package/docs/23_adapter_slash_autocomplete/prd.md +46 -0
  59. package/docs/23_adapter_slash_autocomplete/questions.md +6 -0
  60. package/docs/23_adapter_slash_autocomplete/tickets.md +21 -0
  61. package/docs/24_subagent_job_policy_fixes/development_log.md +22 -0
  62. package/docs/24_subagent_job_policy_fixes/notes.md +28 -0
  63. package/docs/24_subagent_job_policy_fixes/prd.md +59 -0
  64. package/docs/24_subagent_job_policy_fixes/questions.md +3 -0
  65. package/docs/24_subagent_job_policy_fixes/tickets.md +49 -0
  66. package/docs/25_e2e_test_improvements/development_log.md +30 -0
  67. package/docs/25_e2e_test_improvements/notes.md +29 -0
  68. package/docs/25_e2e_test_improvements/prd.md +43 -0
  69. package/docs/25_e2e_test_improvements/questions.md +12 -0
  70. package/docs/25_e2e_test_improvements/tickets-2.md +22 -0
  71. package/docs/25_e2e_test_improvements/tickets.md +22 -0
  72. package/docs/25_policy_cwd/development_log.md +30 -0
  73. package/docs/25_policy_cwd/notes.md +28 -0
  74. package/docs/25_policy_cwd/prd.md +77 -0
  75. package/docs/25_policy_cwd/questions.md +6 -0
  76. package/docs/25_policy_cwd/tickets.md +77 -0
  77. package/docs/CLI_REFERENCE.md +3 -1
  78. package/docs/PHILOSOPHY.md +35 -0
  79. package/docs/adapter-visibility/SPEC.md +461 -0
  80. package/docs/adapter-visibility/SPEC_v2.md +202 -0
  81. package/docs/auto-update/SPEC.md +344 -0
  82. package/docs/backups/SPEC.md +296 -0
  83. package/docs/backups/clawmini.gitignore +69 -0
  84. package/docs/guides/assets/clawmini-avatar.png +0 -0
  85. package/docs/guides/backups.md +332 -0
  86. package/docs/guides/discord_adapter_setup.md +1 -1
  87. package/docs/guides/google_chat_adapter_setup.md +81 -0
  88. package/docs/unified-startup/SPEC.md +203 -0
  89. package/e2e/_helpers/test-environment.test.ts +49 -0
  90. package/e2e/_helpers/test-environment.ts +548 -0
  91. package/e2e/adapters/_google-chat-fixtures.ts +340 -0
  92. package/{src/cli/e2e → e2e/adapters}/adapter-discord.test.ts +22 -23
  93. package/e2e/adapters/adapter-google-chat-downtime.test.ts +157 -0
  94. package/e2e/adapters/adapter-google-chat-inbound.test.ts +697 -0
  95. package/e2e/adapters/adapter-google-chat-outbound.test.ts +297 -0
  96. package/e2e/adapters/adapter-google-chat-roundtrip.test.ts +56 -0
  97. package/e2e/adapters/adapter-google-chat-threads.test.ts +1078 -0
  98. package/e2e/agents/custom-api-env.test.ts +80 -0
  99. package/e2e/agents/export-lite-func.test.ts +104 -0
  100. package/e2e/agents/fallbacks.test.ts +124 -0
  101. package/e2e/agents/interrupt.test.ts +50 -0
  102. package/e2e/agents/no-reply-necessary.test.ts +57 -0
  103. package/e2e/agents/session-timeout-subagents.test.ts +76 -0
  104. package/e2e/agents/subagent-authorization.test.ts +246 -0
  105. package/e2e/agents/subagent-env.test.ts +49 -0
  106. package/e2e/agents/subagent-lifecycle.test.ts +782 -0
  107. package/e2e/agents/subagents-depth.test.ts +47 -0
  108. package/e2e/cli/agents.test.ts +176 -0
  109. package/e2e/cli/auto-update.test.ts +741 -0
  110. package/e2e/cli/basic.test.ts +44 -0
  111. package/{src/cli/e2e → e2e/cli}/export-lite.test.ts +16 -12
  112. package/e2e/cli/init-gitignore.test.ts +86 -0
  113. package/e2e/cli/init.test.ts +76 -0
  114. package/e2e/cli/messages.test.ts +363 -0
  115. package/e2e/cli/serve.test.ts +76 -0
  116. package/{src/cli/e2e → e2e/cli}/skills.test.ts +11 -10
  117. package/{src/cli/e2e → e2e/daemon}/daemon.test.ts +57 -195
  118. package/e2e/jobs/agent-jobs.test.ts +216 -0
  119. package/e2e/jobs/cron.test.ts +64 -0
  120. package/e2e/jobs/restart.test.ts +108 -0
  121. package/e2e/policies/approval-session.test.ts +69 -0
  122. package/e2e/policies/auto-create-policies-file.test.ts +35 -0
  123. package/e2e/policies/builtin-manage-policies.test.ts +184 -0
  124. package/e2e/policies/builtin-run-host.test.ts +180 -0
  125. package/e2e/policies/environment-policies.test.ts +177 -0
  126. package/e2e/policies/manage-policies.test.ts +566 -0
  127. package/e2e/policies/output-size.test.ts +98 -0
  128. package/e2e/policies/policies-context-cwd.test.ts +160 -0
  129. package/e2e/policies/relative-script-path.test.ts +60 -0
  130. package/e2e/policies/requests-show.test.ts +135 -0
  131. package/e2e/policies/requests.test.ts +208 -0
  132. package/e2e/policies/slash-policies.test.ts +308 -0
  133. package/e2e/policies/startup-cleanup.test.ts +48 -0
  134. package/e2e/routers/session-timeout.test.ts +106 -0
  135. package/e2e/routers/slash-model.test.ts +152 -0
  136. package/e2e/routers/slash-new.test.ts +50 -0
  137. package/e2e/routers/slash-restart-adapter.test.ts +96 -0
  138. package/e2e/routers/slash-restart.test.ts +114 -0
  139. package/e2e/routers/slash-shutdown.test.ts +55 -0
  140. package/e2e/routers/slash-stop.test.ts +232 -0
  141. package/e2e/routers/slash-upgrade.test.ts +88 -0
  142. package/{src/cli/e2e → e2e/sandbox}/environments.test.ts +14 -13
  143. package/eslint.config.js +6 -0
  144. package/napkin.md +1 -1
  145. package/package.json +8 -3
  146. package/src/adapter-discord/commands.test.ts +42 -0
  147. package/src/adapter-discord/commands.ts +33 -0
  148. package/src/adapter-discord/config.ts +12 -0
  149. package/src/adapter-discord/forwarder.test.ts +499 -21
  150. package/src/adapter-discord/forwarder.ts +343 -124
  151. package/src/adapter-discord/inbound-cache.test.ts +47 -0
  152. package/src/adapter-discord/inbound-cache.ts +37 -0
  153. package/src/adapter-discord/index.test.ts +67 -2
  154. package/src/adapter-discord/index.ts +84 -216
  155. package/src/adapter-discord/interactions.test.ts +54 -3
  156. package/src/adapter-discord/interactions.ts +97 -53
  157. package/src/adapter-discord/processMessage.ts +239 -0
  158. package/src/adapter-discord/state.ts +1 -0
  159. package/src/adapter-google-chat/auth.test.ts +9 -5
  160. package/src/adapter-google-chat/auth.ts +29 -23
  161. package/src/adapter-google-chat/cards.ts +7 -2
  162. package/src/adapter-google-chat/client.test.ts +37 -2
  163. package/src/adapter-google-chat/client.ts +138 -38
  164. package/src/adapter-google-chat/config.ts +19 -0
  165. package/src/adapter-google-chat/forwarder.test.ts +81 -56
  166. package/src/adapter-google-chat/forwarder.ts +394 -185
  167. package/src/adapter-google-chat/inbound-cache.test.ts +61 -0
  168. package/src/adapter-google-chat/inbound-cache.ts +36 -0
  169. package/src/adapter-google-chat/state.test.ts +1 -0
  170. package/src/adapter-google-chat/state.ts +9 -1
  171. package/src/adapter-google-chat/subscriptions.ts +8 -6
  172. package/src/cli/builtin-policies.ts +44 -0
  173. package/src/cli/commands/agents.ts +59 -5
  174. package/src/cli/commands/down.ts +54 -2
  175. package/src/cli/commands/environments.ts +8 -2
  176. package/src/cli/commands/init.ts +31 -0
  177. package/src/cli/commands/logs.ts +116 -0
  178. package/src/cli/commands/policies.ts +6 -4
  179. package/src/cli/commands/serve.test.ts +67 -0
  180. package/src/cli/commands/serve.ts +284 -0
  181. package/src/cli/commands/up.ts +122 -2
  182. package/src/cli/commands/web-api/agents.ts +3 -2
  183. package/src/cli/index.ts +4 -0
  184. package/src/cli/install-detection.test.ts +72 -0
  185. package/src/cli/install-detection.ts +48 -0
  186. package/src/cli/lite.ts +54 -22
  187. package/src/cli/manage-policies-utils.ts +104 -0
  188. package/src/cli/manage-policies.ts +291 -0
  189. package/src/cli/run-host.ts +45 -0
  190. package/src/cli/supervisor-actions.ts +267 -0
  191. package/src/cli/supervisor-control.test.ts +129 -0
  192. package/src/cli/supervisor-control.ts +155 -0
  193. package/src/cli/supervisor-pid.ts +68 -0
  194. package/src/cli/supervisor.ts +277 -0
  195. package/src/daemon/agent/agent-context.ts +11 -11
  196. package/src/daemon/agent/agent-session.ts +8 -1
  197. package/src/daemon/agent/chat-logger.test.ts +78 -9
  198. package/src/daemon/agent/chat-logger.ts +25 -5
  199. package/src/daemon/agent/turn-registry.test.ts +89 -0
  200. package/src/daemon/agent/turn-registry.ts +94 -0
  201. package/src/daemon/agent/types.ts +2 -0
  202. package/src/daemon/api/agent-policy-endpoints.ts +263 -0
  203. package/src/daemon/api/agent-router.ts +47 -126
  204. package/src/daemon/api/index.test.ts +1 -0
  205. package/src/daemon/api/policy-request.test.ts +7 -5
  206. package/src/daemon/api/router-utils.ts +6 -5
  207. package/src/daemon/api/subagent-router.ts +110 -74
  208. package/src/daemon/api/subagent-utils.test.ts +60 -0
  209. package/src/daemon/api/subagent-utils.ts +113 -87
  210. package/src/daemon/api/user-router.ts +34 -8
  211. package/src/daemon/auth.ts +1 -0
  212. package/src/daemon/cron.test.ts +62 -4
  213. package/src/daemon/cron.ts +42 -16
  214. package/src/daemon/events.ts +65 -0
  215. package/src/daemon/index.ts +24 -1
  216. package/src/daemon/message-interruption.test.ts +1 -0
  217. package/src/daemon/message-jobs.test.ts +1 -0
  218. package/src/daemon/message.ts +78 -14
  219. package/src/daemon/observation.test.ts +26 -18
  220. package/src/daemon/pending-replies.test.ts +112 -0
  221. package/src/daemon/pending-replies.ts +162 -0
  222. package/src/daemon/policy-request-service.ts +3 -1
  223. package/src/daemon/policy-utils.test.ts +66 -1
  224. package/src/daemon/policy-utils.ts +126 -1
  225. package/src/daemon/request-store.ts +31 -0
  226. package/src/daemon/routers/session-timeout.ts +4 -0
  227. package/src/daemon/routers/slash-model.test.ts +344 -0
  228. package/src/daemon/routers/slash-model.ts +207 -0
  229. package/src/daemon/routers/slash-policies.test.ts +38 -32
  230. package/src/daemon/routers/slash-policies.ts +84 -33
  231. package/src/daemon/routers/slash-restart.test.ts +69 -0
  232. package/src/daemon/routers/slash-restart.ts +36 -0
  233. package/src/daemon/routers/slash-shutdown.test.ts +50 -0
  234. package/src/daemon/routers/slash-shutdown.ts +28 -0
  235. package/src/daemon/routers/slash-upgrade.test.ts +116 -0
  236. package/src/daemon/routers/slash-upgrade.ts +76 -0
  237. package/src/daemon/routers/types.ts +7 -0
  238. package/src/daemon/routers.ts +16 -0
  239. package/src/shared/adapters/blockquote.test.ts +28 -0
  240. package/src/shared/adapters/blockquote.ts +20 -0
  241. package/src/shared/adapters/filtering.test.ts +224 -10
  242. package/src/shared/adapters/filtering.ts +95 -7
  243. package/src/shared/adapters/inbound-cache.test.ts +48 -0
  244. package/src/shared/adapters/inbound-cache.ts +54 -0
  245. package/src/shared/adapters/turn-log-buffer.ts +266 -0
  246. package/src/shared/adapters/turn-log.test.ts +389 -0
  247. package/src/shared/adapters/turn-log.ts +357 -0
  248. package/src/shared/agent-utils.ts +12 -5
  249. package/src/shared/chats.test.ts +4 -0
  250. package/src/shared/chats.ts +9 -0
  251. package/src/shared/config.ts +16 -1
  252. package/src/shared/lite.ts +76 -2
  253. package/src/shared/policies.ts +26 -0
  254. package/src/shared/template-manifest.ts +267 -0
  255. package/src/shared/utils/shell.ts +61 -0
  256. package/src/shared/version.ts +34 -0
  257. package/src/shared/workspace.test.ts +217 -0
  258. package/src/shared/workspace.ts +626 -48
  259. package/templates/environments/cladding/allowlist-domain.mjs +125 -0
  260. package/templates/environments/cladding/env.json +21 -1
  261. package/templates/environments/cladding/run-with-network.mjs +54 -0
  262. package/templates/environments/macos-proxy/allowlist-domain.mjs +95 -0
  263. package/templates/environments/macos-proxy/env.json +8 -1
  264. package/templates/environments/macos-proxy/proxy.mjs +0 -1
  265. package/templates/gemini/template.json +5 -0
  266. package/templates/gemini-claw/template.json +13 -0
  267. package/templates/skills/clawmini-requests/SKILL.md +69 -10
  268. package/templates/skills/run-host/SKILL.md +51 -0
  269. package/templates/skills/skill-creator/SKILL.md +4 -3
  270. package/templates/skills/skill-creator/scripts/validate.sh +52 -0
  271. package/tsdown.config.ts +10 -1
  272. package/vitest.config.ts +2 -2
  273. package/web/.svelte-kit/ambient.d.ts +292 -118
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +126 -136
  276. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Drm9vgeP.js → 3AZlWB6U.js} +1 -1
  277. package/web/.svelte-kit/output/client/_app/immutable/chunks/BhRSsUCh.js +2 -0
  278. package/web/.svelte-kit/output/client/_app/immutable/chunks/BiLeM2i1.js +1 -0
  279. package/{dist/web/_app/immutable/chunks/CME08kGM.js → web/.svelte-kit/output/client/_app/immutable/chunks/BmBj85Ll.js} +1 -1
  280. package/web/.svelte-kit/output/client/_app/immutable/chunks/BrERcKAH.js +1 -0
  281. package/web/.svelte-kit/output/client/_app/immutable/chunks/Bv9252RM.js +1 -0
  282. package/web/.svelte-kit/output/client/_app/immutable/chunks/CIXNBPKi.js +1 -0
  283. package/web/.svelte-kit/output/client/_app/immutable/chunks/DISKL3GN.js +2 -0
  284. package/web/.svelte-kit/output/client/_app/immutable/chunks/{Zeh-C-mx.js → DcpaLzmX.js} +1 -1
  285. package/web/.svelte-kit/output/client/_app/immutable/chunks/DnQ3vS13.js +1 -0
  286. package/web/.svelte-kit/output/client/_app/immutable/chunks/KsloHTKS.js +1 -0
  287. package/{dist/web/_app/immutable/chunks/Ck-be5J2.js → web/.svelte-kit/output/client/_app/immutable/chunks/RsHsUj-8.js} +2 -2
  288. package/web/.svelte-kit/output/client/_app/immutable/chunks/{vDehDcuJ.js → wpfV79dV.js} +1 -1
  289. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CIw1Qj0n.js +2 -0
  290. package/web/.svelte-kit/output/client/_app/immutable/entry/start.Di0-Jhte.js +1 -0
  291. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CUGC2p-K.js → 0.DYyUA1au.js} +1 -1
  292. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D-3QEMMZ.js +1 -0
  293. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.BnwnD1Ki.js → 2.4olHnH7U.js} +1 -1
  294. package/{dist/web/_app/immutable/nodes/3.0arZe_Uf.js → web/.svelte-kit/output/client/_app/immutable/nodes/3.4w0bE-m2.js} +3 -3
  295. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.CZvjhVHt.js +60 -0
  296. package/web/.svelte-kit/output/client/_app/immutable/nodes/{5.Bq2JzCEj.js → 5.DLbPVJY2.js} +1 -1
  297. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  298. package/web/.svelte-kit/output/server/.vite/manifest.json +12 -10
  299. package/web/.svelte-kit/output/server/chunks/Icon.js +1 -1
  300. package/web/.svelte-kit/output/server/chunks/client.js +1 -1
  301. package/web/.svelte-kit/output/server/chunks/exports.js +1 -1
  302. package/web/.svelte-kit/output/server/chunks/index-server.js +2 -1
  303. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  304. package/web/.svelte-kit/output/server/chunks/render-context.js +77 -0
  305. package/web/.svelte-kit/output/server/chunks/root.js +739 -788
  306. package/web/.svelte-kit/output/server/chunks/shared.js +234 -21
  307. package/web/.svelte-kit/output/server/index.js +126 -90
  308. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  309. package/web/.svelte-kit/output/server/manifest.js +1 -1
  310. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  311. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  312. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  313. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  314. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  315. package/web/.svelte-kit/output/server/nodes/5.js +1 -1
  316. package/web/.svelte-kit/output/server/remote-entry.js +245 -81
  317. package/web/.svelte-kit/tsconfig.json +4 -1
  318. package/dist/cli/propose-policy.mjs.map +0 -1
  319. package/dist/lite-CBxOT1y5.mjs +0 -241
  320. package/dist/lite-CBxOT1y5.mjs.map +0 -1
  321. package/dist/routing-D8rTxtaV.mjs +0 -245
  322. package/dist/routing-D8rTxtaV.mjs.map +0 -1
  323. package/dist/web/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  324. package/dist/web/_app/immutable/chunks/BmRlVmv6.js +0 -1
  325. package/dist/web/_app/immutable/chunks/CK9JZLaG.js +0 -2
  326. package/dist/web/_app/immutable/chunks/Ck3rYNON.js +0 -1
  327. package/dist/web/_app/immutable/chunks/D5iV40bG.js +0 -1
  328. package/dist/web/_app/immutable/chunks/DMtIqaiV.js +0 -2
  329. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  331. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  336. package/dist/workspace-BJmJBfKi.mjs +0 -456
  337. package/dist/workspace-BJmJBfKi.mjs.map +0 -1
  338. package/src/cli/e2e/agents.test.ts +0 -140
  339. package/src/cli/e2e/basic.test.ts +0 -43
  340. package/src/cli/e2e/cron.test.ts +0 -132
  341. package/src/cli/e2e/export-lite-func.test.ts +0 -206
  342. package/src/cli/e2e/fallbacks.test.ts +0 -175
  343. package/src/cli/e2e/init.test.ts +0 -77
  344. package/src/cli/e2e/messages.test.ts +0 -332
  345. package/src/cli/e2e/propose-policy.test.ts +0 -203
  346. package/src/cli/e2e/requests.test.ts +0 -180
  347. package/src/cli/e2e/session-timeout.test.ts +0 -192
  348. package/src/cli/e2e/slash-new.test.ts +0 -93
  349. package/src/cli/e2e/subagents.test.ts +0 -106
  350. package/src/cli/e2e/utils.ts +0 -66
  351. package/src/cli/propose-policy.ts +0 -91
  352. package/web/.svelte-kit/output/client/_app/immutable/chunks/B6YN0Nuq.js +0 -1
  353. package/web/.svelte-kit/output/client/_app/immutable/chunks/BmRlVmv6.js +0 -1
  354. package/web/.svelte-kit/output/client/_app/immutable/chunks/CK9JZLaG.js +0 -2
  355. package/web/.svelte-kit/output/client/_app/immutable/chunks/Ck3rYNON.js +0 -1
  356. package/web/.svelte-kit/output/client/_app/immutable/chunks/D5iV40bG.js +0 -1
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DMtIqaiV.js +0 -2
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.BCSV3nrG.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.D4eLEZUM.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.CGC_42IQ.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.ClM1bXLE.js +0 -60
  365. package/web/.svelte-kit/output/server/chunks/false.js +0 -4
  366. /package/dist/cli/{propose-policy.d.mts → manage-policies.d.mts} +0 -0
  367. /package/{src/cli/e2e → e2e/_helpers}/global-setup.ts +0 -0
@@ -0,0 +1,267 @@
1
+ import fs from 'node:fs';
2
+ import fsPromises from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { createHash, randomBytes } from 'node:crypto';
5
+ import { z } from 'zod';
6
+
7
+ export type FileMode = 'track' | 'seed-once';
8
+
9
+ const TemplateManifestSchema = z.looseObject({
10
+ files: z.record(z.string(), z.enum(['track', 'seed-once'])).optional(),
11
+ });
12
+
13
+ export type TemplateManifest = z.infer<typeof TemplateManifestSchema>;
14
+
15
+ const InstalledFilesSchema = z.looseObject({
16
+ files: z
17
+ .record(
18
+ z.string(),
19
+ z.looseObject({
20
+ sha: z.string(),
21
+ })
22
+ )
23
+ .optional(),
24
+ });
25
+
26
+ export type InstalledFiles = z.infer<typeof InstalledFilesSchema>;
27
+
28
+ export function sha256(content: string | Buffer): string {
29
+ return createHash('sha256').update(content).digest('hex');
30
+ }
31
+
32
+ export async function fileSha(filePath: string): Promise<string | null> {
33
+ try {
34
+ const buf = await fsPromises.readFile(filePath);
35
+ return sha256(buf);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ export async function readTemplateManifest(templateDir: string): Promise<TemplateManifest | null> {
42
+ const manifestPath = path.join(templateDir, 'template.json');
43
+ try {
44
+ const raw = await fsPromises.readFile(manifestPath, 'utf-8');
45
+ const parsed = TemplateManifestSchema.safeParse(JSON.parse(raw));
46
+ return parsed.success ? parsed.data : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // Resolve the manifest mode for a given relative path. Exact entries win; if
53
+ // no exact match, walk the prefix directory entries (longest first). Fallback
54
+ // is the provided default mode.
55
+ export function getFileMode(
56
+ relPath: string,
57
+ manifest: TemplateManifest | null,
58
+ defaultMode: FileMode
59
+ ): FileMode {
60
+ if (!manifest?.files) return defaultMode;
61
+ const normalized = relPath.split(path.sep).join('/');
62
+ if (manifest.files[normalized]) return manifest.files[normalized];
63
+
64
+ const dirEntries = Object.entries(manifest.files).filter(([k]) => k.endsWith('/'));
65
+ dirEntries.sort((a, b) => b[0].length - a[0].length);
66
+ for (const [entry, mode] of dirEntries) {
67
+ if (normalized.startsWith(entry)) return mode;
68
+ }
69
+ return defaultMode;
70
+ }
71
+
72
+ // Walk every file under `dir`, returning relative paths (posix-style). Skips
73
+ // template.json and settings.json at the root — both are template metadata
74
+ // that is read for its specific purpose but never copied to the target.
75
+ export async function walkTemplateFiles(
76
+ dir: string,
77
+ opts: { skipRoot?: string[] } = {}
78
+ ): Promise<string[]> {
79
+ const skipRoot = new Set(opts.skipRoot ?? ['template.json', 'settings.json']);
80
+ const out: string[] = [];
81
+ async function walk(current: string, prefix: string): Promise<void> {
82
+ let entries: fs.Dirent[];
83
+ try {
84
+ entries = await fsPromises.readdir(current, { withFileTypes: true });
85
+ } catch {
86
+ return;
87
+ }
88
+ for (const entry of entries) {
89
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
90
+ if (entry.isDirectory()) {
91
+ await walk(path.join(current, entry.name), rel);
92
+ } else if (entry.isFile()) {
93
+ if (!prefix && skipRoot.has(entry.name)) continue;
94
+ out.push(rel);
95
+ }
96
+ }
97
+ }
98
+ await walk(dir, '');
99
+ return out;
100
+ }
101
+
102
+ export type FileAction =
103
+ | { action: 'write'; relPath: string; reason: 'new' | 'refresh' }
104
+ | { action: 'skip-unchanged'; relPath: string }
105
+ | { action: 'skip-diverged'; relPath: string; reason: 'edited' | 'no-recorded-sha' }
106
+ | { action: 'skip-seed-once'; relPath: string }
107
+ | { action: 'skip-absent-from-template'; relPath: string };
108
+
109
+ export interface RefreshPlan {
110
+ actions: FileAction[];
111
+ nextInstalled: InstalledFiles;
112
+ }
113
+
114
+ export interface RefreshOptions {
115
+ defaultMode: FileMode;
116
+ firstInstall?: boolean;
117
+ accept?: boolean;
118
+ }
119
+
120
+ // Produce the per-file plan + the `installed-files.json` state that would
121
+ // follow from applying it. Does not touch disk — the caller decides whether
122
+ // to execute the plan.
123
+ export async function planRefresh(
124
+ templateDir: string,
125
+ targetDir: string,
126
+ manifest: TemplateManifest | null,
127
+ installed: InstalledFiles | null,
128
+ options: RefreshOptions
129
+ ): Promise<RefreshPlan> {
130
+ const templateFiles = await walkTemplateFiles(templateDir);
131
+ const actions: FileAction[] = [];
132
+ const nextFiles: Record<string, { sha: string }> = {
133
+ ...(installed?.files ?? {}),
134
+ };
135
+
136
+ for (const rel of templateFiles) {
137
+ const mode = getFileMode(rel, manifest, options.defaultMode);
138
+ const templatePath = path.join(templateDir, rel);
139
+ const targetPath = path.join(targetDir, rel);
140
+ const templateHash = sha256(await fsPromises.readFile(templatePath));
141
+
142
+ if (options.firstInstall) {
143
+ actions.push({ action: 'write', relPath: rel, reason: 'new' });
144
+ nextFiles[rel] = { sha: templateHash };
145
+ continue;
146
+ }
147
+
148
+ if (mode === 'seed-once') {
149
+ const diskHash = await fileSha(targetPath);
150
+ const recorded = installed?.files?.[rel]?.sha;
151
+ if (diskHash === null && !recorded) {
152
+ // Never seeded before — safe to drop the initial copy.
153
+ actions.push({ action: 'write', relPath: rel, reason: 'new' });
154
+ nextFiles[rel] = { sha: templateHash };
155
+ } else {
156
+ // Either present, or previously seeded and then deleted by the user.
157
+ actions.push({ action: 'skip-seed-once', relPath: rel });
158
+ }
159
+ continue;
160
+ }
161
+
162
+ // mode === 'track'
163
+ const recorded = installed?.files?.[rel]?.sha;
164
+ const diskHash = await fileSha(targetPath);
165
+
166
+ if (diskHash === null) {
167
+ // File vanished — re-seed it and update the SHA.
168
+ actions.push({ action: 'write', relPath: rel, reason: 'refresh' });
169
+ nextFiles[rel] = { sha: templateHash };
170
+ continue;
171
+ }
172
+
173
+ if (!recorded) {
174
+ if (options.accept) {
175
+ actions.push({ action: 'write', relPath: rel, reason: 'refresh' });
176
+ nextFiles[rel] = { sha: templateHash };
177
+ } else {
178
+ actions.push({ action: 'skip-diverged', relPath: rel, reason: 'no-recorded-sha' });
179
+ }
180
+ continue;
181
+ }
182
+
183
+ if (diskHash === templateHash) {
184
+ actions.push({ action: 'skip-unchanged', relPath: rel });
185
+ nextFiles[rel] = { sha: templateHash };
186
+ continue;
187
+ }
188
+
189
+ if (diskHash === recorded || options.accept) {
190
+ actions.push({ action: 'write', relPath: rel, reason: 'refresh' });
191
+ nextFiles[rel] = { sha: templateHash };
192
+ } else {
193
+ actions.push({ action: 'skip-diverged', relPath: rel, reason: 'edited' });
194
+ }
195
+ }
196
+
197
+ return {
198
+ actions,
199
+ nextInstalled: { files: nextFiles },
200
+ };
201
+ }
202
+
203
+ export async function applyPlan(
204
+ templateDir: string,
205
+ targetDir: string,
206
+ plan: RefreshPlan
207
+ ): Promise<void> {
208
+ for (const action of plan.actions) {
209
+ if (action.action !== 'write') continue;
210
+ const src = path.join(templateDir, action.relPath);
211
+ const dst = path.join(targetDir, action.relPath);
212
+ await fsPromises.mkdir(path.dirname(dst), { recursive: true });
213
+ await fsPromises.copyFile(src, dst);
214
+ }
215
+ }
216
+
217
+ export async function readInstalledFiles(filePath: string): Promise<InstalledFiles | null> {
218
+ try {
219
+ const raw = await fsPromises.readFile(filePath, 'utf-8');
220
+ const parsed = InstalledFilesSchema.safeParse(JSON.parse(raw));
221
+ return parsed.success ? parsed.data : null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ export async function writeInstalledFiles(filePath: string, data: InstalledFiles): Promise<void> {
228
+ await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
229
+ // Atomic write: plain writeFile truncates before writing, so a crash mid-
230
+ // write drops the SHA state entirely and turns every tracked file into a
231
+ // `no-recorded-sha` skip on the next refresh. rename(2) on the same
232
+ // filesystem is atomic — readers always see the old or new content.
233
+ const tmpPath = `${filePath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
234
+ await fsPromises.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
235
+ await fsPromises.rename(tmpPath, filePath);
236
+ }
237
+
238
+ // Return an installed-files slice scoped to keys starting with `prefix + '/'`,
239
+ // with the prefix stripped. Used by skill refresh, which runs a plan per
240
+ // skill against a shared `installed-files.json`.
241
+ export function sliceInstalledUnder(
242
+ installed: InstalledFiles | null,
243
+ prefix: string
244
+ ): InstalledFiles | null {
245
+ if (!installed?.files) return null;
246
+ const p = `${prefix}/`;
247
+ const filtered: Record<string, { sha: string }> = {};
248
+ for (const [k, v] of Object.entries(installed.files)) {
249
+ if (k.startsWith(p)) filtered[k.slice(p.length)] = v;
250
+ }
251
+ return { files: filtered };
252
+ }
253
+
254
+ // Re-key a plan's `actions` and `nextInstalled` entries by prefixing every
255
+ // `relPath`. Used for merging a skill plan (with skill-internal paths) back
256
+ // into the workdir-relative installed-files store.
257
+ export function prefixPlanKeys(plan: RefreshPlan, prefix: string): RefreshPlan {
258
+ const p = prefix ? `${prefix}/` : '';
259
+ return {
260
+ actions: plan.actions.map((a) => ({ ...a, relPath: p + a.relPath })),
261
+ nextInstalled: {
262
+ files: Object.fromEntries(
263
+ Object.entries(plan.nextInstalled.files ?? {}).map(([k, v]) => [p + k, v])
264
+ ),
265
+ },
266
+ };
267
+ }
@@ -0,0 +1,61 @@
1
+ // Splits a command string into argv the way a POSIX shell would, minus
2
+ // expansions: single-quoted strings are literal, double-quoted strings honor
3
+ // `\"` and `\\`, and an unquoted backslash escapes the next character.
4
+ // Throws when a quote is left open.
5
+ export function parseShellArgs(input: string): string[] {
6
+ const args: string[] = [];
7
+ let current = '';
8
+ let inSingle = false;
9
+ let inDouble = false;
10
+ let hasContent = false;
11
+
12
+ for (let i = 0; i < input.length; i++) {
13
+ const ch = input[i];
14
+ if (inSingle) {
15
+ if (ch === "'") inSingle = false;
16
+ else current += ch;
17
+ continue;
18
+ }
19
+ if (inDouble) {
20
+ if (ch === '"') {
21
+ inDouble = false;
22
+ } else if (ch === '\\' && i + 1 < input.length) {
23
+ const next = input[i + 1];
24
+ if (next === '"' || next === '\\') {
25
+ current += next;
26
+ i++;
27
+ } else {
28
+ current += ch;
29
+ }
30
+ } else {
31
+ current += ch;
32
+ }
33
+ continue;
34
+ }
35
+ if (ch === "'") {
36
+ inSingle = true;
37
+ hasContent = true;
38
+ } else if (ch === '"') {
39
+ inDouble = true;
40
+ hasContent = true;
41
+ } else if (ch === '\\' && i + 1 < input.length) {
42
+ current += input[i + 1];
43
+ i++;
44
+ hasContent = true;
45
+ } else if (ch === ' ' || ch === '\t') {
46
+ if (hasContent) {
47
+ args.push(current);
48
+ current = '';
49
+ hasContent = false;
50
+ }
51
+ } else {
52
+ current += ch;
53
+ hasContent = true;
54
+ }
55
+ }
56
+ if (inSingle || inDouble) {
57
+ throw new Error('Unterminated quote in command string.');
58
+ }
59
+ if (hasContent) args.push(current);
60
+ return args;
61
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ let cached: string | null = null;
6
+
7
+ /**
8
+ * Read the version from the clawmini package.json. Walks up from the current
9
+ * module's location until a package.json named "clawmini" is found.
10
+ */
11
+ export function getClawminiVersion(): string {
12
+ if (cached !== null) return cached;
13
+ let dir = path.dirname(fileURLToPath(import.meta.url));
14
+ while (dir !== path.parse(dir).root) {
15
+ const pkgPath = path.join(dir, 'package.json');
16
+ if (fs.existsSync(pkgPath)) {
17
+ try {
18
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as {
19
+ name?: string;
20
+ version?: string;
21
+ };
22
+ if (pkg.name === 'clawmini' && typeof pkg.version === 'string') {
23
+ cached = pkg.version;
24
+ return cached;
25
+ }
26
+ } catch {
27
+ // try parent
28
+ }
29
+ }
30
+ dir = path.dirname(dir);
31
+ }
32
+ cached = 'unknown';
33
+ return cached;
34
+ }
@@ -24,8 +24,12 @@ import {
24
24
  readEnvironment,
25
25
  getActiveEnvironmentName,
26
26
  resolveTargetAgentSkillsDir,
27
+ resolvePolicies,
28
+ readEnvironmentPoliciesForPath,
29
+ readPoliciesForPath,
27
30
  } from './workspace.js';
28
31
  import type { Agent, Settings, Environment } from './config.js';
32
+ import { BUILTIN_POLICIES } from './policies.js';
29
33
 
30
34
  describe('workspace utilities', () => {
31
35
  const testDir = path.join(process.cwd(), '.clawmini-test-workspace');
@@ -189,6 +193,49 @@ describe('workspace utilities', () => {
189
193
  list = await listAgents(testDir);
190
194
  expect(list).not.toContain('agent-to-delete');
191
195
  });
196
+
197
+ it('should serialize concurrent updateAgentOverlay calls', async () => {
198
+ const { updateAgentOverlay, agentSettingsLocks } = await import('./workspace.js');
199
+ await writeAgentSettings('lock-agent', { env: { COUNT: '0' } }, testDir);
200
+
201
+ // Fire many concurrent increments. Without the lock, the read-modify-write
202
+ // races and the final count is < N.
203
+ const N = 20;
204
+ await Promise.all(
205
+ Array.from({ length: N }, () =>
206
+ updateAgentOverlay(
207
+ 'lock-agent',
208
+ (overlay) => {
209
+ const prev = Number((overlay.env?.COUNT as string) ?? '0');
210
+ return { ...overlay, env: { ...(overlay.env ?? {}), COUNT: String(prev + 1) } };
211
+ },
212
+ testDir
213
+ )
214
+ )
215
+ );
216
+
217
+ const final = await getAgent('lock-agent', testDir);
218
+ expect(final?.env?.COUNT).toBe(String(N));
219
+ expect(agentSettingsLocks.size).toBe(0);
220
+ });
221
+
222
+ it('should throw from updateAgentOverlay when the overlay is missing', async () => {
223
+ const { updateAgentOverlay } = await import('./workspace.js');
224
+ await expect(
225
+ updateAgentOverlay('ghost-agent', (overlay) => overlay, testDir)
226
+ ).rejects.toThrow(/no settings overlay/);
227
+ });
228
+
229
+ it('should skip the write when updateAgentOverlay updater returns null', async () => {
230
+ const { updateAgentOverlay } = await import('./workspace.js');
231
+ await writeAgentSettings('skip-agent', { env: { KEEP: '1' } }, testDir);
232
+
233
+ const wrote = await updateAgentOverlay('skip-agent', () => null, testDir);
234
+ expect(wrote).toBe(false);
235
+
236
+ const after = await getAgent('skip-agent', testDir);
237
+ expect(after?.env?.KEEP).toBe('1');
238
+ });
192
239
  });
193
240
 
194
241
  describe('resolveTargetAgentSkillsDir', () => {
@@ -386,4 +433,174 @@ describe('workspace utilities', () => {
386
433
  expect(await getActiveEnvironmentName('./', testDir)).toBeNull();
387
434
  });
388
435
  });
436
+
437
+ describe('resolvePolicies', () => {
438
+ const scriptsDir = path.join(clawminiDir, 'policy-scripts');
439
+ const proposeScript = path.join(scriptsDir, 'manage-policies.js');
440
+
441
+ function installProposePolicyScript() {
442
+ fs.mkdirSync(scriptsDir, { recursive: true });
443
+ fs.writeFileSync(proposeScript, '#!/usr/bin/env node\n');
444
+ }
445
+
446
+ it('returns null when the file is null', () => {
447
+ expect(resolvePolicies(null, clawminiDir)).toBeNull();
448
+ });
449
+
450
+ it('injects manage-policies when its script is installed', () => {
451
+ installProposePolicyScript();
452
+ const resolved = resolvePolicies({ policies: {} }, clawminiDir);
453
+ expect(resolved?.policies['manage-policies']).toEqual({
454
+ ...BUILTIN_POLICIES['manage-policies'],
455
+ command: proposeScript,
456
+ });
457
+ });
458
+
459
+ it('resolves relative command paths against the workspace root', () => {
460
+ const resolved = resolvePolicies(
461
+ {
462
+ policies: {
463
+ local: { command: './scripts/tool.sh' },
464
+ parent: { command: '../outside/tool.sh' },
465
+ absolute: { command: '/usr/bin/env' },
466
+ bare: { command: 'echo' },
467
+ },
468
+ },
469
+ clawminiDir
470
+ );
471
+ const workspaceRoot = path.dirname(clawminiDir);
472
+ expect(resolved?.policies.local?.command).toBe(
473
+ path.resolve(workspaceRoot, 'scripts/tool.sh')
474
+ );
475
+ expect(resolved?.policies.parent?.command).toBe(
476
+ path.resolve(workspaceRoot, '../outside/tool.sh')
477
+ );
478
+ expect(resolved?.policies.absolute?.command).toBe('/usr/bin/env');
479
+ expect(resolved?.policies.bare?.command).toBe('echo');
480
+ });
481
+
482
+ it('omits manage-policies when its script is missing', () => {
483
+ const resolved = resolvePolicies({ policies: {} }, clawminiDir);
484
+ expect(resolved?.policies['manage-policies']).toBeUndefined();
485
+ });
486
+
487
+ it('omits manage-policies when explicitly disabled, even if script exists', () => {
488
+ installProposePolicyScript();
489
+ const resolved = resolvePolicies({ policies: { 'manage-policies': false } }, clawminiDir);
490
+ expect(resolved?.policies['manage-policies']).toBeUndefined();
491
+ });
492
+
493
+ it('preserves a user-defined manage-policies without overwriting', () => {
494
+ installProposePolicyScript();
495
+ const custom = { command: 'echo', args: ['custom'], description: 'mine' };
496
+ const resolved = resolvePolicies({ policies: { 'manage-policies': custom } }, clawminiDir);
497
+ expect(resolved?.policies['manage-policies']).toEqual(custom);
498
+ });
499
+
500
+ it('strips arbitrary entries set to false', () => {
501
+ const resolved = resolvePolicies(
502
+ {
503
+ policies: {
504
+ keep: { command: 'ls' },
505
+ drop: false,
506
+ },
507
+ },
508
+ clawminiDir
509
+ );
510
+ expect(resolved?.policies.keep).toEqual({ command: 'ls' });
511
+ expect(resolved?.policies.drop).toBeUndefined();
512
+ });
513
+
514
+ it('does not mutate its input', () => {
515
+ installProposePolicyScript();
516
+ const file = { policies: { drop: false as const, keep: { command: 'ls' } } };
517
+ const before = JSON.parse(JSON.stringify(file));
518
+ resolvePolicies(file, clawminiDir);
519
+ expect(file).toEqual(before);
520
+ });
521
+ });
522
+
523
+ describe('environment-scoped policies', () => {
524
+ async function writeEnvWithPolicies(
525
+ envName: string,
526
+ policies: Record<string, unknown>
527
+ ): Promise<string> {
528
+ const envDir = path.join(clawminiDir, 'environments', envName);
529
+ await fsPromises.mkdir(envDir, { recursive: true });
530
+ await fsPromises.writeFile(
531
+ path.join(envDir, 'env.json'),
532
+ JSON.stringify({ policies }),
533
+ 'utf-8'
534
+ );
535
+ return envDir;
536
+ }
537
+
538
+ it('returns empty when there is no active environment', async () => {
539
+ const result = await readEnvironmentPoliciesForPath('./', testDir);
540
+ expect(result).toEqual({});
541
+ });
542
+
543
+ it('returns empty when the active env has no policies field', async () => {
544
+ const envDir = path.join(clawminiDir, 'environments', 'bare');
545
+ await fsPromises.mkdir(envDir, { recursive: true });
546
+ await fsPromises.writeFile(path.join(envDir, 'env.json'), JSON.stringify({}), 'utf-8');
547
+ await writeSettings({ environments: { './': 'bare' } }, testDir);
548
+ const result = await readEnvironmentPoliciesForPath('./', testDir);
549
+ expect(result).toEqual({});
550
+ });
551
+
552
+ it('resolves relative commands against the env directory', async () => {
553
+ const envDir = await writeEnvWithPolicies('sandbox-env', {
554
+ 'allowlist-domain': {
555
+ description: 'Adds a domain',
556
+ command: './allowlist-domain.mjs',
557
+ allowHelp: false,
558
+ },
559
+ });
560
+ await writeSettings({ environments: { './': 'sandbox-env' } }, testDir);
561
+
562
+ const result = await readEnvironmentPoliciesForPath('./', testDir);
563
+ expect(result['allowlist-domain']?.command).toBe(path.join(envDir, 'allowlist-domain.mjs'));
564
+ expect(result['allowlist-domain']?.description).toBe('Adds a domain');
565
+ });
566
+
567
+ it('leaves non-relative commands untouched', async () => {
568
+ await writeEnvWithPolicies('cmd-env', {
569
+ echo: { command: 'echo' },
570
+ });
571
+ await writeSettings({ environments: { './': 'cmd-env' } }, testDir);
572
+
573
+ const result = await readEnvironmentPoliciesForPath('./', testDir);
574
+ expect(result.echo?.command).toBe('echo');
575
+ });
576
+
577
+ it('hides env policies when the target is outside the env subpath', async () => {
578
+ await writeEnvWithPolicies('scoped-env', {
579
+ 'only-here': { command: './cmd.js' },
580
+ });
581
+ await writeSettings({ environments: { './agents/scoped': 'scoped-env' } }, testDir);
582
+
583
+ expect(await readEnvironmentPoliciesForPath('./', testDir)).toEqual({});
584
+ expect(await readEnvironmentPoliciesForPath('./other', testDir)).toEqual({});
585
+
586
+ const scopedResult = await readEnvironmentPoliciesForPath('./agents/scoped', testDir);
587
+ expect(scopedResult['only-here']).toBeDefined();
588
+ });
589
+
590
+ it('merges env policies on top of global ones via readPoliciesForPath', async () => {
591
+ await fsPromises.mkdir(path.join(clawminiDir, 'policy-scripts'), { recursive: true });
592
+ await fsPromises.writeFile(
593
+ path.join(clawminiDir, 'policies.json'),
594
+ JSON.stringify({ policies: { global: { command: 'globalcmd' } } }),
595
+ 'utf-8'
596
+ );
597
+ await writeEnvWithPolicies('merge-env', {
598
+ 'env-only': { command: './env.js' },
599
+ });
600
+ await writeSettings({ environments: { './': 'merge-env' } }, testDir);
601
+
602
+ const result = await readPoliciesForPath('./', testDir);
603
+ expect(Object.keys(result?.policies || {}).sort()).toEqual(['env-only', 'global']);
604
+ });
605
+ });
389
606
  });