@songsid/agend 2.1.12 → 2.2.0-alpha.2

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 (436) hide show
  1. package/README.md +4 -2
  2. package/README.zh-TW.md +4 -2
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +68 -12
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-token-header.d.ts +13 -0
  8. package/dist/agent-token-header.js +16 -0
  9. package/dist/agent-token-header.js.map +1 -0
  10. package/dist/auth/one-time-code.d.ts +19 -0
  11. package/dist/auth/one-time-code.js +49 -0
  12. package/dist/auth/one-time-code.js.map +1 -0
  13. package/dist/auth-api.d.ts +52 -0
  14. package/dist/auth-api.js +346 -0
  15. package/dist/auth-api.js.map +1 -0
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/claude-code.d.ts +4 -0
  19. package/dist/backend/claude-code.js +97 -0
  20. package/dist/backend/claude-code.js.map +1 -1
  21. package/dist/backend/codex.js +59 -30
  22. package/dist/backend/codex.js.map +1 -1
  23. package/dist/backend/grok.js +1 -1
  24. package/dist/backend/grok.js.map +1 -1
  25. package/dist/backend/kiro-agent.d.ts +66 -0
  26. package/dist/backend/kiro-agent.js +302 -0
  27. package/dist/backend/kiro-agent.js.map +1 -0
  28. package/dist/backend/kiro-identity.d.ts +80 -0
  29. package/dist/backend/kiro-identity.js +537 -0
  30. package/dist/backend/kiro-identity.js.map +1 -0
  31. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  32. package/dist/backend/kiro-v2-store-worker.js +87 -0
  33. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  34. package/dist/backend/kiro-v2-store.d.ts +54 -0
  35. package/dist/backend/kiro-v2-store.js +131 -0
  36. package/dist/backend/kiro-v2-store.js.map +1 -0
  37. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  38. package/dist/backend/kiro-v3-identity.js +22 -0
  39. package/dist/backend/kiro-v3-identity.js.map +1 -1
  40. package/dist/backend/kiro.d.ts +90 -7
  41. package/dist/backend/kiro.js +500 -124
  42. package/dist/backend/kiro.js.map +1 -1
  43. package/dist/backend/muse.js +1 -1
  44. package/dist/backend/muse.js.map +1 -1
  45. package/dist/backend/types.d.ts +51 -3
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-analysis.d.ts +92 -0
  48. package/dist/cache-analysis.js +147 -0
  49. package/dist/cache-analysis.js.map +1 -0
  50. package/dist/cache-ledger.d.ts +108 -0
  51. package/dist/cache-ledger.js +230 -0
  52. package/dist/cache-ledger.js.map +1 -0
  53. package/dist/cache-prices.d.ts +32 -0
  54. package/dist/cache-prices.js +73 -0
  55. package/dist/cache-prices.js.map +1 -0
  56. package/dist/cache-scan.d.ts +39 -0
  57. package/dist/cache-scan.js +115 -0
  58. package/dist/cache-scan.js.map +1 -0
  59. package/dist/cache-service.d.ts +115 -0
  60. package/dist/cache-service.js +417 -0
  61. package/dist/cache-service.js.map +1 -0
  62. package/dist/channel/adapters/discord.d.ts +1 -0
  63. package/dist/channel/adapters/discord.js +29 -10
  64. package/dist/channel/adapters/discord.js.map +1 -1
  65. package/dist/channel/adapters/telegram.d.ts +1 -0
  66. package/dist/channel/adapters/telegram.js +13 -1
  67. package/dist/channel/adapters/telegram.js.map +1 -1
  68. package/dist/channel/types.d.ts +9 -1
  69. package/dist/classic-channel-manager.d.ts +18 -10
  70. package/dist/classic-channel-manager.js +45 -18
  71. package/dist/classic-channel-manager.js.map +1 -1
  72. package/dist/cli-entry.d.ts +19 -0
  73. package/dist/cli-entry.js +36 -0
  74. package/dist/cli-entry.js.map +1 -0
  75. package/dist/cli.js +471 -146
  76. package/dist/cli.js.map +1 -1
  77. package/dist/command-table.d.ts +5 -3
  78. package/dist/command-table.js +29 -27
  79. package/dist/command-table.js.map +1 -1
  80. package/dist/config-validator.js +46 -0
  81. package/dist/config-validator.js.map +1 -1
  82. package/dist/config.js +12 -0
  83. package/dist/config.js.map +1 -1
  84. package/dist/connection-secrets.d.ts +6 -0
  85. package/dist/connection-secrets.js +4 -0
  86. package/dist/connection-secrets.js.map +1 -1
  87. package/dist/daemon.d.ts +95 -4
  88. package/dist/daemon.js +748 -111
  89. package/dist/daemon.js.map +1 -1
  90. package/dist/darwin-memory.d.ts +6 -1
  91. package/dist/darwin-memory.js +17 -6
  92. package/dist/darwin-memory.js.map +1 -1
  93. package/dist/delivery-consumed-watch.d.ts +85 -0
  94. package/dist/delivery-consumed-watch.js +244 -0
  95. package/dist/delivery-consumed-watch.js.map +1 -0
  96. package/dist/delivery-outbox.d.ts +61 -0
  97. package/dist/delivery-outbox.js +120 -2
  98. package/dist/delivery-outbox.js.map +1 -1
  99. package/dist/delivery-reconciliation.d.ts +14 -5
  100. package/dist/delivery-reconciliation.js +27 -4
  101. package/dist/delivery-reconciliation.js.map +1 -1
  102. package/dist/delivery-reconciliation.test.js +35 -1
  103. package/dist/delivery-reconciliation.test.js.map +1 -1
  104. package/dist/event-log.d.ts +4 -1
  105. package/dist/event-log.js +6 -3
  106. package/dist/event-log.js.map +1 -1
  107. package/dist/event-loop-watch.d.ts +28 -0
  108. package/dist/event-loop-watch.js +75 -3
  109. package/dist/event-loop-watch.js.map +1 -1
  110. package/dist/fleet-context.d.ts +22 -2
  111. package/dist/fleet-control-audit.d.ts +5 -0
  112. package/dist/fleet-control-audit.js +7 -0
  113. package/dist/fleet-control-audit.js.map +1 -1
  114. package/dist/fleet-level-config.d.ts +2 -2
  115. package/dist/fleet-level-config.js +28 -1
  116. package/dist/fleet-level-config.js.map +1 -1
  117. package/dist/fleet-manager.d.ts +339 -12
  118. package/dist/fleet-manager.js +2623 -461
  119. package/dist/fleet-manager.js.map +1 -1
  120. package/dist/full-restart.js +2 -3
  121. package/dist/full-restart.js.map +1 -1
  122. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  123. package/dist/host-memory.d.ts +4 -0
  124. package/dist/host-memory.js.map +1 -1
  125. package/dist/install-lock.d.ts +49 -0
  126. package/dist/install-lock.js +200 -0
  127. package/dist/install-lock.js.map +1 -0
  128. package/dist/instance-config-impact.js +8 -2
  129. package/dist/instance-config-impact.js.map +1 -1
  130. package/dist/instance-lifecycle.d.ts +10 -5
  131. package/dist/instance-lifecycle.js +162 -53
  132. package/dist/instance-lifecycle.js.map +1 -1
  133. package/dist/locale.js +164 -16
  134. package/dist/locale.js.map +1 -1
  135. package/dist/login-controller.d.ts +2 -0
  136. package/dist/login-controller.js +17 -5
  137. package/dist/login-controller.js.map +1 -1
  138. package/dist/memory-pressure.d.ts +7 -8
  139. package/dist/memory-pressure.js +76 -15
  140. package/dist/memory-pressure.js.map +1 -1
  141. package/dist/monotonic-budget.d.ts +2 -0
  142. package/dist/monotonic-budget.js +28 -0
  143. package/dist/monotonic-budget.js.map +1 -0
  144. package/dist/needs-you-hub.d.ts +114 -0
  145. package/dist/needs-you-hub.js +370 -0
  146. package/dist/needs-you-hub.js.map +1 -0
  147. package/dist/needs-you-live.d.ts +116 -0
  148. package/dist/needs-you-live.js +228 -0
  149. package/dist/needs-you-live.js.map +1 -0
  150. package/dist/needs-you.d.ts +132 -0
  151. package/dist/needs-you.js +164 -0
  152. package/dist/needs-you.js.map +1 -0
  153. package/dist/node-version-guard.d.ts +30 -0
  154. package/dist/node-version-guard.js +50 -0
  155. package/dist/node-version-guard.js.map +1 -0
  156. package/dist/operator-control-paths.d.ts +4 -0
  157. package/dist/operator-control-paths.js +24 -0
  158. package/dist/operator-control-paths.js.map +1 -0
  159. package/dist/outbound-handlers.d.ts +2 -0
  160. package/dist/outbound-handlers.js +9 -2
  161. package/dist/outbound-handlers.js.map +1 -1
  162. package/dist/outbound-schemas.d.ts +5 -5
  163. package/dist/outbound-schemas.js +1 -1
  164. package/dist/outbound-schemas.js.map +1 -1
  165. package/dist/package-preimage.d.ts +26 -0
  166. package/dist/package-preimage.js +96 -0
  167. package/dist/package-preimage.js.map +1 -0
  168. package/dist/profile-control.js +4 -20
  169. package/dist/profile-control.js.map +1 -1
  170. package/dist/public-web-gateway.d.ts +18 -0
  171. package/dist/public-web-gateway.js +141 -0
  172. package/dist/public-web-gateway.js.map +1 -0
  173. package/dist/public-web-link.d.ts +53 -0
  174. package/dist/public-web-link.js +175 -0
  175. package/dist/public-web-link.js.map +1 -0
  176. package/dist/quickstart-api.d.ts +8 -1
  177. package/dist/quickstart-api.js +86 -48
  178. package/dist/quickstart-api.js.map +1 -1
  179. package/dist/quickstart.d.ts +1 -0
  180. package/dist/quickstart.js +16 -6
  181. package/dist/quickstart.js.map +1 -1
  182. package/dist/restart-guard.d.ts +52 -0
  183. package/dist/restart-guard.js +109 -0
  184. package/dist/restart-guard.js.map +1 -0
  185. package/dist/scheduler/db.d.ts +7 -1
  186. package/dist/scheduler/db.js +44 -2
  187. package/dist/scheduler/db.js.map +1 -1
  188. package/dist/scheduler/index.d.ts +1 -1
  189. package/dist/scheduler/retry-label.d.ts +11 -0
  190. package/dist/scheduler/retry-label.js +24 -0
  191. package/dist/scheduler/retry-label.js.map +1 -0
  192. package/dist/scheduler/scheduler.d.ts +55 -2
  193. package/dist/scheduler/scheduler.js +220 -8
  194. package/dist/scheduler/scheduler.js.map +1 -1
  195. package/dist/scheduler/types.d.ts +22 -0
  196. package/dist/scheduler/types.js.map +1 -1
  197. package/dist/secret-store.d.ts +5 -0
  198. package/dist/secret-store.js +20 -0
  199. package/dist/secret-store.js.map +1 -1
  200. package/dist/service-activation.d.ts +152 -0
  201. package/dist/service-activation.js +407 -0
  202. package/dist/service-activation.js.map +1 -0
  203. package/dist/service-installer.d.ts +63 -1
  204. package/dist/service-installer.js +141 -16
  205. package/dist/service-installer.js.map +1 -1
  206. package/dist/service-plan.d.ts +65 -0
  207. package/dist/service-plan.js +108 -0
  208. package/dist/service-plan.js.map +1 -0
  209. package/dist/service-restart-selection.d.ts +6 -5
  210. package/dist/service-restart-selection.js +6 -5
  211. package/dist/service-restart-selection.js.map +1 -1
  212. package/dist/settings-api.d.ts +10 -2
  213. package/dist/settings-api.js +216 -72
  214. package/dist/settings-api.js.map +1 -1
  215. package/dist/settings-baseline.d.ts +20 -0
  216. package/dist/settings-baseline.js +104 -0
  217. package/dist/settings-baseline.js.map +1 -0
  218. package/dist/settings-change.d.ts +21 -0
  219. package/dist/settings-change.js +161 -0
  220. package/dist/settings-change.js.map +1 -0
  221. package/dist/settings-confirmation.d.ts +105 -0
  222. package/dist/settings-confirmation.js +232 -0
  223. package/dist/settings-confirmation.js.map +1 -0
  224. package/dist/settings-control.d.ts +41 -0
  225. package/dist/settings-control.js +294 -0
  226. package/dist/settings-control.js.map +1 -0
  227. package/dist/settings-effect.d.ts +24 -0
  228. package/dist/settings-effect.js +169 -0
  229. package/dist/settings-effect.js.map +1 -0
  230. package/dist/settings-http-confirmation.d.ts +29 -0
  231. package/dist/settings-http-confirmation.js +123 -0
  232. package/dist/settings-http-confirmation.js.map +1 -0
  233. package/dist/settings-instance-patch.d.ts +4 -0
  234. package/dist/settings-instance-patch.js +26 -0
  235. package/dist/settings-instance-patch.js.map +1 -0
  236. package/dist/settings-request-capability.d.ts +16 -0
  237. package/dist/settings-request-capability.js +32 -0
  238. package/dist/settings-request-capability.js.map +1 -0
  239. package/dist/settings-transaction.d.ts +65 -0
  240. package/dist/settings-transaction.js +223 -0
  241. package/dist/settings-transaction.js.map +1 -0
  242. package/dist/setup-auth.d.ts +10 -18
  243. package/dist/setup-auth.js +14 -38
  244. package/dist/setup-auth.js.map +1 -1
  245. package/dist/setup-form.d.ts +1 -1
  246. package/dist/setup-form.js +75 -18
  247. package/dist/setup-form.js.map +1 -1
  248. package/dist/setup-guide.d.ts +13 -0
  249. package/dist/setup-guide.js +25 -0
  250. package/dist/setup-guide.js.map +1 -0
  251. package/dist/setup-host.d.ts +16 -0
  252. package/dist/setup-host.js +199 -35
  253. package/dist/setup-host.js.map +1 -1
  254. package/dist/setup-wizard.js +5 -1
  255. package/dist/setup-wizard.js.map +1 -1
  256. package/dist/slash-authz.d.ts +17 -3
  257. package/dist/slash-authz.js +5 -0
  258. package/dist/slash-authz.js.map +1 -1
  259. package/dist/spawn-gate.js +3 -3
  260. package/dist/spawn-gate.js.map +1 -1
  261. package/dist/sqlite-open-errors.d.ts +12 -0
  262. package/dist/sqlite-open-errors.js +13 -0
  263. package/dist/sqlite-open-errors.js.map +1 -0
  264. package/dist/statusline-watcher.d.ts +5 -0
  265. package/dist/statusline-watcher.js +2 -0
  266. package/dist/statusline-watcher.js.map +1 -1
  267. package/dist/steer-capability.d.ts +6 -0
  268. package/dist/steer-capability.js +8 -0
  269. package/dist/steer-capability.js.map +1 -1
  270. package/dist/sync-work-attribution.d.ts +14 -0
  271. package/dist/sync-work-attribution.js +43 -8
  272. package/dist/sync-work-attribution.js.map +1 -1
  273. package/dist/tips.js +10 -5
  274. package/dist/tips.js.map +1 -1
  275. package/dist/tmux-control.d.ts +57 -5
  276. package/dist/tmux-control.js +379 -61
  277. package/dist/tmux-control.js.map +1 -1
  278. package/dist/tmux-manager.d.ts +20 -6
  279. package/dist/tmux-manager.js +90 -47
  280. package/dist/tmux-manager.js.map +1 -1
  281. package/dist/tmux-read.d.ts +59 -0
  282. package/dist/tmux-read.js +213 -0
  283. package/dist/tmux-read.js.map +1 -0
  284. package/dist/topic-commands.d.ts +21 -10
  285. package/dist/topic-commands.js +205 -63
  286. package/dist/topic-commands.js.map +1 -1
  287. package/dist/transcript-delta-reader.js +2 -1
  288. package/dist/transcript-delta-reader.js.map +1 -1
  289. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  290. package/dist/tunnel/cloudflared-install.js +1 -1
  291. package/dist/tunnel/cloudflared-install.js.map +1 -1
  292. package/dist/tunnel/purpose-lane.d.ts +14 -0
  293. package/dist/tunnel/purpose-lane.js +43 -0
  294. package/dist/tunnel/purpose-lane.js.map +1 -0
  295. package/dist/turn-reply-guard.d.ts +2 -0
  296. package/dist/turn-reply-guard.js +10 -1
  297. package/dist/turn-reply-guard.js.map +1 -1
  298. package/dist/types.d.ts +33 -0
  299. package/dist/ui/app.html +18 -0
  300. package/dist/ui/chat-commands.js +43 -0
  301. package/dist/ui/chat-render.js +478 -0
  302. package/dist/ui/chat-store.js +318 -0
  303. package/dist/ui/chat-strings.js +89 -0
  304. package/dist/ui/chat-thread.js +246 -0
  305. package/dist/ui/chat-tour.js +116 -0
  306. package/dist/ui/fleet-cache.js +168 -0
  307. package/dist/ui/fleet-org.js +142 -0
  308. package/dist/ui/panel-chat.js +504 -0
  309. package/dist/ui/panel-fleet.js +427 -0
  310. package/dist/ui/panel-needs.js +202 -0
  311. package/dist/ui/panel-settings.js +754 -0
  312. package/dist/ui/preview-panel.js +202 -0
  313. package/dist/ui/preview.js +270 -0
  314. package/dist/ui/settings-apply.js +239 -0
  315. package/dist/ui/settings-confirm.js +186 -0
  316. package/dist/ui/settings-dialogs.js +546 -0
  317. package/dist/ui/settings-model.js +277 -0
  318. package/dist/ui/settings-strings.js +654 -0
  319. package/dist/ui/settings-wizard.js +140 -0
  320. package/dist/ui/shared/agend-auth.js +93 -0
  321. package/dist/ui/shared/app-ctx.js +80 -0
  322. package/dist/ui/shared/app-html.js +9 -0
  323. package/dist/ui/shared/app-i18n.js +95 -0
  324. package/dist/ui/shared/app-nav.js +73 -0
  325. package/dist/ui/shared/app-needs.js +114 -0
  326. package/dist/ui/shared/app-route.js +73 -0
  327. package/dist/ui/shared/app-session.js +66 -0
  328. package/dist/ui/shared/app-shell.js +346 -0
  329. package/dist/ui/shared/app-store.js +51 -0
  330. package/dist/ui/shared/app-stream.js +201 -0
  331. package/dist/ui/shared/app.css +820 -0
  332. package/dist/ui/shared/app.js +88 -0
  333. package/dist/ui/shared/fonts/OFL.txt +92 -0
  334. package/dist/ui/shared/fonts/README.md +21 -0
  335. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  336. package/dist/ui/shared/htm.module.js +1 -0
  337. package/dist/ui/shared/panel-view.js +655 -0
  338. package/dist/ui/shared/preact-hooks.module.js +2 -0
  339. package/dist/ui/shared/preact.module.js +2 -0
  340. package/dist/ui/shared/signin.js +142 -0
  341. package/dist/ui/shared/theme.js +29 -0
  342. package/dist/ui/shared/tokens.css +153 -0
  343. package/dist/ui/shared/ui-confirm.js +43 -0
  344. package/dist/ui/shared/ui-dialog.js +45 -0
  345. package/dist/ui/shared/ui-icons.js +63 -0
  346. package/dist/ui/shared/ui-menu.js +39 -0
  347. package/dist/ui/shared/ui-states.js +20 -0
  348. package/dist/ui/shared/ui-toast.js +18 -0
  349. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  350. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  351. package/dist/ui/shared/vendor/vendor.json +39 -0
  352. package/dist/ui/shared/view-strings.js +309 -0
  353. package/dist/ui/signin.html +52 -0
  354. package/dist/update-check.d.ts +6 -2
  355. package/dist/update-check.js +12 -3
  356. package/dist/update-check.js.map +1 -1
  357. package/dist/update-dispatch.d.ts +28 -0
  358. package/dist/update-dispatch.js +73 -0
  359. package/dist/update-dispatch.js.map +1 -0
  360. package/dist/update-install.d.ts +129 -0
  361. package/dist/update-install.js +262 -0
  362. package/dist/update-install.js.map +1 -0
  363. package/dist/upgrade-notices.d.ts +17 -0
  364. package/dist/upgrade-notices.js +93 -0
  365. package/dist/upgrade-notices.js.map +1 -0
  366. package/dist/usage/statusline-usage.d.ts +2 -0
  367. package/dist/usage/statusline-usage.js +14 -10
  368. package/dist/usage/statusline-usage.js.map +1 -1
  369. package/dist/view-api.d.ts +6 -1
  370. package/dist/view-api.js +62 -51
  371. package/dist/view-api.js.map +1 -1
  372. package/dist/web-api.d.ts +85 -14
  373. package/dist/web-api.js +557 -77
  374. package/dist/web-api.js.map +1 -1
  375. package/dist/web-auth.d.ts +71 -27
  376. package/dist/web-auth.js +169 -69
  377. package/dist/web-auth.js.map +1 -1
  378. package/dist/web-body.d.ts +3 -0
  379. package/dist/web-body.js +55 -0
  380. package/dist/web-body.js.map +1 -0
  381. package/dist/web-channel-echo.d.ts +9 -0
  382. package/dist/web-channel-echo.js +55 -0
  383. package/dist/web-channel-echo.js.map +1 -0
  384. package/dist/web-chat-history.d.ts +125 -0
  385. package/dist/web-chat-history.js +184 -0
  386. package/dist/web-chat-history.js.map +1 -0
  387. package/dist/web-commands.d.ts +77 -0
  388. package/dist/web-commands.js +81 -0
  389. package/dist/web-commands.js.map +1 -0
  390. package/dist/web-continuation.d.ts +7 -0
  391. package/dist/web-continuation.js +22 -0
  392. package/dist/web-continuation.js.map +1 -0
  393. package/dist/web-host-guard.d.ts +34 -0
  394. package/dist/web-host-guard.js +57 -1
  395. package/dist/web-host-guard.js.map +1 -1
  396. package/dist/web-login.d.ts +72 -0
  397. package/dist/web-login.js +132 -0
  398. package/dist/web-login.js.map +1 -0
  399. package/dist/web-org.d.ts +55 -0
  400. package/dist/web-org.js +67 -0
  401. package/dist/web-org.js.map +1 -0
  402. package/dist/web-preview.d.ts +77 -0
  403. package/dist/web-preview.js +240 -0
  404. package/dist/web-preview.js.map +1 -0
  405. package/dist/web-request-context.d.ts +10 -0
  406. package/dist/web-request-context.js +13 -0
  407. package/dist/web-request-context.js.map +1 -0
  408. package/dist/web-session.d.ts +167 -0
  409. package/dist/web-session.js +389 -0
  410. package/dist/web-session.js.map +1 -0
  411. package/dist/web-shell-routes.d.ts +59 -0
  412. package/dist/web-shell-routes.js +96 -0
  413. package/dist/web-shell-routes.js.map +1 -0
  414. package/dist/web-upload.d.ts +183 -0
  415. package/dist/web-upload.js +468 -0
  416. package/dist/web-upload.js.map +1 -0
  417. package/dist/website-tips-renderer.d.ts +2 -2
  418. package/dist/website-tips-renderer.js +3 -3
  419. package/dist/website-tips-renderer.js.map +1 -1
  420. package/launcher/agend +110 -0
  421. package/launcher/agend-agent +110 -0
  422. package/launcher/agend-agent.cjs +4 -0
  423. package/launcher/agend.cjs +4 -0
  424. package/launcher/install-admission.cjs +105 -0
  425. package/launcher/launch.cjs +64 -0
  426. package/launcher/postinstall.cjs +114 -0
  427. package/launcher/run-in-process.cjs +13 -0
  428. package/launcher/runtime-platform.cjs +86 -0
  429. package/launcher/runtime-select.cjs +217 -0
  430. package/package.json +18 -7
  431. package/scripts/preinstall-guard.cjs +55 -0
  432. package/templates/launchd.plist.ejs +2 -2
  433. package/templates/systemd.service.ejs +1 -1
  434. package/dist/ui/dashboard.html +0 -774
  435. package/dist/ui/settings.html +0 -2165
  436. package/dist/ui/view.html +0 -1139
package/dist/cli.js CHANGED
@@ -4,20 +4,28 @@ import { applyNetworkReliabilityDefaults } from "./network-family.js";
4
4
  // Preserve Node's dual-stack Happy Eyeballs behavior, but avoid its 250ms
5
5
  // premature-connect failure on WSL/VPN/high-load hosts (#658).
6
6
  applyNetworkReliabilityDefaults();
7
+ // ── Runtime version guard ───────────────────────────────────────────────────
8
+ // better-sqlite3 v13 requires Node-API 10 (>=22.14.0 / >=23.6.0 / >=24).
9
+ // This MUST run at bootstrap so that even `agend --version` exits 1 on an
10
+ // incompatible runtime. The old updater checks `agend --version` before
11
+ // restarting; a non-zero exit makes it call failUpdate() and abort — the fleet
12
+ // never restarts into a binary that would crash at the first native DB open.
13
+ import { checkNodeVersion } from "./node-version-guard.js";
14
+ checkNodeVersion();
7
15
  import { Command } from "commander";
8
16
  import { join, dirname } from "node:path";
9
17
  import { SchedulerDb } from "./scheduler/db.js";
10
18
  import { Cron } from "croner";
11
- import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, } from "node:fs";
19
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, unlinkSync, mkdirSync, chmodSync, readdirSync, rmSync, statSync, realpathSync, } from "node:fs";
12
20
  import { homedir, totalmem, freemem, userInfo } from "node:os";
13
21
  import { fileURLToPath } from "node:url";
14
- import { spawn, execSync, execFileSync } from "node:child_process";
22
+ import { spawn, spawnSync, execSync, execFileSync } from "node:child_process";
15
23
  import { getAgendHome, getTmuxSocketName } from "./paths.js";
16
24
  import { hasPausedMarker } from "./pause-marker.js";
17
25
  import { COMPLETION_SHELLS, completionScript } from "./completion.js";
18
26
  import { getUpdateSelector, isExactVersion, isUnrequestedDowngrade, lookupTargetVersion, reportUpdateRestart, shouldSkipUpdate, runningFleetPredatesInstall, processStartMs, } from "./update-check.js";
19
27
  import { clearUpdateMarker, markUpdateInProgress, setUpdateProgressStage } from "./update-marker.js";
20
- import { describeSignalSource, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
28
+ import { describeSignalSource, forceAllowed, gateFleetControl, recordInstanceControl, withOrigin } from "./fleet-control-audit.js";
21
29
  import { acquireFleetLock, isFleetStartCommandLine, readProcessCommandLine, releaseProcessFleetLock, setProcessFleetLock } from "./fleet-lock.js";
22
30
  import { limitFleetCoreDumps } from "./coredump-filter.js";
23
31
  import { SYSTEMD_RESTART_TIMEOUT_MS } from "./service-installer.js";
@@ -27,6 +35,8 @@ import { setLocale, t } from "./locale.js";
27
35
  import { DeliveryOutbox, deliveryStatusSelector } from "./delivery-outbox.js";
28
36
  import { DeliveryStatusArgs } from "./outbound-schemas.js";
29
37
  import { requestCpuProfile } from "./profile-control.js";
38
+ import { requestSettingsConfirmation } from "./settings-control.js";
39
+ import { canonicalCliEntry, selfCommand } from "./cli-entry.js";
30
40
  /** Prefix tmux args with -L when socket isolation is active. */
31
41
  function tmuxArgs(args) {
32
42
  const socket = getTmuxSocketName();
@@ -35,6 +45,39 @@ function tmuxArgs(args) {
35
45
  const __filename = fileURLToPath(import.meta.url);
36
46
  const __dirname = dirname(__filename);
37
47
  const DATA_DIR = getAgendHome();
48
+ /** Real-filesystem, real-process deps for the launchd plan (service-plan.ts) and its activation. */
49
+ function planDeps() {
50
+ return {
51
+ run: (command, args) => {
52
+ const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
53
+ return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
54
+ },
55
+ readFile: (path) => { try {
56
+ return readFileSync(path, "utf-8");
57
+ }
58
+ catch {
59
+ return null;
60
+ } },
61
+ writeFile: (path, content) => writeFileSync(path, content),
62
+ removeFile: (path) => { try {
63
+ unlinkSync(path);
64
+ }
65
+ catch { /* absent */ } },
66
+ realpath: (path) => { try {
67
+ return realpathSync(path);
68
+ }
69
+ catch {
70
+ return null;
71
+ } },
72
+ isExecutable: (path) => { try {
73
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
74
+ }
75
+ catch {
76
+ return false;
77
+ } },
78
+ now: () => new Date(),
79
+ };
80
+ }
38
81
  const FLEET_CONFIG_PATH = join(DATA_DIR, "fleet.yaml");
39
82
  // CLI commands run outside FleetManager. Honor an explicit fleet locale while
40
83
  // keeping the historical English CLI default when config is missing/unset.
@@ -90,6 +133,47 @@ function signalFleetReload() {
90
133
  }
91
134
  // === Fleet commands ===
92
135
  const fleet = program.command("fleet").description("Fleet management");
136
+ const settingsCommand = program.command("settings").description("Confirm sensitive Settings changes on this host");
137
+ settingsCommand.command("confirm <id>").option("--yes", "Explicit noninteractive confirmation after printing the authoritative diff")
138
+ .action(async (id, options) => {
139
+ try {
140
+ const inspected = await requestSettingsConfirmation(DATA_DIR, { action: "inspect", id });
141
+ console.log(`Source: ${inspected.pending_change.source}\nRequester: ${inspected.pending_change.requested_by}\n${inspected.pending_change.summary.join("\n")}`);
142
+ let confirmed = options.yes === true;
143
+ if (!confirmed) {
144
+ if (!process.stdin.isTTY)
145
+ throw new Error("Interactive confirmation required; --yes explicitly confirms the printed diff.");
146
+ const { createInterface } = await import("node:readline/promises");
147
+ const input = createInterface({ input: process.stdin, output: process.stdout });
148
+ try {
149
+ confirmed = (await input.question("Apply this exact change? [y/N] ")).trim().toLowerCase() === "y";
150
+ }
151
+ finally {
152
+ input.close();
153
+ }
154
+ }
155
+ const result = await requestSettingsConfirmation(DATA_DIR, { action: confirmed ? "confirm" : "reject", ticket: inspected.ticket });
156
+ console.log(result.pending_change.outcome?.message ?? result.pending_change.state);
157
+ if (result.pending_change.state !== "applied" && confirmed)
158
+ process.exitCode = 1;
159
+ }
160
+ catch (err) {
161
+ console.error(err.message);
162
+ process.exitCode = 1;
163
+ }
164
+ });
165
+ settingsCommand.command("reject <id>").action(async (id) => {
166
+ try {
167
+ const inspected = await requestSettingsConfirmation(DATA_DIR, { action: "inspect", id });
168
+ console.log(`Source: ${inspected.pending_change.source}\nRequester: ${inspected.pending_change.requested_by}\n${inspected.pending_change.summary.join("\n")}`);
169
+ const result = await requestSettingsConfirmation(DATA_DIR, { action: "reject", ticket: inspected.ticket });
170
+ console.log(result.pending_change.outcome?.message ?? result.pending_change.state);
171
+ }
172
+ catch (err) {
173
+ console.error(err.message);
174
+ process.exitCode = 1;
175
+ }
176
+ });
93
177
  program.command("profile")
94
178
  .description("Record the running fleet's CPU profile (local operator only; no restart)")
95
179
  .argument("[seconds]", "Recording duration, 1–1800 seconds", "60")
@@ -223,6 +307,7 @@ fleet
223
307
  }
224
308
  });
225
309
  await fm.startCpuProfileControl().catch(err => console.warn(`Local profile control unavailable: ${String(err)}`));
310
+ await fm.startSettingsConfirmationControl().catch(err => console.warn(`Local Settings confirmation unavailable: ${String(err)}`));
226
311
  if (stopping)
227
312
  return;
228
313
  cpuProfile = await fm.startEnvironmentCpuProfile();
@@ -956,13 +1041,20 @@ backend
956
1041
  const svcPath = join(homedir(), ".config/systemd/user/com.agend.fleet.service");
957
1042
  if (existsSync(svcPath)) {
958
1043
  const svc = readFileSync(svcPath, "utf-8");
959
- const match = svc.match(/ExecStart=(\S+)/);
960
- const svcBin = match?.[1] ?? "";
961
- const currentBin = es("which agend", { stdio: "pipe" }).toString().trim();
962
- if (svcBin === currentBin)
1044
+ // The CLI the unit starts — `<entry> fleet start`, or `<node> <entry> fleet start` — against this CLI's own
1045
+ // canonical entry (#1450: `agend` on PATH is the sh launcher, never what a unit records).
1046
+ const { unitCliEntry } = await import("./service-installer.js");
1047
+ const svcEntry = unitCliEntry(svc);
1048
+ let svcReal = svcEntry;
1049
+ try {
1050
+ svcReal = realpathSync(svcEntry);
1051
+ }
1052
+ catch { /* reported as is */ }
1053
+ const current = canonicalCliEntry();
1054
+ if (svcReal === current)
963
1055
  ok(`service ExecStart${" ".repeat(3)} matches current binary`);
964
1056
  else
965
- fail(`service ExecStart${" ".repeat(3)} ${svcBin} ≠ ${currentBin}`);
1057
+ fail(`service ExecStart${" ".repeat(3)} ${svcEntry || "(none)"} ≠ ${current}`);
966
1058
  // Check Restart=on-failure
967
1059
  if (svc.includes("Restart=on-failure")) {
968
1060
  ok(`service restart${" ".repeat(5)} Restart=on-failure configured`);
@@ -1377,7 +1469,9 @@ program
1377
1469
  // part of it, not a second command for the agent-session guard.
1378
1470
  env: withOrigin("agend-update"),
1379
1471
  });
1380
- if (!reportUpdateRestart(restartResult.status)) {
1472
+ const restartOutcome = reportUpdateRestart(restartResult.status);
1473
+ // "pending": systemd is still running the restart job; the replacement fleet settles the marker.
1474
+ if (restartOutcome === "failed") {
1381
1475
  // No new fleet is coming up to clear the marker — do it here, or the next
1382
1476
  // 15 minutes of genuine crashes would go unreported.
1383
1477
  if (!setUpdateProgressStage(DATA_DIR, "failed", { error: "fleet restart failed" })) {
@@ -1385,6 +1479,118 @@ program
1385
1479
  }
1386
1480
  process.exitCode = 1;
1387
1481
  }
1482
+ return restartOutcome;
1483
+ };
1484
+ const nvmSh = join(homedir(), ".nvm", "nvm.sh");
1485
+ /**
1486
+ * Activate a VERIFIED install (#1449 review), on both the update path and the already-installed retry: refresh the
1487
+ * service through the verified binary, require the authoritative service definition to start code from the
1488
+ * verified package, then restart through the verified binary — never through whatever invoked this command
1489
+ * (process.argv[1] may be another checkout). A failed target check leaves the running fleet alone.
1490
+ */
1491
+ const activateVerified = async (verified, viaNvm) => {
1492
+ const { newAgendInvocation, retireSystemCopy, activationSettled } = await import("./update-install.js");
1493
+ // What the restart came to — carried, never inferred from the exit code (pending ≠ restarted).
1494
+ let restartResult = null;
1495
+ const { activateService } = await import("./service-activation.js");
1496
+ const { getServicePath, getSystemServicePath, detectPlatform } = await import("./service-installer.js");
1497
+ const { accessSync, constants } = await import("node:fs");
1498
+ const newAgend = newAgendInvocation({ viaNvm, nvmSh }, verified.agendPath);
1499
+ // The authoritative manager, chosen as `agend restart` chooses: system unit > user unit / launchd plist > none.
1500
+ const systemUnit = getSystemServicePath();
1501
+ const ownService = systemUnit ? null : getServicePath();
1502
+ const manager = systemUnit ? { kind: "systemd", unit: "agend", user: false, unitPath: systemUnit }
1503
+ : ownService && detectPlatform() === "macos" ? { kind: "launchd", label: "com.agend.fleet", plistPath: ownService, domain: `gui/${process.getuid?.() ?? 501}` }
1504
+ : ownService ? { kind: "systemd", unit: "com.agend.fleet", user: true, unitPath: ownService }
1505
+ : { kind: "detached" };
1506
+ // C6: a failure before the fleet runs the new install puts the previous package back too (taken before npm).
1507
+ const { restorePackagePreimage, prunePreimages } = await import("./package-preimage.js");
1508
+ const rollback = verified.rollback;
1509
+ const restorePackage = rollback?.preimage ? () => {
1510
+ const back = restorePackagePreimage(rollback.root, rollback.prefix, rollback.preimage);
1511
+ return back.ok ? `The previous package (v${rollback.preimage.version}) is back in place` : `The previous package could NOT be put back: ${back.reason}`;
1512
+ } : undefined;
1513
+ const capture = (command, args) => {
1514
+ const result = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
1515
+ return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1516
+ };
1517
+ const outcome = activateService(manager, verified, {
1518
+ run: capture,
1519
+ readFile: path => { try {
1520
+ return readFileSync(path, "utf-8");
1521
+ }
1522
+ catch {
1523
+ return null;
1524
+ } },
1525
+ writeFile: (path, content) => writeFileSync(path, content),
1526
+ realpath: path => { try {
1527
+ return realpathSync(path);
1528
+ }
1529
+ catch {
1530
+ return null;
1531
+ } },
1532
+ readFirstLine: path => { try {
1533
+ return readFileSync(path, "utf-8").split("\n", 1)[0] ?? null;
1534
+ }
1535
+ catch {
1536
+ return null;
1537
+ } },
1538
+ isExecutable: path => { try {
1539
+ accessSync(path, constants.X_OK);
1540
+ return statSync(path).isFile();
1541
+ }
1542
+ catch {
1543
+ return false;
1544
+ } },
1545
+ // `agend install` activates by default; the update owns stop/start, so only the file is refreshed here.
1546
+ refresh: () => {
1547
+ const result = capture(newAgend.command, [...newAgend.args, "install", "--no-activate"]);
1548
+ if (result.status === 0)
1549
+ console.log(" ✓ Service file refreshed");
1550
+ return result;
1551
+ },
1552
+ // Through the verified binary: never this process's code, which may be the old version or another checkout.
1553
+ restart: () => {
1554
+ // The completion script embeds this version's subcommand names; --refresh only rewrites existing artifacts.
1555
+ try {
1556
+ spawnSync(newAgend.command, [...newAgend.args, "completion", "install", "--refresh"], { encoding: "utf-8", timeout: 15_000, stdio: "ignore" });
1557
+ }
1558
+ catch { /* cosmetic */ }
1559
+ restartResult = restartFleetForUpdate(newAgend.command, newAgend.args, verified.version);
1560
+ },
1561
+ log: message => console.log(message),
1562
+ restorePackage,
1563
+ });
1564
+ // Settled: after a success only this transition's preimage is kept (for a repair); a failure consumed it.
1565
+ if (outcome.ok && rollback)
1566
+ prunePreimages(rollback.prefix, rollback.preimage);
1567
+ // nvm transition: the old system copy is what the previous service ran. Only once the fleet POSITIVELY runs the
1568
+ // new install (activationSettled) is it removed; pending, failed or refused, it stays, so the old unit still starts.
1569
+ if (verified.retireSystemCopy) {
1570
+ if (activationSettled(outcome, restartResult)) {
1571
+ retireSystemCopy({ run: (command, args) => capture(command, args), log: message => console.log(message) });
1572
+ }
1573
+ else {
1574
+ console.log(" Note: the old system install was kept, since the new one is not running yet.");
1575
+ }
1576
+ }
1577
+ // The refresh above (`install --no-activate`) records a launchd plan; this activation consumed it, whatever it
1578
+ // came to — never leave one for a later `agend restart` to act on.
1579
+ if (manager.kind === "launchd") {
1580
+ const { planPath } = await import("./service-plan.js");
1581
+ planDeps().removeFile(planPath(DATA_DIR));
1582
+ }
1583
+ if (outcome.ok && outcome.via === "launchd-activation") {
1584
+ console.log(" ✓ Service restarted (launchd loaded the verified job)");
1585
+ // The new fleet clears the update marker once it is up.
1586
+ return;
1587
+ }
1588
+ if (!outcome.ok) {
1589
+ console.error(outcome.message);
1590
+ if (!setUpdateProgressStage(DATA_DIR, "failed", { error: outcome.message.trim() }))
1591
+ clearUpdateMarker(DATA_DIR);
1592
+ process.exitCode = 1;
1593
+ }
1388
1594
  };
1389
1595
  if (shouldSkipUpdate(pkgVersion, targetVersion, opts.force)) {
1390
1596
  // Installed already — but is the running fleet? An update whose restart
@@ -1407,9 +1613,27 @@ program
1407
1613
  processStartMs: pid => processStartMs(pid),
1408
1614
  isFleetProcess: pid => isFleetStartCommandLine(readProcessCommandLine(pid)),
1409
1615
  })) {
1410
- console.log(`\n ✓ v${pkgVersion} is installed, but the running fleet started before it was — restarting it onto v${pkgVersion}.\n`);
1616
+ console.log(`\n v${pkgVersion} is installed, but the running fleet started before it was — verifying it before restarting onto it.`);
1617
+ // The earlier update may have stopped exactly because this install failed verification (#1449 review): a
1618
+ // second `agend update` must not restart onto it unchecked.
1619
+ const { verifyInstalledPackage } = await import("./update-install.js");
1620
+ const verified = verifyInstalledPackage({ pkg: `@songsid/agend@${pkgVersion}`, targetVersion: pkgVersion, viaNvm: false, nvmSh }, {
1621
+ run: (command, args, options = {}) => {
1622
+ const result = spawnSync(command, args, { encoding: "utf-8", timeout: options.timeoutMs, stdio: ["ignore", "pipe", "pipe"] });
1623
+ return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1624
+ },
1625
+ log: message => console.log(message),
1626
+ });
1627
+ if (!verified.ok) {
1628
+ const message = `${verified.message}\n Not restarting the fleet onto it. Reinstall a working version: npm install -g @songsid/agend@<version>`;
1629
+ console.error(message);
1630
+ setUpdateProgressStage(DATA_DIR, "failed", { error: message.trim() });
1631
+ process.exitCode = 1;
1632
+ return;
1633
+ }
1634
+ console.log(` ✓ v${pkgVersion} verified — restarting the fleet onto it.\n`);
1411
1635
  markUpdateInProgress(DATA_DIR);
1412
- restartFleetForUpdate(process.execPath, [process.argv[1]], pkgVersion);
1636
+ await activateVerified(verified, false);
1413
1637
  return;
1414
1638
  }
1415
1639
  console.log(`\n ✓ Already up to date (v${pkgVersion})\n`);
@@ -1445,24 +1669,6 @@ program
1445
1669
  process.exit(1);
1446
1670
  };
1447
1671
  setUpdateProgressStage(DATA_DIR, "downloading");
1448
- // Detect and remove stale npm link (local build that shadows global install)
1449
- try {
1450
- const agendPath = execSync("which agend", { encoding: "utf-8", stdio: "pipe" }).trim();
1451
- const resolved = execSync(`readlink -f "${agendPath}"`, { encoding: "utf-8", stdio: "pipe" }).trim();
1452
- if (resolved.includes("/Projects/") || resolved.includes("@songsid") || resolved.includes("@suzuke") || resolved.includes("/src/")) {
1453
- console.log(` ⚠️ Detected local npm link: ${resolved}`);
1454
- console.log(" Removing link to allow global install...\n");
1455
- try {
1456
- execSync("npm unlink -g @suzuke/agend", { stdio: "pipe", timeout: 15000 });
1457
- }
1458
- catch { }
1459
- try {
1460
- execSync("npm unlink -g @songsid/agend", { stdio: "pipe", timeout: 15000 });
1461
- }
1462
- catch { }
1463
- }
1464
- }
1465
- catch { /* which/readlink failed — no agend installed, fine */ }
1466
1672
  // ── Check if npm global needs sudo ──
1467
1673
  let needsSudo = false;
1468
1674
  try {
@@ -1478,8 +1684,6 @@ program
1478
1684
  catch { /* assume no sudo needed */ }
1479
1685
  if (needsSudo) {
1480
1686
  // ── nvm path: install without sudo ──
1481
- const nvmDir = join(homedir(), ".nvm");
1482
- const nvmSh = join(nvmDir, "nvm.sh");
1483
1687
  if (!existsSync(nvmSh)) {
1484
1688
  console.log(" Installing nvm (npm global requires sudo)...");
1485
1689
  try {
@@ -1490,89 +1694,56 @@ program
1490
1694
  }
1491
1695
  }
1492
1696
  console.log(" Using nvm to install Node 22...");
1493
- const nvmPrefix = `source ${nvmSh} && nvm install 22 && nvm use 22`;
1494
- try {
1495
- execSync(`bash -c '${nvmPrefix} && npm install -g ${pkg}'`, { stdio: "inherit" });
1496
- }
1497
- catch {
1498
- failUpdate(" Failed to install via nvm.");
1499
- }
1500
- // Try to remove old system binary
1501
- console.log(" Note: removing old system install (may require sudo)...");
1502
- // Never wait for a password in an agent/non-interactive terminal. This is
1503
- // best-effort cleanup; the new nvm install already succeeded.
1504
- spawnSync("sudo", ["-n", "npm", "uninstall", "-g", "@songsid/agend"], {
1505
- stdio: "inherit",
1506
- timeout: 10_000,
1507
- });
1508
- }
1509
- else {
1510
- // ── Direct install ──
1511
- try {
1512
- execSync(`npm install -g ${pkg}`, { stdio: "inherit" });
1513
- }
1514
- catch {
1515
- failUpdate(` Failed to update. Try: npm install -g ${pkg}`);
1516
- }
1517
- }
1518
- // ── Verify installation ──
1519
- console.log("\n Verifying installation...");
1520
- const nvmSh2 = join(homedir(), ".nvm", "nvm.sh");
1521
- const agendPath = needsSudo
1522
- ? spawnSync("bash", ["-c", `source ${nvmSh2} && nvm use 22 > /dev/null 2>&1 && which agend`], { encoding: "utf-8" }).stdout?.trim()
1523
- : spawnSync("which", ["agend"], { encoding: "utf-8" }).stdout?.trim();
1524
- if (!agendPath) {
1525
- if (needsSudo)
1526
- console.error(" You may need to add nvm to your shell profile and restart.");
1527
- failUpdate(" ✗ Verification failed: agend not found in PATH after install.");
1528
- }
1529
- const verifyResult = spawnSync(agendPath, ["--version"], { encoding: "utf-8", timeout: 5000 });
1530
- if (verifyResult.status !== 0) {
1531
- failUpdate(" ✗ Verification failed: agend --version returned error.");
1532
- }
1533
- const newVersion = (verifyResult.stdout ?? "").trim();
1534
- console.log(` ✓ Installed: ${newVersion}`);
1535
- setUpdateProgressStage(DATA_DIR, "installed", { version: newVersion.replace(/^v/, "") });
1536
- // ── Update service file ──
1537
- if (agendPath) {
1538
- try {
1539
- // Use the NEW binary to install service (old binary's templates may be deleted)
1540
- // `agend install` activates by default. During update the existing
1541
- // restart stage owns stop/start and progress reporting, so only refresh
1542
- // the service file here to avoid restarting the fleet twice.
1543
- const installResult = spawnSync(agendPath, ["install", "--no-activate"], { encoding: "utf-8", timeout: 15000 });
1544
- if (installResult.status === 0) {
1545
- console.log(` ✓ Service updated`);
1546
- }
1547
- else {
1548
- console.log(` ⚠ Service file update failed (non-fatal): ${(installResult.stderr || installResult.stdout || "unknown error").trim()}`);
1549
- }
1550
- }
1551
- catch (e) {
1552
- console.log(` ⚠ Service file update failed (non-fatal): ${e.message}`);
1553
- }
1554
- }
1555
- // ── Refresh installed shell completions ──
1556
- // The completion script embeds this version's subcommand names, so a
1557
- // previously installed static file goes stale on update. --refresh only
1558
- // rewrites artifacts that already exist — an update never starts
1559
- // installing completions the user didn't ask for. Run through the NEW
1560
- // binary so the regenerated names are the new version's.
1561
- try {
1562
- spawnSync(agendPath, ["completion", "install", "--refresh"], {
1563
- encoding: "utf-8",
1564
- timeout: 15_000,
1565
- stdio: "ignore",
1566
- });
1567
1697
  }
1568
- catch { /* cosmetic — never block an update on completion refresh */ }
1569
- // ── Restart fleet ──
1570
- // Run the restart through the NEWLY-INSTALLED binary (agendPath), not inline.
1571
- // The inline restart would execute the OLD binary's code — exactly the logic
1572
- // that may be missing or buggy on the version being upgraded from. `agend
1573
- // restart` (new binary) does the 4-environment service detection (system
1574
- // systemd → user systemd → launchd → detached pid).
1575
- restartFleetForUpdate(agendPath, [], newVersion.replace(/^v/, ""));
1698
+ // ── Install, then verify, then clean up (#1446): nothing is removed before the new install is proven ──
1699
+ // #1450 C6: npm replaces THIS package in place. Everything the rest of the update runs — verification, rollback,
1700
+ // service work — is loaded now, before npm; a later import would load the NEW package's files into this process.
1701
+ await Promise.all([
1702
+ import("./service-activation.js"), import("./service-installer.js"), import("./service-plan.js"),
1703
+ import("./restart-guard.js"), import("./package-preimage.js"), import("./install-lock.js"), import("./update-check.js"),
1704
+ ]);
1705
+ const { runUpdateInstall } = await import("./update-install.js");
1706
+ // #1450 C1: this update owns the npm prefix from before npm runs until it settles (verify, service, restart) —
1707
+ // released when this process exits, however it exits; a crash leaves a lock the next update reclaims.
1708
+ const { acquireInstallLock } = await import("./install-lock.js");
1709
+ const { randomBytes } = await import("node:crypto");
1710
+ const { createRequire } = await import("node:module");
1711
+ const admission = createRequire(import.meta.url)("../launcher/install-admission.cjs");
1712
+ let releaseLock = () => { };
1713
+ process.once("exit", () => releaseLock());
1714
+ const installed = runUpdateInstall({
1715
+ pkg, targetVersion, viaNvm: needsSudo, nvmSh, rollback: true,
1716
+ lock: prefix => {
1717
+ const lock = acquireInstallLock(prefix, { spec: pkg, agendHome: DATA_DIR }, {
1718
+ pid: process.pid, processStart: admission.processStart, newToken: () => randomBytes(16).toString("hex"),
1719
+ now: () => new Date(), log: message => console.log(message),
1720
+ });
1721
+ if (!lock.ok)
1722
+ return lock;
1723
+ releaseLock = lock.release;
1724
+ return { ok: true, token: lock.token };
1725
+ },
1726
+ }, {
1727
+ run: (command, args, options = {}) => {
1728
+ if (options.inherit)
1729
+ console.log("");
1730
+ const result = spawnSync(command, args, {
1731
+ encoding: "utf-8",
1732
+ timeout: options.timeoutMs,
1733
+ stdio: options.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
1734
+ ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
1735
+ });
1736
+ return { status: result.status, signal: result.signal, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
1737
+ },
1738
+ log: message => console.log(message),
1739
+ });
1740
+ if (!installed.ok)
1741
+ return failUpdate(installed.message);
1742
+ const agendPath = installed.agendPath;
1743
+ const newVersion = installed.version;
1744
+ console.log(`\n ✓ Installed: v${newVersion} (${agendPath}; opened a database on its Node)`);
1745
+ setUpdateProgressStage(DATA_DIR, "installed", { version: newVersion });
1746
+ await activateVerified(installed, needsSudo);
1576
1747
  });
1577
1748
  program
1578
1749
  .command("reload")
@@ -1600,15 +1771,45 @@ program
1600
1771
  .option("--activate", "Deprecated; service activation is now the default")
1601
1772
  .option("--no-activate", "Write the service file without activating it")
1602
1773
  .action(async (opts) => {
1603
- const { installService, activateService, detectPlatform } = await import("./service-installer.js");
1604
- const execPath = process.argv[1];
1605
- const svcPath = installService({
1774
+ const { installService, activateService, detectPlatform, renderLaunchdPlist, launchdPlistPath } = await import("./service-installer.js");
1775
+ const execPath = canonicalCliEntry();
1776
+ const serviceVars = {
1606
1777
  label: "com.agend.fleet",
1607
1778
  execPath,
1608
1779
  path: process.env.PATH,
1609
1780
  workingDirectory: DATA_DIR,
1610
1781
  logPath: join(DATA_DIR, "fleet.log"),
1611
- });
1782
+ };
1783
+ // #1450 C6: on launchd, writing a new plist without activating is a PLANNED activation — proven, recorded, and
1784
+ // left for `agend restart` (a reload there would itself start the job). See service-plan.ts.
1785
+ if (opts.activate === false && detectPlatform() === "macos") {
1786
+ const { refreshLaunchdWithoutActivating, planPath } = await import("./service-plan.js");
1787
+ // A refresh revokes any earlier plan first, whatever it comes to (a failed refresh leaves nothing to activate).
1788
+ planDeps().removeFile(planPath(DATA_DIR));
1789
+ const { expectedTuple } = await import("./restart-guard.js");
1790
+ const expectation = expectedTuple();
1791
+ if (!expectation.ok) {
1792
+ console.error(` ✗ ${expectation.reason}`);
1793
+ process.exitCode = 1;
1794
+ return;
1795
+ }
1796
+ const uid = process.getuid?.() ?? 501;
1797
+ const plistPath = launchdPlistPath(serviceVars.label);
1798
+ mkdirSync(dirname(plistPath), { recursive: true });
1799
+ const outcome = refreshLaunchdWithoutActivating({
1800
+ label: serviceVars.label, target: `gui/${uid}/${serviceVars.label}`, plistPath, newPlist: renderLaunchdPlist(serviceVars),
1801
+ expected: expectation.expected, pkgDir: dirname(dirname(execPath)), agendHome: DATA_DIR,
1802
+ }, planDeps());
1803
+ if (!outcome.ok) {
1804
+ console.error(` ✗ ${outcome.message}`);
1805
+ process.exitCode = 1;
1806
+ return;
1807
+ }
1808
+ console.log(t("install.path", plistPath));
1809
+ console.log(` ${outcome.message}`);
1810
+ return;
1811
+ }
1812
+ const svcPath = installService(serviceVars);
1612
1813
  console.log(t("install.path", svcPath));
1613
1814
  if (opts.activate !== false) {
1614
1815
  const pidPath = join(DATA_DIR, "fleet.pid");
@@ -1743,7 +1944,9 @@ program
1743
1944
  if (!userServicePath && !systemServicePath) {
1744
1945
  console.log("No service installed. Starting fleet directly...");
1745
1946
  const { spawn } = await import("node:child_process");
1746
- const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
1947
+ // This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
1948
+ const start = selfCommand(["fleet", "start"]);
1949
+ const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
1747
1950
  child.unref();
1748
1951
  console.log("Fleet starting in background.");
1749
1952
  return;
@@ -1764,15 +1967,56 @@ program
1764
1967
  .command("restart")
1765
1968
  .description("Restart the AgEnD service (auto-detects systemd/launchd/detached)")
1766
1969
  .option("-y, --yes", "Confirm when run from a fleet agent session (this affects every instance)")
1970
+ .option("--force", "Restart even when the loaded service does not start this install on its selected Node (operators only)")
1767
1971
  .action(async (opts) => {
1972
+ // --force overrides the C6 guard: an operator's own decision, never a fleet agent's or a fleet-internal spawn's.
1973
+ if (opts.force && !forceAllowed()) {
1974
+ console.error(" ✗ Not restarting: --force is for an operator's own shell, not a fleet agent session or the fleet's own commands. Nothing was stopped.");
1975
+ process.exit(1);
1976
+ }
1768
1977
  if (!gateFleetControl(DATA_DIR, "restart", { yes: opts.yes }))
1769
1978
  process.exit(1);
1979
+ // #1450 C6: what the restarted fleet must run, by this package's own selection (C2) — decided before anything stops.
1980
+ const guard = await import("./restart-guard.js");
1981
+ const guardDeps = {
1982
+ realpath: (path) => { try {
1983
+ return realpathSync(path);
1984
+ }
1985
+ catch {
1986
+ return null;
1987
+ } },
1988
+ isExecutable: (path) => { try {
1989
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
1990
+ }
1991
+ catch {
1992
+ return false;
1993
+ } },
1994
+ };
1995
+ const captureRun = (command, args) => {
1996
+ const r = spawnSync(command, args, { encoding: "utf-8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
1997
+ return { status: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
1998
+ };
1999
+ const expectation = guard.expectedTuple();
2000
+ /** Refuse (nothing stopped) unless the definition about to run is proven; `--force` overrides for an operator. */
2001
+ const refuses = (judged) => {
2002
+ if (judged.ok)
2003
+ return false;
2004
+ if (opts.force) {
2005
+ console.log(` ⚠ --force: ${judged.reason}; restarting anyway.`);
2006
+ return false;
2007
+ }
2008
+ console.error(` ✗ Not restarting: ${judged.reason}. Nothing was stopped.`);
2009
+ process.exitCode = 1;
2010
+ return true;
2011
+ };
2012
+ if (!expectation.ok && refuses(expectation))
2013
+ return;
1770
2014
  // Try each runtime environment in order and stop at the first that succeeds.
1771
2015
  // Don't gate on fleet.pid: a fleet under systemd/launchd writes its pid in the
1772
2016
  // service's own HOME, which may differ from what this command resolves (sudo,
1773
2017
  // AGEND_HOME) — an absent pid file does NOT mean it's stopped. Ask the service
1774
2018
  // manager first. This is the canonical restart the `update` flow spawns.
1775
- const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState, restartSystemdService, ensureSystemdUnitHardening, effectiveUnitDropIns, unitCoredumpFilterState, } = await import("./service-installer.js");
2019
+ const { detectPlatform, getServicePath, getSystemServicePath, getSystemdServiceState, systemdRestartOutcome, ensureSystemdUnitHardening, effectiveUnitDropIns, unitCoredumpFilterState, } = await import("./service-installer.js");
1776
2020
  const { spawn, spawnSync } = await import("node:child_process");
1777
2021
  const plat = detectPlatform();
1778
2022
  const pidPath = join(DATA_DIR, "fleet.pid");
@@ -1916,6 +2160,8 @@ program
1916
2160
  }
1917
2161
  if (filterCustom)
1918
2162
  console.log(` ⚠ ${unitPath} sets its own CoredumpFilter; left as is. CoredumpFilter=0 keeps crash dumps to a few KB (#1113).`);
2163
+ if (expectation.ok && refuses(guard.guardSystemd(captureRun, systemdTarget.user, systemdTarget.unit, expectation.expected, guardDeps)))
2164
+ return;
1919
2165
  // Clears the failed state AND the start-limit counter (#1113:
1920
2166
  // StartLimitBurst=4 in 30min) for whichever unit this restart targets,
1921
2167
  // so an operator's restart is never refused by the limit.
@@ -1923,30 +2169,87 @@ program
1923
2169
  execSync(`systemctl${systemdTarget.user ? " --user" : ""} reset-failed ${systemdTarget.unit}`, { stdio: "pipe", timeout: 5000 });
1924
2170
  }
1925
2171
  catch { /* best effort */ }
1926
- if (restartSystemdService(systemdTarget.unit, systemdTarget.user)) {
2172
+ const restartOutcome = systemdRestartOutcome(systemdTarget.unit, systemdTarget.user);
2173
+ if (restartOutcome === "restarted") {
1927
2174
  console.log(systemdTarget.user ? "Service restarted (user)." : "Service restarted (system).");
1928
2175
  return;
1929
2176
  }
1930
2177
  // An installed service remains authoritative even when this invocation
1931
2178
  // fails. Falling through would create a second detached fleet.
1932
- console.log(` ⚠ ${scope} restart reported failure, but the service exists — systemd will auto-retry.`);
1933
- console.log(` Check: ${statusCommand}`);
1934
- process.exitCode = SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE;
2179
+ if (restartOutcome === "timed-out") {
2180
+ // The job may still finish: the replacement fleet settles the update marker, not this command.
2181
+ console.log(` ⚠ ${scope} restart is still running after ${Math.round(SYSTEMD_RESTART_TIMEOUT_MS / 60_000)} minutes; systemd keeps the job going.`);
2182
+ console.log(` Check: ${statusCommand}`);
2183
+ process.exitCode = SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE;
2184
+ return;
2185
+ }
2186
+ console.error(` ✗ ${scope} restart failed (systemctl reported an error). systemd may retry it on its own.`);
2187
+ console.error(` Check: ${statusCommand}`);
2188
+ process.exitCode = 1;
1935
2189
  return;
1936
2190
  }
1937
- // 3. launchd (macOS)
2191
+ // 3. launchd (macOS). "Loaded" is launchctl print's own answer (113 = not loaded); nothing here may fall through
2192
+ // to the detached restart once a job is known to be loaded.
1938
2193
  if (plat === "macos") {
1939
2194
  const uid = process.getuid?.() ?? 501;
1940
2195
  const label = "com.agend.fleet";
1941
- try {
1942
- // print succeeds only if the service is loaded in launchd
1943
- execSync(`launchctl print gui/${uid}/${label}`, { stdio: "pipe", timeout: 5000 });
1944
- if (run(`launchctl kickstart -k gui/${uid}/${label}`)) {
2196
+ const target = `gui/${uid}/${label}`;
2197
+ const deps = planDeps();
2198
+ const printed = deps.run("launchctl", ["print", target]);
2199
+ if (printed.status === 0) {
2200
+ const plistPath = getServicePath() ?? join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
2201
+ // Path 2 (#1450 C6): a proven new plist waits on disk with its plan — exactly one bootout + bootstrap.
2202
+ const { readPlan, admitPlan, planPath } = await import("./service-plan.js");
2203
+ const plan = opts.force ? null : readPlan(DATA_DIR, deps);
2204
+ if (plan && expectation.ok) {
2205
+ const pkgDir = dirname(dirname(expectation.expected.entry));
2206
+ const admitted = admitPlan(plan, { target, expected: expectation.expected, pkgDir }, deps);
2207
+ if (!admitted.ok) {
2208
+ console.error(` ✗ Not activating the planned launchd job: ${admitted.reason}. Nothing was stopped.`);
2209
+ process.exitCode = 1;
2210
+ return;
2211
+ }
2212
+ const { activateService } = await import("./service-activation.js");
2213
+ const outcome = activateService({ kind: "launchd", label, plistPath: plan.plistPath, domain: `gui/${uid}` }, { bin: expectation.expected.entry, entry: expectation.expected.entry, node: expectation.expected.node, dir: pkgDir }, {
2214
+ ...deps,
2215
+ readFirstLine: path => deps.readFile(path)?.split("\n", 1)[0] ?? null,
2216
+ isExecutable: path => { try {
2217
+ return statSync(path).isFile() && (statSync(path).mode & 0o111) !== 0;
2218
+ }
2219
+ catch {
2220
+ return false;
2221
+ } },
2222
+ refresh: () => ({ status: 0, signal: null, stdout: "", stderr: "" }),
2223
+ restart: () => { },
2224
+ log: message => console.log(message),
2225
+ launchdPreimage: plan.preimage.plist,
2226
+ });
2227
+ deps.removeFile(planPath(DATA_DIR));
2228
+ if (outcome.ok) {
2229
+ console.log("Service restarted (launchd loaded the planned job).");
2230
+ return;
2231
+ }
2232
+ console.error(outcome.message);
2233
+ process.exitCode = 1;
2234
+ return;
2235
+ }
2236
+ // Path 1: an unchanged loaded job.
2237
+ if (expectation.ok && refuses(guard.guardLaunchd(captureRun, target, plistPath, deps.readFile, expectation.expected, guardDeps)))
2238
+ return;
2239
+ if (run(`launchctl kickstart -k ${target}`)) {
1945
2240
  console.log("Service restarted.");
1946
2241
  return;
1947
2242
  }
2243
+ console.error(` ✗ launchctl kickstart -k ${target} failed.`);
2244
+ process.exitCode = 1;
2245
+ return;
2246
+ }
2247
+ const { LAUNCHD_NOT_LOADED } = await import("./service-plan.js");
2248
+ if (!LAUNCHD_NOT_LOADED.includes(printed.status ?? -1) || printed.signal !== null) {
2249
+ console.error(` ✗ Could not tell whether ${label} is loaded (launchctl print ${printed.signal ? `killed by ${printed.signal}` : `exited ${printed.status}`}); refusing a detached restart that could duplicate the fleet.`);
2250
+ process.exitCode = 1;
2251
+ return;
1948
2252
  }
1949
- catch { /* not loaded — fall through */ }
1950
2253
  }
1951
2254
  // 4. Detached process tracked by fleet.pid (no service manager)
1952
2255
  if (existsSync(pidPath)) {
@@ -1964,6 +2267,9 @@ program
1964
2267
  return isFleetStartCommandLine(readProcessCommandLine(oldPid));
1965
2268
  };
1966
2269
  if (oldPid && isOurFleet()) {
2270
+ // The replacement is `<this Node> <entry> fleet start` (C5): this Node must be the selected one.
2271
+ if (expectation.ok && refuses(guard.guardDetached(process.execPath, expectation.expected, guardDeps)))
2272
+ return;
1967
2273
  try {
1968
2274
  process.kill(oldPid, "SIGTERM");
1969
2275
  }
@@ -1989,7 +2295,9 @@ program
1989
2295
  unlinkSync(pidPath);
1990
2296
  }
1991
2297
  catch { /* best effort */ }
1992
- const child = spawn("sh", ["-c", "agend fleet start"], { detached: true, stdio: "ignore" });
2298
+ // This Node on this CLI, not `sh -c "agend fleet start"` through PATH (#1450 C5).
2299
+ const start = selfCommand(["fleet", "start"]);
2300
+ const child = spawn(start.command, start.args, { detached: true, stdio: "ignore" });
1993
2301
  child.unref();
1994
2302
  console.log("Fleet restarting in background.");
1995
2303
  return;
@@ -2115,8 +2423,9 @@ program
2115
2423
  });
2116
2424
  program
2117
2425
  .command("web")
2118
- .description("Open the Web UI dashboard in your browser")
2119
- .action(async () => {
2426
+ .description("Open the Web UI sign-in page in your browser and print a one-time sign-in code")
2427
+ .option("--code", "Only print the sign-in page and code; do not open a browser")
2428
+ .action(async (opts) => {
2120
2429
  const tokenPath = join(DATA_DIR, "web.token");
2121
2430
  if (!existsSync(tokenPath)) {
2122
2431
  console.error("Web token not found. Is the fleet running?");
@@ -2126,20 +2435,36 @@ program
2126
2435
  const { loadFleetConfig } = await import("./config.js");
2127
2436
  const fleet = loadFleetConfig(FLEET_CONFIG_PATH);
2128
2437
  const port = fleet.health_port ?? 19280;
2129
- const url = `http://localhost:${port}/ui?token=${encodeURIComponent(token)}`;
2130
- console.log(`Opening ${url}`);
2131
- // The token is sensitive: passing it on argv would expose it via `ps`,
2132
- // and exec(`${cmd} "${url}"`) additionally goes through a shell. Instead,
2133
- // write a 0600-mode HTML redirect into a per-user temp dir and open that
2134
- // file path — the token only ever lives on disk under user-only perms.
2135
- const { mkdtempSync } = await import("node:fs");
2136
- const { tmpdir } = await import("node:os");
2137
- const tmpDir = mkdtempSync(join(tmpdir(), "agend-web-"));
2138
- const htmlPath = join(tmpDir, "open.html");
2139
- const htmlUrl = url.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2140
- writeFileSync(htmlPath, `<!doctype html><meta http-equiv="refresh" content="0; url=${htmlUrl}">`, { mode: 0o600 });
2438
+ // The code is minted by the running fleet (it lives in that process's memory), so ask it — with
2439
+ // the header token, the one credential a local script has. No credential ever goes into a URL:
2440
+ // the browser is opened on the sign-in page and the code is typed there.
2441
+ let code;
2442
+ let minutes;
2443
+ try {
2444
+ const resp = await fetch(`http://127.0.0.1:${port}/auth/issue-code`, {
2445
+ method: "POST",
2446
+ headers: { "X-Agend-Token": token },
2447
+ signal: AbortSignal.timeout(5000),
2448
+ });
2449
+ const body = await resp.json().catch(() => ({}));
2450
+ if (!resp.ok || !body.code) {
2451
+ console.error(`Could not get a sign-in code: ${body.error ?? resp.statusText}`);
2452
+ process.exit(1);
2453
+ }
2454
+ code = body.code;
2455
+ minutes = Math.max(1, Math.round(((body.expiresAt ?? Date.now()) - Date.now()) / 60_000));
2456
+ }
2457
+ catch {
2458
+ console.error(`Cannot connect to fleet (port ${port}). Is the fleet running?`);
2459
+ process.exit(1);
2460
+ }
2461
+ const signin = `http://localhost:${port}/signin`;
2462
+ console.log(`Sign-in page: ${signin}`);
2463
+ console.log(`Code: ${code} (works once, valid ~${minutes} min)`);
2464
+ if (opts.code)
2465
+ return;
2141
2466
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2142
- const child = spawn(cmd, [htmlPath], { detached: true, stdio: "ignore" });
2467
+ const child = spawn(cmd, [signin], { detached: true, stdio: "ignore" });
2143
2468
  child.unref();
2144
2469
  });
2145
2470
  program
@@ -2158,7 +2483,7 @@ program
2158
2483
  console.log("Web token rotated.");
2159
2484
  console.log("Every previously issued dashboard link and browser session is now rejected.");
2160
2485
  console.log("A running fleet picks this up immediately — no restart needed.");
2161
- console.log("Get a new link with `agend web`, or /dashboard in your chat channel.");
2486
+ console.log("Sign in again with `agend web` (prints a one-time code), or /dashboard in your chat channel.");
2162
2487
  });
2163
2488
  program
2164
2489
  .command("view")