clawmini 0.0.7 → 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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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 +42 -13
  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 -176
  274. package/web/.svelte-kit/generated/server/internal.js +1 -1
  275. package/web/.svelte-kit/output/client/.vite/manifest.json +127 -137
  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/{G_zz-Gou.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.CYS8iApT.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.Dr0ot9sV.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.BBGQ_i84.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/DMtIqaiV.js +0 -2
  328. package/dist/web/_app/immutable/chunks/DhD271EB.js +0 -1
  329. package/dist/web/_app/immutable/chunks/DpuLqk8d.js +0 -1
  330. package/dist/web/_app/immutable/chunks/DsIToJCP.js +0 -1
  331. package/dist/web/_app/immutable/chunks/bBmtyQMj.js +0 -1
  332. package/dist/web/_app/immutable/entry/app.CJmSwntr.js +0 -2
  333. package/dist/web/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  334. package/dist/web/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  335. package/dist/web/_app/immutable/nodes/4.oBhvQhcA.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/DMtIqaiV.js +0 -2
  357. package/web/.svelte-kit/output/client/_app/immutable/chunks/DhD271EB.js +0 -1
  358. package/web/.svelte-kit/output/client/_app/immutable/chunks/DpuLqk8d.js +0 -1
  359. package/web/.svelte-kit/output/client/_app/immutable/chunks/DsIToJCP.js +0 -1
  360. package/web/.svelte-kit/output/client/_app/immutable/chunks/bBmtyQMj.js +0 -1
  361. package/web/.svelte-kit/output/client/_app/immutable/entry/app.CJmSwntr.js +0 -2
  362. package/web/.svelte-kit/output/client/_app/immutable/entry/start.ZpUrT2ak.js +0 -1
  363. package/web/.svelte-kit/output/client/_app/immutable/nodes/1.Bli0Hqzn.js +0 -1
  364. package/web/.svelte-kit/output/client/_app/immutable/nodes/4.oBhvQhcA.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
@@ -1,6 +1,12 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { execSync } from 'node:child_process';
3
- import type { PolicyConfig } from './policies.js';
3
+ import {
4
+ BUILTIN_POLICIES,
5
+ type PolicyConfig,
6
+ type PolicyConfigFile,
7
+ type PolicyDefinition,
8
+ } from './policies.js';
9
+ import crypto from 'node:crypto';
4
10
  import fs from 'node:fs';
5
11
  import fsPromises from 'node:fs/promises';
6
12
  import path from 'node:path';
@@ -18,6 +24,19 @@ import {
18
24
  SettingsSchema,
19
25
  } from './config.js';
20
26
  import { pathIsInsideDir } from './utils/fs.js';
27
+ import {
28
+ readTemplateManifest,
29
+ planRefresh,
30
+ applyPlan,
31
+ walkTemplateFiles,
32
+ writeInstalledFiles,
33
+ readInstalledFiles,
34
+ sliceInstalledUnder,
35
+ prefixPlanKeys,
36
+ type InstalledFiles,
37
+ type RefreshPlan,
38
+ type FileMode,
39
+ } from './template-manifest.js';
21
40
 
22
41
  export function getWorkspaceRoot(startDir = process.cwd()): string {
23
42
  let curr = startDir;
@@ -53,11 +72,15 @@ export function resolveAgentWorkDir(
53
72
  return dirPath;
54
73
  }
55
74
 
75
+ // Returns null when the agent has explicitly opted out of skills via
76
+ // `"skillsDir": null` in its settings. Callers must handle null by
77
+ // skipping any skill-related install/refresh work.
56
78
  export function resolveAgentSkillsDir(
57
79
  agentId: string,
58
80
  agentData: Agent,
59
81
  startDir = process.cwd()
60
- ): string {
82
+ ): string | null {
83
+ if (agentData.skillsDir === null) return null;
61
84
  const workDir = resolveAgentWorkDir(agentId, agentData.directory, startDir);
62
85
  return path.resolve(workDir, agentData.skillsDir || '.agents/skills');
63
86
  }
@@ -112,6 +135,10 @@ export function getAgentSettingsPath(agentId: string, startDir = process.cwd()):
112
135
  return path.join(getAgentDir(agentId, startDir), 'settings.json');
113
136
  }
114
137
 
138
+ export function getInstalledFilesPath(agentId: string, startDir = process.cwd()): string {
139
+ return path.join(getAgentDir(agentId, startDir), 'installed-files.json');
140
+ }
141
+
115
142
  export function getAgentSessionSettingsPath(
116
143
  agentId: string,
117
144
  sessionId: string,
@@ -142,7 +169,12 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
142
169
  async function writeJsonFile(filePath: string, data: Record<string, unknown>): Promise<void> {
143
170
  const dir = path.dirname(filePath);
144
171
  await fsPromises.mkdir(dir, { recursive: true });
145
- await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
172
+ // Atomic write: a plain writeFile truncates then writes, so a concurrent
173
+ // reader can observe an empty file and throw `JSON.parse("")`. rename(2)
174
+ // on the same filesystem is atomic, so readers always see old or new.
175
+ const tmpPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
176
+ await fsPromises.writeFile(tmpPath, JSON.stringify(data, null, 2), 'utf-8');
177
+ await fsPromises.rename(tmpPath, filePath);
146
178
  }
147
179
 
148
180
  export async function readChatSettings(
@@ -214,7 +246,13 @@ export async function writeAgentSessionSettings(
214
246
  );
215
247
  }
216
248
 
217
- export async function getAgent(agentId: string, startDir = process.cwd()): Promise<Agent | null> {
249
+ // Reads only the on-disk overlay (local settings.json). Used when editing the
250
+ // overlay — callers that want the fully-resolved agent (template fields
251
+ // merged in) use `getAgent` instead.
252
+ export async function getAgentOverlay(
253
+ agentId: string,
254
+ startDir = process.cwd()
255
+ ): Promise<Agent | null> {
218
256
  const filePath = getAgentSettingsPath(agentId, startDir);
219
257
  let dataStr: string;
220
258
  try {
@@ -239,6 +277,62 @@ export async function getAgent(agentId: string, startDir = process.cwd()): Promi
239
277
  return parsed.data;
240
278
  }
241
279
 
280
+ async function readAgentTemplateSettings(
281
+ templateName: string,
282
+ startDir: string
283
+ ): Promise<Agent | null> {
284
+ let templatePath: string;
285
+ try {
286
+ templatePath = await resolveTemplatePath(templateName, startDir);
287
+ } catch {
288
+ return null;
289
+ }
290
+ const settingsPath = path.join(templatePath, 'settings.json');
291
+ const data = await readJsonFile(settingsPath);
292
+ if (!data) return null;
293
+ const parsed = AgentSchema.safeParse(data);
294
+ if (!parsed.success) return null;
295
+ // `directory` in a template is never used — the overlay declares the work
296
+ // directory instead. Strip it so it doesn't pollute the merge.
297
+ const result = { ...parsed.data };
298
+ delete result.directory;
299
+ return result;
300
+ }
301
+
302
+ // Returns the fully-resolved agent: reads the local overlay, resolves any
303
+ // `extends` template, then shallow-merges the overlay over the template
304
+ // field-by-field. `env` and `subagentEnv` are deep-merged one level so the
305
+ // overlay can add one entry without dropping the template's defaults.
306
+ export async function getAgent(agentId: string, startDir = process.cwd()): Promise<Agent | null> {
307
+ const overlay = await getAgentOverlay(agentId, startDir);
308
+ if (!overlay) return null;
309
+ if (!overlay.extends) return overlay;
310
+
311
+ const template = await readAgentTemplateSettings(overlay.extends, startDir);
312
+ if (!template) return overlay;
313
+
314
+ const {
315
+ env: overlayEnv,
316
+ subagentEnv: overlaySub,
317
+ modelShorthands: overlayShorthands,
318
+ ...overlayRest
319
+ } = overlay;
320
+ const {
321
+ env: templateEnv,
322
+ subagentEnv: templateSub,
323
+ modelShorthands: templateShorthands,
324
+ ...templateRest
325
+ } = template;
326
+ const merged: Agent = { ...templateRest, ...overlayRest };
327
+ const mergedEnv = mergeOneLevel(templateEnv, overlayEnv);
328
+ if (mergedEnv) merged.env = mergedEnv;
329
+ const mergedSub = mergeOneLevel(templateSub, overlaySub);
330
+ if (mergedSub) merged.subagentEnv = mergedSub;
331
+ const mergedShorthands = mergeOneLevel(templateShorthands, overlayShorthands);
332
+ if (mergedShorthands) merged.modelShorthands = mergedShorthands;
333
+ return merged;
334
+ }
335
+
242
336
  export async function writeAgentSettings(
243
337
  agentId: string,
244
338
  data: Agent,
@@ -248,6 +342,45 @@ export async function writeAgentSettings(
248
342
  await writeJsonFile(getAgentSettingsPath(agentId, startDir), data as Record<string, unknown>);
249
343
  }
250
344
 
345
+ export const agentSettingsLocks = new Map<string, Promise<void>>();
346
+
347
+ // Read-modify-write the agent's on-disk overlay under a per-agent lock so
348
+ // concurrent callers can't lose updates. Throws if the overlay does not
349
+ // exist — callers that intend to *create* an agent should use
350
+ // `writeAgentSettings` directly. Returning `null` from the updater skips
351
+ // the write (useful when a fresh read shows the change is a no-op).
352
+ // Returns `true` iff a write happened.
353
+ export async function updateAgentOverlay(
354
+ agentId: string,
355
+ updater: (overlay: Agent) => Agent | null | Promise<Agent | null>,
356
+ startDir = process.cwd()
357
+ ): Promise<boolean> {
358
+ const prevLock = agentSettingsLocks.get(agentId) || Promise.resolve();
359
+ let release!: () => void;
360
+ const nextLock = new Promise<void>((resolve) => {
361
+ release = resolve;
362
+ });
363
+ const nextLockPromise = prevLock.catch(() => {}).then(() => nextLock);
364
+ agentSettingsLocks.set(agentId, nextLockPromise);
365
+
366
+ try {
367
+ await prevLock;
368
+ const overlay = await getAgentOverlay(agentId, startDir);
369
+ if (!overlay) {
370
+ throw new Error(`Agent '${agentId}' has no settings overlay.`);
371
+ }
372
+ const updated = await updater(overlay);
373
+ if (updated === null) return false;
374
+ await writeAgentSettings(agentId, updated, startDir);
375
+ return true;
376
+ } finally {
377
+ release();
378
+ if (agentSettingsLocks.get(agentId) === nextLockPromise) {
379
+ agentSettingsLocks.delete(agentId);
380
+ }
381
+ }
382
+ }
383
+
251
384
  export async function listAgents(startDir = process.cwd()): Promise<string[]> {
252
385
  const agentsDir = path.join(getClawminiDir(startDir), 'agents');
253
386
  try {
@@ -371,23 +504,30 @@ export async function copyTemplateBase(
371
504
  }
372
505
  }
373
506
 
374
- // Recursively copy
375
- await fsPromises.cp(templatePath, targetDir, { recursive: true, force: true });
507
+ // Recursively copy. The template.json manifest is never copied — it's
508
+ // metadata about how to handle the other files.
509
+ const rootTemplateJson = path.resolve(templatePath, 'template.json');
510
+ await fsPromises.cp(templatePath, targetDir, {
511
+ recursive: true,
512
+ force: true,
513
+ filter: (src) => path.resolve(src) !== rootTemplateJson,
514
+ });
376
515
  }
377
516
 
378
517
  export async function copyTemplate(
379
518
  templateName: string,
380
519
  targetDir: string,
381
- startDir = process.cwd()
520
+ startDir = process.cwd(),
521
+ opts: { force?: boolean } = {}
382
522
  ): Promise<void> {
383
523
  const templatePath = await resolveTemplatePath(templateName, startDir);
384
- await copyTemplateBase(templatePath, targetDir, false);
524
+ await copyTemplateBase(templatePath, targetDir, false, opts.force ?? false);
385
525
  }
386
526
 
387
527
  export async function resolveTargetAgentSkillsDir(
388
528
  agentId: string,
389
529
  startDir = process.cwd()
390
- ): Promise<string> {
530
+ ): Promise<string | null> {
391
531
  const agentDir = getAgentDir(agentId, startDir);
392
532
  try {
393
533
  const stat = await fsPromises.stat(agentDir);
@@ -431,6 +571,9 @@ export async function copyAgentSkills(
431
571
  overwrite = false
432
572
  ): Promise<void> {
433
573
  const targetDir = await resolveTargetAgentSkillsDir(agentId, startDir);
574
+ if (targetDir === null) {
575
+ throw new Error(`Agent '${agentId}' has skills disabled (skillsDir is null).`);
576
+ }
434
577
  const templatePath = await resolveSkillsTemplatePath(startDir);
435
578
  await copyTemplateBase(templatePath, targetDir, true, overwrite);
436
579
  }
@@ -442,6 +585,9 @@ export async function copyAgentSkill(
442
585
  overwrite = false
443
586
  ): Promise<void> {
444
587
  const targetDir = await resolveTargetAgentSkillsDir(agentId, startDir);
588
+ if (targetDir === null) {
589
+ throw new Error(`Agent '${agentId}' has skills disabled (skillsDir is null).`);
590
+ }
445
591
  const templatePath = await resolveSkillsTemplatePath(startDir);
446
592
  const specificSkillPath = path.join(templatePath, skillName);
447
593
 
@@ -461,50 +607,236 @@ export async function copyAgentSkill(
461
607
  await copyTemplateBase(specificSkillPath, skillTargetDir, true, overwrite);
462
608
  }
463
609
 
610
+ // Return the subset of template files that already exist in the target
611
+ // directory. Used to refuse a silent overwrite on first install.
612
+ async function collectTemplateCollisions(
613
+ templateDir: string,
614
+ targetDir: string
615
+ ): Promise<string[]> {
616
+ const templateFiles = await walkTemplateFiles(templateDir);
617
+ const collisions: string[] = [];
618
+ for (const rel of templateFiles) {
619
+ try {
620
+ await fsPromises.access(path.join(targetDir, rel));
621
+ collisions.push(rel);
622
+ } catch {
623
+ // not present — no collision
624
+ }
625
+ }
626
+ return collisions;
627
+ }
628
+
629
+ function formatCollisionError(collisions: string[]): string {
630
+ const preview = collisions
631
+ .slice(0, 5)
632
+ .map((p) => ` ${p}`)
633
+ .join('\n');
634
+ const suffix = collisions.length > 5 ? `\n ... and ${collisions.length - 5} more` : '';
635
+ return `Target directory has existing files that the template would overwrite:\n${preview}${suffix}\nRe-run with --force to overwrite.`;
636
+ }
637
+
464
638
  export async function applyTemplateToAgent(
465
639
  agentId: string,
466
640
  templateName: string,
467
641
  overrides: Agent,
468
- startDir = process.cwd()
642
+ startDir = process.cwd(),
643
+ opts: { fork?: boolean; force?: boolean } = {}
469
644
  ): Promise<void> {
470
645
  const agentWorkDir = resolveAgentWorkDir(agentId, overrides.directory, startDir);
471
- await copyTemplate(templateName, agentWorkDir, startDir);
472
646
 
473
- const settingsPath = path.join(agentWorkDir, 'settings.json');
474
- try {
475
- const rawSettings = await fsPromises.readFile(settingsPath, 'utf-8');
476
- const parsedSettings = JSON.parse(rawSettings);
477
- const validation = AgentSchema.safeParse(parsedSettings);
478
-
479
- if (validation.success) {
480
- const templateData = validation.data;
481
- if (templateData.directory) {
482
- console.warn(
483
- `Warning: Ignoring 'directory' field from template settings.json. Using default or provided directory.`
484
- );
485
- delete templateData.directory;
647
+ if (opts.fork) {
648
+ // Legacy path: copy everything, merge template settings into the local
649
+ // file, then strip the template metadata files from the workdir.
650
+ await copyTemplate(templateName, agentWorkDir, startDir, { force: opts.force ?? false });
651
+
652
+ const settingsPath = path.join(agentWorkDir, 'settings.json');
653
+ const manifestPath = path.join(agentWorkDir, 'template.json');
654
+
655
+ try {
656
+ const rawSettings = await fsPromises.readFile(settingsPath, 'utf-8');
657
+ const parsedSettings = JSON.parse(rawSettings);
658
+ const validation = AgentSchema.safeParse(parsedSettings);
659
+
660
+ if (validation.success) {
661
+ const templateData = validation.data;
662
+ if (templateData.directory) {
663
+ console.warn(
664
+ `Warning: Ignoring 'directory' field from template settings.json. Using default or provided directory.`
665
+ );
666
+ delete templateData.directory;
667
+ }
668
+
669
+ const mergedEnv = { ...(templateData.env || {}), ...(overrides.env || {}) };
670
+ const mergedData: Agent = { ...templateData, ...overrides };
671
+ delete mergedData.extends;
672
+ if (Object.keys(mergedEnv).length > 0) mergedData.env = mergedEnv;
673
+
674
+ await writeAgentSettings(agentId, mergedData, startDir);
486
675
  }
676
+ } catch {
677
+ // Ignore parsing or file not found errors
678
+ }
487
679
 
488
- // Merge: overrides take precedence over templateData
489
- const mergedEnv = { ...(templateData.env || {}), ...(overrides.env || {}) };
490
- const mergedData: Agent = { ...templateData, ...overrides };
491
- if (Object.keys(mergedEnv).length > 0) {
492
- mergedData.env = mergedEnv;
680
+ for (const tmp of [settingsPath, manifestPath]) {
681
+ try {
682
+ await fsPromises.rm(tmp);
683
+ } catch {
684
+ // Ignore if it doesn't exist
493
685
  }
686
+ }
687
+ return;
688
+ }
494
689
 
495
- await writeAgentSettings(agentId, mergedData, startDir);
690
+ // Overlay mode: install files via the manifest, record SHAs, and write the
691
+ // overlay pointing at the template. settings.json and template.json in the
692
+ // template are metadata — neither gets copied.
693
+ const templateDir = await resolveTemplatePath(templateName, startDir);
694
+ const manifest = await readTemplateManifest(templateDir);
695
+ await fsPromises.mkdir(agentWorkDir, { recursive: true });
696
+
697
+ if (!opts.force) {
698
+ const collisions = await collectTemplateCollisions(templateDir, agentWorkDir);
699
+ if (collisions.length > 0) {
700
+ throw new Error(formatCollisionError(collisions));
496
701
  }
702
+ }
703
+
704
+ const plan = await planRefresh(templateDir, agentWorkDir, manifest, null, {
705
+ defaultMode: 'seed-once',
706
+ firstInstall: true,
707
+ });
708
+ await applyPlan(templateDir, agentWorkDir, plan);
709
+ await writeInstalledFiles(getInstalledFilesPath(agentId, startDir), plan.nextInstalled);
710
+
711
+ const overlay: Agent = { extends: templateName, ...overrides };
712
+ await writeAgentSettings(agentId, overlay, startDir);
713
+ }
714
+
715
+ // Refresh all `track` files in the agent's working directory against the
716
+ // template content. Diverged files are skipped unless `accept` is true.
717
+ // Returns the full plan so callers can report / dry-run as needed.
718
+ export async function refreshAgentTemplate(
719
+ agentId: string,
720
+ agent: Agent,
721
+ startDir = process.cwd(),
722
+ opts: { accept?: boolean; dryRun?: boolean } = {}
723
+ ): Promise<RefreshPlan | null> {
724
+ if (!agent.extends) return null;
725
+ const templateDir = await resolveTemplatePath(agent.extends, startDir);
726
+ const agentWorkDir = resolveAgentWorkDir(agentId, agent.directory, startDir);
727
+ const manifest = await readTemplateManifest(templateDir);
728
+ const installedPath = getInstalledFilesPath(agentId, startDir);
729
+ const installed = await readInstalledFiles(installedPath);
730
+
731
+ const plan = await planRefresh(templateDir, agentWorkDir, manifest, installed, {
732
+ defaultMode: 'seed-once',
733
+ ...(opts.accept === undefined ? {} : { accept: opts.accept }),
734
+ });
735
+
736
+ if (opts.dryRun) return plan;
737
+
738
+ await applyPlan(templateDir, agentWorkDir, plan);
739
+ await writeInstalledFiles(installedPath, plan.nextInstalled);
740
+ return plan;
741
+ }
742
+
743
+ // Refresh the agent's template skills. Skills default to `track` for files
744
+ // unlisted in their manifest — the opposite of agent workdir files — because
745
+ // the authoring model differs: clawmini ships skill content, agents edit it.
746
+ // SHAs share the agent's installed-files.json keyed by the path relative to
747
+ // the agent's working directory (e.g. `.gemini/skills/skill-creator/SKILL.md`).
748
+ export async function refreshAgentSkills(
749
+ agentId: string,
750
+ agent: Agent,
751
+ startDir = process.cwd(),
752
+ opts: { accept?: boolean; dryRun?: boolean; firstInstall?: boolean } = {}
753
+ ): Promise<RefreshPlan | null> {
754
+ const skillsTargetDir = resolveAgentSkillsDir(agentId, agent, startDir);
755
+ if (skillsTargetDir === null) return null;
756
+
757
+ let skillsTemplateRoot: string;
758
+ try {
759
+ skillsTemplateRoot = await resolveSkillsTemplatePath(startDir);
497
760
  } catch {
498
- // Ignore parsing or file not found errors
499
- } finally {
500
- try {
501
- await fsPromises.rm(settingsPath);
502
- } catch {
503
- // Ignore if it doesn't exist
761
+ return null;
762
+ }
763
+
764
+ const agentWorkDir = resolveAgentWorkDir(agentId, agent.directory, startDir);
765
+ const prefixRel = path.relative(agentWorkDir, skillsTargetDir).split(path.sep).join('/');
766
+
767
+ let skillDirs: fs.Dirent[];
768
+ try {
769
+ skillDirs = await fsPromises.readdir(skillsTemplateRoot, { withFileTypes: true });
770
+ } catch {
771
+ return null;
772
+ }
773
+
774
+ const installedPath = getInstalledFilesPath(agentId, startDir);
775
+ let installed = await readInstalledFiles(installedPath);
776
+ const allActions: RefreshPlan['actions'] = [];
777
+
778
+ for (const entry of skillDirs) {
779
+ if (!entry.isDirectory()) continue;
780
+ const skillName = entry.name;
781
+ const skillTemplateDir = path.join(skillsTemplateRoot, skillName);
782
+ const skillTargetDir = path.join(skillsTargetDir, skillName);
783
+ const keyPrefix = `${prefixRel}/${skillName}`;
784
+
785
+ const manifest = await readTemplateManifest(skillTemplateDir);
786
+ const slice = sliceInstalledUnder(installed, keyPrefix);
787
+
788
+ const plan = await planRefresh(skillTemplateDir, skillTargetDir, manifest, slice, {
789
+ defaultMode: 'track',
790
+ ...(opts.firstInstall ? { firstInstall: true } : {}),
791
+ ...(opts.accept === undefined ? {} : { accept: opts.accept }),
792
+ });
793
+
794
+ const prefixed = prefixPlanKeys(plan, keyPrefix);
795
+ allActions.push(...prefixed.actions);
796
+
797
+ if (!opts.dryRun) {
798
+ await applyPlan(skillTemplateDir, skillTargetDir, plan);
799
+ installed = {
800
+ files: {
801
+ ...(installed?.files ?? {}),
802
+ ...(prefixed.nextInstalled.files ?? {}),
803
+ },
804
+ };
504
805
  }
505
806
  }
807
+
808
+ if (!opts.dryRun && installed) {
809
+ await writeInstalledFiles(installedPath, installed);
810
+ }
811
+
812
+ return { actions: allActions, nextInstalled: installed ?? { files: {} } };
813
+ }
814
+
815
+ // Human-readable per-action lines for logging / dry-run. Prefixed with the
816
+ // agent id for readability when invoked over multiple agents at once.
817
+ export function formatPlanActions(
818
+ plan: RefreshPlan,
819
+ opts: { agentId?: string; prefix?: string } = {}
820
+ ): string[] {
821
+ const prefix = opts.prefix ?? (opts.agentId ? `[${opts.agentId}] ` : '');
822
+ return plan.actions.map((action) => {
823
+ switch (action.action) {
824
+ case 'write':
825
+ return `${prefix}${action.reason === 'new' ? 'install' : 'refresh'} ${action.relPath}`;
826
+ case 'skip-unchanged':
827
+ return `${prefix}unchanged ${action.relPath}`;
828
+ case 'skip-seed-once':
829
+ return `${prefix}seed-once ${action.relPath}`;
830
+ case 'skip-diverged':
831
+ return `${prefix}diverged ${action.relPath} (${action.reason})`;
832
+ case 'skip-absent-from-template':
833
+ return `${prefix}absent ${action.relPath}`;
834
+ }
835
+ });
506
836
  }
507
837
 
838
+ export type { InstalledFiles, RefreshPlan, FileMode };
839
+
508
840
  export async function readSettings(startDir = process.cwd()): Promise<Settings | null> {
509
841
  const data = await readJsonFile(getSettingsPath(startDir));
510
842
  if (!data) return null;
@@ -516,28 +848,260 @@ export async function writeSettings(data: Settings, startDir = process.cwd()): P
516
848
  await writeJsonFile(getSettingsPath(startDir), data as Record<string, unknown>);
517
849
  }
518
850
 
519
- export async function readPolicies(startDir = process.cwd()): Promise<PolicyConfig | null> {
851
+ export async function readPoliciesFile(startDir = process.cwd()): Promise<PolicyConfigFile | null> {
520
852
  const data = await readJsonFile(getPoliciesPath(startDir));
521
853
  if (!data) return null;
522
- // Basic validation, assuming PolicyConfig structure
523
854
  if (data.policies && typeof data.policies === 'object') {
524
- return data as unknown as PolicyConfig;
855
+ return data as unknown as PolicyConfigFile;
525
856
  }
526
857
  return null;
527
858
  }
528
859
 
860
+ // Merge built-ins, drop any user entries explicitly set to `false`. Pure: never
861
+ // mutates the input. A built-in is only injected when its installed script
862
+ // exists on disk, so the resolved config never advertises a command we know is
863
+ // missing. Relative `command` paths are resolved against the workspace root so
864
+ // the policy points at a real on-disk script regardless of the caller's cwd.
865
+ export function resolvePolicies(
866
+ file: PolicyConfigFile | null,
867
+ clawminiDir: string
868
+ ): PolicyConfig | null {
869
+ if (!file) return null;
870
+ const workspaceRoot = path.dirname(clawminiDir);
871
+ const resolveCommand = (definition: PolicyDefinition): PolicyDefinition => {
872
+ if (!definition.command.startsWith('./') && !definition.command.startsWith('../')) {
873
+ return definition;
874
+ }
875
+ return { ...definition, command: path.resolve(workspaceRoot, definition.command) };
876
+ };
877
+
878
+ const resolved: Record<string, PolicyDefinition> = {};
879
+ for (const [name, value] of Object.entries(file.policies)) {
880
+ if (value !== false) resolved[name] = resolveCommand(value);
881
+ }
882
+ for (const [name, definition] of Object.entries(BUILTIN_POLICIES)) {
883
+ if (name in file.policies) continue;
884
+ const scriptPath = path.join(clawminiDir, 'policy-scripts', `${name}.js`);
885
+ if (!fs.existsSync(scriptPath)) continue;
886
+ resolved[name] = resolveCommand(definition);
887
+ }
888
+ return { policies: resolved };
889
+ }
890
+
891
+ async function readBasePolicies(startDir = process.cwd()): Promise<PolicyConfig | null> {
892
+ const file = await readPoliciesFile(startDir);
893
+ return resolvePolicies(file, getClawminiDir(startDir));
894
+ }
895
+
896
+ // Resolves env-scoped policies for the active environment at `targetPath`.
897
+ // Relative `command` paths are resolved against the layered env search dirs
898
+ // (overlay first, then built-in template), so overlays can point at a
899
+ // built-in script without copying it.
900
+ export async function readEnvironmentPoliciesForPath(
901
+ targetPath: string,
902
+ startDir = process.cwd()
903
+ ): Promise<Record<string, PolicyDefinition>> {
904
+ const envInfo = await getActiveEnvironmentInfo(targetPath, startDir);
905
+ if (!envInfo) return {};
906
+
907
+ const envConfig = await readEnvironment(envInfo.name, startDir);
908
+ if (!envConfig?.policies) return {};
909
+
910
+ const searchDirs = await getEnvironmentSearchDirs(envInfo.name, startDir);
911
+ const resolved: Record<string, PolicyDefinition> = {};
912
+ for (const [name, definition] of Object.entries(envConfig.policies)) {
913
+ const command =
914
+ definition.command.startsWith('./') || definition.command.startsWith('../')
915
+ ? resolveLayeredRelativePath(definition.command, searchDirs)
916
+ : definition.command;
917
+ const entries = Object.entries({ ...definition, command }).filter(
918
+ ([, value]) => value !== undefined
919
+ );
920
+ resolved[name] = Object.fromEntries(entries) as unknown as PolicyDefinition;
921
+ }
922
+ return resolved;
923
+ }
924
+
925
+ export async function readPoliciesForPath(
926
+ targetPath: string,
927
+ startDir = process.cwd()
928
+ ): Promise<PolicyConfig | null> {
929
+ const base = await readBasePolicies(startDir);
930
+ const envPolicies = await readEnvironmentPoliciesForPath(targetPath, startDir);
931
+ if (Object.keys(envPolicies).length === 0) return base;
932
+ return {
933
+ policies: {
934
+ ...(base?.policies || {}),
935
+ ...envPolicies,
936
+ },
937
+ };
938
+ }
939
+
529
940
  export function getEnvironmentPath(name: string, startDir = process.cwd()): string {
530
941
  return path.join(getClawminiDir(startDir), 'environments', name);
531
942
  }
532
943
 
944
+ // Deep-merge one level of the nested value (used for env/policies). Local wins
945
+ // on conflict, missing keys flow through from the base.
946
+ function mergeOneLevel<T>(
947
+ base: Record<string, T> | undefined,
948
+ overlay: Record<string, T> | undefined
949
+ ): Record<string, T> | undefined {
950
+ if (!base && !overlay) return undefined;
951
+ return { ...(base || {}), ...(overlay || {}) };
952
+ }
953
+
954
+ async function readEnvironmentRaw(name: string, startDir: string): Promise<Environment | null> {
955
+ const localPath = path.join(getEnvironmentPath(name, startDir), 'env.json');
956
+ const local = await readJsonFile(localPath);
957
+ if (local) {
958
+ const parsed = EnvironmentSchema.safeParse(local);
959
+ if (parsed.success) return parsed.data;
960
+ }
961
+
962
+ // Parent references in `extends` resolve against built-in templates.
963
+ try {
964
+ const builtinDir = await resolveEnvironmentTemplatePath(name, startDir);
965
+ const builtinData = await readJsonFile(path.join(builtinDir, 'env.json'));
966
+ if (builtinData) {
967
+ const parsed = EnvironmentSchema.safeParse(builtinData);
968
+ if (parsed.success) return parsed.data;
969
+ }
970
+ } catch {
971
+ // No built-in template with this name
972
+ }
973
+
974
+ return null;
975
+ }
976
+
977
+ async function readBuiltinEnvironment(name: string, startDir: string): Promise<Environment | null> {
978
+ let builtinDir: string;
979
+ try {
980
+ builtinDir = await resolveEnvironmentTemplatePath(name, startDir);
981
+ } catch {
982
+ return null;
983
+ }
984
+ const data = await readJsonFile(path.join(builtinDir, 'env.json'));
985
+ if (!data) return null;
986
+ const parsed = EnvironmentSchema.safeParse(data);
987
+ return parsed.success ? parsed.data : null;
988
+ }
989
+
990
+ async function resolveEnvironmentWithSeen(
991
+ name: string,
992
+ startDir: string,
993
+ seen: Set<string>
994
+ ): Promise<Environment | null> {
995
+ if (seen.has(name)) {
996
+ throw new Error(`Environment extends cycle detected at '${name}'`);
997
+ }
998
+ seen.add(name);
999
+
1000
+ const local = await readEnvironmentRaw(name, startDir);
1001
+ if (!local || !local.extends) return local;
1002
+
1003
+ // Self-extends (`.clawmini/environments/macos` with `extends: "macos"`)
1004
+ // pivots from the overlay layer down to the built-in template of the same
1005
+ // name. Without this branch, the recursion would hit `seen` and throw.
1006
+ const parent =
1007
+ local.extends === name
1008
+ ? await readBuiltinEnvironment(name, startDir)
1009
+ : await resolveEnvironmentWithSeen(local.extends, startDir, seen);
1010
+ if (!parent) return local;
1011
+
1012
+ const { env: localEnv, policies: localPolicies, ...localRestRaw } = local;
1013
+ delete (localRestRaw as { extends?: string }).extends;
1014
+ const { env: parentEnv, policies: parentPolicies, ...parentRest } = parent;
1015
+ const merged: Environment = { ...parentRest, ...localRestRaw };
1016
+ const mergedEnv = mergeOneLevel(parentEnv, localEnv);
1017
+ if (mergedEnv) merged.env = mergedEnv;
1018
+ const mergedPolicies = mergeOneLevel(parentPolicies, localPolicies);
1019
+ if (mergedPolicies) merged.policies = mergedPolicies;
1020
+ return merged;
1021
+ }
1022
+
533
1023
  export async function readEnvironment(
534
1024
  name: string,
535
1025
  startDir = process.cwd()
536
1026
  ): Promise<Environment | null> {
537
- const data = await readJsonFile(path.join(getEnvironmentPath(name, startDir), 'env.json'));
538
- if (!data) return null;
539
- const parsed = EnvironmentSchema.safeParse(data);
540
- return parsed.success ? parsed.data : null;
1027
+ return resolveEnvironmentWithSeen(name, startDir, new Set());
1028
+ }
1029
+
1030
+ // The ordered list of directories an {ENV_DIR}-relative path should resolve
1031
+ // against. The local overlay always comes first. If the overlay extends
1032
+ // another environment, the parent's local overlay (if any) and the parent's
1033
+ // built-in template dir are appended, walking up the chain. Consumers pick
1034
+ // the first dir that actually contains the referenced file.
1035
+ export async function getEnvironmentSearchDirs(
1036
+ name: string,
1037
+ startDir = process.cwd()
1038
+ ): Promise<string[]> {
1039
+ const dirs: string[] = [];
1040
+ const seen = new Set<string>();
1041
+
1042
+ let currentName: string | undefined = name;
1043
+ while (currentName && !seen.has(currentName)) {
1044
+ seen.add(currentName);
1045
+ const overlayDir = getEnvironmentPath(currentName, startDir);
1046
+ if (fs.existsSync(overlayDir) && !dirs.includes(overlayDir)) dirs.push(overlayDir);
1047
+
1048
+ let builtinDir: string | null = null;
1049
+ try {
1050
+ builtinDir = await resolveEnvironmentTemplatePath(currentName, startDir);
1051
+ } catch {
1052
+ // No built-in — overlay is self-contained
1053
+ }
1054
+ if (builtinDir && !dirs.includes(builtinDir)) dirs.push(builtinDir);
1055
+
1056
+ const overlayEnvPath = path.join(overlayDir, 'env.json');
1057
+ const overlayData = await readJsonFile(overlayEnvPath);
1058
+ const overlayParsed = overlayData ? EnvironmentSchema.safeParse(overlayData) : null;
1059
+ if (overlayParsed?.success && overlayParsed.data.extends) {
1060
+ currentName = overlayParsed.data.extends;
1061
+ continue;
1062
+ }
1063
+
1064
+ if (builtinDir) {
1065
+ const builtinEnvPath = path.join(builtinDir, 'env.json');
1066
+ const builtinData = await readJsonFile(builtinEnvPath);
1067
+ const builtinParsed = builtinData ? EnvironmentSchema.safeParse(builtinData) : null;
1068
+ if (builtinParsed?.success && builtinParsed.data.extends) {
1069
+ currentName = builtinParsed.data.extends;
1070
+ continue;
1071
+ }
1072
+ }
1073
+
1074
+ currentName = undefined;
1075
+ }
1076
+
1077
+ return dirs;
1078
+ }
1079
+
1080
+ // Replace {ENV_DIR}[/subpath] occurrences with the first search dir that
1081
+ // actually contains the subpath on disk. If no dir has it, the first search
1082
+ // dir is used (so errors at exec time name a consistent location).
1083
+ export function substituteLayeredEnvDir(input: string, searchDirs: string[]): string {
1084
+ if (searchDirs.length === 0) return input;
1085
+ return input.replace(/\{ENV_DIR\}(?:\/([^\s'"}]+))?/g, (_match, sub?: string) => {
1086
+ if (!sub) return searchDirs[0]!;
1087
+ for (const dir of searchDirs) {
1088
+ const candidate = path.resolve(dir, sub);
1089
+ if (fs.existsSync(candidate)) return candidate;
1090
+ }
1091
+ return path.resolve(searchDirs[0]!, sub);
1092
+ });
1093
+ }
1094
+
1095
+ // Resolve a relative (`./foo` or `../foo`) policy/command path against the
1096
+ // layered search dirs, preferring overlay first. Returns the first match; if
1097
+ // none exists, returns the overlay-dir resolution for a stable error path.
1098
+ export function resolveLayeredRelativePath(relPath: string, searchDirs: string[]): string {
1099
+ if (searchDirs.length === 0) return relPath;
1100
+ for (const dir of searchDirs) {
1101
+ const candidate = path.resolve(dir, relPath);
1102
+ if (fs.existsSync(candidate)) return candidate;
1103
+ }
1104
+ return path.resolve(searchDirs[0]!, relPath);
541
1105
  }
542
1106
 
543
1107
  export async function getActiveEnvironmentInfo(
@@ -579,16 +1143,30 @@ export async function getActiveEnvironmentName(
579
1143
  export async function enableEnvironment(
580
1144
  name: string,
581
1145
  targetPath: string = './',
582
- startDir = process.cwd()
1146
+ startDir = process.cwd(),
1147
+ opts: { fork?: boolean } = {}
583
1148
  ): Promise<void> {
584
1149
  const targetDir = getEnvironmentPath(name, startDir);
585
1150
 
586
- // Copy template to targetDir if it does not already exist
1151
+ // Default: write a minimal overlay (`{extends: name}`) pointing at the
1152
+ // built-in. Fork: clone the whole built-in template directory (legacy).
587
1153
  if (!fs.existsSync(targetDir)) {
588
- await copyEnvironmentTemplate(name, targetDir, startDir);
589
- console.log(`Copied environment template '${name}'.`);
1154
+ if (opts.fork) {
1155
+ await copyEnvironmentTemplate(name, targetDir, startDir);
1156
+ console.log(`Forked environment template '${name}'.`);
1157
+ } else {
1158
+ // Require the built-in to exist so we don't write a dangling overlay.
1159
+ await resolveEnvironmentTemplatePath(name, startDir);
1160
+ await fsPromises.mkdir(targetDir, { recursive: true });
1161
+ await fsPromises.writeFile(
1162
+ path.join(targetDir, 'env.json'),
1163
+ JSON.stringify({ extends: name }, null, 2),
1164
+ 'utf-8'
1165
+ );
1166
+ console.log(`Enabled environment overlay '${name}' (extends built-in).`);
1167
+ }
590
1168
  } else {
591
- console.log(`Environment template '${name}' already exists in workspace.`);
1169
+ console.log(`Environment '${name}' already exists in workspace.`);
592
1170
  }
593
1171
 
594
1172
  const settings = (await readSettings(startDir)) || { chats: { defaultId: '' } };