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
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {
5
+ TestEnvironment,
6
+ type ChatMessage,
7
+ type SystemMessage,
8
+ } from '../_helpers/test-environment.js';
9
+
10
+ async function waitUntil(
11
+ cond: () => boolean,
12
+ timeoutMs: number,
13
+ intervalMs = 100
14
+ ): Promise<boolean> {
15
+ const deadline = Date.now() + timeoutMs;
16
+ while (Date.now() < deadline) {
17
+ if (cond()) return true;
18
+ await new Promise((r) => setTimeout(r, intervalMs));
19
+ }
20
+ return cond();
21
+ }
22
+
23
+ function readPackageVersion(): string {
24
+ const pkgPath = path.resolve(__dirname, '../../package.json');
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string };
26
+ return pkg.version;
27
+ }
28
+
29
+ function isSystemReplyContaining(text: string) {
30
+ return (m: ChatMessage): m is SystemMessage =>
31
+ m.role === 'system' && typeof m.content === 'string' && m.content.includes(text);
32
+ }
33
+
34
+ // Distinct from slash-restart.test.ts which only inspects the chat log on
35
+ // disk. The pending-replies → drainPendingReplies path is justified by the
36
+ // claim that adapters reconnect to the tRPC subscription with a
37
+ // lastMessageId cursor and replay the post-restart SystemMessage. This test
38
+ // exercises that path end-to-end through tRPC.
39
+ describe('/restart adapter delivery e2e', () => {
40
+ let env: TestEnvironment;
41
+
42
+ beforeAll(async () => {
43
+ env = new TestEnvironment('e2e-slash-restart-adapter');
44
+ await env.setup();
45
+ await env.init();
46
+ }, 30000);
47
+
48
+ afterAll(async () => {
49
+ await env.teardown();
50
+ }, 30000);
51
+
52
+ it('replays the post-restart SystemMessage to a tRPC subscriber that reconnects with lastMessageId', async () => {
53
+ const { code } = await env.runCli(['serve', '--detach']);
54
+ expect(code).toBe(0);
55
+
56
+ const socketPath = env.getClawminiPath('daemon.sock');
57
+ expect(await waitUntil(() => fs.existsSync(socketPath), 10000)).toBe(true);
58
+
59
+ // Subscribe BEFORE sending /restart so we get the ack on the live stream.
60
+ const sub1 = await env.connect('default');
61
+
62
+ await env.sendMessage('/restart', { noWait: true });
63
+
64
+ const ack = await sub1.waitForMessage(
65
+ isSystemReplyContaining('Restarting clawmini...'),
66
+ 15000
67
+ );
68
+ expect(ack.id).toBeTruthy();
69
+
70
+ // Drop the first subscription before the daemon goes down so we
71
+ // explicitly model an adapter that reconnected. The httpSubscriptionLink
72
+ // would otherwise try to auto-reconnect on its own and cloud the test.
73
+ await sub1.disconnect();
74
+
75
+ // Wait for the daemon to actually restart (socket disappears, then comes
76
+ // back). Old daemon dies → socket gone; supervisor's restartService spins
77
+ // up a new daemon that re-binds the socket.
78
+ expect(await waitUntil(() => !fs.existsSync(socketPath), 30000)).toBe(true);
79
+ expect(await waitUntil(() => fs.existsSync(socketPath), 60000)).toBe(true);
80
+
81
+ // Reconnect with the lastMessageId cursor — exactly what the adapter
82
+ // forwarder does when it re-establishes its subscription. The catch-up
83
+ // path in waitForMessages should yield the post-restart SystemMessage
84
+ // that drainPendingReplies appended on daemon startup.
85
+ const sub2 = await env.connect('default', { lastMessageId: ack.id });
86
+
87
+ const expectedVersion = readPackageVersion();
88
+ const restartedMsg = await sub2.waitForMessage(
89
+ isSystemReplyContaining(`Clawmini restarted (v${expectedVersion}).`),
90
+ 30000
91
+ );
92
+ expect(restartedMsg.role).toBe('system');
93
+
94
+ await sub2.disconnect();
95
+ }, 120000);
96
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { TestEnvironment } from '../_helpers/test-environment.js';
5
+
6
+ async function waitUntil(
7
+ cond: () => boolean,
8
+ timeoutMs: number,
9
+ intervalMs = 100
10
+ ): Promise<boolean> {
11
+ const deadline = Date.now() + timeoutMs;
12
+ while (Date.now() < deadline) {
13
+ if (cond()) return true;
14
+ await new Promise((r) => setTimeout(r, intervalMs));
15
+ }
16
+ return cond();
17
+ }
18
+
19
+ interface LogLine {
20
+ role?: string;
21
+ content?: string;
22
+ }
23
+
24
+ function readChatLog(filePath: string): LogLine[] {
25
+ if (!fs.existsSync(filePath)) return [];
26
+ return fs
27
+ .readFileSync(filePath, 'utf-8')
28
+ .split('\n')
29
+ .filter((line) => line.trim().length > 0)
30
+ .map((line) => {
31
+ try {
32
+ return JSON.parse(line) as LogLine;
33
+ } catch {
34
+ return {};
35
+ }
36
+ });
37
+ }
38
+
39
+ function readPid(pidPath: string): string {
40
+ return fs.readFileSync(pidPath, 'utf-8').trim().split(':')[0] ?? '';
41
+ }
42
+
43
+ function readPackageVersion(): string {
44
+ const pkgPath = path.resolve(__dirname, '../../package.json');
45
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string };
46
+ return pkg.version;
47
+ }
48
+
49
+ describe('/restart router e2e', () => {
50
+ let env: TestEnvironment;
51
+
52
+ beforeAll(async () => {
53
+ env = new TestEnvironment('e2e-slash-restart');
54
+ await env.setup();
55
+ await env.init();
56
+ }, 30000);
57
+
58
+ afterAll(async () => {
59
+ await env.teardown();
60
+ }, 30000);
61
+
62
+ it('restarts the daemon and posts a "restarted vX.Y.Z" reply', async () => {
63
+ const { code } = await env.runCli(['serve', '--detach']);
64
+ expect(code).toBe(0);
65
+
66
+ const pidPath = env.getClawminiPath('supervisor.pid');
67
+ const socketPath = env.getClawminiPath('daemon.sock');
68
+ expect(await waitUntil(() => fs.existsSync(pidPath), 5000)).toBe(true);
69
+ expect(await waitUntil(() => fs.existsSync(socketPath), 10000)).toBe(true);
70
+
71
+ const supervisorPidBefore = readPid(pidPath);
72
+ expect(supervisorPidBefore).toMatch(/^\d+$/);
73
+
74
+ await env.sendMessage('/restart', { noWait: true });
75
+
76
+ // The supervisor's pid file is preserved across a daemon-only restart.
77
+ // The daemon socket disappears (old daemon dies) and reappears (new
78
+ // daemon binds). Watch for the disappearance + reappearance to confirm
79
+ // we actually restarted, then check the post-restart reply landed.
80
+ const chatLog = env.getChatPath('default', 'chat.jsonl');
81
+ const expectedVersion = readPackageVersion();
82
+
83
+ const sawPostRestart = await waitUntil(() => {
84
+ const lines = readChatLog(chatLog);
85
+ return lines.some(
86
+ (l) =>
87
+ l.role === 'system' &&
88
+ typeof l.content === 'string' &&
89
+ l.content === `Clawmini restarted (v${expectedVersion}).`
90
+ );
91
+ }, 45000);
92
+ expect(sawPostRestart).toBe(true);
93
+
94
+ // Pre-restart ack should also be present.
95
+ const lines = readChatLog(chatLog);
96
+ expect(
97
+ lines.some(
98
+ (l) => typeof l.content === 'string' && l.content.includes('Restarting clawmini...')
99
+ )
100
+ ).toBe(true);
101
+
102
+ // Supervisor pid must be unchanged — only the child processes (daemon
103
+ // plus any adapters) were bounced.
104
+ expect(fs.existsSync(pidPath)).toBe(true);
105
+ expect(readPid(pidPath)).toBe(supervisorPidBefore);
106
+
107
+ // Daemon socket should be live again.
108
+ expect(fs.existsSync(socketPath)).toBe(true);
109
+
110
+ // Pending replies file must be drained.
111
+ const pendingPath = env.getClawminiPath('pending-replies.json');
112
+ expect(fs.existsSync(pendingPath)).toBe(false);
113
+ }, 90000);
114
+ });
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import { TestEnvironment } from '../_helpers/test-environment.js';
4
+
5
+ async function waitUntil(
6
+ cond: () => boolean,
7
+ timeoutMs: number,
8
+ intervalMs = 100
9
+ ): Promise<boolean> {
10
+ const deadline = Date.now() + timeoutMs;
11
+ while (Date.now() < deadline) {
12
+ if (cond()) return true;
13
+ await new Promise((r) => setTimeout(r, intervalMs));
14
+ }
15
+ return cond();
16
+ }
17
+
18
+ describe('/shutdown router e2e', () => {
19
+ let env: TestEnvironment;
20
+
21
+ beforeAll(async () => {
22
+ env = new TestEnvironment('e2e-slash-shutdown');
23
+ await env.setup();
24
+ await env.init();
25
+ }, 30000);
26
+
27
+ afterAll(async () => {
28
+ await env.teardown();
29
+ }, 30000);
30
+
31
+ it('tears down the supervisor when /shutdown is sent', async () => {
32
+ const { code } = await env.runCli(['serve', '--detach']);
33
+ expect(code).toBe(0);
34
+
35
+ const pidPath = env.getClawminiPath('supervisor.pid');
36
+ const socketPath = env.getClawminiPath('daemon.sock');
37
+ const controlPath = env.getClawminiPath('super.sock');
38
+ expect(await waitUntil(() => fs.existsSync(pidPath), 5000)).toBe(true);
39
+ expect(await waitUntil(() => fs.existsSync(socketPath), 10000)).toBe(true);
40
+ expect(await waitUntil(() => fs.existsSync(controlPath), 10000)).toBe(true);
41
+
42
+ await env.sendMessage('/shutdown', { noWait: true });
43
+
44
+ // The supervisor exits asynchronously after ack — wait for it to clean up.
45
+ expect(await waitUntil(() => !fs.existsSync(pidPath), 30000)).toBe(true);
46
+ expect(await waitUntil(() => !fs.existsSync(socketPath), 30000)).toBe(true);
47
+ expect(await waitUntil(() => !fs.existsSync(controlPath), 30000)).toBe(true);
48
+
49
+ // The synthetic ack should have been recorded in the chat log before the
50
+ // daemon went away.
51
+ const chatLog = env.getChatPath('default', 'chat.jsonl');
52
+ const content = fs.readFileSync(chatLog, 'utf-8');
53
+ expect(content).toContain('Shutting down clawmini supervisor');
54
+ }, 60000);
55
+ });
@@ -0,0 +1,232 @@
1
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import {
6
+ TestEnvironment,
7
+ type ChatSubscription,
8
+ commandWith,
9
+ } from '../_helpers/test-environment.js';
10
+
11
+ function pidIsAlive(pid: number): boolean {
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ // Polls for a PID file written by the spawned shell. Returns the PID once
21
+ // the file exists and contains a valid integer (avoids a flush race between
22
+ // `echo $$ > pid` and our read).
23
+ async function waitForPidFile(filePath: string, timeoutMs: number): Promise<number> {
24
+ const start = Date.now();
25
+ while (Date.now() - start < timeoutMs) {
26
+ if (fs.existsSync(filePath)) {
27
+ const content = fs.readFileSync(filePath, 'utf8').trim();
28
+ if (/^\d+$/.test(content)) return parseInt(content, 10);
29
+ }
30
+ await new Promise((r) => setTimeout(r, 100));
31
+ }
32
+ throw new Error(`Timed out waiting for pid file ${filePath}`);
33
+ }
34
+
35
+ describe('/stop Router E2E', () => {
36
+ let env: TestEnvironment;
37
+ let chat: ChatSubscription | undefined;
38
+ const tmpDirs: string[] = [];
39
+
40
+ beforeAll(async () => {
41
+ env = new TestEnvironment('e2e-slash-stop');
42
+ await env.setup();
43
+ await env.setupSubagentEnv();
44
+ }, 30000);
45
+
46
+ afterAll(async () => {
47
+ for (const dir of tmpDirs) {
48
+ fs.rmSync(dir, { recursive: true, force: true });
49
+ }
50
+ await env.teardown();
51
+ }, 30000);
52
+ afterEach(() => env.disconnectAll());
53
+
54
+ function makeTmp(label: string): string {
55
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `claw-stop-${label}-`));
56
+ tmpDirs.push(dir);
57
+ return dir;
58
+ }
59
+
60
+ it('aborts the in-flight task and suppresses its command log', async () => {
61
+ await env.runCli(['agents', 'add', 'stop-agent']);
62
+ // Output lands only after the sleep, so if /stop aborts correctly, DONE
63
+ // must never appear in the chat.
64
+ env.writeAgentSettings('stop-agent', {
65
+ commands: { new: 'sleep 3 && echo DONE' },
66
+ });
67
+
68
+ await env.addChat('stop-chat', 'stop-agent');
69
+ chat = await env.connect('stop-chat');
70
+
71
+ await env.sendMessage('long', { chat: 'stop-chat', noWait: true });
72
+ // Give the scheduler a moment to dispatch into runCommand/spawn.
73
+ await new Promise((r) => setTimeout(r, 500));
74
+
75
+ await env.sendMessage('/stop', { chat: 'stop-chat', noWait: true });
76
+
77
+ // The slash-stop router replies with this synthetic message before aborting.
78
+ await chat.waitForMessage(
79
+ (m) => typeof m.content === 'string' && m.content.includes('Stopping current task...'),
80
+ 5000
81
+ );
82
+
83
+ // Wait past the original sleep window so any surviving task would have
84
+ // produced output by now.
85
+ await new Promise((r) => setTimeout(r, 3500));
86
+ const leaked = chat.messageBuffer.some(commandWith('DONE'));
87
+ expect(leaked).toBe(false);
88
+ }, 20000);
89
+
90
+ it('leaves the session usable for subsequent messages', async () => {
91
+ await env.runCli(['agents', 'add', 'stop-recovery-agent']);
92
+ env.writeAgentSettings('stop-recovery-agent', {
93
+ commands: { new: 'sleep 3 && echo DONE' },
94
+ });
95
+
96
+ await env.addChat('stop-recovery-chat', 'stop-recovery-agent');
97
+ chat = await env.connect('stop-recovery-chat');
98
+
99
+ await env.sendMessage('long', { chat: 'stop-recovery-chat', noWait: true });
100
+ await new Promise((r) => setTimeout(r, 500));
101
+ await env.sendMessage('/stop', { chat: 'stop-recovery-chat', noWait: true });
102
+ await chat.waitForMessage(
103
+ (m) => typeof m.content === 'string' && m.content.includes('Stopping current task...'),
104
+ 5000
105
+ );
106
+
107
+ // Swap in a fast-returning command so the follow-up test completes quickly.
108
+ env.writeAgentSettings('stop-recovery-agent', {
109
+ commands: { new: 'echo RECOVERED' },
110
+ });
111
+
112
+ await env.sendMessage('after', { chat: 'stop-recovery-chat' });
113
+ const ok = await chat.waitForMessage(commandWith('RECOVERED'), 10000);
114
+ expect(ok.exitCode).toBe(0);
115
+ }, 25000);
116
+
117
+ // The subagent tests below use file-based markers (a pid file written
118
+ // before sleep, a leak file touched after sleep) instead of relying on
119
+ // chat-buffer absence. This addresses three weak signals in the older
120
+ // version of this test:
121
+ // 1. Subagent might never have started — a no-op spawn would pass
122
+ // trivially. waitForPidFile asserts the subagent actually ran.
123
+ // 2. Output suppression for unrelated reasons could mask a live
124
+ // subagent — the leak file is written by the OS regardless of
125
+ // whether output is plumbed back to the chat.
126
+ // 3. A 500ms dispatch + 3.5s wait for a 3s sleep gives only ~500ms
127
+ // margin under load. The 5s sleep + 8s wait gives 3s of slack and
128
+ // uses a pid-file gate instead of a fixed pre-/stop sleep.
129
+ // pidIsAlive is a corroborating signal — kernels reap killed shells fast
130
+ // enough that within 8s `process.kill(pid, 0)` reliably throws ESRCH.
131
+
132
+ it('aborts an active async subagent: process dies and leak marker is never written', async () => {
133
+ const tmp = makeTmp('async');
134
+ await env.addChat('stop-sub-async', 'debug-agent');
135
+ chat = await env.connect('stop-sub-async');
136
+
137
+ const cmd = `echo $$ > ${tmp}/pid && sleep 5 && touch ${tmp}/leaked`;
138
+ await env.sendMessage(`clawmini-lite.js subagents spawn --async '${cmd}'`, {
139
+ chat: 'stop-sub-async',
140
+ agent: 'debug-agent',
141
+ });
142
+
143
+ const pid = await waitForPidFile(path.join(tmp, 'pid'), 10000);
144
+ expect(pidIsAlive(pid)).toBe(true);
145
+
146
+ await env.sendMessage('/stop', { chat: 'stop-sub-async', noWait: true });
147
+ await chat.waitForMessage(
148
+ (m) => typeof m.content === 'string' && m.content.includes('Stopping current task...'),
149
+ 5000
150
+ );
151
+
152
+ await new Promise((r) => setTimeout(r, 8000));
153
+ expect(fs.existsSync(path.join(tmp, 'leaked'))).toBe(false);
154
+ expect(pidIsAlive(pid)).toBe(false);
155
+ }, 30000);
156
+
157
+ it('aborts an active sync subagent and leaves the parent session usable', async () => {
158
+ const tmp = makeTmp('sync');
159
+ await env.addChat('stop-sub-sync', 'debug-agent');
160
+ chat = await env.connect('stop-sub-sync');
161
+
162
+ // No --async flag: the parent's lite invocation blocks on subagentWait,
163
+ // so use noWait on sendMessage to avoid blocking the test runner too.
164
+ const cmd = `echo $$ > ${tmp}/pid && sleep 5 && touch ${tmp}/leaked`;
165
+ await env.sendMessage(`clawmini-lite.js subagents spawn '${cmd}'`, {
166
+ chat: 'stop-sub-sync',
167
+ agent: 'debug-agent',
168
+ noWait: true,
169
+ });
170
+
171
+ const pid = await waitForPidFile(path.join(tmp, 'pid'), 10000);
172
+ expect(pidIsAlive(pid)).toBe(true);
173
+
174
+ await env.sendMessage('/stop', { chat: 'stop-sub-sync', noWait: true });
175
+ await chat.waitForMessage(
176
+ (m) => typeof m.content === 'string' && m.content.includes('Stopping current task...'),
177
+ 5000
178
+ );
179
+
180
+ await new Promise((r) => setTimeout(r, 8000));
181
+ expect(fs.existsSync(path.join(tmp, 'leaked'))).toBe(false);
182
+ expect(pidIsAlive(pid)).toBe(false);
183
+
184
+ // Recovery: a sync spawn can wedge the parent if /stop fails to unblock
185
+ // the parent's subagentWait poll. A follow-up message proves it didn't.
186
+ await env.sendMessage('echo RECOVERED_SYNC', {
187
+ chat: 'stop-sub-sync',
188
+ agent: 'debug-agent',
189
+ });
190
+ const ok = await chat.waitForMessage(commandWith('RECOVERED_SYNC'), 10000);
191
+ expect(ok.exitCode).toBe(0);
192
+ }, 30000);
193
+
194
+ it('aborts nested subagents at depth 2: both layers die and neither leaks', async () => {
195
+ const tmp = makeTmp('nested');
196
+ await env.addChat('stop-sub-nested', 'debug-agent');
197
+ chat = await env.connect('stop-sub-nested');
198
+
199
+ // Quoting note: this command is re-eval'd at three levels (parent
200
+ // shell, subagent A's shell, subagent B's shell). The inner `\$\$`
201
+ // survives the parent's outer single quotes literally, then loses one
202
+ // backslash inside subagent A's double-quoted invocation of lite, so
203
+ // subagent B finally sees a bare `$$` that expands to its own PID.
204
+ const inner = `echo \\$\\$ > ${tmp}/B.pid && sleep 5 && touch ${tmp}/B.leaked`;
205
+ const outer =
206
+ `echo $$ > ${tmp}/A.pid && ` +
207
+ `clawmini-lite.js subagents spawn --async "${inner}" && ` +
208
+ `sleep 5 && touch ${tmp}/A.leaked`;
209
+
210
+ await env.sendMessage(`clawmini-lite.js subagents spawn --async '${outer}'`, {
211
+ chat: 'stop-sub-nested',
212
+ agent: 'debug-agent',
213
+ });
214
+
215
+ const aPid = await waitForPidFile(path.join(tmp, 'A.pid'), 15000);
216
+ const bPid = await waitForPidFile(path.join(tmp, 'B.pid'), 15000);
217
+ expect(pidIsAlive(aPid)).toBe(true);
218
+ expect(pidIsAlive(bPid)).toBe(true);
219
+
220
+ await env.sendMessage('/stop', { chat: 'stop-sub-nested', noWait: true });
221
+ await chat.waitForMessage(
222
+ (m) => typeof m.content === 'string' && m.content.includes('Stopping current task...'),
223
+ 5000
224
+ );
225
+
226
+ await new Promise((r) => setTimeout(r, 8000));
227
+ expect(fs.existsSync(path.join(tmp, 'A.leaked'))).toBe(false);
228
+ expect(fs.existsSync(path.join(tmp, 'B.leaked'))).toBe(false);
229
+ expect(pidIsAlive(aPid)).toBe(false);
230
+ expect(pidIsAlive(bPid)).toBe(false);
231
+ }, 40000);
232
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import { TestEnvironment } from '../_helpers/test-environment.js';
4
+
5
+ async function waitUntil(
6
+ cond: () => boolean,
7
+ timeoutMs: number,
8
+ intervalMs = 100
9
+ ): Promise<boolean> {
10
+ const deadline = Date.now() + timeoutMs;
11
+ while (Date.now() < deadline) {
12
+ if (cond()) return true;
13
+ await new Promise((r) => setTimeout(r, intervalMs));
14
+ }
15
+ return cond();
16
+ }
17
+
18
+ interface LogLine {
19
+ role?: string;
20
+ content?: string;
21
+ }
22
+
23
+ function readChatLog(filePath: string): LogLine[] {
24
+ if (!fs.existsSync(filePath)) return [];
25
+ return fs
26
+ .readFileSync(filePath, 'utf-8')
27
+ .split('\n')
28
+ .filter((line) => line.trim().length > 0)
29
+ .map((line) => {
30
+ try {
31
+ return JSON.parse(line) as LogLine;
32
+ } catch {
33
+ return {};
34
+ }
35
+ });
36
+ }
37
+
38
+ describe('/upgrade router e2e (dev checkout path)', () => {
39
+ let env: TestEnvironment;
40
+
41
+ beforeAll(async () => {
42
+ env = new TestEnvironment('e2e-slash-upgrade');
43
+ await env.setup();
44
+ await env.init();
45
+ }, 30000);
46
+
47
+ afterAll(async () => {
48
+ await env.teardown();
49
+ }, 30000);
50
+
51
+ // We deliberately don't exercise the actual `npm install -g` path in e2e —
52
+ // it would mutate the host's globally installed clawmini. Instead we verify
53
+ // the safety branch: when the running binary is *not* under `npm root -g`
54
+ // (which is always true in the e2e harness, since dist/cli/index.mjs lives
55
+ // in the repo), /upgrade must reply with a refusal and leave the
56
+ // supervisor running.
57
+ it('refuses the upgrade and keeps the supervisor running when not installed via npm', async () => {
58
+ const { code } = await env.runCli(['serve', '--detach']);
59
+ expect(code).toBe(0);
60
+
61
+ const pidPath = env.getClawminiPath('supervisor.pid');
62
+ const socketPath = env.getClawminiPath('daemon.sock');
63
+ expect(await waitUntil(() => fs.existsSync(pidPath), 5000)).toBe(true);
64
+ expect(await waitUntil(() => fs.existsSync(socketPath), 10000)).toBe(true);
65
+
66
+ await env.sendMessage('/upgrade', { noWait: true });
67
+
68
+ const chatLog = env.getChatPath('default', 'chat.jsonl');
69
+ const sawRefusal = await waitUntil(() => {
70
+ const lines = readChatLog(chatLog);
71
+ return lines.some(
72
+ (l) =>
73
+ typeof l.content === 'string' &&
74
+ l.content.includes('Cannot upgrade') &&
75
+ l.content.includes('not installed via')
76
+ );
77
+ }, 15000);
78
+ expect(sawRefusal).toBe(true);
79
+
80
+ // Supervisor and daemon must both still be alive.
81
+ expect(fs.existsSync(pidPath)).toBe(true);
82
+ expect(fs.existsSync(socketPath)).toBe(true);
83
+
84
+ // No pending reply was queued for an upgrade that never happened.
85
+ const pendingPath = env.getClawminiPath('pending-replies.json');
86
+ expect(fs.existsSync(pendingPath)).toBe(false);
87
+ }, 30000);
88
+ });
@@ -1,25 +1,26 @@
1
1
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
2
  import path from 'node:path';
3
3
  import fs from 'node:fs';
4
- import { createE2EContext } from './utils.js';
4
+ import { TestEnvironment } from '../_helpers/test-environment.js';
5
5
 
6
6
  describe('Environments E2E', () => {
7
- const { runCli, e2eDir, setupE2E, teardownE2E } = createE2EContext('e2e-env');
7
+ let env: TestEnvironment;
8
8
 
9
9
  beforeAll(async () => {
10
- await setupE2E();
11
- await runCli(['init']);
10
+ env = new TestEnvironment('e2e-env');
11
+ await env.setup();
12
+ await env.init();
12
13
  }, 30000);
13
14
 
14
- afterAll(teardownE2E, 30000);
15
+ afterAll(() => env.teardown(), 30000);
15
16
 
16
17
  it('should run environment up and down commands on daemon start and stop', async () => {
17
18
  // Create an environment with up and down commands
18
- const envDir = path.join(e2eDir, '.clawmini', 'environments', 'test-env');
19
+ const envDir = path.join(env.e2eDir, '.clawmini', 'environments', 'test-env');
19
20
  fs.mkdirSync(envDir, { recursive: true });
20
21
 
21
- const upHookPath = path.join(e2eDir, 'up-hook-run.txt');
22
- const downHookPath = path.join(e2eDir, 'down-hook-run.txt');
22
+ const upHookPath = path.join(env.e2eDir, 'up-hook-run.txt');
23
+ const downHookPath = path.join(env.e2eDir, 'down-hook-run.txt');
23
24
 
24
25
  fs.writeFileSync(
25
26
  path.join(envDir, 'env.json'),
@@ -30,10 +31,10 @@ describe('Environments E2E', () => {
30
31
  );
31
32
 
32
33
  // Enable the environment
33
- await runCli(['environments', 'enable', 'test-env']);
34
+ await env.runCli(['environments', 'enable', 'test-env']);
34
35
 
35
36
  // Start the daemon (should trigger up hook)
36
- const { stdout: upStdout, code: upCode } = await runCli(['up']);
37
+ const { stdout: upStdout, code: upCode } = await env.runCli(['up']);
37
38
  expect(upCode).toBe(0);
38
39
  expect(upStdout).toMatch(
39
40
  /(Daemon is already running\.|Successfully started clawmini daemon\.)/
@@ -44,7 +45,7 @@ describe('Environments E2E', () => {
44
45
 
45
46
  // The daemon might have already been running since beforeAll init might start it.
46
47
  // So let's actually shut it down, then start it to be sure.
47
- await runCli(['down']);
48
+ await env.runCli(['down']);
48
49
  await new Promise((resolve) => setTimeout(resolve, 1000));
49
50
 
50
51
  // Clear out files if they exist
@@ -52,13 +53,13 @@ describe('Environments E2E', () => {
52
53
  if (fs.existsSync(downHookPath)) fs.unlinkSync(downHookPath);
53
54
 
54
55
  // Start daemon
55
- await runCli(['up']);
56
+ await env.runCli(['up']);
56
57
  await new Promise((resolve) => setTimeout(resolve, 1000));
57
58
  expect(fs.existsSync(upHookPath)).toBe(true);
58
59
  expect(fs.readFileSync(upHookPath, 'utf8').trim()).toBe('env-up');
59
60
 
60
61
  // Stop daemon
61
- await runCli(['down']);
62
+ await env.runCli(['down']);
62
63
  await new Promise((resolve) => setTimeout(resolve, 1000));
63
64
  expect(fs.existsSync(downHookPath)).toBe(true);
64
65
  expect(fs.readFileSync(downHookPath, 'utf8').trim()).toBe('env-down');
package/eslint.config.js CHANGED
@@ -55,5 +55,11 @@ export default defineConfig([
55
55
  'no-restricted-syntax': 'off',
56
56
  },
57
57
  },
58
+ {
59
+ files: ['templates/environments/**/*.{js,mjs,cjs}'],
60
+ languageOptions: {
61
+ globals: globals.node,
62
+ },
63
+ },
58
64
  eslintConfigPrettier,
59
65
  ]);
package/napkin.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - **shadcn-svelte commands:** Always use the `-y` flag when adding a component (e.g., `npx shadcn-svelte@latest add -y <component>`) to bypass the interactive prompt.\n- **Svelte Check:** Always run `npm run check -w web` instead of `npx svelte-check` to run Svelte diagnostics.
19
19
  - Added shadcn-svelte dialog for chat creation and replaced native window.prompt.
20
20
  - **E2E and Unit Test File Splitting:** When splitting `.test.ts` files, remember that `vi.mock()` must be at the top level of the file where the module is imported. Moving it to a shared setup utility will break mocking in Vitest.\nAlso, when splitting tests that share an environment setup (like E2E tests building dist), ensure tests run sequentially using `fileParallelism: false` in `vitest.config.ts` to prevent race conditions on shared global processes.
21
- - **E2E Tests Performance Optimization:** To make E2E tests run faster, moved the `npm run build` command out of the repeated E2E file setup and into a Vitest `globalSetup` hook (`src/cli/e2e/global-setup.ts`). Restored `fileParallelism: true` to `vitest.config.ts`, so test files run concurrently in workers while sharing a single initial build output. This dropped test suite execution time from ~50s down to ~18s.
21
+ - **E2E Tests Performance Optimization:** To make E2E tests run faster, moved the `npm run build` command out of the repeated E2E file setup and into a Vitest `globalSetup` hook (`e2e/_helpers/global-setup.ts`). Restored `fileParallelism: true` to `vitest.config.ts`, so test files run concurrently in workers while sharing a single initial build output. This dropped test suite execution time from ~50s down to ~18s.
22
22
  - **TypeScript `noUncheckedIndexedAccess` with string chunking:** When accessing an element of a string array like `chunks[i]`, TypeScript with `noUncheckedIndexedAccess` enabled will infer the type as `string | undefined`. If you know the index is safe (like inside a traditional `for` loop `for (let i = 0; i < chunks.length; i++)`), explicitly cast it using `as string` (e.g., `chunks[i] as string`) when passing it to strict API objects.
23
23
  - **Fixing CI missing optional dependencies (e.g. `@rollup/rollup-linux-x64-gnu`)**: If GitHub Actions on Linux fails during `npm ci` because an optional dependency for Linux x64 is missing from `package-lock.json` (often created on macOS), run `rm -rf package-lock.json node_modules web/node_modules && npm install` to regenerate the lockfile with cross-platform optional dependencies included.
24
24
  - **When mocking state modules like state.js**, ensure `vi.mock('./state.js', ...)` is at the top level and includes the correct return shapes so subsequent tests relying on default properties (like `channelChatMap`) don't fail.