@phnx-labs/agents-cli 1.22.34 → 1.22.37

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 (493) hide show
  1. package/CHANGELOG.md +672 -2
  2. package/README.md +82 -35
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.d.ts +15 -0
  5. package/dist/bootstrap.js +1211 -0
  6. package/dist/commands/accounts.d.ts +4 -11
  7. package/dist/commands/accounts.js +159 -27
  8. package/dist/commands/attach.js +17 -4
  9. package/dist/commands/audit.d.ts +8 -8
  10. package/dist/commands/audit.js +57 -47
  11. package/dist/commands/browser.js +15 -11
  12. package/dist/commands/cloud.js +1 -1
  13. package/dist/commands/commands.js +0 -11
  14. package/dist/commands/computer-actions.js +33 -0
  15. package/dist/commands/computer-sessions-picker.d.ts +21 -0
  16. package/dist/commands/computer-sessions-picker.js +141 -0
  17. package/dist/commands/computer.d.ts +16 -0
  18. package/dist/commands/computer.js +37 -0
  19. package/dist/commands/config.d.ts +17 -0
  20. package/dist/commands/config.js +401 -0
  21. package/dist/commands/cp.d.ts +84 -0
  22. package/dist/commands/cp.js +231 -0
  23. package/dist/commands/daemon.js +243 -14
  24. package/dist/commands/detach-core.d.ts +1 -1
  25. package/dist/commands/detach-core.js +12 -2
  26. package/dist/commands/detach.js +1 -1
  27. package/dist/commands/doctor.js +64 -5
  28. package/dist/commands/events.d.ts +24 -12
  29. package/dist/commands/events.js +144 -28
  30. package/dist/commands/exec.js +85 -41
  31. package/dist/commands/feed.d.ts +1 -1
  32. package/dist/commands/feed.js +16 -8
  33. package/dist/commands/focus.d.ts +33 -1
  34. package/dist/commands/focus.js +177 -17
  35. package/dist/commands/funnel.d.ts +1 -1
  36. package/dist/commands/go.js +9 -8
  37. package/dist/commands/harness-wizard.d.ts +1 -0
  38. package/dist/commands/harness-wizard.js +18 -20
  39. package/dist/commands/harness.d.ts +2 -0
  40. package/dist/commands/harness.js +59 -26
  41. package/dist/commands/hooks.js +0 -11
  42. package/dist/commands/inspect.d.ts +94 -1
  43. package/dist/commands/inspect.js +866 -40
  44. package/dist/commands/lease.d.ts +4 -4
  45. package/dist/commands/lease.js +6 -6
  46. package/dist/commands/login.js +0 -13
  47. package/dist/commands/logs.d.ts +2 -2
  48. package/dist/commands/logs.js +2 -2
  49. package/dist/commands/mcp.js +7 -2
  50. package/dist/commands/memory.js +7 -2
  51. package/dist/commands/message.d.ts +1 -1
  52. package/dist/commands/message.js +37 -6
  53. package/dist/commands/models.js +4 -0
  54. package/dist/commands/monitors.d.ts +0 -1
  55. package/dist/commands/monitors.js +199 -16
  56. package/dist/commands/open.d.ts +18 -0
  57. package/dist/commands/open.js +57 -0
  58. package/dist/commands/packages.js +2 -107
  59. package/dist/commands/plugins.js +6 -3
  60. package/dist/commands/profiles.d.ts +1 -0
  61. package/dist/commands/profiles.js +16 -160
  62. package/dist/commands/projects.d.ts +28 -0
  63. package/dist/commands/projects.js +125 -5
  64. package/dist/commands/reconnect.js +9 -3
  65. package/dist/commands/repo.js +42 -1
  66. package/dist/commands/resource-view.d.ts +17 -0
  67. package/dist/commands/resource-view.js +36 -14
  68. package/dist/commands/resume.js +9 -2
  69. package/dist/commands/routines.d.ts +0 -2
  70. package/dist/commands/routines.js +473 -289
  71. package/dist/commands/secrets.js +52 -9
  72. package/dist/commands/sessions-export.d.ts +47 -0
  73. package/dist/commands/sessions-export.js +116 -6
  74. package/dist/commands/sessions-import.d.ts +17 -0
  75. package/dist/commands/sessions-import.js +114 -6
  76. package/dist/commands/sessions-migrate.d.ts +27 -0
  77. package/dist/commands/sessions-migrate.js +50 -9
  78. package/dist/commands/sessions-picker.js +23 -4
  79. package/dist/commands/sessions-resume.d.ts +5 -3
  80. package/dist/commands/sessions-resume.js +30 -8
  81. package/dist/commands/sessions-watch.d.ts +2 -0
  82. package/dist/commands/sessions-watch.js +53 -0
  83. package/dist/commands/sessions.d.ts +66 -3
  84. package/dist/commands/sessions.js +298 -47
  85. package/dist/commands/set.d.ts +1 -1
  86. package/dist/commands/set.js +2 -2
  87. package/dist/commands/setup-browser.js +2 -2
  88. package/dist/commands/setup-mine.js +0 -1
  89. package/dist/commands/setup-preferences.js +1 -1
  90. package/dist/commands/setup-share.js +34 -4
  91. package/dist/commands/setup.d.ts +5 -0
  92. package/dist/commands/setup.js +26 -0
  93. package/dist/commands/share.d.ts +85 -0
  94. package/dist/commands/share.js +379 -8
  95. package/dist/commands/skills.js +0 -11
  96. package/dist/commands/snapshot.js +1 -1
  97. package/dist/commands/ssh.d.ts +18 -0
  98. package/dist/commands/ssh.js +549 -210
  99. package/dist/commands/sync.d.ts +5 -0
  100. package/dist/commands/sync.js +212 -32
  101. package/dist/commands/teams.d.ts +13 -0
  102. package/dist/commands/teams.js +187 -30
  103. package/dist/commands/tickets.d.ts +2 -0
  104. package/dist/commands/tickets.js +43 -0
  105. package/dist/commands/update.js +2 -4
  106. package/dist/commands/usage.js +5 -3
  107. package/dist/commands/versions.js +11 -5
  108. package/dist/commands/view.js +15 -7
  109. package/dist/commands/webhook.d.ts +2 -2
  110. package/dist/commands/webhook.js +7 -7
  111. package/dist/index.d.ts +18 -2
  112. package/dist/index.js +53 -1264
  113. package/dist/lib/account-catalog.d.ts +18 -0
  114. package/dist/lib/account-catalog.js +38 -0
  115. package/dist/lib/account-provider-registry.d.ts +18 -0
  116. package/dist/lib/account-provider-registry.js +59 -0
  117. package/dist/lib/account-registry.d.ts +40 -0
  118. package/dist/lib/account-registry.js +239 -0
  119. package/dist/lib/account-schema.d.ts +44 -0
  120. package/dist/lib/account-schema.js +91 -0
  121. package/dist/lib/account-state-service.d.ts +21 -0
  122. package/dist/lib/account-state-service.js +60 -0
  123. package/dist/lib/activity.d.ts +1 -1
  124. package/dist/lib/activity.js +8 -1
  125. package/dist/lib/add-dir.d.ts +80 -0
  126. package/dist/lib/add-dir.js +241 -0
  127. package/dist/lib/agent-cli-commands.d.ts +11 -0
  128. package/dist/lib/agent-cli-commands.js +30 -0
  129. package/dist/lib/agents.js +44 -36
  130. package/dist/lib/answer-router.js +1 -1
  131. package/dist/lib/audit/log.d.ts +10 -27
  132. package/dist/lib/audit/log.js +20 -33
  133. package/dist/lib/auth-health.d.ts +9 -2
  134. package/dist/lib/auth-health.js +42 -9
  135. package/dist/lib/auto-pull-worker.js +7 -3
  136. package/dist/lib/auto-pull.d.ts +44 -3
  137. package/dist/lib/auto-pull.js +88 -5
  138. package/dist/lib/binary-shadow.d.ts +28 -0
  139. package/dist/lib/binary-shadow.js +121 -0
  140. package/dist/lib/brand.js +9 -3
  141. package/dist/lib/browser/ipc.d.ts +30 -0
  142. package/dist/lib/browser/ipc.js +81 -4
  143. package/dist/lib/browser/profiles.d.ts +3 -2
  144. package/dist/lib/browser/profiles.js +65 -22
  145. package/dist/lib/browser/service.d.ts +4 -0
  146. package/dist/lib/browser/service.js +30 -3
  147. package/dist/lib/browser/sessions-list.d.ts +22 -2
  148. package/dist/lib/browser/sessions-list.js +54 -6
  149. package/dist/lib/browser/stream.d.ts +2 -0
  150. package/dist/lib/browser/stream.js +1 -0
  151. package/dist/lib/browser/types.d.ts +19 -0
  152. package/dist/lib/byok-usage.d.ts +9 -2
  153. package/dist/lib/byok-usage.js +133 -31
  154. package/dist/lib/cli-entry.d.ts +11 -0
  155. package/dist/lib/cli-entry.js +46 -0
  156. package/dist/lib/cloud/host.d.ts +1 -1
  157. package/dist/lib/cloud/host.js +2 -2
  158. package/dist/lib/cloud/registry.js +1 -1
  159. package/dist/lib/cloud/types.d.ts +1 -1
  160. package/dist/lib/codex-policy.d.ts +9 -1
  161. package/dist/lib/codex-policy.js +17 -2
  162. package/dist/lib/commands.d.ts +2 -0
  163. package/dist/lib/commands.js +2 -0
  164. package/dist/lib/computer/sessions-list.d.ts +150 -0
  165. package/dist/lib/computer/sessions-list.js +363 -0
  166. package/dist/lib/config-keys.d.ts +86 -0
  167. package/dist/lib/config-keys.js +193 -0
  168. package/dist/lib/config-machine-keys.d.ts +12 -0
  169. package/dist/lib/config-machine-keys.js +17 -0
  170. package/dist/lib/config-transfer.js +3 -4
  171. package/dist/lib/crabbox/cli.d.ts +2 -2
  172. package/dist/lib/crabbox/cli.js +3 -3
  173. package/dist/lib/daemon-health.d.ts +22 -0
  174. package/dist/lib/daemon-health.js +33 -3
  175. package/dist/lib/daemon-services.d.ts +50 -0
  176. package/dist/lib/daemon-services.js +147 -0
  177. package/dist/lib/daemon-ticks.d.ts +35 -66
  178. package/dist/lib/daemon-ticks.js +77 -159
  179. package/dist/lib/daemon.d.ts +72 -4
  180. package/dist/lib/daemon.js +792 -202
  181. package/dist/lib/deeplink/register.d.ts +49 -0
  182. package/dist/lib/deeplink/register.js +253 -0
  183. package/dist/lib/deeplink/url.d.ts +34 -0
  184. package/dist/lib/deeplink/url.js +78 -0
  185. package/dist/lib/device-config.d.ts +124 -29
  186. package/dist/lib/device-config.js +346 -141
  187. package/dist/lib/devices/config-migration.d.ts +42 -0
  188. package/dist/lib/devices/config-migration.js +203 -0
  189. package/dist/lib/devices/connect.js +14 -7
  190. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  191. package/dist/lib/devices/doctor-findings.js +20 -1
  192. package/dist/lib/devices/harness-inventory.d.ts +11 -2
  193. package/dist/lib/devices/harness-inventory.js +0 -0
  194. package/dist/lib/devices/health.d.ts +1 -1
  195. package/dist/lib/devices/health.js +1 -1
  196. package/dist/lib/devices/pending.d.ts +22 -1
  197. package/dist/lib/devices/pending.js +67 -2
  198. package/dist/lib/devices/registry.d.ts +0 -25
  199. package/dist/lib/devices/registry.js +2 -83
  200. package/dist/lib/devices/resolve-profile.d.ts +20 -0
  201. package/dist/lib/devices/resolve-profile.js +44 -0
  202. package/dist/lib/devices/rollout-verify.d.ts +101 -0
  203. package/dist/lib/devices/rollout-verify.js +193 -0
  204. package/dist/lib/devices/ssh-config.js +10 -6
  205. package/dist/lib/event-families.d.ts +28 -0
  206. package/dist/lib/event-families.js +159 -0
  207. package/dist/lib/event-stream.d.ts +9 -1
  208. package/dist/lib/event-stream.js +26 -9
  209. package/dist/lib/events.d.ts +9 -1
  210. package/dist/lib/events.js +21 -6
  211. package/dist/lib/exec.d.ts +30 -0
  212. package/dist/lib/exec.js +220 -122
  213. package/dist/lib/feed-broadcast.d.ts +25 -0
  214. package/dist/lib/feed-broadcast.js +33 -0
  215. package/dist/lib/feed.d.ts +1 -1
  216. package/dist/lib/feed.js +12 -2
  217. package/dist/lib/fleet/capture.js +15 -0
  218. package/dist/lib/fleet/manifest.js +9 -0
  219. package/dist/lib/fleet/types.d.ts +9 -0
  220. package/dist/lib/fleet-status.js +10 -8
  221. package/dist/lib/git.d.ts +20 -6
  222. package/dist/lib/git.js +121 -17
  223. package/dist/lib/hosts/dispatch.d.ts +8 -3
  224. package/dist/lib/hosts/dispatch.js +29 -3
  225. package/dist/lib/hosts/logs.d.ts +2 -2
  226. package/dist/lib/hosts/logs.js +2 -2
  227. package/dist/lib/hosts/option.js +1 -1
  228. package/dist/lib/hosts/passthrough.d.ts +2 -3
  229. package/dist/lib/hosts/passthrough.js +10 -24
  230. package/dist/lib/hosts/progress.js +2 -2
  231. package/dist/lib/hosts/providers/devices.d.ts +3 -3
  232. package/dist/lib/hosts/providers/devices.js +18 -12
  233. package/dist/lib/hosts/ready.d.ts +5 -1
  234. package/dist/lib/hosts/ready.js +9 -3
  235. package/dist/lib/hosts/reconcile.d.ts +2 -2
  236. package/dist/lib/hosts/reconcile.js +2 -2
  237. package/dist/lib/hosts/registry.js +14 -9
  238. package/dist/lib/hosts/remote-cmd.js +1 -1
  239. package/dist/lib/hosts/remote-os.d.ts +9 -6
  240. package/dist/lib/hosts/remote-os.js +13 -6
  241. package/dist/lib/hosts/routing-flag.d.ts +23 -0
  242. package/dist/lib/hosts/routing-flag.js +41 -0
  243. package/dist/lib/hosts/run-target.js +1 -1
  244. package/dist/lib/hosts/session-index.js +1 -1
  245. package/dist/lib/hosts/tasks.d.ts +6 -6
  246. package/dist/lib/hosts/tasks.js +4 -4
  247. package/dist/lib/hosts/types.js +1 -2
  248. package/dist/lib/installations/resolve.js +2 -8
  249. package/dist/lib/isolation-boundary-report.js +0 -1
  250. package/dist/lib/linear-cache.d.ts +7 -0
  251. package/dist/lib/linear-cache.js +31 -4
  252. package/dist/lib/linear-project-counts.d.ts +1 -1
  253. package/dist/lib/linear-project-counts.js +2 -3
  254. package/dist/lib/linear-projects.js +1 -1
  255. package/dist/lib/loop.d.ts +2 -2
  256. package/dist/lib/loop.js +1 -1
  257. package/dist/lib/mailbox-target.d.ts +1 -1
  258. package/dist/lib/mailbox-target.js +1 -1
  259. package/dist/lib/manifest.js +7 -4
  260. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  261. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  262. package/dist/lib/menubar/snapshot.d.ts +3 -2
  263. package/dist/lib/menubar/snapshot.js +6 -2
  264. package/dist/lib/migrate-fold.d.ts +33 -0
  265. package/dist/lib/migrate-fold.js +130 -0
  266. package/dist/lib/migrate.d.ts +34 -34
  267. package/dist/lib/migrate.js +106 -128
  268. package/dist/lib/model-tier-overrides.d.ts +1 -1
  269. package/dist/lib/model-tier-overrides.js +15 -2
  270. package/dist/lib/monitors/config.d.ts +17 -3
  271. package/dist/lib/monitors/config.js +52 -18
  272. package/dist/lib/monitors/engine.d.ts +32 -1
  273. package/dist/lib/monitors/engine.js +94 -11
  274. package/dist/lib/monitors/fingerprint.d.ts +46 -0
  275. package/dist/lib/monitors/fingerprint.js +100 -0
  276. package/dist/lib/monitors/remote.d.ts +47 -0
  277. package/dist/lib/monitors/remote.js +81 -0
  278. package/dist/lib/monitors/state.d.ts +39 -0
  279. package/dist/lib/monitors/state.js +56 -0
  280. package/dist/lib/onepassword.d.ts +17 -0
  281. package/dist/lib/onepassword.js +52 -22
  282. package/dist/lib/open-url.js +1 -1
  283. package/dist/lib/picker.d.ts +14 -2
  284. package/dist/lib/picker.js +35 -13
  285. package/dist/lib/placement.d.ts +1 -1
  286. package/dist/lib/placement.js +1 -1
  287. package/dist/lib/pr-land-detach.d.ts +32 -0
  288. package/dist/lib/pr-land-detach.js +79 -0
  289. package/dist/lib/profiles-presets.js +13 -0
  290. package/dist/lib/profiles.d.ts +2 -0
  291. package/dist/lib/profiles.js +44 -1
  292. package/dist/lib/project-focus.d.ts +1 -1
  293. package/dist/lib/project-focus.js +1 -1
  294. package/dist/lib/project-key.d.ts +17 -0
  295. package/dist/lib/project-key.js +26 -0
  296. package/dist/lib/project-probe.d.ts +4 -1
  297. package/dist/lib/project-probe.js +5 -4
  298. package/dist/lib/project-resources.js +2 -18
  299. package/dist/lib/project-root.d.ts +16 -0
  300. package/dist/lib/project-root.js +20 -2
  301. package/dist/lib/projects.d.ts +20 -1
  302. package/dist/lib/projects.js +78 -1
  303. package/dist/lib/refresh-coordinator.d.ts +24 -0
  304. package/dist/lib/refresh-coordinator.js +52 -0
  305. package/dist/lib/registry.d.ts +2 -2
  306. package/dist/lib/registry.js +1 -1
  307. package/dist/lib/resource-aliases.d.ts +13 -0
  308. package/dist/lib/resource-aliases.js +26 -0
  309. package/dist/lib/resources/types.d.ts +1 -1
  310. package/dist/lib/resources.d.ts +16 -0
  311. package/dist/lib/resources.js +66 -10
  312. package/dist/lib/rotate.d.ts +3 -4
  313. package/dist/lib/rotate.js +4 -5
  314. package/dist/lib/routine-activation.d.ts +22 -2
  315. package/dist/lib/routine-activation.js +63 -16
  316. package/dist/lib/routine-context.d.ts +9 -2
  317. package/dist/lib/routine-context.js +21 -6
  318. package/dist/lib/routines-project.d.ts +58 -48
  319. package/dist/lib/routines-project.js +190 -159
  320. package/dist/lib/routines.d.ts +41 -6
  321. package/dist/lib/routines.js +92 -34
  322. package/dist/lib/run-defaults.d.ts +5 -0
  323. package/dist/lib/run-defaults.js +11 -1
  324. package/dist/lib/runner.d.ts +10 -8
  325. package/dist/lib/runner.js +122 -39
  326. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  327. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  328. package/dist/lib/secrets/agent.d.ts +10 -3
  329. package/dist/lib/secrets/agent.js +43 -8
  330. package/dist/lib/secrets/bundles.d.ts +1 -0
  331. package/dist/lib/secrets/bundles.js +92 -80
  332. package/dist/lib/secrets/index.js +32 -9
  333. package/dist/lib/secrets/install-helper.d.ts +4 -4
  334. package/dist/lib/secrets/install-helper.js +4 -4
  335. package/dist/lib/secrets/push.d.ts +40 -6
  336. package/dist/lib/secrets/push.js +58 -11
  337. package/dist/lib/secrets/read-backoff.d.ts +1 -1
  338. package/dist/lib/secrets/read-backoff.js +1 -1
  339. package/dist/lib/secrets/remote.d.ts +24 -0
  340. package/dist/lib/secrets/remote.js +54 -4
  341. package/dist/lib/self-update.d.ts +108 -0
  342. package/dist/lib/self-update.js +276 -1
  343. package/dist/lib/session/active.d.ts +89 -5
  344. package/dist/lib/session/active.js +165 -10
  345. package/dist/lib/session/actor-sidecar.d.ts +1 -1
  346. package/dist/lib/session/actor-sidecar.js +2 -1
  347. package/dist/lib/session/db.d.ts +176 -2
  348. package/dist/lib/session/db.js +484 -17
  349. package/dist/lib/session/discover.js +27 -4
  350. package/dist/lib/session/hook-sessions.js +2 -2
  351. package/dist/lib/session/host-link.d.ts +2 -2
  352. package/dist/lib/session/host-link.js +2 -2
  353. package/dist/lib/session/pid-registry.d.ts +21 -0
  354. package/dist/lib/session/pid-registry.js +57 -0
  355. package/dist/lib/session/remote-active.js +15 -1
  356. package/dist/lib/session/remote.js +2 -2
  357. package/dist/lib/session/session-cache.d.ts +14 -7
  358. package/dist/lib/session/session-cache.js +51 -9
  359. package/dist/lib/session/state.d.ts +2 -2
  360. package/dist/lib/session/state.js +1 -1
  361. package/dist/lib/session/sync/config.d.ts +10 -10
  362. package/dist/lib/session/sync/config.js +10 -10
  363. package/dist/lib/session/sync/r2.d.ts +34 -0
  364. package/dist/lib/session/sync/r2.js +123 -0
  365. package/dist/lib/session/throughput.d.ts +1 -1
  366. package/dist/lib/session/throughput.js +1 -1
  367. package/dist/lib/session/types.d.ts +23 -1
  368. package/dist/lib/session/types.js +15 -0
  369. package/dist/lib/session/viewing-in.d.ts +1 -1
  370. package/dist/lib/session/viewing-in.js +1 -1
  371. package/dist/lib/session/watch.d.ts +101 -0
  372. package/dist/lib/session/watch.js +242 -0
  373. package/dist/lib/share/config.d.ts +4 -0
  374. package/dist/lib/share/config.js +1 -0
  375. package/dist/lib/share/delete.d.ts +93 -0
  376. package/dist/lib/share/delete.js +127 -0
  377. package/dist/lib/share/provision.d.ts +38 -0
  378. package/dist/lib/share/provision.js +50 -0
  379. package/dist/lib/share/publish.d.ts +40 -1
  380. package/dist/lib/share/publish.js +116 -3
  381. package/dist/lib/share/worker-template.js +71 -6
  382. package/dist/lib/shims.d.ts +17 -3
  383. package/dist/lib/shims.js +136 -27
  384. package/dist/lib/signin-badge.js +3 -2
  385. package/dist/lib/skills.js +2 -0
  386. package/dist/lib/smart-launch.d.ts +8 -8
  387. package/dist/lib/smart-launch.js +80 -59
  388. package/dist/lib/ssh-exec.d.ts +1 -1
  389. package/dist/lib/ssh-exec.js +1 -1
  390. package/dist/lib/staleness/prune.d.ts +88 -0
  391. package/dist/lib/staleness/prune.js +69 -0
  392. package/dist/lib/staleness/registry.d.ts +1 -1
  393. package/dist/lib/staleness/writers/commands.js +34 -2
  394. package/dist/lib/staleness/writers/hooks.js +16 -0
  395. package/dist/lib/staleness/writers/kinds.d.ts +6 -0
  396. package/dist/lib/staleness/writers/skills.js +19 -0
  397. package/dist/lib/staleness/writers/subagents.d.ts +2 -3
  398. package/dist/lib/staleness/writers/subagents.js +0 -4
  399. package/dist/lib/staleness/writers/types.d.ts +32 -0
  400. package/dist/lib/startup/command-registry.d.ts +6 -12
  401. package/dist/lib/startup/command-registry.js +14 -31
  402. package/dist/lib/startup/dev-build.d.ts +10 -0
  403. package/dist/lib/startup/dev-build.js +13 -1
  404. package/dist/lib/startup/spellcheck.d.ts +18 -0
  405. package/dist/lib/startup/spellcheck.js +45 -0
  406. package/dist/lib/state.d.ts +36 -9
  407. package/dist/lib/state.js +96 -46
  408. package/dist/lib/subagents-registry.d.ts +0 -7
  409. package/dist/lib/subagents-registry.js +38 -65
  410. package/dist/lib/subagents.d.ts +0 -33
  411. package/dist/lib/subagents.js +0 -75
  412. package/dist/lib/sync-umbrella.js +1 -1
  413. package/dist/lib/teams/agents.d.ts +41 -1
  414. package/dist/lib/teams/agents.js +187 -9
  415. package/dist/lib/teams/api.d.ts +11 -1
  416. package/dist/lib/teams/api.js +11 -2
  417. package/dist/lib/teams/delivery.d.ts +41 -0
  418. package/dist/lib/teams/delivery.js +60 -0
  419. package/dist/lib/teams/registry.d.ts +14 -0
  420. package/dist/lib/teams/registry.js +51 -1
  421. package/dist/lib/teams/remoteWorktree.d.ts +19 -0
  422. package/dist/lib/teams/remoteWorktree.js +29 -0
  423. package/dist/lib/teams/scheduler.d.ts +1 -1
  424. package/dist/lib/teams/scheduler.js +1 -1
  425. package/dist/lib/teams/worktree.d.ts +24 -0
  426. package/dist/lib/teams/worktree.js +42 -0
  427. package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
  428. package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
  429. package/dist/lib/terminal/engine.d.ts +4 -0
  430. package/dist/lib/terminal/engine.js +8 -2
  431. package/dist/lib/terminal/types.d.ts +21 -2
  432. package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
  433. package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
  434. package/dist/lib/tickets/list.d.ts +53 -0
  435. package/dist/lib/tickets/list.js +153 -0
  436. package/dist/lib/tmux/orphan-reap.d.ts +320 -0
  437. package/dist/lib/tmux/orphan-reap.js +644 -0
  438. package/dist/lib/tmux/session.d.ts +68 -3
  439. package/dist/lib/tmux/session.js +95 -5
  440. package/dist/lib/triggers/handlers.d.ts +20 -0
  441. package/dist/lib/triggers/handlers.js +6 -1
  442. package/dist/lib/types.d.ts +46 -34
  443. package/dist/lib/usage-fleet.d.ts +32 -0
  444. package/dist/lib/usage-fleet.js +125 -0
  445. package/dist/lib/usage-refresh.d.ts +3 -3
  446. package/dist/lib/usage-refresh.js +19 -16
  447. package/dist/lib/usage.d.ts +51 -47
  448. package/dist/lib/usage.js +145 -153
  449. package/dist/lib/version-duplicates.js +2 -2
  450. package/dist/lib/versions.d.ts +56 -13
  451. package/dist/lib/versions.js +195 -45
  452. package/dist/lib/watchdog/log.d.ts +1 -1
  453. package/dist/lib/watchdog/log.js +3 -3
  454. package/dist/lib/watchdog/rotate.d.ts +6 -6
  455. package/dist/lib/watchdog/rotate.js +6 -6
  456. package/dist/lib/workflows.d.ts +1 -1
  457. package/dist/lib/yaml-io.d.ts +47 -0
  458. package/dist/lib/yaml-io.js +55 -0
  459. package/package.json +2 -1
  460. package/scripts/install-helper.js +2 -2
  461. package/scripts/postinstall.js +23 -20
  462. package/dist/commands/defaults.d.ts +0 -7
  463. package/dist/commands/defaults.js +0 -107
  464. package/dist/commands/export.d.ts +0 -11
  465. package/dist/commands/export.js +0 -215
  466. package/dist/commands/helper.d.ts +0 -12
  467. package/dist/commands/helper.js +0 -87
  468. package/dist/commands/hosts.d.ts +0 -11
  469. package/dist/commands/hosts.js +0 -330
  470. package/dist/commands/lock.d.ts +0 -12
  471. package/dist/commands/lock.js +0 -70
  472. package/dist/commands/pull.d.ts +0 -17
  473. package/dist/commands/pull.js +0 -39
  474. package/dist/commands/push.d.ts +0 -14
  475. package/dist/commands/push.js +0 -30
  476. package/dist/commands/wallet.d.ts +0 -20
  477. package/dist/commands/wallet.js +0 -216
  478. package/dist/commands/worktree.d.ts +0 -19
  479. package/dist/commands/worktree.js +0 -272
  480. package/dist/lib/account-labels.d.ts +0 -24
  481. package/dist/lib/account-labels.js +0 -72
  482. package/dist/lib/auto-dispatch-linear.d.ts +0 -18
  483. package/dist/lib/auto-dispatch-linear.js +0 -107
  484. package/dist/lib/auto-dispatch-provider.d.ts +0 -10
  485. package/dist/lib/auto-dispatch-provider.js +0 -30
  486. package/dist/lib/auto-dispatch.d.ts +0 -93
  487. package/dist/lib/auto-dispatch.js +0 -128
  488. package/dist/lib/export.d.ts +0 -72
  489. package/dist/lib/export.js +0 -269
  490. package/dist/lib/lock.d.ts +0 -93
  491. package/dist/lib/lock.js +0 -207
  492. package/dist/lib/wallet/index.d.ts +0 -78
  493. package/dist/lib/wallet/index.js +0 -253
@@ -21,7 +21,8 @@ import { machineId } from '../lib/session/sync/config.js';
21
21
  import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
22
22
  import { registerFleetCaptureCommand } from './fleet-capture.js';
23
23
  import { registerFleetApplyAlias } from './apply.js';
24
- import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, setAutoLaunchEnabled, setAutoLaunchPreferred, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
24
+ import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
25
+ import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
25
26
  import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
26
27
  import { addControlToken } from '../lib/serve/token.js';
27
28
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
@@ -36,6 +37,7 @@ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentity
36
37
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
37
38
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
38
39
  import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
40
+ import { isRolloutSuccess, verifyFleetRollout, } from '../lib/devices/rollout-verify.js';
39
41
  import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
40
42
  import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
41
43
  import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
@@ -43,19 +45,24 @@ import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
43
45
  import { checkSyncStatus, countOrphans } from '../lib/drift.js';
44
46
  import { checkAllClis } from '../lib/teams/agents.js';
45
47
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
48
+ import { listTasks, resolveTaskRef } from '../lib/hosts/tasks.js';
49
+ import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
50
+ import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
51
+ import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
46
52
  import { sshExecAsync } from '../lib/ssh-exec.js';
47
53
  import { ALL_AGENT_IDS } from '../lib/agents.js';
48
54
  import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
49
55
  import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
50
- import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
51
- import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
56
+ import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
57
+ import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
52
58
  import { runFleetLogin } from '../lib/fleet/remote-login.js';
53
- import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from '../lib/device-config.js';
54
- import { setHelpSections } from '../lib/help.js';
59
+ import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec } from '../lib/device-config.js';
60
+ import { registerCommandGroups, setHelpSections } from '../lib/help.js';
55
61
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
56
62
  * command is running on so it stands out from the rest of the tailnet.
57
- * `isInteractive` marks the configured interactive host (`devices set-interactive`). */
63
+ * `isInteractive` marks the configured interactive host (`devices config <name> interactive.host`). */
58
64
  function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
65
+ d = resolveDeviceProfile(d);
59
66
  const addr = hostNameFor(d) ?? chalk.gray('no address');
60
67
  // Prefer a fresh live verdict (this run's probe, else the written-back
61
68
  // reachability) over the stale tailscale.online snapshot (RUSH-1965).
@@ -113,7 +120,9 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
113
120
  chalk.gray('headroom');
114
121
  lines.push(head);
115
122
  for (const name of names) {
116
- const d = reg[name];
123
+ // Effective profile: the operator's central config (ssh.*/platform/user)
124
+ // overlays the discovery record, so the table shows what would be dialed.
125
+ const d = resolveDeviceProfile(reg[name]);
117
126
  const isSelf = name === self;
118
127
  const marker = isSelf ? chalk.cyan('▸ ') : ' ';
119
128
  const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
@@ -180,7 +189,7 @@ export function renderLeasedBoxesSection(boxes, nowSecs) {
180
189
  chalk.gray(fmtIdleShort(b, nowSecs).padEnd(12)) +
181
190
  chalk.gray(fmtExpiresShort(b, nowSecs)));
182
191
  }
183
- lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents lease stop <slug>`'));
192
+ lines.push(chalk.gray(' Reuse a box with `agents run --box <slug>` · stop with `agents devices lease stop <slug>`'));
184
193
  return lines;
185
194
  }
186
195
  /** The leased-box rows for the devices list, or [] when crabbox can't be read. */
@@ -309,26 +318,51 @@ async function runInteractiveDeviceSync() {
309
318
  ].filter(Boolean);
310
319
  console.log(parts.join(chalk.gray(' · ')));
311
320
  }
312
- /** Print a per-device result table for fleet update/run. */
313
- function printFleetResults(results) {
321
+ /**
322
+ * Print a per-device result table for fleet update/run.
323
+ *
324
+ * `verifications` is supplied only by the rollout (`agents fleet update`), which
325
+ * re-probes what `agents` resolves to on each upgraded box. A box that upgraded
326
+ * with `exit 0` but still resolves to another copy — the dev-install shadow of
327
+ * RUSH-2446 — is rendered `stale` / `unverified`, counted as **not upgraded**,
328
+ * and makes the command exit non-zero. An `exit 0` alone never reads as `ok` on
329
+ * a rollout again.
330
+ */
331
+ function printFleetResults(results, verifications) {
314
332
  const nameW = Math.max(8, ...results.map((r) => r.name.length));
315
333
  console.log(chalk.bold('DEVICE'.padEnd(nameW)) + ' ' +
316
- chalk.bold('STATUS'.padEnd(8)) + ' ' +
334
+ chalk.bold('STATUS'.padEnd(10)) + ' ' +
317
335
  chalk.bold('DETAIL'));
336
+ let notUpgraded = 0;
318
337
  for (const r of results) {
319
- const status = r.status === 'ok' ? chalk.green('ok'.padEnd(8)) :
320
- r.status === 'skipped' ? chalk.gray('skipped'.padEnd(8)) :
321
- chalk.red('failed'.padEnd(8));
338
+ const verified = r.status === 'ok' ? verifications?.get(r.name) : undefined;
339
+ if (verified && !isRolloutSuccess(verified.verdict))
340
+ notUpgraded++;
341
+ const label = r.status === 'skipped' ? chalk.gray('skipped'.padEnd(10)) :
342
+ r.status === 'failed' ? chalk.red('failed'.padEnd(10)) :
343
+ verified === undefined ? chalk.green('ok'.padEnd(10)) :
344
+ verified.verdict === 'on-target' ? chalk.green('ok'.padEnd(10)) :
345
+ verified.verdict === 'unverified' ? chalk.yellow('unverified'.padEnd(10)) :
346
+ chalk.red('stale'.padEnd(10));
322
347
  const detail = r.status === 'skipped' ? chalk.gray(skipLabel(r.reason)) :
323
348
  r.status === 'failed' ? chalk.red(r.detail || `exit ${r.code ?? '?'}`) :
324
- chalk.gray(r.code === 0 ? 'exit 0' : '');
325
- console.log(`${r.name.padEnd(nameW)} ${status} ${detail}`);
349
+ verified === undefined ? chalk.gray(r.code === 0 ? 'exit 0' : '') :
350
+ verified.verdict === 'on-target' ? chalk.gray(verified.detail) :
351
+ verified.verdict === 'unverified' ? chalk.yellow(verified.detail) :
352
+ chalk.red(verified.detail);
353
+ console.log(`${r.name.padEnd(nameW)} ${label} ${detail}`);
326
354
  }
327
- const ok = results.filter((r) => r.status === 'ok').length;
328
355
  const failed = results.filter((r) => r.status === 'failed').length;
329
356
  const skipped = results.filter((r) => r.status === 'skipped').length;
330
- console.log(chalk.gray(`${ok} ok · ${failed} failed · ${skipped} skipped`));
331
- if (failed > 0)
357
+ const ok = results.filter((r) => r.status === 'ok').length - notUpgraded;
358
+ const parts = [`${ok} ok`, `${failed} failed`, `${skipped} skipped`];
359
+ if (verifications)
360
+ parts.splice(1, 0, `${notUpgraded} not upgraded`);
361
+ console.log(chalk.gray(parts.join(' · ')));
362
+ if (notUpgraded > 0) {
363
+ console.log(chalk.yellow('A box that upgraded but still resolves elsewhere runs OLD code — remove the stale install that owns the `agents` name on that box, or reorder PATH. `agents doctor` names it.'));
364
+ }
365
+ if (failed > 0 || notUpgraded > 0)
332
366
  process.exitCode = 1;
333
367
  }
334
368
  async function localHealthRow(self, stats) {
@@ -386,6 +420,16 @@ async function probeRemoteHealth(target) {
386
420
  inventory: parsed.fleet,
387
421
  };
388
422
  }
423
+ /** Effective device-scoped config, keyed by the canonical YAML key. */
424
+ function deviceConfigJson(name) {
425
+ const config = {};
426
+ for (const entry of listConfig({ device: name })) {
427
+ if (entry.spec.scope !== 'device' || entry.value === undefined)
428
+ continue;
429
+ config[entry.spec.yamlKey] = entry.value;
430
+ }
431
+ return Object.keys(config).length > 0 ? config : undefined;
432
+ }
389
433
  async function runFleetStatus(opts) {
390
434
  const reg = await loadDevices();
391
435
  const self = machineId();
@@ -418,7 +462,7 @@ async function runFleetStatus(opts) {
418
462
  const remoteTargets = remoteFleetTargets(planned, self)
419
463
  .map((t) => ({
420
464
  name: t.device.name,
421
- platform: t.device.platform,
465
+ platform: resolveDeviceProfile(t.device).platform,
422
466
  // Fail fast: gate the expensive version+doctor dials on the reachability
423
467
  // verdict the cheap stats probe already computed one step earlier. A box
424
468
  // it found unreachable skips straight to an `unreachable` row instead of
@@ -513,7 +557,20 @@ async function runFleetStatus(opts) {
513
557
  }
514
558
  const report = buildFleetHealthReport(rows, new Date(), { self });
515
559
  if (opts.json) {
516
- console.log(JSON.stringify(report, null, 2));
560
+ const interactiveHost = getConfigValue('interactive.host').value;
561
+ console.log(JSON.stringify({
562
+ ...report,
563
+ devices: report.devices.map((row) => {
564
+ const registered = reg[row.name];
565
+ const config = deviceConfigJson(row.name);
566
+ return {
567
+ ...row,
568
+ profile: registered ? resolveDeviceProfile(registered) : { name: row.name },
569
+ interactive: row.name === interactiveHost,
570
+ ...(config ? { config } : {}),
571
+ };
572
+ }),
573
+ }, null, 2));
517
574
  }
518
575
  else if (opts.verbose) {
519
576
  // Full grid: the auth/CLI/sync/version columns and the warnings rollup.
@@ -575,11 +632,10 @@ export async function raceFleetPingDeadline(fanOut, remoteTargets, overallTimeou
575
632
  }
576
633
  async function runFleetPing(opts) {
577
634
  const self = machineId();
578
- const cliVersion = getCliVersion();
635
+ const { refreshLocalFleetAuthState } = await import('../lib/daemon-ticks.js');
579
636
  // --local: probe just this host. Used both directly and as the fan-out worker.
580
637
  if (opts.local) {
581
- const rows = await probeLocalFleetAuth({ cliVersion });
582
- writeFleetAuthRows(self, rows);
638
+ const { authRows: rows } = await refreshLocalFleetAuthState();
583
639
  if (opts.json) {
584
640
  console.log(JSON.stringify({ host: self, rows }));
585
641
  }
@@ -596,12 +652,11 @@ async function runFleetPing(opts) {
596
652
  const reg = await loadDevices();
597
653
  const planned = planFleetTargets(reg);
598
654
  const results = [];
599
- const localRows = await probeLocalFleetAuth({ cliVersion });
600
- writeFleetAuthRows(self, localRows);
655
+ const { authRows: localRows } = await refreshLocalFleetAuthState();
601
656
  results.push({ host: self, rows: localRows });
602
657
  const remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
603
658
  name: t.device.name,
604
- platform: t.device.platform,
659
+ platform: resolveDeviceProfile(t.device).platform,
605
660
  skip: t.skip,
606
661
  dialTarget: fleetDialTarget(t.device),
607
662
  extraSshArgs: deviceIdentityArgs(t.device),
@@ -671,7 +726,7 @@ async function probeRemoteHarnesses(target, refresh) {
671
726
  * the local and remote rows. Bounded by the same per-device + overall deadlines
672
727
  * as `fleet ping`, so one unreachable box can never stall the glance.
673
728
  */
674
- async function collectFleetHarnesses(opts) {
729
+ export async function collectFleetHarnesses(opts) {
675
730
  const self = machineId();
676
731
  const want = opts.devices?.length ? new Set(opts.devices) : null;
677
732
  const results = [];
@@ -683,7 +738,7 @@ async function collectFleetHarnesses(opts) {
683
738
  const planned = planFleetTargets(reg);
684
739
  let remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
685
740
  name: t.device.name,
686
- platform: t.device.platform,
741
+ platform: resolveDeviceProfile(t.device).platform,
687
742
  skip: t.skip,
688
743
  dialTarget: fleetDialTarget(t.device),
689
744
  extraSshArgs: deviceIdentityArgs(t.device),
@@ -736,7 +791,7 @@ async function runDevicesHarnesses(opts) {
736
791
  for (const line of renderHarnessMatrix(results))
737
792
  console.log(line);
738
793
  }
739
- async function runDevicesAccounts(opts) {
794
+ export async function runDevicesAccounts(opts) {
740
795
  if (opts.local) {
741
796
  const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
742
797
  if (opts.json)
@@ -880,26 +935,45 @@ function registerDevicesCommands(program) {
880
935
  const devicesCmd = program
881
936
  .command('devices')
882
937
  .alias('fleet')
883
- .description('Registry of SSH device profiles (platform, user, address, auth), self-populated from Tailscale. Alias: fleet.')
884
- .addHelpText('after', `
885
- Typical workflow:
886
- agents devices sync # curate: pick which tailscale nodes to keep (TTY)
887
- agents devices sync --yes # non-interactive: register all non-ignored nodes
888
- agents devices list # see what's registered (★ = interactive host)
889
- agents devices ignore ipad165 # dismiss a node so it's never re-suggested
890
- agents devices disable zion # exclude a device from Factory auto-launch
891
- agents devices prefer mac-mini # boost a device in Factory auto-launch ranking
892
- agents devices set-interactive zion # where agents show YOU artifacts
893
- agents devices configure mac-mini --max-agents 4 --scheduler off
894
- agents devices note mac-mini "runs the releases — don't reboot"
895
- agents devices set win-mini --auth password --bundle muqsit
896
- agents devices set worker --auth key --identity-file ~/.ssh/worker_ed25519
897
- agents devices render --write # write ~/.ssh/config.d/agents include
898
- agents fleet update # roll out latest agents-cli to every online device
899
- agents fleet run uname -a # run a command on every online device
938
+ .description('Registry of SSH device profiles (platform, user, address, auth), self-populated from Tailscale. Alias: fleet.');
939
+ setHelpSections(devicesCmd, {
940
+ examples: `
941
+ Discover & register:
942
+ agents devices sync # pick which tailscale nodes to keep (TTY)
943
+ agents devices sync --yes # register all non-ignored nodes
944
+ agents devices ignore ipad165 # dismiss a node so it's never re-suggested
945
+
946
+ Inspect:
947
+ agents devices list # what's registered (★ = interactive host)
948
+ agents devices status # live reachability + load
949
+ agents devices ping # quick liveness probe
950
+ agents devices lease list # disposable crabbox devices available for reuse
951
+
952
+ Configure a device:
953
+ agents devices config mac-mini # settings menu (TTY) / print (piped)
954
+ agents devices config mac-mini agents.max-concurrent 4
955
+ agents devices config mac-mini scheduler.enabled off
956
+ agents devices config mac-mini notes "runs the releases"
957
+ agents devices config win-mini ssh.auth password
958
+ agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
959
+ agents devices config mac-mini auto-launch.enabled off
960
+ agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
961
+ agents devices render --write # write ~/.ssh/config.d/agents include
900
962
 
901
- \`agents fleet\` is an alias for \`agents devices\` — same subcommands.
902
- `);
963
+ Fleet operations:
964
+ agents fleet update # roll out latest agents-cli everywhere
965
+ agents fleet run uname -a # run a command on every online device
966
+ `,
967
+ notes: '`agents fleet` is an alias for `agents devices` — same subcommands.',
968
+ });
969
+ registerLeaseCommand(devicesCmd);
970
+ registerCommandGroups(devicesCmd, [
971
+ { title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'rm'] },
972
+ { title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts'] },
973
+ { title: 'Disposable devices', names: ['lease'] },
974
+ { title: 'Configure a device', names: ['config', 'render'] },
975
+ { title: 'Fleet operations', names: ['update', 'run', 'login', 'pair-ios', 'capture', 'apply'] },
976
+ ]);
903
977
  devicesCmd
904
978
  .command('sync')
905
979
  .description('Ingest `tailscale status --json` into device profiles. In a terminal, opens a checkbox to register/unregister nodes; with --yes, registers every non-ignored node.')
@@ -972,14 +1046,247 @@ Typical workflow:
972
1046
  }
973
1047
  console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
974
1048
  });
1049
+ // ─── devices config (unified settings surface) ────────────────────────────
1050
+ //
1051
+ // ONE command for every per-device setting: `agents devices config <name>
1052
+ // [key] [value] [--unset] [--json]`. The retired subcommands (configure,
1053
+ // note, set-interactive, set, enable/disable/prefer/unprefer) are hidden
1054
+ // tombstones below — each prints a deprecation notice on STDERR (so a --json
1055
+ // consumer's stdout stays parseable) and delegates to this same engine,
1056
+ // preserving its old output shape and exit codes.
1057
+ /** Parse a raw CLI string into a config key's typed value (bool/int pass validation, strings verbatim). */
1058
+ const parseConfigValueInput = (spec, raw) => {
1059
+ switch (spec.type) {
1060
+ case 'int': {
1061
+ const n = Number(raw);
1062
+ if (!Number.isInteger(n))
1063
+ throw new Error(`Config key '${spec.name}' expects an integer, got '${raw}'.`);
1064
+ return n;
1065
+ }
1066
+ case 'bool': {
1067
+ if (raw === 'on' || raw === 'true')
1068
+ return true;
1069
+ if (raw === 'off' || raw === 'false')
1070
+ return false;
1071
+ throw new Error(`Config key '${spec.name}' expects on/off (or true/false), got '${raw}'.`);
1072
+ }
1073
+ default:
1074
+ return raw;
1075
+ }
1076
+ };
1077
+ const writeJson = (payload) => {
1078
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
1079
+ };
1080
+ /** The device-scope config entries for `name` (what the config surface edits). */
1081
+ const deviceConfigEntries = (name) => listConfig({ device: name }).filter((e) => e.spec.scope === 'device');
1082
+ /** Print the full resolved config for a device (bare invocation, non-menu). */
1083
+ const printDevicesConfig = (name, json) => {
1084
+ const entries = deviceConfigEntries(name);
1085
+ if (json) {
1086
+ const config = {};
1087
+ for (const e of entries)
1088
+ if (e.value !== undefined)
1089
+ config[e.spec.name] = e.value;
1090
+ writeJson({ device: name, config });
1091
+ return;
1092
+ }
1093
+ console.log(chalk.bold(`Config for '${name}'`));
1094
+ for (const e of entries) {
1095
+ const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
1096
+ console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
1097
+ }
1098
+ };
1099
+ /**
1100
+ * The `devices config` engine — shared by the config command's action and
1101
+ * every tombstone. `quiet` performs the write with no output (a tombstone
1102
+ * that prints its own legacy shape). Throws on a bad key/value; callers map
1103
+ * that to exit 1.
1104
+ */
1105
+ const runDevicesConfig = async (name, key, valueParts, opts) => {
1106
+ const spec = key ? configKeySpec(key) : undefined; // unknown key → throw listing the valid keys
1107
+ // User-scope keys (interactive.host) are stored centrally — the device name
1108
+ // is syntax only, so an unregistered name is not an error for them.
1109
+ if (!spec || spec.scope === 'device')
1110
+ await mustGetDevice(name);
1111
+ if (opts.unset) {
1112
+ if (!spec)
1113
+ throw new Error('--unset needs a key: agents devices config <name> <key> --unset');
1114
+ unsetConfigValue(spec.name, { device: name });
1115
+ if (opts.quiet)
1116
+ return;
1117
+ if (opts.json)
1118
+ writeJson({ device: name, key: spec.name, value: null });
1119
+ else
1120
+ console.log(chalk.green(`Unset ${spec.name}`) + chalk.gray(` on '${name}' — back to the default.`));
1121
+ return;
1122
+ }
1123
+ if (spec && valueParts.length > 0) {
1124
+ let value;
1125
+ if (spec.type === 'string-list') {
1126
+ // List keys (notes) APPEND — one entry per invocation.
1127
+ const existing = getConfigValue(spec.name, { device: name }).value ?? [];
1128
+ value = [...existing, valueParts.join(' ')];
1129
+ }
1130
+ else {
1131
+ value = parseConfigValueInput(spec, valueParts.join(' '));
1132
+ }
1133
+ setConfigValue(spec.name, value, { device: name });
1134
+ if (opts.quiet)
1135
+ return;
1136
+ if (opts.json)
1137
+ writeJson({ device: name, key: spec.name, value });
1138
+ else
1139
+ console.log(chalk.green(`Set ${spec.name} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
1140
+ return;
1141
+ }
1142
+ if (spec) {
1143
+ const entry = getConfigValue(spec.name, { device: name });
1144
+ if (opts.quiet)
1145
+ return;
1146
+ if (opts.json) {
1147
+ writeJson({ device: name, key: spec.name, value: entry.value ?? null });
1148
+ }
1149
+ else if (entry.value === undefined) {
1150
+ console.log(` ${spec.name.padEnd(24)} ${chalk.gray('— (default)')}${chalk.gray(` ${spec.description}`)}`);
1151
+ }
1152
+ else {
1153
+ console.log(` ${spec.name.padEnd(24)} ${chalk.cyan(JSON.stringify(entry.value))}${chalk.gray(` ${spec.description}`)}`);
1154
+ }
1155
+ return;
1156
+ }
1157
+ // Bare: TTY → the interactive settings menu; piped/--json → print.
1158
+ if (opts.json || !isInteractiveTerminal()) {
1159
+ printDevicesConfig(name, Boolean(opts.json));
1160
+ return;
1161
+ }
1162
+ await runDevicesConfigMenu(name);
1163
+ };
1164
+ /** The interactive settings menu: pick a key, edit it, repeat. TTY-only. */
1165
+ const runDevicesConfigMenu = async (name) => {
1166
+ const { select, input, confirm } = await import('@inquirer/prompts');
1167
+ const DONE = '__done__';
1168
+ try {
1169
+ for (;;) {
1170
+ const entries = deviceConfigEntries(name);
1171
+ const picked = await select({
1172
+ message: `Config for '${name}' — pick a key to edit:`,
1173
+ pageSize: Math.min(entries.length + 1, 20),
1174
+ choices: [
1175
+ ...entries.map((e) => {
1176
+ const value = e.value !== undefined
1177
+ ? chalk.cyan(JSON.stringify(e.value))
1178
+ : chalk.gray(e.spec.defaultValue !== undefined
1179
+ ? `default: ${JSON.stringify(e.spec.defaultValue)}`
1180
+ : 'unset (default)');
1181
+ return { value: e.spec.name, name: `${e.spec.name.padEnd(24)} ${value} ${chalk.gray(e.spec.description)}` };
1182
+ }),
1183
+ { value: DONE, name: 'Done' },
1184
+ ],
1185
+ });
1186
+ if (picked === DONE)
1187
+ return;
1188
+ const spec = configKeySpec(picked);
1189
+ if (spec.type === 'bool') {
1190
+ const current = getConfigValue(picked, { device: name }).value;
1191
+ const next = await confirm({
1192
+ message: `${picked} — enable?`,
1193
+ default: current ?? spec.defaultValue ?? true,
1194
+ });
1195
+ setConfigValue(picked, next, { device: name });
1196
+ console.log(chalk.green(`Set ${picked} = ${next}`) + chalk.gray(` on '${name}'.`));
1197
+ }
1198
+ else if (spec.type === 'string-list') {
1199
+ const text = await input({ message: `${picked} — append an entry (empty to go back):` });
1200
+ if (text.trim().length > 0) {
1201
+ const existing = getConfigValue(picked, { device: name }).value ?? [];
1202
+ setConfigValue(picked, [...existing, text.trim()], { device: name });
1203
+ console.log(chalk.green(`Noted on '${name}':`) + ` ${text.trim()}`);
1204
+ }
1205
+ }
1206
+ else {
1207
+ const current = getConfigValue(picked, { device: name }).value;
1208
+ const raw = await input({
1209
+ message: `${picked}:`,
1210
+ default: current === undefined ? undefined : String(current),
1211
+ });
1212
+ if (raw.trim().length === 0)
1213
+ continue;
1214
+ const value = parseConfigValueInput(spec, raw.trim());
1215
+ setConfigValue(spec.name, value, { device: name });
1216
+ console.log(chalk.green(`Set ${spec.name} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
1217
+ }
1218
+ }
1219
+ }
1220
+ catch (err) {
1221
+ if (isPromptCancelled(err))
1222
+ return; // ctrl-c / esc — leave the menu quietly
1223
+ throw err;
1224
+ }
1225
+ };
1226
+ const configCmd = devicesCmd
1227
+ .command('config <name> [key] [value...]')
1228
+ .description('Get, set, or unset a device’s settings (scheduler, agent cap, ssh overrides, auto-launch, notes). ' +
1229
+ 'Bare opens an interactive settings menu (TTY) or prints the resolved config (piped). ' +
1230
+ 'Stored centrally in ~/.agents/agents.yaml under fleet.devices.<name>.config — synced, so any box can configure any device.')
1231
+ .option('--unset', 'reset the key to its default (removes the stored value)')
1232
+ .option('--json', 'output machine-readable JSON')
1233
+ .action(async (name, key, valueParts, opts) => {
1234
+ try {
1235
+ await runDevicesConfig(name, key, valueParts ?? [], opts);
1236
+ }
1237
+ catch (err) {
1238
+ console.error(chalk.red(err.message));
1239
+ process.exit(1);
1240
+ }
1241
+ });
1242
+ setHelpSections(configCmd, {
1243
+ examples: `
1244
+ agents devices config mac-mini # settings menu (TTY) / print config (piped)
1245
+ agents devices config mac-mini agents.max-concurrent 4 # cap concurrent agents
1246
+ agents devices config mac-mini scheduler.enabled off # no routines firing there
1247
+ agents devices config mac-mini scheduler.enabled # read one key back
1248
+ agents devices config mac-mini scheduler.enabled --unset # back to the default
1249
+ agents devices config mac-mini notes "runs the releases" # append an operator note
1250
+ agents devices config win-mini ssh.auth password # password auth…
1251
+ agents devices config win-mini ssh.bundle muqsit # …from this secrets bundle
1252
+ agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
1253
+ agents devices config mac-mini auto-launch.enabled off # exclude from AGI EXT auto-launch
1254
+ agents devices config mac-mini auto-launch.preferred on # boost in auto-launch ranking
1255
+ agents devices config zion interactive.host zion # user scope: where agents show YOU artifacts
1256
+ agents devices config mac-mini --json # machine-readable
1257
+ `,
1258
+ notes: `
1259
+ Keys: agents.max-concurrent, scheduler.enabled, daemon.enabled,
1260
+ watchdog.enabled, browser.remote-control, browser.profile, notes,
1261
+ ssh.user, ssh.auth (key|password), ssh.bundle, ssh.bundle-key,
1262
+ ssh.identity-file, platform (windows|linux|macos|unknown),
1263
+ auto-launch.enabled, auto-launch.preferred — plus the user-scope
1264
+ interactive.host (stored centrally; the device name is syntax only).
1265
+
1266
+ Booleans take on/off (or true/false). 'notes' appends one entry per
1267
+ invocation. Values land in ~/.agents/agents.yaml under
1268
+ fleet.devices.<name>.config and sync with 'agents repo push/pull'.
1269
+ ssh.* / platform / user overlay the discovered registry profile at dial
1270
+ time. scheduler.enabled / daemon.enabled take effect when the daemon
1271
+ reloads or restarts on that device.
1272
+
1273
+ The retired subcommands still work and forward here: configure, note,
1274
+ set, set-interactive, enable, disable, prefer, unprefer.
1275
+ `,
1276
+ });
1277
+ /** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
1278
+ const configTombstoneNotice = (retired, replacement) => {
1279
+ console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
1280
+ };
975
1281
  devicesCmd
976
- .command('enable <name>')
977
- .description('Allow a registered device to be auto-picked by Factory agent launches.')
1282
+ .command('enable <name>', { hidden: true })
978
1283
  .action(async (name) => {
979
1284
  try {
1285
+ configTombstoneNotice('enable <name>', 'config <name> auto-launch.enabled on');
980
1286
  await mustGetDevice(name);
981
- await setAutoLaunchEnabled(name, true);
982
- console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for Factory auto-launch.'));
1287
+ // Back to the default (enabled) = remove the key.
1288
+ await runDevicesConfig(name, 'auto-launch.enabled', [], { unset: true, quiet: true });
1289
+ console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
983
1290
  }
984
1291
  catch (err) {
985
1292
  console.error(chalk.red(err.message));
@@ -987,13 +1294,13 @@ Typical workflow:
987
1294
  }
988
1295
  });
989
1296
  devicesCmd
990
- .command('disable <name>')
991
- .description('Exclude a registered device from Factory auto-launch. It can still be picked manually via (Pick Host).')
1297
+ .command('disable <name>', { hidden: true })
992
1298
  .action(async (name) => {
993
1299
  try {
1300
+ configTombstoneNotice('disable <name>', 'config <name> auto-launch.enabled off');
994
1301
  await mustGetDevice(name);
995
- await setAutoLaunchEnabled(name, false);
996
- console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for Factory auto-launch.'));
1302
+ await runDevicesConfig(name, 'auto-launch.enabled', ['off'], { quiet: true });
1303
+ console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
997
1304
  }
998
1305
  catch (err) {
999
1306
  console.error(chalk.red(err.message));
@@ -1001,13 +1308,13 @@ Typical workflow:
1001
1308
  }
1002
1309
  });
1003
1310
  devicesCmd
1004
- .command('prefer <name>')
1005
- .description('Boost a registered device in Factory auto-launch ranking.')
1311
+ .command('prefer <name>', { hidden: true })
1006
1312
  .action(async (name) => {
1007
1313
  try {
1314
+ configTombstoneNotice('prefer <name>', 'config <name> auto-launch.preferred on');
1008
1315
  await mustGetDevice(name);
1009
- await setAutoLaunchPreferred(name, true);
1010
- console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for Factory auto-launch.'));
1316
+ await runDevicesConfig(name, 'auto-launch.preferred', ['on'], { quiet: true });
1317
+ console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1011
1318
  }
1012
1319
  catch (err) {
1013
1320
  console.error(chalk.red(err.message));
@@ -1015,30 +1322,31 @@ Typical workflow:
1015
1322
  }
1016
1323
  });
1017
1324
  devicesCmd
1018
- .command('unprefer <name>')
1019
- .description('Remove the auto-launch preference boost from a device.')
1325
+ .command('unprefer <name>', { hidden: true })
1020
1326
  .action(async (name) => {
1021
1327
  try {
1328
+ configTombstoneNotice('unprefer <name>', 'config <name> auto-launch.preferred off');
1022
1329
  await mustGetDevice(name);
1023
- await setAutoLaunchPreferred(name, false);
1024
- console.log(chalk.green(`No longer preferring '${name}'`) + chalk.gray(' for Factory auto-launch.'));
1330
+ // Back to the default (not preferred) = remove the key.
1331
+ await runDevicesConfig(name, 'auto-launch.preferred', [], { unset: true, quiet: true });
1332
+ console.log(chalk.green(`No longer preferring '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1025
1333
  }
1026
1334
  catch (err) {
1027
1335
  console.error(chalk.red(err.message));
1028
1336
  process.exit(1);
1029
1337
  }
1030
1338
  });
1031
- const setInteractiveCmd = devicesCmd
1032
- .command('set-interactive [name]')
1033
- .description('Get or set the interactive host — the one device that shows YOU artifacts (browser opens, dashboards, rendered plans). Stored fleet-wide as config.interactiveHost in central agents.yaml.')
1339
+ devicesCmd
1340
+ .command('set-interactive [name]', { hidden: true })
1034
1341
  .option('--unset', 'clear the interactive host')
1035
1342
  .option('--json', 'output machine-readable JSON')
1036
1343
  .action(async (name, opts) => {
1037
1344
  try {
1345
+ configTombstoneNotice('set-interactive [name]', 'config <name> interactive.host <name>');
1038
1346
  if (opts.unset) {
1039
1347
  unsetConfigValue('interactive.host');
1040
1348
  if (opts.json)
1041
- process.stdout.write(JSON.stringify({ interactiveHost: null }, null, 2) + '\n');
1349
+ writeJson({ interactiveHost: null });
1042
1350
  else
1043
1351
  console.log(chalk.green('Cleared the interactive host.'));
1044
1352
  return;
@@ -1047,20 +1355,20 @@ Typical workflow:
1047
1355
  await mustGetDevice(name);
1048
1356
  setConfigValue('interactive.host', name);
1049
1357
  if (opts.json)
1050
- process.stdout.write(JSON.stringify({ interactiveHost: name }, null, 2) + '\n');
1358
+ writeJson({ interactiveHost: name });
1051
1359
  else
1052
1360
  console.log(chalk.green(`Interactive host: '${name}'`) + chalk.gray(' — agents show you artifacts there. Clear with --unset.'));
1053
1361
  return;
1054
1362
  }
1055
1363
  const current = getConfigValue('interactive.host').value;
1056
1364
  if (opts.json) {
1057
- process.stdout.write(JSON.stringify({ interactiveHost: current ?? null }, null, 2) + '\n');
1365
+ writeJson({ interactiveHost: current ?? null });
1058
1366
  }
1059
1367
  else if (current) {
1060
1368
  console.log(`${chalk.bold('Interactive host:')} ${chalk.cyan(current)}`);
1061
1369
  }
1062
1370
  else {
1063
- console.log(chalk.gray("No interactive host set. Set one with 'agents devices set-interactive <name>'."));
1371
+ console.log(chalk.gray("No interactive host set. Set one with 'agents devices config <name> interactive.host <name>'."));
1064
1372
  }
1065
1373
  }
1066
1374
  catch (err) {
@@ -1068,123 +1376,65 @@ Typical workflow:
1068
1376
  process.exit(1);
1069
1377
  }
1070
1378
  });
1071
- setHelpSections(setInteractiveCmd, {
1072
- examples: `
1073
- agents devices set-interactive zion # zion is where artifacts open for you
1074
- agents devices set-interactive # print the current interactive host
1075
- agents devices set-interactive --unset # back to no interactive host
1076
- agents devices set-interactive --json # machine-readable (for skills)
1077
- `,
1078
- notes: `
1079
- The interactive host answers "which online macOS device do I show this on?"
1080
- so skills stop guessing. It is marked ★ interactive in 'agents devices list'.
1081
- The value syncs fleet-wide (central agents.yaml, config.interactiveHost);
1082
- per-device settings live under 'agents devices configure' instead.
1083
- `,
1084
- });
1085
- const configureCmd = devicesCmd
1086
- .command('configure <name>')
1087
- .description('Get or set per-device config: --max-agents, --scheduler. Written to ~/.agents/devices/<name>/agents.yaml (works for any device — the devices/ tree syncs). Unset = default behavior.')
1088
- .option('--max-agents <n>', 'cap concurrent agents (Factory auto-launch counts device-wide; teams placement counts the team’s roster on the device)')
1089
- .option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device (takes effect on daemon reload/restart)')
1379
+ devicesCmd
1380
+ .command('configure <name>', { hidden: true })
1381
+ .option('--max-agents <n>', 'cap concurrent agents')
1382
+ .option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device')
1090
1383
  .option('--json', 'output machine-readable JSON')
1091
1384
  .action(async (name, opts) => {
1092
1385
  try {
1386
+ configTombstoneNotice('configure <name> [--max-agents N] [--scheduler on|off]', 'config <name> <key> <value>');
1093
1387
  await mustGetDevice(name);
1094
- const parseOnOff = (flag, raw) => {
1095
- if (raw === 'on')
1096
- return true;
1097
- if (raw === 'off')
1098
- return false;
1099
- throw new Error(`--${flag} expects 'on' or 'off', got '${raw}'.`);
1100
- };
1101
1388
  const writes = [];
1102
- if (opts.maxAgents !== undefined) {
1103
- const n = Number(opts.maxAgents);
1104
- if (!Number.isInteger(n))
1105
- throw new Error(`--max-agents expects an integer, got '${opts.maxAgents}'.`);
1106
- writes.push(['agents.max-concurrent', n]);
1107
- }
1108
- if (opts.scheduler !== undefined)
1109
- writes.push(['scheduler.enabled', parseOnOff('scheduler', opts.scheduler)]);
1110
- if (writes.length > 0) {
1111
- for (const [key, value] of writes)
1112
- setConfigValue(key, value, { device: name });
1113
- if (!opts.json) {
1114
- for (const [key, value] of writes) {
1115
- console.log(chalk.green(`Set ${key} = ${JSON.stringify(value)}`) + chalk.gray(` on '${name}'.`));
1116
- }
1389
+ if (opts.maxAgents !== undefined)
1390
+ writes.push(['agents.max-concurrent', opts.maxAgents]);
1391
+ if (opts.scheduler !== undefined) {
1392
+ if (opts.scheduler !== 'on' && opts.scheduler !== 'off') {
1393
+ throw new Error(`--scheduler expects 'on' or 'off', got '${opts.scheduler}'.`);
1117
1394
  }
1395
+ writes.push(['scheduler.enabled', opts.scheduler]);
1118
1396
  }
1119
- if (opts.json || writes.length === 0) {
1120
- const entries = listConfig({ device: name }).filter((e) => e.spec.scope === 'device');
1121
- if (opts.json) {
1122
- const config = {};
1123
- for (const e of entries)
1124
- if (e.value !== undefined)
1125
- config[e.spec.name] = e.value;
1126
- process.stdout.write(JSON.stringify({ device: name, config }, null, 2) + '\n');
1127
- }
1128
- else {
1129
- console.log(chalk.bold(`Config for '${name}'`));
1130
- for (const e of entries) {
1131
- const value = e.value === undefined ? chalk.gray('— (default)') : chalk.cyan(JSON.stringify(e.value));
1132
- console.log(` ${e.spec.name.padEnd(24)} ${value}${chalk.gray(` ${e.spec.description}`)}`);
1133
- }
1134
- }
1397
+ for (const [key, value] of writes) {
1398
+ await runDevicesConfig(name, key, [value], { quiet: Boolean(opts.json) });
1135
1399
  }
1400
+ if (opts.json)
1401
+ printDevicesConfig(name, true);
1402
+ else if (writes.length === 0)
1403
+ printDevicesConfig(name, false);
1136
1404
  }
1137
1405
  catch (err) {
1138
1406
  console.error(chalk.red(err.message));
1139
1407
  process.exit(1);
1140
1408
  }
1141
1409
  });
1142
- setHelpSections(configureCmd, {
1143
- examples: `
1144
- agents devices configure mac-mini --max-agents 4 # cap concurrent agents
1145
- agents devices configure mac-mini --scheduler off # no routines firing there
1146
- agents devices configure mac-mini # print its current config
1147
- agents devices configure mac-mini --json # machine-readable
1148
- `,
1149
- notes: `
1150
- Run it on any machine for any device: the value lands in
1151
- ~/.agents/devices/<name>/agents.yaml locally and reaches the device on the
1152
- next 'agents repo push/pull'. Unset keys keep today's behavior.
1153
- --scheduler takes effect when the daemon reloads or restarts on that
1154
- device ('agents routines start' / the reload a 'routines add' sends).
1155
- For the default browser profile use 'agents browser profiles set-default
1156
- <name>'; for free-form text use 'agents devices note'.
1157
- `,
1158
- });
1159
- const noteCmd = devicesCmd
1160
- .command('note <name> [text...]')
1161
- .description('Append a free-form note to a device (repeat to append more). No text prints the notes; --clear empties them.')
1410
+ devicesCmd
1411
+ .command('note <name> [text...]', { hidden: true })
1162
1412
  .option('--clear', 'remove all notes from the device')
1163
1413
  .option('--json', 'output machine-readable JSON')
1164
1414
  .action(async (name, text, opts) => {
1165
1415
  try {
1416
+ configTombstoneNotice('note <name> [text...]', 'config <name> notes <text>');
1166
1417
  await mustGetDevice(name);
1167
1418
  if (opts.clear) {
1168
- unsetConfigValue('notes', { device: name });
1419
+ await runDevicesConfig(name, 'notes', [], { unset: true, quiet: true });
1169
1420
  if (opts.json)
1170
- process.stdout.write(JSON.stringify({ device: name, notes: [] }, null, 2) + '\n');
1421
+ writeJson({ device: name, notes: [] });
1171
1422
  else
1172
1423
  console.log(chalk.green(`Cleared notes on '${name}'.`));
1173
1424
  return;
1174
1425
  }
1175
1426
  if (text.length > 0) {
1176
- const existing = getConfigValue('notes', { device: name }).value ?? [];
1177
- const notes = [...existing, text.join(' ')];
1178
- setConfigValue('notes', notes, { device: name });
1427
+ await runDevicesConfig(name, 'notes', text, { quiet: true });
1428
+ const notes = getConfigValue('notes', { device: name }).value ?? [];
1179
1429
  if (opts.json)
1180
- process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
1430
+ writeJson({ device: name, notes });
1181
1431
  else
1182
1432
  console.log(chalk.green(`Noted on '${name}':`) + ` ${text.join(' ')}`);
1183
1433
  return;
1184
1434
  }
1185
1435
  const notes = getConfigValue('notes', { device: name }).value ?? [];
1186
1436
  if (opts.json) {
1187
- process.stdout.write(JSON.stringify({ device: name, notes }, null, 2) + '\n');
1437
+ writeJson({ device: name, notes });
1188
1438
  }
1189
1439
  else if (notes.length > 0) {
1190
1440
  console.log(chalk.bold(`Notes for '${name}'`));
@@ -1192,7 +1442,7 @@ Typical workflow:
1192
1442
  console.log(` ${chalk.gray('•')} ${n}`);
1193
1443
  }
1194
1444
  else {
1195
- console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices note ${name} "..."'.`));
1445
+ console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices config ${name} notes "..."'.`));
1196
1446
  }
1197
1447
  }
1198
1448
  catch (err) {
@@ -1200,45 +1450,15 @@ Typical workflow:
1200
1450
  process.exit(1);
1201
1451
  }
1202
1452
  });
1203
- setHelpSections(noteCmd, {
1204
- examples: `
1205
- agents devices note mac-mini "runs the releases — don't reboot"
1206
- agents devices note mac-mini "4 displays attached" # appends a second note
1207
- agents devices note mac-mini # print its notes
1208
- agents devices note mac-mini --clear # drop them all
1209
- `,
1210
- notes: `
1211
- Notes are operator memory for a box (why it exists, what to never do to
1212
- it). They sync like every other device doc; 'agents devices list --json'
1213
- carries them under config.notes.
1214
- `,
1215
- });
1216
- /** Device-scope config block for `list --json`, keyed by yamlKey (set keys only). */
1217
- const deviceConfigJson = (name) => {
1218
- const config = {};
1219
- for (const entry of listConfig({ device: name })) {
1220
- if (entry.spec.scope !== 'device' || entry.value === undefined)
1221
- continue;
1222
- config[entry.spec.yamlKey] = entry.value;
1223
- }
1224
- return Object.keys(config).length > 0 ? config : undefined;
1225
- };
1226
1453
  const runList = async (opts = {}) => {
1227
1454
  const reg = await loadDevices();
1228
1455
  const names = Object.keys(reg).sort();
1229
1456
  const interactiveHost = getConfigValue('interactive.host').value;
1230
- if (opts.json) {
1231
- // Registry + local device docs, always fast — the Factory extension polls
1232
- // this path. Each row carries its device-scope `config` (maxAgents,
1233
- // schedulerEnabled, notes, defaultBrowserProfile — set keys
1234
- // only) and an `interactive` flag for the configured interactive host.
1235
- process.stdout.write(JSON.stringify(names.map((n) => {
1236
- const config = deviceConfigJson(n);
1237
- return { ...reg[n], interactive: n === interactiveHost, ...(config ? { config } : {}) };
1238
- }), null, 2) + '\n');
1239
- return;
1240
- }
1241
1457
  if (names.length === 0) {
1458
+ if (opts.json) {
1459
+ process.stdout.write('[]\n');
1460
+ return;
1461
+ }
1242
1462
  console.log(chalk.gray("No devices. Run 'agents devices sync' or 'agents devices add <name> <user@host>'."));
1243
1463
  return;
1244
1464
  }
@@ -1272,6 +1492,19 @@ Typical workflow:
1272
1492
  if (statsMap)
1273
1493
  await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
1274
1494
  }
1495
+ if (opts.json) {
1496
+ process.stdout.write(JSON.stringify(names.map((name) => {
1497
+ const config = deviceConfigJson(name);
1498
+ const health = statsMap?.get(name);
1499
+ return {
1500
+ ...resolveDeviceProfile(reg[name]),
1501
+ interactive: name === interactiveHost,
1502
+ ...(config ? { config } : {}),
1503
+ ...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
1504
+ };
1505
+ }), null, 2) + '\n');
1506
+ return;
1507
+ }
1275
1508
  console.log(chalk.bold(`Devices (${names.length})`));
1276
1509
  for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost))
1277
1510
  console.log(line);
@@ -1295,7 +1528,7 @@ Typical workflow:
1295
1528
  .command('list')
1296
1529
  .alias('ls')
1297
1530
  .description('List registered devices with platform, address, reachability, and live resource headroom.')
1298
- .option('--json', 'output the registry as a JSON array (for scripts and hooks)')
1531
+ .option('--json', 'output effective device profiles, config, and health as a JSON array')
1299
1532
  .option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
1300
1533
  .option('--refresh', 'force a live probe of every device, bypassing the cache')
1301
1534
  .option('--live', 'alias of --refresh (shorter to type)')
@@ -1444,8 +1677,7 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1444
1677
  }
1445
1678
  });
1446
1679
  devicesCmd
1447
- .command('set <name>')
1448
- .description('Update fields on an existing device (platform, user, auth).')
1680
+ .command('set <name>', { hidden: true })
1449
1681
  .option('--platform <platform>', 'windows | linux | macos')
1450
1682
  .option('--user <user>', 'login user')
1451
1683
  .option('--auth <method>', 'key | password')
@@ -1455,21 +1687,35 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1455
1687
  .option('--clear-identity-file', 'return key auth to ssh-agent/default-key discovery')
1456
1688
  .action(async (name, opts) => {
1457
1689
  try {
1690
+ configTombstoneNotice('set <name> [--platform|--user|--auth|--bundle|--bundle-key|--identity-file …]', 'config <name> <ssh.*|platform> <value>');
1458
1691
  const existing = await mustGetDevice(name);
1459
- const nextMethod = opts.auth ?? existing.auth.method;
1692
+ const nextMethod = opts.auth ?? resolveDeviceProfile(existing).auth.method;
1460
1693
  if (opts.identityFile && nextMethod !== 'key') {
1461
1694
  throw new Error('--identity-file requires key auth; pass --auth key in the same command.');
1462
1695
  }
1463
- const auth = opts.auth || opts.bundle || opts.bundleKey || opts.identityFile || opts.clearIdentityFile
1464
- ? nextMethod === 'key'
1465
- ? { method: nextMethod, identityFile: opts.clearIdentityFile ? undefined : (opts.identityFile ?? existing.auth.identityFile) }
1466
- : { method: nextMethod, bundle: opts.bundle ?? existing.auth.bundle, bundleKey: opts.bundleKey ?? existing.auth.bundleKey }
1467
- : undefined;
1468
- const d = await upsertDevice(name, {
1469
- platform: opts.platform ?? undefined,
1470
- user: opts.user ?? undefined,
1471
- auth,
1472
- });
1696
+ const writes = [];
1697
+ if (opts.platform)
1698
+ writes.push({ key: 'platform', value: opts.platform });
1699
+ if (opts.user)
1700
+ writes.push({ key: 'ssh.user', value: opts.user });
1701
+ if (opts.auth)
1702
+ writes.push({ key: 'ssh.auth', value: opts.auth });
1703
+ if (opts.bundle)
1704
+ writes.push({ key: 'ssh.bundle', value: opts.bundle });
1705
+ if (opts.bundleKey)
1706
+ writes.push({ key: 'ssh.bundle-key', value: opts.bundleKey });
1707
+ if (opts.identityFile)
1708
+ writes.push({ key: 'ssh.identity-file', value: opts.identityFile });
1709
+ if (opts.clearIdentityFile)
1710
+ writes.push({ key: 'ssh.identity-file' }); // unset
1711
+ if (writes.length === 0) {
1712
+ printDevicesConfig(name, false);
1713
+ return;
1714
+ }
1715
+ for (const w of writes) {
1716
+ await runDevicesConfig(name, w.key, w.value !== undefined ? [w.value] : [], { unset: w.value === undefined, quiet: true });
1717
+ }
1718
+ const d = resolveDeviceProfile((await mustGetDevice(name)));
1473
1719
  console.log(chalk.green(`Updated device '${name}'`) + chalk.gray(` (auth: ${d.auth.method}${d.auth.bundle ? ` via ${d.auth.bundle}` : ''})`));
1474
1720
  }
1475
1721
  catch (err) {
@@ -1550,8 +1796,25 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1550
1796
  });
1551
1797
  devicesCmd
1552
1798
  .command('update')
1553
- .description('Roll out agents-cli to every online registered device (`agents upgrade --yes` on each). Offline devices are skipped.')
1799
+ .description('Roll out agents-cli to every online registered device (`agents upgrade --yes` on each), then verify each box actually runs the new version. Offline devices are skipped.')
1554
1800
  .argument('[version]', 'Target version or dist-tag (default: latest)')
1801
+ .addHelpText('after', `
1802
+ Examples:
1803
+ agents fleet update # roll out latest, then verify each box
1804
+ agents fleet update 1.22.35 # pin the target version
1805
+ agents devices update # same command under the devices group
1806
+
1807
+ After each upgrade the rollout asks the box what \`agents\` resolves to and what
1808
+ version that copy reports. A box that upgraded with exit 0 but still resolves to
1809
+ another install — a stale copy in a second node prefix, a Homebrew shim, or a
1810
+ hand-made link that sits earlier on PATH than the npm global — is reported
1811
+ \`stale\` with its resolved path, counted as NOT upgraded, and makes the command
1812
+ exit non-zero. Remove the install that owns the name, or reorder PATH on that
1813
+ box; \`agents doctor\` names it.
1814
+
1815
+ A box whose probe cannot answer (no POSIX shell, e.g. Windows) is reported
1816
+ \`unverified\` rather than counted as a success.
1817
+ `)
1555
1818
  .action(async (version) => {
1556
1819
  let cmd;
1557
1820
  try {
@@ -1568,8 +1831,13 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1568
1831
  return;
1569
1832
  }
1570
1833
  console.log(chalk.gray(`Running \`${cmd.join(' ')}\` on ${targets.filter((t) => !t.skip).length} online device(s)…`));
1571
- const results = runFleet(targets, cmd, { self: machineId() });
1572
- printFleetResults(results);
1834
+ const self = machineId();
1835
+ const results = runFleet(targets, cmd, { self });
1836
+ // `exit 0` from the upgrade only proves the npm global moved. Ask each box
1837
+ // what `agents` actually resolves to before calling the rollout a success
1838
+ // (RUSH-2446) — a dev install shadowing the global keeps running old code.
1839
+ const verifications = verifyFleetRollout(targets, results, version, { self });
1840
+ printFleetResults(results, verifications);
1573
1841
  });
1574
1842
  devicesCmd
1575
1843
  .command('run <cmd...>')
@@ -1590,6 +1858,74 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
1590
1858
  const results = runFleet(targets, cmd, { self: machineId() });
1591
1859
  printFleetResults(results);
1592
1860
  });
1861
+ devicesCmd
1862
+ .command('ps')
1863
+ .description('List agent tasks dispatched to devices with `agents run --device <name> --no-follow`. Reconciles each still-`running` record against the remote before listing. View a log with `agents logs <id>`.')
1864
+ .option('--json', 'Output JSON')
1865
+ .action((opts) => doDeviceTaskPs(!!opts.json));
1866
+ devicesCmd
1867
+ .command('stop <id>')
1868
+ .alias('kill')
1869
+ .description('Terminate a running dispatched task from this machine (SIGTERM the remote process group; marks it failed/143).')
1870
+ .action((id) => doDeviceTaskStop(id));
1871
+ }
1872
+ /**
1873
+ * `agents devices ps` — list tasks dispatched to devices (`agents run --device
1874
+ * <name> --no-follow`). Heals any 'running' record whose local follower died
1875
+ * (dropped connection, laptop sleep) against the remote `.exit` before listing,
1876
+ * so a finished run never shows stuck at 'running'.
1877
+ */
1878
+ async function doDeviceTaskPs(json) {
1879
+ const tasks = reconcileRunningTasks(listTasks());
1880
+ if (json) {
1881
+ console.log(JSON.stringify(tasks, null, 2));
1882
+ return;
1883
+ }
1884
+ if (tasks.length === 0) {
1885
+ console.log(chalk.gray('No dispatched tasks yet. Dispatch one: agents run <agent> "<task>" --device <name> --no-follow'));
1886
+ return;
1887
+ }
1888
+ const cols = terminalWidth();
1889
+ console.log(chalk.bold('ID').padEnd(11) + chalk.bold('NAME').padEnd(16) + chalk.bold('DEVICE').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
1890
+ for (const t of tasks) {
1891
+ const status = t.status === 'completed' ? chalk.green(t.status) : t.status === 'failed' ? chalk.red(t.status) : chalk.yellow(t.status);
1892
+ const nameCol = truncateToWidth(t.name ?? chalk.gray('-'), 15).padEnd(16);
1893
+ const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 16 + 10 + 11)));
1894
+ console.log(t.id.padEnd(11) + nameCol + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
1895
+ }
1896
+ }
1897
+ /** `agents devices stop <id>` — terminate a running dispatched task from the origin machine. */
1898
+ async function doDeviceTaskStop(ref) {
1899
+ // Heal first so we don't try to kill a process that already exited.
1900
+ const current = resolveTaskRef(ref);
1901
+ if (!current) {
1902
+ console.log(chalk.red(`Unknown task "${ref}".`));
1903
+ process.exitCode = 1;
1904
+ return;
1905
+ }
1906
+ const task = reconcileRunningTasks([current])[0] ?? current;
1907
+ if (task.status !== 'running') {
1908
+ console.log(chalk.gray(`Task ${task.id} is already ${task.status}` + (task.exitCode !== undefined ? ` (exit ${task.exitCode})` : '') + '.'));
1909
+ return;
1910
+ }
1911
+ try {
1912
+ const stopped = stopDispatchedTask(task);
1913
+ const statusColor = stopped.status === 'completed' ? chalk.green : chalk.yellow;
1914
+ const exitNote = stopped.exitCode === 143
1915
+ ? 'exit 143 / SIGTERM'
1916
+ : stopped.exitCode !== undefined
1917
+ ? `exit ${stopped.exitCode}`
1918
+ : stopped.status;
1919
+ console.log(chalk.green(`Stopped ${stopped.id}`) +
1920
+ chalk.gray(` on ${stopped.host}`) +
1921
+ ' ' + statusColor(stopped.status) +
1922
+ chalk.gray(` (${exitNote})`));
1923
+ console.log(chalk.gray(`Logs: agents logs ${stopped.id}`));
1924
+ }
1925
+ catch (err) {
1926
+ console.error(chalk.red(err?.message ?? err));
1927
+ process.exitCode = 1;
1928
+ }
1593
1929
  }
1594
1930
  /** Register the `agents ssh` smart wrapper. */
1595
1931
  function registerSshWrapper(program) {
@@ -1644,14 +1980,17 @@ the target when it exists, else the remote home. Same portable-cwd rule as
1644
1980
  // `user@device` (same device, login user overridden — dialed via its
1645
1981
  // Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
1646
1982
  // A bare unregistered alias still errors as "Unknown device".
1647
- const device = await resolveDeviceTarget(target);
1648
- if (!device) {
1983
+ const resolvedTarget = await resolveDeviceTarget(target);
1984
+ if (!resolvedTarget) {
1649
1985
  // Not a registered device — it may be a leased crabbox box slug. ssh into
1650
1986
  // it directly (crabbox@<tailnet|ip>:2222) before giving up.
1651
1987
  trySshLeasedBox(target, cmd); // exits the process on a match
1652
1988
  console.error(chalk.red(`Unknown device '${target}'. See 'agents devices list'.`));
1653
1989
  process.exit(1);
1654
1990
  }
1991
+ // The effective profile: central config (ssh.*/platform/user) overlaid on
1992
+ // the discovery record.
1993
+ const device = resolveDeviceProfile(resolvedTarget);
1655
1994
  // Preflight: a device Tailscale last saw offline would otherwise hang
1656
1995
  // for the full ConnectTimeout. Fail fast with a clear message instead.
1657
1996
  if (device.tailscale && !device.tailscale.online) {