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,741 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import { TestEnvironment } from '../_helpers/test-environment.js';
5
+ import {
6
+ readEnvironment,
7
+ getEnvironmentSearchDirs,
8
+ substituteLayeredEnvDir,
9
+ readEnvironmentPoliciesForPath,
10
+ writeSettings,
11
+ getAgent,
12
+ getAgentOverlay,
13
+ writeAgentSettings,
14
+ } from '../../src/shared/workspace.js';
15
+
16
+ describe('E2E Auto-update: lite refresh on `up`', () => {
17
+ let env: TestEnvironment;
18
+
19
+ beforeEach(async () => {
20
+ env = new TestEnvironment('e2e-au-lite');
21
+ await env.setup();
22
+ await env.init();
23
+ }, 30000);
24
+
25
+ afterEach(() => env.teardown(), 30000);
26
+
27
+ async function enableMacos() {
28
+ const { code } = await env.runCli(['environments', 'enable', 'macos']);
29
+ expect(code).toBe(0);
30
+ }
31
+
32
+ it('up refreshes clawmini-lite.js for active environments', async () => {
33
+ await enableMacos();
34
+ const { code } = await env.up();
35
+ expect(code).toBe(0);
36
+
37
+ const litePath = path.join(env.e2eDir, '.local', 'bin', 'clawmini-lite.js');
38
+ expect(fs.existsSync(litePath)).toBe(true);
39
+
40
+ const content = fs.readFileSync(litePath, 'utf8');
41
+ expect(content).toContain('clawmini-lite - A standalone client');
42
+ expect(content.startsWith('#!')).toBe(true);
43
+ });
44
+
45
+ it('up skips lite write when content matches (no mtime touch)', async () => {
46
+ await enableMacos();
47
+ await env.up();
48
+ const litePath = path.join(env.e2eDir, '.local', 'bin', 'clawmini-lite.js');
49
+ const firstMtime = fs.statSync(litePath).mtimeMs;
50
+
51
+ // Small wait to ensure mtime resolution could advance if we did rewrite
52
+ await new Promise((r) => setTimeout(r, 50));
53
+
54
+ await env.down();
55
+ await env.up();
56
+ const secondMtime = fs.statSync(litePath).mtimeMs;
57
+ expect(secondMtime).toBe(firstMtime);
58
+ });
59
+
60
+ it('up refuses to overwrite a non-clawmini file at the export path', async () => {
61
+ await enableMacos();
62
+ const litePath = path.join(env.e2eDir, '.local', 'bin', 'clawmini-lite.js');
63
+ fs.mkdirSync(path.dirname(litePath), { recursive: true });
64
+ const sentinel = 'totally not a lite script\n';
65
+ fs.writeFileSync(litePath, sentinel);
66
+
67
+ const { stderr } = await env.up();
68
+ expect(fs.readFileSync(litePath, 'utf8')).toBe(sentinel);
69
+ expect(stderr).toContain('Refusing to overwrite');
70
+ });
71
+
72
+ it('environments enable writes a thin overlay by default', async () => {
73
+ const { code } = await env.runCli(['environments', 'enable', 'macos']);
74
+ expect(code).toBe(0);
75
+ const envDir = env.getClawminiPath('environments', 'macos');
76
+ const envJson = JSON.parse(fs.readFileSync(path.join(envDir, 'env.json'), 'utf8'));
77
+ expect(envJson).toEqual({ extends: 'macos' });
78
+ // Built-in files are not cloned into the overlay dir.
79
+ expect(fs.existsSync(path.join(envDir, 'sandbox.sb'))).toBe(false);
80
+ });
81
+
82
+ it('environments enable --fork clones the built-in template directory', async () => {
83
+ const { code } = await env.runCli(['environments', 'enable', 'macos', '--fork']);
84
+ expect(code).toBe(0);
85
+ const envDir = env.getClawminiPath('environments', 'macos');
86
+ const envJson = JSON.parse(fs.readFileSync(path.join(envDir, 'env.json'), 'utf8'));
87
+ expect(envJson.extends).toBeUndefined();
88
+ expect(envJson.prefix).toContain('sandbox-exec');
89
+ expect(fs.existsSync(path.join(envDir, 'sandbox.sb'))).toBe(true);
90
+ });
91
+
92
+ it('overlay with self-extends resolves through the built-in', async () => {
93
+ const { code } = await env.runCli(['environments', 'enable', 'macos']);
94
+ expect(code).toBe(0);
95
+ const resolved = await readEnvironment('macos', env.e2eDir);
96
+ expect(resolved).toBeTruthy();
97
+ expect(resolved!.prefix).toContain('sandbox-exec');
98
+ expect(resolved!.exportLiteTo).toBe('.local/bin/clawmini-lite.js');
99
+ expect(typeof resolved!.env?.PATH).toBe('string');
100
+ });
101
+
102
+ it('up does not place a default lite shim when no environment is active', async () => {
103
+ const { code } = await env.up();
104
+ expect(code).toBe(0);
105
+
106
+ // Nothing should place clawmini-lite.js anywhere outside .clawmini
107
+ const walk = (dir: string): string[] => {
108
+ const result: string[] = [];
109
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
110
+ if (entry.name === '.clawmini' || entry.name === '.git' || entry.name === 'node_modules') {
111
+ continue;
112
+ }
113
+ const full = path.join(dir, entry.name);
114
+ if (entry.isDirectory()) {
115
+ result.push(...walk(full));
116
+ } else {
117
+ result.push(full);
118
+ }
119
+ }
120
+ return result;
121
+ };
122
+ const files = walk(env.e2eDir);
123
+ const stray = files.filter((p) => path.basename(p) === 'clawmini-lite.js');
124
+ expect(stray).toEqual([]);
125
+ });
126
+ });
127
+
128
+ describe('E2E Auto-update: environment overlay (`extends`)', () => {
129
+ let env: TestEnvironment;
130
+
131
+ beforeEach(async () => {
132
+ env = new TestEnvironment('e2e-au-env');
133
+ await env.setup();
134
+ await env.init();
135
+ }, 30000);
136
+
137
+ afterEach(() => env.teardown(), 30000);
138
+
139
+ function writeOverlayEnv(name: string, data: Record<string, unknown>) {
140
+ const overlayDir = env.getClawminiPath('environments', name);
141
+ fs.mkdirSync(overlayDir, { recursive: true });
142
+ fs.writeFileSync(path.join(overlayDir, 'env.json'), JSON.stringify(data, null, 2));
143
+ return overlayDir;
144
+ }
145
+
146
+ it('environment with extends inherits built-in env.json fields', async () => {
147
+ writeOverlayEnv('my-macos', {
148
+ extends: 'macos',
149
+ env: { MY_VAR: 'v' },
150
+ });
151
+ await writeSettings({ environments: { './': 'my-macos' } }, env.e2eDir);
152
+
153
+ const resolved = await readEnvironment('my-macos', env.e2eDir);
154
+ expect(resolved).toBeTruthy();
155
+ // `prefix` and `exportLiteTo` from the built-in macos flow through.
156
+ expect(resolved!.prefix).toContain('sandbox-exec');
157
+ expect(resolved!.exportLiteTo).toBe('.local/bin/clawmini-lite.js');
158
+ // `env` is deep-merged at one level — MY_VAR added without dropping PATH.
159
+ expect(resolved!.env?.MY_VAR).toBe('v');
160
+ expect(typeof resolved!.env?.PATH).toBe('string');
161
+ });
162
+
163
+ it('{ENV_DIR} resolution falls back to built-in for missing local files', async () => {
164
+ const overlayDir = writeOverlayEnv('my-macos', { extends: 'macos' });
165
+ await writeSettings({ environments: { './': 'my-macos' } }, env.e2eDir);
166
+
167
+ const searchDirs = await getEnvironmentSearchDirs('my-macos', env.e2eDir);
168
+ expect(searchDirs[0]).toBe(overlayDir);
169
+ // The built-in macos template dir should come second.
170
+ const builtinDir = searchDirs.find((d) => d.includes('templates/environments/macos'));
171
+ expect(builtinDir).toBeTruthy();
172
+
173
+ const resolved = substituteLayeredEnvDir('{ENV_DIR}/sandbox.sb', searchDirs);
174
+ expect(resolved).toBe(path.join(builtinDir!, 'sandbox.sb'));
175
+
176
+ // Dropping a local sandbox.sb makes the overlay win.
177
+ fs.writeFileSync(path.join(overlayDir, 'sandbox.sb'), '; local\n');
178
+ const resolvedLocal = substituteLayeredEnvDir('{ENV_DIR}/sandbox.sb', searchDirs);
179
+ expect(resolvedLocal).toBe(path.join(overlayDir, 'sandbox.sb'));
180
+ });
181
+
182
+ it('layered resolution picks overlay policy script when it exists', async () => {
183
+ const overlayDir = writeOverlayEnv('my-cladding', {
184
+ extends: 'cladding',
185
+ policies: {
186
+ 'allowlist-domain': { command: './allowlist-domain.mjs' },
187
+ },
188
+ });
189
+ fs.writeFileSync(path.join(overlayDir, 'allowlist-domain.mjs'), '#!/usr/bin/env node\n');
190
+ await writeSettings({ environments: { './': 'my-cladding' } }, env.e2eDir);
191
+
192
+ const policies = await readEnvironmentPoliciesForPath('./', env.e2eDir);
193
+ expect(policies['allowlist-domain']?.command).toBe(
194
+ path.join(overlayDir, 'allowlist-domain.mjs')
195
+ );
196
+ });
197
+
198
+ it('layered resolution falls back to built-in policy script', async () => {
199
+ writeOverlayEnv('my-cladding', { extends: 'cladding' });
200
+ await writeSettings({ environments: { './': 'my-cladding' } }, env.e2eDir);
201
+
202
+ const policies = await readEnvironmentPoliciesForPath('./', env.e2eDir);
203
+ const cmd = policies['allowlist-domain']?.command;
204
+ expect(cmd).toBeTruthy();
205
+ expect(cmd).toContain('templates/environments/cladding/allowlist-domain.mjs');
206
+ });
207
+ });
208
+
209
+ describe('E2E Auto-update: agent overlay (`extends`)', () => {
210
+ let env: TestEnvironment;
211
+
212
+ beforeEach(async () => {
213
+ env = new TestEnvironment('e2e-au-agent');
214
+ await env.setup();
215
+ await env.init();
216
+ }, 30000);
217
+
218
+ afterEach(() => env.teardown(), 30000);
219
+
220
+ it('agents add --template writes overlay shape with extends', async () => {
221
+ const { code } = await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
222
+ expect(code).toBe(0);
223
+
224
+ const overlay = env.getAgentSettings('bob');
225
+ expect(overlay.extends).toBe('gemini-claw');
226
+ // The overlay should not contain merged-in template fields like
227
+ // `commands`, `apiTokenEnvVar`, etc.
228
+ expect(overlay.commands).toBeUndefined();
229
+ expect(overlay.apiTokenEnvVar).toBeUndefined();
230
+ });
231
+
232
+ it('getAgent merges template fields into local fields', async () => {
233
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
234
+ const resolved = await getAgent('bob', env.e2eDir);
235
+ expect(resolved).toBeTruthy();
236
+ expect(resolved!.apiTokenEnvVar).toBe('GEMINI_CLI_CLAW_API_TOKEN');
237
+ expect(resolved!.commands?.new).toContain('gemini');
238
+ expect(resolved!.skillsDir).toBe('.gemini/skills/');
239
+ expect(resolved!.fallbacks?.length).toBeGreaterThan(0);
240
+ });
241
+
242
+ it('local field overrides template field shallowly', async () => {
243
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
244
+ const overlay = (await getAgentOverlay('bob', env.e2eDir)) || {};
245
+ overlay.commands = { new: 'echo local-command' };
246
+ await writeAgentSettings('bob', overlay, env.e2eDir);
247
+
248
+ const resolved = await getAgent('bob', env.e2eDir);
249
+ expect(resolved!.commands?.new).toBe('echo local-command');
250
+ // Shallow override — template's `append` is also replaced.
251
+ expect(resolved!.commands?.append).toBeUndefined();
252
+ });
253
+
254
+ it('env deep-merges one level', async () => {
255
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
256
+ const overlay = (await getAgentOverlay('bob', env.e2eDir)) || {};
257
+ overlay.env = { MODEL: 'gemini-custom' };
258
+ await writeAgentSettings('bob', overlay, env.e2eDir);
259
+
260
+ const resolved = await getAgent('bob', env.e2eDir);
261
+ expect(resolved!.env?.MODEL).toBe('gemini-custom');
262
+ // Template entries flow through (GEMINI_SYSTEM_MD, GEMINI_API_KEY).
263
+ expect(resolved!.env?.GEMINI_SYSTEM_MD).toBe('true');
264
+ expect(resolved!.env?.GEMINI_API_KEY).toBe(true);
265
+ });
266
+
267
+ it('agents add --fork copies everything and writes no extends', async () => {
268
+ const { code } = await env.runCli([
269
+ 'agents',
270
+ 'add',
271
+ 'bob',
272
+ '--template',
273
+ 'gemini-claw',
274
+ '--fork',
275
+ ]);
276
+ expect(code).toBe(0);
277
+ const overlay = env.getAgentSettings('bob');
278
+ expect(overlay.extends).toBeUndefined();
279
+ // Fork mode merges template fields into the local file.
280
+ expect(overlay.apiTokenEnvVar).toBe('GEMINI_CLI_CLAW_API_TOKEN');
281
+ expect(overlay.skillsDir).toBe('.gemini/skills/');
282
+ });
283
+
284
+ it('agents add --fork records skill SHAs in installed-files.json', async () => {
285
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw', '--fork']);
286
+ const installed = JSON.parse(
287
+ fs.readFileSync(env.getAgentPath('bob', 'installed-files.json'), 'utf8')
288
+ );
289
+ expect(installed.files['.gemini/skills/skill-creator/SKILL.md']).toBeTruthy();
290
+ expect(typeof installed.files['.gemini/skills/skill-creator/SKILL.md'].sha).toBe('string');
291
+ });
292
+
293
+ it('existing agent without extends is unchanged after up when skillsDir is null', async () => {
294
+ // skillsDir: null is the explicit opt-out from the universal skill-track
295
+ // refresh — without it, `up` installs skills into the agent's workdir.
296
+ fs.mkdirSync(path.join(env.e2eDir, 'legacy'), { recursive: true });
297
+ env.writeAgentSettings('legacy', {
298
+ directory: './legacy',
299
+ commands: { new: 'echo legacy' },
300
+ skillsDir: null,
301
+ });
302
+ const before = JSON.stringify(env.getAgentSettings('legacy'));
303
+
304
+ const { code } = await env.up();
305
+ expect(code).toBe(0);
306
+ const after = JSON.stringify(env.getAgentSettings('legacy'));
307
+ expect(after).toBe(before);
308
+ // Confirms the workdir wasn't seeded with skills either.
309
+ expect(fs.existsSync(path.join(env.e2eDir, 'legacy', '.agents'))).toBe(false);
310
+ }, 30000);
311
+
312
+ it('hand-written agent without extends gets skills installed on up', async () => {
313
+ fs.mkdirSync(path.join(env.e2eDir, 'legacy'), { recursive: true });
314
+ env.writeAgentSettings('legacy', {
315
+ directory: './legacy',
316
+ commands: { new: 'echo legacy' },
317
+ });
318
+
319
+ const { code } = await env.up();
320
+ expect(code).toBe(0);
321
+ const skill = path.join(
322
+ env.e2eDir,
323
+ 'legacy',
324
+ '.agents',
325
+ 'skills',
326
+ 'skill-creator',
327
+ 'SKILL.md'
328
+ );
329
+ expect(fs.existsSync(skill)).toBe(true);
330
+ const installed = JSON.parse(
331
+ fs.readFileSync(env.getAgentPath('legacy', 'installed-files.json'), 'utf8')
332
+ );
333
+ expect(installed.files['.agents/skills/skill-creator/SKILL.md']).toBeTruthy();
334
+ }, 30000);
335
+
336
+ it('template.json and settings.json are not copied into the agent workdir', async () => {
337
+ const { code } = await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
338
+ expect(code).toBe(0);
339
+ expect(fs.existsSync(path.join(env.e2eDir, 'bob', 'settings.json'))).toBe(false);
340
+ expect(fs.existsSync(path.join(env.e2eDir, 'bob', 'template.json'))).toBe(false);
341
+ });
342
+
343
+ it('agents add refuses to overwrite a colliding file without --force', async () => {
344
+ const workdir = path.join(env.e2eDir, 'bob');
345
+ fs.mkdirSync(path.join(workdir, '.gemini'), { recursive: true });
346
+ const systemPath = path.join(workdir, '.gemini', 'system.md');
347
+ fs.writeFileSync(systemPath, 'user content');
348
+
349
+ const { code, stderr } = await env.runCli([
350
+ 'agents',
351
+ 'add',
352
+ 'bob',
353
+ '--template',
354
+ 'gemini-claw',
355
+ ]);
356
+ expect(code).not.toBe(0);
357
+ expect(stderr).toContain('.gemini/system.md');
358
+ expect(stderr).toContain('--force');
359
+ expect(fs.readFileSync(systemPath, 'utf-8')).toBe('user content');
360
+ });
361
+
362
+ it('agents add --force overwrites colliding files', async () => {
363
+ const workdir = path.join(env.e2eDir, 'bob');
364
+ fs.mkdirSync(path.join(workdir, '.gemini'), { recursive: true });
365
+ const systemPath = path.join(workdir, '.gemini', 'system.md');
366
+ fs.writeFileSync(systemPath, 'user content');
367
+
368
+ const { code } = await env.runCli([
369
+ 'agents',
370
+ 'add',
371
+ 'bob',
372
+ '--template',
373
+ 'gemini-claw',
374
+ '--force',
375
+ ]);
376
+ expect(code).toBe(0);
377
+ expect(fs.readFileSync(systemPath, 'utf-8')).not.toBe('user content');
378
+ });
379
+
380
+ it('agents add leaves unrelated workdir files alone', async () => {
381
+ const workdir = path.join(env.e2eDir, 'bob');
382
+ fs.mkdirSync(workdir, { recursive: true });
383
+ const unrelated = path.join(workdir, 'README.md');
384
+ fs.writeFileSync(unrelated, 'project readme');
385
+
386
+ const { code } = await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
387
+ expect(code).toBe(0);
388
+ expect(fs.readFileSync(unrelated, 'utf-8')).toBe('project readme');
389
+ });
390
+
391
+ it('skillsDir: null opts out of skill refresh on up', async () => {
392
+ const { code: addCode } = await env.runCli([
393
+ 'agents',
394
+ 'add',
395
+ 'bob',
396
+ '--template',
397
+ 'gemini-claw',
398
+ ]);
399
+ expect(addCode).toBe(0);
400
+ const skillsDir = path.join(env.e2eDir, 'bob', '.gemini', 'skills');
401
+ expect(fs.existsSync(skillsDir)).toBe(true);
402
+
403
+ // User opts out and removes the installed skills.
404
+ const overlay = (await getAgentOverlay('bob', env.e2eDir)) || {};
405
+ overlay.skillsDir = null;
406
+ await writeAgentSettings('bob', overlay, env.e2eDir);
407
+ fs.rmSync(skillsDir, { recursive: true, force: true });
408
+
409
+ const { code: upCode } = await env.up();
410
+ expect(upCode).toBe(0);
411
+ // Skills directory stays removed — refresh skipped the opted-out agent.
412
+ expect(fs.existsSync(skillsDir)).toBe(false);
413
+ }, 30000);
414
+ });
415
+
416
+ describe('E2E Auto-update: template.json + SHA tracking', () => {
417
+ let env: TestEnvironment;
418
+
419
+ beforeEach(async () => {
420
+ env = new TestEnvironment('e2e-au-sha');
421
+ await env.setup();
422
+ await env.init();
423
+ }, 30000);
424
+
425
+ afterEach(() => env.teardown(), 30000);
426
+
427
+ it('new agent with --template records SHAs and creates installed-files.json', async () => {
428
+ const { code } = await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
429
+ expect(code).toBe(0);
430
+
431
+ const installedPath = env.getAgentPath('bob', 'installed-files.json');
432
+ expect(fs.existsSync(installedPath)).toBe(true);
433
+ const installed = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
434
+ // Manifest declares GEMINI.md as track; an entry must exist.
435
+ expect(installed.files['GEMINI.md']).toBeTruthy();
436
+ expect(typeof installed.files['GEMINI.md'].sha).toBe('string');
437
+ // Seed-once files are also recorded at first install.
438
+ expect(installed.files['SOUL.md']).toBeTruthy();
439
+ });
440
+
441
+ it('recorded SHAs match the template content at create time', async () => {
442
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
443
+
444
+ const installedPath = env.getAgentPath('bob', 'installed-files.json');
445
+ const installed = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
446
+
447
+ const workspaceRoot = path.resolve(__dirname, '..', '..');
448
+ const templateGemini = path.join(workspaceRoot, 'templates', 'gemini-claw', 'GEMINI.md');
449
+ const templateContent = fs.readFileSync(templateGemini);
450
+ const { createHash } = await import('node:crypto');
451
+ const expected = createHash('sha256').update(templateContent).digest('hex');
452
+ expect(installed.files['GEMINI.md'].sha).toBe(expected);
453
+ });
454
+
455
+ it('up does not copy template.json to the destination', async () => {
456
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
457
+ expect(fs.existsSync(path.join(env.e2eDir, 'bob', 'template.json'))).toBe(false);
458
+ const { code } = await env.up();
459
+ expect(code).toBe(0);
460
+ expect(fs.existsSync(path.join(env.e2eDir, 'bob', 'template.json'))).toBe(false);
461
+ });
462
+
463
+ it('directory entries in the manifest apply recursively', async () => {
464
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
465
+ const installedPath = env.getAgentPath('bob', 'installed-files.json');
466
+ const installed = JSON.parse(fs.readFileSync(installedPath, 'utf8'));
467
+ // Everything under .gemini/ is 'track' per the manifest.
468
+ const gemini = Object.keys(installed.files).filter((k) => k.startsWith('.gemini/'));
469
+ expect(gemini.length).toBeGreaterThan(0);
470
+ });
471
+ });
472
+
473
+ describe('E2E Auto-update: agent refresh on `up`', () => {
474
+ let env: TestEnvironment;
475
+
476
+ beforeEach(async () => {
477
+ env = new TestEnvironment('e2e-au-up');
478
+ await env.setup();
479
+ await env.init();
480
+ }, 30000);
481
+
482
+ afterEach(() => env.teardown(), 30000);
483
+
484
+ it('up skips a track file when the agent edited it', async () => {
485
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
486
+ const tracked = path.join(env.e2eDir, 'bob', '.gemini', 'system.md');
487
+ const original = fs.readFileSync(tracked, 'utf8');
488
+ fs.writeFileSync(tracked, original + '\n<!-- agent edit -->');
489
+
490
+ const { stderr, code } = await env.up();
491
+ expect(code).toBe(0);
492
+ expect(stderr).toContain('differs from template');
493
+ expect(fs.readFileSync(tracked, 'utf8')).toContain('<!-- agent edit -->');
494
+ });
495
+
496
+ it('up never touches seed-once files', async () => {
497
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
498
+ const memory = path.join(env.e2eDir, 'bob', 'MEMORY.md');
499
+ fs.writeFileSync(memory, 'agent memory content\n');
500
+ const { code } = await env.up();
501
+ expect(code).toBe(0);
502
+ expect(fs.readFileSync(memory, 'utf8')).toBe('agent memory content\n');
503
+ });
504
+
505
+ it('up never touches files outside the manifest in the agent dir', async () => {
506
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
507
+ const extra = path.join(env.e2eDir, 'bob', 'agent-notes.txt');
508
+ fs.writeFileSync(extra, 'notes\n');
509
+ const { code } = await env.up();
510
+ expect(code).toBe(0);
511
+ expect(fs.readFileSync(extra, 'utf8')).toBe('notes\n');
512
+ });
513
+
514
+ it('up re-creates a track file that was deleted', async () => {
515
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
516
+ const tracked = path.join(env.e2eDir, 'bob', '.gemini', 'system.md');
517
+ fs.unlinkSync(tracked);
518
+
519
+ const { code } = await env.up();
520
+ expect(code).toBe(0);
521
+ expect(fs.existsSync(tracked)).toBe(true);
522
+ });
523
+
524
+ it('up does not re-create a seed-once file that was deleted', async () => {
525
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
526
+ const bootstrap = path.join(env.e2eDir, 'bob', 'BOOTSTRAP.md');
527
+ expect(fs.existsSync(bootstrap)).toBe(true);
528
+ fs.unlinkSync(bootstrap);
529
+
530
+ const { code } = await env.up();
531
+ expect(code).toBe(0);
532
+ expect(fs.existsSync(bootstrap)).toBe(false);
533
+ });
534
+
535
+ it('agents refresh --accept overwrites diverged files', async () => {
536
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
537
+ const tracked = path.join(env.e2eDir, 'bob', '.gemini', 'system.md');
538
+ fs.writeFileSync(tracked, 'diverged content\n');
539
+
540
+ const { code, stdout } = await env.runCli(['agents', 'refresh', 'bob', '--accept']);
541
+ expect(code).toBe(0);
542
+ expect(stdout).toContain('refresh');
543
+ expect(fs.readFileSync(tracked, 'utf8')).not.toBe('diverged content\n');
544
+ });
545
+
546
+ it('agents refresh scoped to a single agent leaves other agents alone', async () => {
547
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
548
+ await env.runCli(['agents', 'add', 'alice', '--template', 'gemini-claw']);
549
+
550
+ const aliceGemini = path.join(env.e2eDir, 'alice', 'GEMINI.md');
551
+ fs.writeFileSync(aliceGemini, 'alice edited\n');
552
+
553
+ const { code } = await env.runCli(['agents', 'refresh', 'bob', '--accept']);
554
+ expect(code).toBe(0);
555
+ expect(fs.readFileSync(aliceGemini, 'utf8')).toBe('alice edited\n');
556
+ });
557
+
558
+ it('pre-existing agent without recorded SHAs gets strict skip + hint', async () => {
559
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
560
+ // Simulate an old workspace: remove installed-files.json.
561
+ fs.unlinkSync(env.getAgentPath('bob', 'installed-files.json'));
562
+
563
+ const { stderr, code } = await env.up();
564
+ expect(code).toBe(0);
565
+ expect(stderr).toContain('no recorded SHA');
566
+ });
567
+ });
568
+
569
+ describe('E2E Auto-update: skills refresh on `up`', () => {
570
+ let env: TestEnvironment;
571
+
572
+ beforeEach(async () => {
573
+ env = new TestEnvironment('e2e-au-skill');
574
+ await env.setup();
575
+ await env.init();
576
+ }, 30000);
577
+
578
+ afterEach(() => env.teardown(), 30000);
579
+
580
+ function skillPath(agentId: string, skillName: string, file: string): string {
581
+ // gemini-claw's skillsDir is `.gemini/skills/`
582
+ return path.join(env.e2eDir, agentId, '.gemini', 'skills', skillName, file);
583
+ }
584
+
585
+ it('agents add --template installs skills and records SHAs under full paths', async () => {
586
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
587
+ const skill = skillPath('bob', 'skill-creator', 'SKILL.md');
588
+ expect(fs.existsSync(skill)).toBe(true);
589
+
590
+ const installed = JSON.parse(
591
+ fs.readFileSync(env.getAgentPath('bob', 'installed-files.json'), 'utf8')
592
+ );
593
+ expect(installed.files['.gemini/skills/skill-creator/SKILL.md']).toBeTruthy();
594
+ });
595
+
596
+ it('up refreshes all skill files when no template.json is present in the skill', async () => {
597
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
598
+ const skill = skillPath('bob', 'skill-creator', 'SKILL.md');
599
+ // Delete the file; up should re-seed it (track default).
600
+ fs.unlinkSync(skill);
601
+ const { code } = await env.up();
602
+ expect(code).toBe(0);
603
+ expect(fs.existsSync(skill)).toBe(true);
604
+ });
605
+
606
+ it('up skips diverged skill files', async () => {
607
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
608
+ const skill = skillPath('bob', 'skill-creator', 'SKILL.md');
609
+ fs.writeFileSync(skill, 'edited skill content\n');
610
+
611
+ const { stderr, code } = await env.up();
612
+ expect(code).toBe(0);
613
+ expect(stderr).toContain('differs from template');
614
+ expect(fs.readFileSync(skill, 'utf8')).toBe('edited skill content\n');
615
+ });
616
+
617
+ it('agents refresh --accept overwrites diverged skill files', async () => {
618
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
619
+ const skill = skillPath('bob', 'skill-creator', 'SKILL.md');
620
+ fs.writeFileSync(skill, 'edited\n');
621
+
622
+ const { code } = await env.runCli(['agents', 'refresh', 'bob', '--accept']);
623
+ expect(code).toBe(0);
624
+ expect(fs.readFileSync(skill, 'utf8')).not.toBe('edited\n');
625
+ });
626
+
627
+ it('skill template.json is never copied to the destination', async () => {
628
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
629
+ const tmpl = skillPath('bob', 'skill-creator', 'template.json');
630
+ expect(fs.existsSync(tmpl)).toBe(false);
631
+ });
632
+ });
633
+
634
+ describe('E2E Auto-update: --dry-run', () => {
635
+ let env: TestEnvironment;
636
+
637
+ beforeEach(async () => {
638
+ env = new TestEnvironment('e2e-au-dry');
639
+ await env.setup();
640
+ await env.init();
641
+ }, 30000);
642
+
643
+ afterEach(() => env.teardown(), 30000);
644
+
645
+ it('up --dry-run prints a plan and writes nothing', async () => {
646
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
647
+ // Divergence + unchanged mix.
648
+ const gemini = path.join(env.e2eDir, 'bob', 'GEMINI.md');
649
+ fs.writeFileSync(gemini, 'edited\n');
650
+
651
+ const installedBefore = fs.readFileSync(
652
+ env.getAgentPath('bob', 'installed-files.json'),
653
+ 'utf8'
654
+ );
655
+
656
+ const { stdout, code } = await env.runCli(['up', '--dry-run']);
657
+ expect(code).toBe(0);
658
+ expect(stdout).toMatch(/\[bob\]/);
659
+
660
+ // Nothing written back.
661
+ expect(fs.readFileSync(gemini, 'utf8')).toBe('edited\n');
662
+ const installedAfter = fs.readFileSync(env.getAgentPath('bob', 'installed-files.json'), 'utf8');
663
+ expect(installedAfter).toBe(installedBefore);
664
+ });
665
+
666
+ it('agents refresh --dry-run prints scoped plan and writes nothing', async () => {
667
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
668
+ await env.runCli(['agents', 'add', 'alice', '--template', 'gemini-claw']);
669
+ const aliceGemini = path.join(env.e2eDir, 'alice', 'GEMINI.md');
670
+ fs.writeFileSync(aliceGemini, 'alice edit\n');
671
+ const bobGemini = path.join(env.e2eDir, 'bob', 'GEMINI.md');
672
+ fs.writeFileSync(bobGemini, 'bob edit\n');
673
+
674
+ const { stdout, code } = await env.runCli(['agents', 'refresh', 'bob', '--dry-run']);
675
+ expect(code).toBe(0);
676
+ // Output has entries (unchanged/diverged/seed-once).
677
+ expect(stdout.length).toBeGreaterThan(0);
678
+ // Neither file mutated.
679
+ expect(fs.readFileSync(bobGemini, 'utf8')).toBe('bob edit\n');
680
+ expect(fs.readFileSync(aliceGemini, 'utf8')).toBe('alice edit\n');
681
+ });
682
+
683
+ it('up --dry-run reports skill file changes for an extends-template agent', async () => {
684
+ await env.runCli(['agents', 'add', 'bob', '--template', 'gemini-claw']);
685
+ const skill = path.join(
686
+ env.e2eDir,
687
+ 'bob',
688
+ '.gemini',
689
+ 'skills',
690
+ 'skill-creator',
691
+ 'SKILL.md'
692
+ );
693
+ fs.writeFileSync(skill, 'edited skill\n');
694
+
695
+ const { stdout, code } = await env.runCli(['up', '--dry-run']);
696
+ expect(code).toBe(0);
697
+ expect(stdout).toContain('[bob] diverged .gemini/skills/skill-creator/SKILL.md');
698
+ // Nothing written back.
699
+ expect(fs.readFileSync(skill, 'utf8')).toBe('edited skill\n');
700
+ });
701
+
702
+ it('up --dry-run reports skill file changes for a custom (non-extends) agent', async () => {
703
+ // Forked agents have no `extends`, but skills are still tracked: dry-run
704
+ // must report skill plan actions for them just like extends agents.
705
+ await env.runCli(['agents', 'add', 'custom', '--template', 'gemini-claw', '--fork']);
706
+ const overlay = env.getAgentSettings('custom');
707
+ expect(overlay.extends).toBeUndefined();
708
+ const skill = path.join(
709
+ env.e2eDir,
710
+ 'custom',
711
+ '.gemini',
712
+ 'skills',
713
+ 'skill-creator',
714
+ 'SKILL.md'
715
+ );
716
+ expect(fs.existsSync(skill)).toBe(true);
717
+ fs.writeFileSync(skill, 'edited skill\n');
718
+
719
+ const { stdout, code } = await env.runCli(['up', '--dry-run']);
720
+ expect(code).toBe(0);
721
+ expect(stdout).toContain('[custom] diverged .gemini/skills/skill-creator/SKILL.md');
722
+ expect(fs.readFileSync(skill, 'utf8')).toBe('edited skill\n');
723
+ });
724
+
725
+ it('up --dry-run skips skill changes for a custom agent with skillsDir: null', async () => {
726
+ await env.runCli(['agents', 'add', 'custom', '--template', 'gemini-claw', '--fork']);
727
+ const skillsDir = path.join(env.e2eDir, 'custom', '.gemini', 'skills');
728
+ expect(fs.existsSync(skillsDir)).toBe(true);
729
+ // User opts out and removes the installed skills.
730
+ env.updateAgentSettings('custom', { skillsDir: null });
731
+ fs.rmSync(skillsDir, { recursive: true, force: true });
732
+
733
+ const { stdout, code } = await env.runCli(['up', '--dry-run']);
734
+ expect(code).toBe(0);
735
+ expect(stdout).not.toContain('[custom]');
736
+ expect(stdout).not.toContain('skill-creator');
737
+ expect(stdout).toContain('Dry run: no agents to refresh.');
738
+ // And the skills directory remains absent — dry-run wrote nothing.
739
+ expect(fs.existsSync(skillsDir)).toBe(false);
740
+ });
741
+ });