@phnx-labs/agents-cli 1.22.35 → 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 (487) hide show
  1. package/CHANGELOG.md +666 -2
  2. package/README.md +75 -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 +67 -0
  94. package/dist/commands/share.js +275 -12
  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/commands.d.ts +2 -0
  161. package/dist/lib/commands.js +2 -0
  162. package/dist/lib/computer/sessions-list.d.ts +150 -0
  163. package/dist/lib/computer/sessions-list.js +363 -0
  164. package/dist/lib/config-keys.d.ts +86 -0
  165. package/dist/lib/config-keys.js +193 -0
  166. package/dist/lib/config-machine-keys.d.ts +12 -0
  167. package/dist/lib/config-machine-keys.js +17 -0
  168. package/dist/lib/config-transfer.js +3 -4
  169. package/dist/lib/crabbox/cli.d.ts +2 -2
  170. package/dist/lib/crabbox/cli.js +3 -3
  171. package/dist/lib/daemon-health.d.ts +22 -0
  172. package/dist/lib/daemon-health.js +33 -3
  173. package/dist/lib/daemon-services.d.ts +50 -0
  174. package/dist/lib/daemon-services.js +147 -0
  175. package/dist/lib/daemon-ticks.d.ts +35 -66
  176. package/dist/lib/daemon-ticks.js +77 -159
  177. package/dist/lib/daemon.d.ts +72 -4
  178. package/dist/lib/daemon.js +792 -202
  179. package/dist/lib/deeplink/register.d.ts +49 -0
  180. package/dist/lib/deeplink/register.js +253 -0
  181. package/dist/lib/deeplink/url.d.ts +34 -0
  182. package/dist/lib/deeplink/url.js +78 -0
  183. package/dist/lib/device-config.d.ts +124 -29
  184. package/dist/lib/device-config.js +346 -141
  185. package/dist/lib/devices/config-migration.d.ts +42 -0
  186. package/dist/lib/devices/config-migration.js +203 -0
  187. package/dist/lib/devices/connect.js +14 -7
  188. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  189. package/dist/lib/devices/doctor-findings.js +20 -1
  190. package/dist/lib/devices/harness-inventory.d.ts +11 -2
  191. package/dist/lib/devices/harness-inventory.js +0 -0
  192. package/dist/lib/devices/health.d.ts +1 -1
  193. package/dist/lib/devices/health.js +1 -1
  194. package/dist/lib/devices/pending.d.ts +22 -1
  195. package/dist/lib/devices/pending.js +67 -2
  196. package/dist/lib/devices/registry.d.ts +0 -25
  197. package/dist/lib/devices/registry.js +2 -83
  198. package/dist/lib/devices/resolve-profile.d.ts +20 -0
  199. package/dist/lib/devices/resolve-profile.js +44 -0
  200. package/dist/lib/devices/rollout-verify.d.ts +101 -0
  201. package/dist/lib/devices/rollout-verify.js +193 -0
  202. package/dist/lib/devices/ssh-config.js +10 -6
  203. package/dist/lib/event-families.d.ts +28 -0
  204. package/dist/lib/event-families.js +159 -0
  205. package/dist/lib/event-stream.d.ts +9 -1
  206. package/dist/lib/event-stream.js +26 -9
  207. package/dist/lib/events.d.ts +9 -1
  208. package/dist/lib/events.js +21 -6
  209. package/dist/lib/exec.d.ts +30 -0
  210. package/dist/lib/exec.js +219 -121
  211. package/dist/lib/feed-broadcast.d.ts +25 -0
  212. package/dist/lib/feed-broadcast.js +33 -0
  213. package/dist/lib/feed.d.ts +1 -1
  214. package/dist/lib/feed.js +12 -2
  215. package/dist/lib/fleet/capture.js +15 -0
  216. package/dist/lib/fleet/manifest.js +9 -0
  217. package/dist/lib/fleet/types.d.ts +9 -0
  218. package/dist/lib/fleet-status.js +10 -8
  219. package/dist/lib/git.d.ts +20 -6
  220. package/dist/lib/git.js +121 -17
  221. package/dist/lib/hosts/dispatch.d.ts +8 -3
  222. package/dist/lib/hosts/dispatch.js +29 -3
  223. package/dist/lib/hosts/logs.d.ts +2 -2
  224. package/dist/lib/hosts/logs.js +2 -2
  225. package/dist/lib/hosts/option.js +1 -1
  226. package/dist/lib/hosts/passthrough.d.ts +2 -3
  227. package/dist/lib/hosts/passthrough.js +10 -24
  228. package/dist/lib/hosts/progress.js +2 -2
  229. package/dist/lib/hosts/providers/devices.d.ts +3 -3
  230. package/dist/lib/hosts/providers/devices.js +18 -12
  231. package/dist/lib/hosts/ready.d.ts +5 -1
  232. package/dist/lib/hosts/ready.js +9 -3
  233. package/dist/lib/hosts/reconcile.d.ts +2 -2
  234. package/dist/lib/hosts/reconcile.js +2 -2
  235. package/dist/lib/hosts/registry.js +14 -9
  236. package/dist/lib/hosts/remote-cmd.js +1 -1
  237. package/dist/lib/hosts/remote-os.d.ts +9 -6
  238. package/dist/lib/hosts/remote-os.js +13 -6
  239. package/dist/lib/hosts/routing-flag.d.ts +23 -0
  240. package/dist/lib/hosts/routing-flag.js +41 -0
  241. package/dist/lib/hosts/run-target.js +1 -1
  242. package/dist/lib/hosts/session-index.js +1 -1
  243. package/dist/lib/hosts/tasks.d.ts +6 -6
  244. package/dist/lib/hosts/tasks.js +4 -4
  245. package/dist/lib/hosts/types.js +1 -2
  246. package/dist/lib/installations/resolve.js +2 -8
  247. package/dist/lib/isolation-boundary-report.js +0 -1
  248. package/dist/lib/linear-cache.d.ts +7 -0
  249. package/dist/lib/linear-cache.js +31 -4
  250. package/dist/lib/linear-project-counts.d.ts +1 -1
  251. package/dist/lib/linear-project-counts.js +2 -3
  252. package/dist/lib/linear-projects.js +1 -1
  253. package/dist/lib/loop.d.ts +2 -2
  254. package/dist/lib/loop.js +1 -1
  255. package/dist/lib/mailbox-target.d.ts +1 -1
  256. package/dist/lib/mailbox-target.js +1 -1
  257. package/dist/lib/manifest.js +7 -4
  258. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  259. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  260. package/dist/lib/menubar/snapshot.d.ts +3 -2
  261. package/dist/lib/menubar/snapshot.js +6 -2
  262. package/dist/lib/migrate-fold.d.ts +33 -0
  263. package/dist/lib/migrate-fold.js +130 -0
  264. package/dist/lib/migrate.d.ts +34 -34
  265. package/dist/lib/migrate.js +106 -128
  266. package/dist/lib/model-tier-overrides.d.ts +1 -1
  267. package/dist/lib/model-tier-overrides.js +15 -2
  268. package/dist/lib/monitors/config.d.ts +17 -3
  269. package/dist/lib/monitors/config.js +52 -18
  270. package/dist/lib/monitors/engine.d.ts +32 -1
  271. package/dist/lib/monitors/engine.js +94 -11
  272. package/dist/lib/monitors/fingerprint.d.ts +46 -0
  273. package/dist/lib/monitors/fingerprint.js +100 -0
  274. package/dist/lib/monitors/remote.d.ts +47 -0
  275. package/dist/lib/monitors/remote.js +81 -0
  276. package/dist/lib/monitors/state.d.ts +39 -0
  277. package/dist/lib/monitors/state.js +56 -0
  278. package/dist/lib/onepassword.d.ts +17 -0
  279. package/dist/lib/onepassword.js +52 -22
  280. package/dist/lib/open-url.js +1 -1
  281. package/dist/lib/picker.d.ts +14 -2
  282. package/dist/lib/picker.js +35 -13
  283. package/dist/lib/placement.d.ts +1 -1
  284. package/dist/lib/placement.js +1 -1
  285. package/dist/lib/pr-land-detach.d.ts +32 -0
  286. package/dist/lib/pr-land-detach.js +79 -0
  287. package/dist/lib/profiles-presets.js +13 -0
  288. package/dist/lib/profiles.d.ts +2 -0
  289. package/dist/lib/profiles.js +44 -1
  290. package/dist/lib/project-focus.d.ts +1 -1
  291. package/dist/lib/project-focus.js +1 -1
  292. package/dist/lib/project-probe.d.ts +4 -1
  293. package/dist/lib/project-probe.js +5 -4
  294. package/dist/lib/project-resources.js +2 -18
  295. package/dist/lib/project-root.d.ts +16 -0
  296. package/dist/lib/project-root.js +20 -2
  297. package/dist/lib/projects.d.ts +20 -1
  298. package/dist/lib/projects.js +78 -1
  299. package/dist/lib/refresh-coordinator.d.ts +24 -0
  300. package/dist/lib/refresh-coordinator.js +52 -0
  301. package/dist/lib/registry.d.ts +2 -2
  302. package/dist/lib/registry.js +1 -1
  303. package/dist/lib/resource-aliases.d.ts +13 -0
  304. package/dist/lib/resource-aliases.js +26 -0
  305. package/dist/lib/resources/types.d.ts +1 -1
  306. package/dist/lib/resources.d.ts +16 -0
  307. package/dist/lib/resources.js +66 -10
  308. package/dist/lib/rotate.d.ts +3 -4
  309. package/dist/lib/rotate.js +4 -5
  310. package/dist/lib/routine-activation.d.ts +22 -2
  311. package/dist/lib/routine-activation.js +63 -16
  312. package/dist/lib/routine-context.d.ts +9 -2
  313. package/dist/lib/routine-context.js +21 -6
  314. package/dist/lib/routines-project.d.ts +58 -48
  315. package/dist/lib/routines-project.js +190 -159
  316. package/dist/lib/routines.d.ts +41 -6
  317. package/dist/lib/routines.js +92 -34
  318. package/dist/lib/run-defaults.d.ts +5 -0
  319. package/dist/lib/run-defaults.js +11 -1
  320. package/dist/lib/runner.d.ts +10 -8
  321. package/dist/lib/runner.js +122 -39
  322. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  323. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  324. package/dist/lib/secrets/agent.d.ts +10 -3
  325. package/dist/lib/secrets/agent.js +43 -8
  326. package/dist/lib/secrets/bundles.d.ts +1 -0
  327. package/dist/lib/secrets/bundles.js +92 -80
  328. package/dist/lib/secrets/index.js +32 -9
  329. package/dist/lib/secrets/install-helper.d.ts +4 -4
  330. package/dist/lib/secrets/install-helper.js +4 -4
  331. package/dist/lib/secrets/push.d.ts +40 -6
  332. package/dist/lib/secrets/push.js +58 -11
  333. package/dist/lib/secrets/read-backoff.d.ts +1 -1
  334. package/dist/lib/secrets/read-backoff.js +1 -1
  335. package/dist/lib/secrets/remote.d.ts +24 -0
  336. package/dist/lib/secrets/remote.js +54 -4
  337. package/dist/lib/self-update.d.ts +108 -0
  338. package/dist/lib/self-update.js +276 -1
  339. package/dist/lib/session/active.d.ts +89 -5
  340. package/dist/lib/session/active.js +165 -10
  341. package/dist/lib/session/actor-sidecar.d.ts +1 -1
  342. package/dist/lib/session/actor-sidecar.js +2 -1
  343. package/dist/lib/session/db.d.ts +176 -2
  344. package/dist/lib/session/db.js +484 -17
  345. package/dist/lib/session/discover.js +27 -4
  346. package/dist/lib/session/hook-sessions.js +2 -2
  347. package/dist/lib/session/host-link.d.ts +2 -2
  348. package/dist/lib/session/host-link.js +2 -2
  349. package/dist/lib/session/pid-registry.d.ts +21 -0
  350. package/dist/lib/session/pid-registry.js +57 -0
  351. package/dist/lib/session/remote-active.js +15 -1
  352. package/dist/lib/session/remote.js +2 -2
  353. package/dist/lib/session/session-cache.d.ts +14 -7
  354. package/dist/lib/session/session-cache.js +51 -9
  355. package/dist/lib/session/state.d.ts +2 -2
  356. package/dist/lib/session/state.js +1 -1
  357. package/dist/lib/session/sync/config.d.ts +10 -10
  358. package/dist/lib/session/sync/config.js +10 -10
  359. package/dist/lib/session/sync/r2.d.ts +34 -0
  360. package/dist/lib/session/sync/r2.js +123 -0
  361. package/dist/lib/session/throughput.d.ts +1 -1
  362. package/dist/lib/session/throughput.js +1 -1
  363. package/dist/lib/session/types.d.ts +23 -1
  364. package/dist/lib/session/types.js +15 -0
  365. package/dist/lib/session/viewing-in.d.ts +1 -1
  366. package/dist/lib/session/viewing-in.js +1 -1
  367. package/dist/lib/session/watch.d.ts +101 -0
  368. package/dist/lib/session/watch.js +242 -0
  369. package/dist/lib/share/config.d.ts +4 -0
  370. package/dist/lib/share/config.js +1 -0
  371. package/dist/lib/share/provision.d.ts +38 -0
  372. package/dist/lib/share/provision.js +50 -0
  373. package/dist/lib/share/publish.d.ts +40 -1
  374. package/dist/lib/share/publish.js +116 -3
  375. package/dist/lib/share/worker-template.js +71 -6
  376. package/dist/lib/shims.d.ts +17 -3
  377. package/dist/lib/shims.js +98 -26
  378. package/dist/lib/signin-badge.js +3 -2
  379. package/dist/lib/skills.js +2 -0
  380. package/dist/lib/smart-launch.d.ts +8 -8
  381. package/dist/lib/smart-launch.js +80 -59
  382. package/dist/lib/ssh-exec.d.ts +1 -1
  383. package/dist/lib/ssh-exec.js +1 -1
  384. package/dist/lib/staleness/prune.d.ts +88 -0
  385. package/dist/lib/staleness/prune.js +69 -0
  386. package/dist/lib/staleness/registry.d.ts +1 -1
  387. package/dist/lib/staleness/writers/commands.js +34 -2
  388. package/dist/lib/staleness/writers/hooks.js +16 -0
  389. package/dist/lib/staleness/writers/kinds.d.ts +6 -0
  390. package/dist/lib/staleness/writers/skills.js +19 -0
  391. package/dist/lib/staleness/writers/subagents.d.ts +2 -3
  392. package/dist/lib/staleness/writers/subagents.js +0 -4
  393. package/dist/lib/staleness/writers/types.d.ts +32 -0
  394. package/dist/lib/startup/command-registry.d.ts +6 -12
  395. package/dist/lib/startup/command-registry.js +11 -31
  396. package/dist/lib/startup/dev-build.d.ts +10 -0
  397. package/dist/lib/startup/dev-build.js +13 -1
  398. package/dist/lib/startup/spellcheck.d.ts +18 -0
  399. package/dist/lib/startup/spellcheck.js +45 -0
  400. package/dist/lib/state.d.ts +36 -9
  401. package/dist/lib/state.js +96 -46
  402. package/dist/lib/subagents-registry.d.ts +0 -7
  403. package/dist/lib/subagents-registry.js +38 -65
  404. package/dist/lib/subagents.d.ts +0 -33
  405. package/dist/lib/subagents.js +0 -75
  406. package/dist/lib/sync-umbrella.js +1 -1
  407. package/dist/lib/teams/agents.d.ts +41 -1
  408. package/dist/lib/teams/agents.js +187 -9
  409. package/dist/lib/teams/api.d.ts +11 -1
  410. package/dist/lib/teams/api.js +11 -2
  411. package/dist/lib/teams/delivery.d.ts +41 -0
  412. package/dist/lib/teams/delivery.js +60 -0
  413. package/dist/lib/teams/registry.d.ts +14 -0
  414. package/dist/lib/teams/registry.js +51 -1
  415. package/dist/lib/teams/remoteWorktree.d.ts +19 -0
  416. package/dist/lib/teams/remoteWorktree.js +29 -0
  417. package/dist/lib/teams/scheduler.d.ts +1 -1
  418. package/dist/lib/teams/scheduler.js +1 -1
  419. package/dist/lib/teams/worktree.d.ts +24 -0
  420. package/dist/lib/teams/worktree.js +42 -0
  421. package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
  422. package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
  423. package/dist/lib/terminal/engine.d.ts +4 -0
  424. package/dist/lib/terminal/engine.js +8 -2
  425. package/dist/lib/terminal/types.d.ts +21 -2
  426. package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
  427. package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
  428. package/dist/lib/tickets/list.d.ts +53 -0
  429. package/dist/lib/tickets/list.js +153 -0
  430. package/dist/lib/tmux/orphan-reap.d.ts +320 -0
  431. package/dist/lib/tmux/orphan-reap.js +644 -0
  432. package/dist/lib/tmux/session.d.ts +68 -3
  433. package/dist/lib/tmux/session.js +95 -5
  434. package/dist/lib/triggers/handlers.d.ts +20 -0
  435. package/dist/lib/triggers/handlers.js +6 -1
  436. package/dist/lib/types.d.ts +46 -34
  437. package/dist/lib/usage-fleet.d.ts +32 -0
  438. package/dist/lib/usage-fleet.js +125 -0
  439. package/dist/lib/usage-refresh.d.ts +3 -3
  440. package/dist/lib/usage-refresh.js +19 -16
  441. package/dist/lib/usage.d.ts +51 -47
  442. package/dist/lib/usage.js +145 -153
  443. package/dist/lib/version-duplicates.js +2 -2
  444. package/dist/lib/versions.d.ts +56 -13
  445. package/dist/lib/versions.js +195 -45
  446. package/dist/lib/watchdog/log.d.ts +1 -1
  447. package/dist/lib/watchdog/log.js +3 -3
  448. package/dist/lib/watchdog/rotate.d.ts +6 -6
  449. package/dist/lib/watchdog/rotate.js +6 -6
  450. package/dist/lib/workflows.d.ts +1 -1
  451. package/dist/lib/yaml-io.d.ts +47 -0
  452. package/dist/lib/yaml-io.js +55 -0
  453. package/package.json +2 -1
  454. package/scripts/install-helper.js +2 -2
  455. package/scripts/postinstall.js +23 -20
  456. package/dist/commands/defaults.d.ts +0 -7
  457. package/dist/commands/defaults.js +0 -107
  458. package/dist/commands/export.d.ts +0 -11
  459. package/dist/commands/export.js +0 -215
  460. package/dist/commands/helper.d.ts +0 -12
  461. package/dist/commands/helper.js +0 -87
  462. package/dist/commands/hosts.d.ts +0 -11
  463. package/dist/commands/hosts.js +0 -330
  464. package/dist/commands/lock.d.ts +0 -12
  465. package/dist/commands/lock.js +0 -70
  466. package/dist/commands/pull.d.ts +0 -17
  467. package/dist/commands/pull.js +0 -39
  468. package/dist/commands/push.d.ts +0 -14
  469. package/dist/commands/push.js +0 -30
  470. package/dist/commands/wallet.d.ts +0 -20
  471. package/dist/commands/wallet.js +0 -216
  472. package/dist/commands/worktree.d.ts +0 -19
  473. package/dist/commands/worktree.js +0 -272
  474. package/dist/lib/account-labels.d.ts +0 -24
  475. package/dist/lib/account-labels.js +0 -72
  476. package/dist/lib/auto-dispatch-linear.d.ts +0 -18
  477. package/dist/lib/auto-dispatch-linear.js +0 -107
  478. package/dist/lib/auto-dispatch-provider.d.ts +0 -10
  479. package/dist/lib/auto-dispatch-provider.js +0 -30
  480. package/dist/lib/auto-dispatch.d.ts +0 -93
  481. package/dist/lib/auto-dispatch.js +0 -128
  482. package/dist/lib/export.d.ts +0 -72
  483. package/dist/lib/export.js +0 -269
  484. package/dist/lib/lock.d.ts +0 -93
  485. package/dist/lib/lock.js +0 -207
  486. package/dist/lib/wallet/index.d.ts +0 -78
  487. package/dist/lib/wallet/index.js +0 -253
@@ -32,11 +32,16 @@ import { listHookEntriesFromDir } from '../lib/hooks.js';
32
32
  import { getResourceInventory } from '../lib/resource-inventory.js';
33
33
  import { listMcpServerConfigs, discoverMcpConfigsFromRepo } from '../lib/mcp.js';
34
34
  import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups, inspectPluginCapabilities, pluginCapabilityLabels } from '../lib/plugins.js';
35
+ import { showResourceList } from './resource-view.js';
35
36
  import { PLUGIN_GROUP_COLORS } from './plugins.js';
37
+ import { isInteractiveTerminal } from './utils.js';
36
38
  import { countSessionsInScope } from '../lib/session/discover.js';
37
39
  import { isSessionTrackedAgent } from '../lib/session/types.js';
38
40
  import { damerauLevenshtein } from '../lib/fuzzy.js';
39
41
  import { terminalWidth, truncateToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
42
+ import { readJobFileResult, getJobRunsDir, readRunMeta, isOneShotRoutine, isPastOneShotRoutine, listJobs, } from '../lib/routines.js';
43
+ import { routineDeviceIndex, currentRoutineDevice, routineEnabledOnThisDevice, } from '../lib/routine-activation.js';
44
+ import { humanizeCron } from '../lib/routines-format.js';
40
45
  /** Resource kinds the inspect command can drill into. */
41
46
  const DRILLABLE_KINDS = [
42
47
  'commands',
@@ -47,15 +52,19 @@ const DRILLABLE_KINDS = [
47
52
  'plugins',
48
53
  'workflows',
49
54
  'subagents',
55
+ 'routines',
50
56
  ];
51
57
  /**
52
58
  * Summary-view partition. SIMPLE kinds render as a one-line count + name preview;
53
- * RICH kinds (hooks/plugins/mcp) get their own expanded section showing each
54
- * item's key detail (events/predicates, bundle contents, transport/url). Together
55
- * they cover every DrillableKind.
59
+ * RICH kinds (hooks/plugins/mcp/routines) get their own expanded section showing
60
+ * each item's key detail (events/predicates, bundle contents, transport/url,
61
+ * schedule + where a routine actually fires). Together they cover every
62
+ * DrillableKind — the type below makes that claim load-bearing.
56
63
  */
57
64
  const SIMPLE_KINDS = ['commands', 'skills', 'rules', 'subagents', 'workflows'];
58
- const RICH_KINDS = ['hooks', 'plugins', 'mcp'];
65
+ const RICH_KINDS = ['hooks', 'plugins', 'mcp', 'routines'];
66
+ const _kindsPartitioned = true;
67
+ void _kindsPartitioned;
59
68
  /**
60
69
  * Singular aliases for the plural drill-down flags. `--plugin code` reads as
61
70
  * "show the one plugin named code" — a required-value flag that always lands in
@@ -70,6 +79,7 @@ const SINGULAR_DRILL_ALIASES = {
70
79
  plugin: 'plugins',
71
80
  workflow: 'workflows',
72
81
  subagent: 'subagents',
82
+ routine: 'routines',
73
83
  };
74
84
  const CAPABILITY_NAMES = [
75
85
  'hooks', 'mcp', 'skills', 'commands', 'subagents', 'plugins', 'workflows', 'rules', 'allowlist',
@@ -262,6 +272,13 @@ function isDotAgentsRoot(dir) {
262
272
  return true;
263
273
  }
264
274
  for (const kind of DRILLABLE_KINDS) {
275
+ // `routines/` alone is NOT a marker. Shipping example routine YAML for
276
+ // `agents routines add` is a documented pattern (this repo's own
277
+ // apps/cli/routines/), and that directory is the ONLY kind dir there — so
278
+ // counting it would make `agents inspect apps/cli` resolve a source tree as
279
+ // a DotAgents repo. Same reasoning as the nested-`.agents/` case above.
280
+ if (kind === 'routines')
281
+ continue;
265
282
  if (safeStat(path.join(dir, kind))?.isDirectory())
266
283
  return true;
267
284
  }
@@ -271,9 +288,29 @@ export async function inspectRepo(repo, options) {
271
288
  const drill = pickDrillKind(options);
272
289
  const jsonHead = { repo: repo.label, root: repo.root };
273
290
  if (drill) {
274
- const items = collectRepoKind(repo, drill.kind);
291
+ // Routines carry live state, and an unreadable device document silently
292
+ // removes that device from every `devices` cell — say so on this path too,
293
+ // not only in the overview.
294
+ let items;
295
+ if (drill.kind === 'routines') {
296
+ const provider = defaultRoutineLive();
297
+ items = collectRepoRoutines(repo, provider.live);
298
+ if (!options.json) {
299
+ for (const err of provider.errors) {
300
+ console.error(chalk.yellow(`device config unreadable — ${err}`));
301
+ }
302
+ }
303
+ }
304
+ else {
305
+ items = collectRepoKind(repo, drill.kind);
306
+ }
275
307
  if (drill.query === true || drill.query === undefined) {
276
- renderItemList(repo.label, jsonHead, drill.kind, items, options);
308
+ // A repo's hooks are wired by its OWN agents.yaml, not by whatever this
309
+ // machine has installed centrally — the same manifest the repo overview
310
+ // reads at collectRepoKind.
311
+ await renderItemList(repo.label, jsonHead, drill.kind, items, options, drill.kind === 'hooks'
312
+ ? hookManifestByScript(hookManifestFromFile(path.join(repo.root, 'agents.yaml')))
313
+ : undefined);
277
314
  }
278
315
  else {
279
316
  renderItemDetail(repo.label, jsonHead, drill.kind, String(drill.query), items, options);
@@ -314,8 +351,331 @@ export function collectRepoKind(repo, kind) {
314
351
  if (fs.existsSync(subrulesDir))
315
352
  return readResourceDir(subrulesDir, kind, repo.label);
316
353
  }
354
+ // Routines are YAML job definitions whose real state (does it fire, where, how
355
+ // did it last run) lives outside the file. Their directory also holds
356
+ // `<name>/home/` sandbox overlays, which a flat readdir would count as
357
+ // routines — on this author's box that turned 24 routines into 49.
358
+ if (kind === 'routines')
359
+ return collectRepoRoutines(repo);
317
360
  return readResourceDir(path.join(repo.root, kind), kind, repo.label);
318
361
  }
362
+ const NO_LIVE_STATE = {
363
+ devices: [], materialized: false, thisDevice: '', enabledHere: null, lastStatus: null, lastAt: null,
364
+ };
365
+ /**
366
+ * Whether the routine actually runs on this machine.
367
+ *
368
+ * Exactly `applyDeviceActivation` (`lib/routines.ts`): this device's own
369
+ * allowlist answer wins when it exists, and the file's `enabled:` governs only
370
+ * while that answer is `null`. Measured at the same scope as the daemon's, so
371
+ * `agents inspect --routines` and `agents routines list` cannot disagree.
372
+ */
373
+ function routineEnabledHere(config, live) {
374
+ return live.enabledHere === null ? config.enabled : live.enabledHere;
375
+ }
376
+ /**
377
+ * The last run we can read, without paying for the whole history.
378
+ *
379
+ * `getLatestRun` goes through `listRuns`, which parses EVERY meta.json under the
380
+ * routine — one firing every 3 minutes accumulates hundreds. Run ids are ISO-ish
381
+ * and sort lexicographically, so walking from the newest and stopping at the
382
+ * first readable record gives the same answer for a bounded number of reads.
383
+ */
384
+ function latestRunOf(name, maxProbes = 20) {
385
+ let runIds;
386
+ try {
387
+ runIds = fs.readdirSync(getJobRunsDir(name), { withFileTypes: true })
388
+ .filter(e => e.isDirectory())
389
+ .map(e => e.name)
390
+ .sort()
391
+ .reverse();
392
+ }
393
+ catch {
394
+ return null;
395
+ }
396
+ for (const runId of runIds.slice(0, maxProbes)) {
397
+ const meta = readRunMeta(name, runId);
398
+ if (meta)
399
+ return meta;
400
+ }
401
+ return null;
402
+ }
403
+ /**
404
+ * Default live provider: one fleet index per collect, one bounded run read per
405
+ * routine, and this device's own activation answer per routine.
406
+ *
407
+ * Returns the collected `errors` alongside so the caller can surface an
408
+ * unreadable device document instead of letting the affected device silently
409
+ * vanish from every `devices` cell.
410
+ */
411
+ function defaultRoutineLive() {
412
+ let index;
413
+ try {
414
+ index = routineDeviceIndex();
415
+ }
416
+ catch (err) {
417
+ return { live: () => NO_LIVE_STATE, errors: [err.message] };
418
+ }
419
+ const thisDevice = (() => { try {
420
+ return currentRoutineDevice();
421
+ }
422
+ catch {
423
+ return '';
424
+ } })();
425
+ const errors = [...index.errors];
426
+ // This device's own doc is read through readMeta, which THROWS on a corrupt
427
+ // one. Falling back to the file's `enabled:` silently would be the same
428
+ // silent-failure class the index's error collection just removed, so record it
429
+ // once — the answer is identical for every routine on this machine.
430
+ let ownDocError = null;
431
+ const enabledOnThisDevice = (name) => {
432
+ try {
433
+ return routineEnabledOnThisDevice(name);
434
+ }
435
+ catch (err) {
436
+ if (!ownDocError) {
437
+ ownDocError = `this device's own config is unreadable — ${err.message}`;
438
+ errors.push(ownDocError);
439
+ }
440
+ return null;
441
+ }
442
+ };
443
+ const live = (name) => {
444
+ const run = latestRunOf(name);
445
+ return {
446
+ devices: index.byRoutine.get(name) ?? [],
447
+ materialized: index.materialized,
448
+ thisDevice,
449
+ // The same call applyDeviceActivation makes — per-machine, not fleet-wide.
450
+ enabledHere: enabledOnThisDevice(name),
451
+ lastStatus: run?.status ?? null,
452
+ lastAt: run?.startedAt ?? null,
453
+ };
454
+ };
455
+ return { live, errors };
456
+ }
457
+ /** `2h` / `3d` / `just now` — compact enough for a 10-column cell. */
458
+ export function compactAge(iso, now = new Date()) {
459
+ if (!iso)
460
+ return '';
461
+ const then = Date.parse(iso);
462
+ if (!Number.isFinite(then))
463
+ return '';
464
+ const mins = Math.floor((now.getTime() - then) / 60000);
465
+ if (mins < 1)
466
+ return 'now';
467
+ if (mins < 60)
468
+ return `${mins}m`;
469
+ const hours = Math.floor(mins / 60);
470
+ if (hours < 24)
471
+ return `${hours}h`;
472
+ const days = Math.floor(hours / 24);
473
+ if (days < 365)
474
+ return `${days}d`;
475
+ return `${Math.floor(days / 365)}y`;
476
+ }
477
+ /** What makes this routine fire: a humanized cron, an event trigger, or nothing. */
478
+ export function routineFireLabel(job, now = new Date()) {
479
+ const parts = [];
480
+ if (job.schedule)
481
+ parts.push(humanizeCron(job.schedule, job.timezone));
482
+ if (job.trigger) {
483
+ const t = job.trigger;
484
+ const event = manifestText(t.event) || manifestText(t.type) || 'event';
485
+ const qualifier = [manifestText(t.action), manifestText(t.label)].filter(Boolean).join(' ');
486
+ parts.push(qualifier ? `on ${event} (${qualifier})` : `on ${event}`);
487
+ }
488
+ if (parts.length === 0)
489
+ return 'no schedule or trigger';
490
+ if (isOneShotRoutine(job)) {
491
+ parts.push(isPastOneShotRoutine(job, now) ? '(one-shot, expired)' : '(one-shot)');
492
+ }
493
+ return parts.join(' · ');
494
+ }
495
+ /** What the routine actually executes. Exactly one of agent/workflow/command is set. */
496
+ export function routineTargetLabel(job) {
497
+ const agent = manifestText(job.agent);
498
+ if (agent)
499
+ return `agent ${agent}`;
500
+ const workflow = manifestText(job.workflow);
501
+ if (workflow)
502
+ return `workflow ${workflow}`;
503
+ if (manifestText(job.command))
504
+ return 'command (shell)';
505
+ return 'nothing configured';
506
+ }
507
+ /**
508
+ * A routine's one-line intent.
509
+ *
510
+ * Routine YAML has no `description:` key, so the generic first-prose-line reader
511
+ * returns `name: growth-metrics` for one file and a real sentence for the next.
512
+ * Prefer the prompt's first sentence, then the file's leading comment block, then
513
+ * the first meaningful line of a shell command.
514
+ */
515
+ export function routineDescription(job, raw) {
516
+ const prompt = job ? manifestText(job.prompt).trim() : '';
517
+ if (prompt)
518
+ return summaryLine(truncate(prompt.replace(/\s+/g, ' '), 400));
519
+ const lines = raw.split('\n');
520
+ for (const line of lines) {
521
+ const comment = line.match(/^\s*#\s?(.+)$/);
522
+ if (!comment) {
523
+ if (line.trim())
524
+ break;
525
+ else
526
+ continue;
527
+ }
528
+ const text = comment[1].trim().replace(/^Routine:\s*/i, '');
529
+ // Skip the shebang-ish and install-instruction noise that opens many files.
530
+ if (!text || /^(yaml-language-server|Install:|Usage:)/i.test(text))
531
+ continue;
532
+ return summaryLine(text);
533
+ }
534
+ const command = job ? manifestText(job.command) : '';
535
+ for (const line of command.split('\n')) {
536
+ const text = line.trim();
537
+ if (!text || text.startsWith('#') || /^set\s+-/.test(text))
538
+ continue;
539
+ return truncate(text, 120);
540
+ }
541
+ return '';
542
+ }
543
+ /**
544
+ * Read every routine one DotAgents repo declares.
545
+ *
546
+ * Reads exactly `<root>/routines/`, never the layered `listJobs` — `collectRepoKind`
547
+ * is a single-root view, and returning `~/.agents` routines while inspecting a
548
+ * project repo would be a lie. `live` is injectable because `getUserAgentsDir()`
549
+ * and `getRunsDir()` have no env override, so tests cannot otherwise keep this
550
+ * off the developer's real fleet state.
551
+ */
552
+ export function collectRepoRoutines(repo, live = defaultRoutineLive().live, now = new Date()) {
553
+ const dir = path.join(repo.root, 'routines');
554
+ let entries;
555
+ try {
556
+ entries = fs.readdirSync(dir, { withFileTypes: true });
557
+ }
558
+ catch {
559
+ return [];
560
+ }
561
+ const items = [];
562
+ const seen = new Map();
563
+ // Files only: `routines/<name>/home/` is a sandbox overlay HOME, not a routine.
564
+ //
565
+ // `.yml` before `.yaml` for the same basename, because that is the order
566
+ // `readJobFromDir` tries — so when both exist we describe the file the daemon
567
+ // actually loads. A plain alphabetical sort put `.yaml` first and reported the
568
+ // wrong schedule and command for a routine that fires from its sibling.
569
+ const files = entries
570
+ .filter(e => e.isFile() && /\.ya?ml$/.test(e.name) && !e.name.startsWith('.'))
571
+ .map(e => e.name)
572
+ .sort((a, b) => {
573
+ const an = a.replace(/\.ya?ml$/, ''), bn = b.replace(/\.ya?ml$/, '');
574
+ if (an !== bn)
575
+ return an.localeCompare(bn);
576
+ return (a.endsWith('.yml') ? 0 : 1) - (b.endsWith('.yml') ? 0 : 1);
577
+ });
578
+ for (const file of files) {
579
+ const filePath = path.join(dir, file);
580
+ const { config, problem } = readJobFileResult(filePath);
581
+ const basename = file.replace(/\.ya?ml$/, '');
582
+ const name = config?.name || basename;
583
+ const existing = seen.get(name);
584
+ if (existing) {
585
+ // `writeJob` refuses to edit a routine with both extensions. Keep the item
586
+ // built from the file that loads, and flag the ambiguity on it — in `extra`
587
+ // for the panes AND in `json` so a --json consumer sees it too.
588
+ const collision = `both .yml and .yaml exist — ${file} is ignored; resolve before editing`;
589
+ existing.extra = [...(existing.extra ?? []), ['problem', collision]];
590
+ if (existing.json)
591
+ existing.json.problem = collision;
592
+ continue;
593
+ }
594
+ const raw = (() => { try {
595
+ return fs.readFileSync(filePath, 'utf-8');
596
+ }
597
+ catch {
598
+ return '';
599
+ } })();
600
+ const state = config ? live(name) : NO_LIVE_STATE;
601
+ const extra = [];
602
+ if (config) {
603
+ extra.push(['fires', routineFireLabel(config, now)]);
604
+ extra.push(['runs', routineTargetLabel(config)]);
605
+ extra.push(['devices', state.devices.length > 0
606
+ ? state.devices.join(', ')
607
+ : state.materialized ? 'no device — will not fire' : 'no device allowlist yet']);
608
+ extra.push(['last', state.lastStatus
609
+ ? `${state.lastStatus}${state.lastAt ? ` · ${compactAge(state.lastAt, now)} ago` : ''}`
610
+ : 'never run']);
611
+ extra.push(['enabled', routineEnabledHere(config, state)
612
+ ? `yes${state.thisDevice ? ` (on ${state.thisDevice})` : ''}`
613
+ : `no${state.thisDevice ? ` (on ${state.thisDevice})` : ''}`]);
614
+ const repoRef = manifestText(config.repo);
615
+ if (repoRef)
616
+ extra.push(['repo', repoRef]);
617
+ const tz = manifestText(config.timezone);
618
+ if (tz)
619
+ extra.push(['timezone', tz]);
620
+ if (config.catchup === false)
621
+ extra.push(['catchup', 'off — a missed fire is lost']);
622
+ const host = manifestText(config.host);
623
+ if (host)
624
+ extra.push(['host', host]);
625
+ const pinned = manifestList(config.devices);
626
+ if (pinned.length > 0)
627
+ extra.push(['pinned', pinned.join(', ')]);
628
+ if (name !== basename)
629
+ extra.push(['file', file]);
630
+ }
631
+ else if (problem) {
632
+ extra.push(['problem', problem]);
633
+ }
634
+ const item = {
635
+ name,
636
+ source: repo.label,
637
+ path: filePath,
638
+ linkTarget: filePath,
639
+ description: routineDescription(config, raw),
640
+ extra,
641
+ // Field names match `agents routines list --json` where they overlap, so
642
+ // one script can consume both. `devices` is what the YAML pins;
643
+ // `enabledDevices` is what the fleet actually allows — the diagnostic pair.
644
+ json: {
645
+ path: filePath,
646
+ schedule: config?.schedule ?? null,
647
+ scheduleHuman: config ? routineFireLabel(config, now) : null,
648
+ trigger: config?.trigger ?? null,
649
+ runs: config ? routineTargetLabel(config) : null,
650
+ agent: config?.agent ?? null,
651
+ workflow: config?.workflow ?? null,
652
+ command: config?.command ?? null,
653
+ timezone: config?.timezone ?? null,
654
+ repo: config?.repo ?? null,
655
+ // Effective on THIS machine, matching `agents routines list`.
656
+ enabled: config ? routineEnabledHere(config, state) : false,
657
+ // What the file itself declares, before device activation overrides it.
658
+ enabledInFile: config?.enabled ?? false,
659
+ catchup: config ? config.catchup !== false : null,
660
+ devices: config ? manifestList(config.devices) : [],
661
+ enabledDevices: state.devices,
662
+ activationMaterialized: state.materialized,
663
+ oneShot: config ? isOneShotRoutine(config) : false,
664
+ expired: config ? isPastOneShotRoutine(config, now) : false,
665
+ lastStatus: state.lastStatus,
666
+ lastRunAt: state.lastAt,
667
+ problem,
668
+ },
669
+ };
670
+ seen.set(name, item);
671
+ items.push(item);
672
+ }
673
+ return items.sort((a, b) => a.name.localeCompare(b.name));
674
+ }
675
+ /** Read one `extra` row back out — the renderers' only accessor. */
676
+ function extraOf(item, key) {
677
+ return item.extra?.find(([k]) => k === key)?.[1] ?? '';
678
+ }
319
679
  /**
320
680
  * Enumerate one directory of resources of `kind`, skipping dotfiles, build caches,
321
681
  * and directory docs. Shared so the rules `subrules/` branch and the default
@@ -391,6 +751,19 @@ export function pathSize(p) {
391
751
  }
392
752
  return { bytes, files };
393
753
  }
754
+ /**
755
+ * Weigh only the entries actually listed, for a kind whose directory holds more
756
+ * than its resources (routines share theirs with sandbox overlay HOMEs).
757
+ */
758
+ function itemsSize(items) {
759
+ let bytes = 0, files = 0;
760
+ for (const item of items) {
761
+ const sub = pathSize(item.path);
762
+ bytes += sub.bytes;
763
+ files += sub.files;
764
+ }
765
+ return { bytes, files };
766
+ }
394
767
  /** Human byte size: "84 KB", "3.1 MB". */
395
768
  export function formatBytes(n) {
396
769
  if (n < 1024)
@@ -442,10 +815,24 @@ function renderRepoSummary(repo, options) {
442
815
  let totalBytes = 0, totalFiles = 0;
443
816
  let repoHookByScript = new Map();
444
817
  let repoMcpConfigs = new Map();
818
+ let routineActivationErrors = [];
445
819
  if (!options.brief) {
446
820
  for (const kind of DRILLABLE_KINDS) {
447
- const items = collectRepoKind(repo, kind);
448
- const size = pathSize(path.join(repo.root, kind));
821
+ // Routines share one live provider across the collect so the fleet index is
822
+ // read once, and so its errors survive to be reported below.
823
+ let items;
824
+ if (kind === 'routines') {
825
+ const provider = defaultRoutineLive();
826
+ routineActivationErrors = provider.errors;
827
+ items = collectRepoRoutines(repo, provider.live);
828
+ }
829
+ else {
830
+ items = collectRepoKind(repo, kind);
831
+ }
832
+ // `routines/` also holds `<name>/home/` sandbox overlays — often far larger
833
+ // than the definitions and not resources at all, so weigh the files listed
834
+ // rather than the directory tree.
835
+ const size = kind === 'routines' ? itemsSize(items) : pathSize(path.join(repo.root, kind));
449
836
  kindData[kind] = { items, size };
450
837
  totalBytes += size.bytes;
451
838
  totalFiles += size.files;
@@ -486,6 +873,12 @@ function renderRepoSummary(repo, options) {
486
873
  version: i.extra?.find(([k]) => k === 'version')?.[1],
487
874
  groups: Object.fromEntries((i.groups ?? []).map(g => [g.label, g.items.length])),
488
875
  })) }];
876
+ if (kind === 'routines')
877
+ return [kind, { ...base, items: items.map(i => ({
878
+ name: i.name,
879
+ description: i.description,
880
+ ...(i.json ?? {}),
881
+ })) }];
489
882
  return [kind, base];
490
883
  })),
491
884
  }, null, 2));
@@ -529,18 +922,35 @@ function renderRepoSummary(repo, options) {
529
922
  }
530
923
  }
531
924
  if (!options.brief) {
532
- console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} files`);
925
+ console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} ${totalFiles === 1 ? 'file' : 'files'}`);
533
926
  console.log('\n' + chalk.bold('Resources'));
534
927
  for (const kind of SIMPLE_KINDS) {
535
928
  const { items, size } = kindData[kind];
536
929
  const count = String(items.length).padStart(4);
537
930
  const sz = items.length > 0 ? formatBytes(size.bytes).padStart(8) : ''.padEnd(8);
538
- const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 4), 60)) : '';
539
- console.log(` ${kind.padEnd(10)} ${count} ${sz} ${preview}`.trimEnd());
931
+ // Width-aware, not a fixed 60: the old cap cut its own "…(+16)" tail off
932
+ // the rules row identically at 80, 100 and 160 columns.
933
+ const prefix = ` ${kind.padEnd(10)} ${count} ${sz} `;
934
+ const preview = items.length > 0
935
+ ? chalk.gray(truncateToWidth(previewNames(items, 4), Math.max(12, terminalWidth() - stringWidth(prefix))))
936
+ : '';
937
+ console.log(`${prefix}${preview}`.trimEnd());
540
938
  }
541
939
  printExpandedSection('Hooks', hookRows(kindData.hooks.items, repoHookByScript));
542
940
  printExpandedSection('Plugins', pluginRows(kindData.plugins.items));
543
941
  printExpandedSection('MCP', mcpRows(kindData.mcp.items, repoMcpConfigs));
942
+ printExpandedSection('Routines', routineRows(kindData.routines.items));
943
+ const dark = routineDarkWarning(kindData.routines.items);
944
+ if (dark) {
945
+ console.log('');
946
+ console.log(` ${chalk.yellow('⚠')} ${dark}`);
947
+ console.log(chalk.gray(` Fix: agents routines devices <name> --set <device>`));
948
+ }
949
+ // An unreadable device document silently removes that device from every
950
+ // `devices` cell, which reads as "not enabled there" — say so instead.
951
+ for (const err of routineActivationErrors) {
952
+ console.log(chalk.gray(` device config unreadable — ${err}`));
953
+ }
544
954
  }
545
955
  console.log('');
546
956
  console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
@@ -685,6 +1095,7 @@ async function renderSummary(agent, version, versionHome, options) {
685
1095
  printExpandedSection('Hook files', hookRows(itemsByKind.hooks, hookByScript));
686
1096
  printExpandedSection('Plugins', pluginRows(itemsByKind.plugins));
687
1097
  printExpandedSection('MCP', mcpRows(itemsByKind.mcp, mcpConfigs));
1098
+ printExpandedSection('Routines', agentRoutineRows(itemsByKind.routines));
688
1099
  }
689
1100
  if (sessions) {
690
1101
  console.log('\n' + chalk.bold('Sessions'));
@@ -698,15 +1109,22 @@ async function renderSummary(agent, version, versionHome, options) {
698
1109
  // ─── List mode ───────────────────────────────────────────────────────────────
699
1110
  async function renderList(agent, version, versionHome, kind, options) {
700
1111
  const items = collectKind(agent, versionHome, kind);
701
- renderItemList(`${agent}@${version}`, { agent, version }, kind, items, options);
1112
+ await renderItemList(`${agent}@${version}`, { agent, version }, kind, items, options);
702
1113
  }
703
- function renderItemList(header, jsonHead, kind, items, options) {
1114
+ async function renderItemList(header, jsonHead, kind, items, options, hookManifest) {
704
1115
  if (options.json) {
705
1116
  console.log(JSON.stringify({
706
1117
  ...jsonHead,
707
1118
  kind,
708
1119
  count: items.length,
709
- items: items.map(i => ({ name: i.name, source: i.source, path: i.path, description: i.description, ...(i.groups ? { groups: i.groups } : {}) })),
1120
+ items: items.map(i => ({
1121
+ name: i.name,
1122
+ source: i.source,
1123
+ path: i.path,
1124
+ description: i.description,
1125
+ ...(i.groups ? { groups: i.groups } : {}),
1126
+ ...(i.json ?? {}),
1127
+ })),
710
1128
  }, null, 2));
711
1129
  return;
712
1130
  }
@@ -716,29 +1134,129 @@ function renderItemList(header, jsonHead, kind, items, options) {
716
1134
  console.log('');
717
1135
  return;
718
1136
  }
719
- for (const item of items) {
720
- const tag = chalk.gray(`[${item.source}]`.padEnd(10));
721
- console.log(` ${tag} ${termLink(chalk.cyan(item.name), item.linkTarget)}`);
722
- if (item.description) {
723
- const prefix = ' ';
724
- console.log(prefix + chalk.gray(truncateToWidth(item.description, Math.max(1, terminalWidth() - stringWidth(prefix)))));
1137
+ // Route through the shared resource view: an interactive picker with a live
1138
+ // preview in a TTY, a plain aligned table when piped. This is the same
1139
+ // renderer `agents skills list`, `agents commands` and four others already
1140
+ // use — inspect was the last drill-down printing its own two-line-per-entry
1141
+ // dump with a `[source]` tag repeated on every row.
1142
+ //
1143
+ // Sync targets are deliberately off: for a repo the resources ARE the source,
1144
+ // so the column would read "no installed versions" for every row.
1145
+ const sources = new Set(items.map(i => i.source));
1146
+ // A hook has no prose description — it is a script. Leaving the column blank
1147
+ // is honest but useless, so it carries what the Hooks view is actually for:
1148
+ // the events that fire it. Same string the overview prints.
1149
+ const hookEvents = kind !== 'hooks' ? null : (hookManifest ?? hookManifestByScript(loadCentralHookManifest()));
1150
+ // A routine's byte size says nothing; whether it last ran, and where it is
1151
+ // allowed to run, is the whole question. Devices needs the room, so it only
1152
+ // takes a column on a wide terminal — below that it folds into the description
1153
+ // rather than being dropped.
1154
+ const isRoutines = kind === 'routines';
1155
+ const routineDevicesColumn = isRoutines && terminalWidth() >= 100;
1156
+ await showResourceList({
1157
+ resourcePlural: kind,
1158
+ resourceSingular: kind.replace(/s$/, ''),
1159
+ extraLabel: isRoutines ? 'Last' : 'Size',
1160
+ // Only carry a source column when the rows actually differ; a uniform
1161
+ // `[.system]` on every row is 13 columns spent on one bit of information.
1162
+ extra2Label: routineDevicesColumn ? 'Devices' : (isRoutines ? undefined : (sources.size > 1 ? 'Source' : undefined)),
1163
+ showSync: false,
1164
+ // inspect's names run long — hook scripts (`00-agent-verify-work-complete`
1165
+ // and its `_test` sibling) and namespaced plugin entries (`/swarm:orchestrate`)
1166
+ // both blow past the 22-char default and truncate to the same string.
1167
+ //
1168
+ // Gate on terminal width, NOT on "do any rows have a description": that
1169
+ // proxy depended on which path built the items — the repo path hardcodes an
1170
+ // empty description so it widened, while the agent path filled one from the
1171
+ // script's shebang so it never did. Same command, two behaviours.
1172
+ nameCap: terminalWidth() >= 120 ? 40 : undefined,
1173
+ // Keep names clickable — the pre-picker list wrapped every name in an OSC-8
1174
+ // link to its file, and losing that would be a silent capability regression.
1175
+ linkFor: row => items.find(i => i.name === row.name)?.linkTarget,
1176
+ emptyMessage: ` (none installed)`,
1177
+ rows: items.map(item => ({
1178
+ name: item.name,
1179
+ description: (() => {
1180
+ const hook = hookEvents?.get(item.name);
1181
+ if (hook)
1182
+ return summarizeHook(hook);
1183
+ if (isRoutines) {
1184
+ const problem = extraOf(item, 'problem');
1185
+ if (problem)
1186
+ return problem;
1187
+ // Compose at the row, leaving item.description pure prose for JSON.
1188
+ // Also makes "daily" and "zion" searchable in the picker's filter.
1189
+ return [
1190
+ extraOf(item, 'fires'),
1191
+ routineDevicesColumn ? '' : compactDeviceCell(extraOf(item, 'devices')),
1192
+ summaryLine(item.description),
1193
+ ].filter(Boolean).join(' · ');
1194
+ }
1195
+ return summaryLine(item.description);
1196
+ })(),
1197
+ extra: isRoutines ? routineLastCell(item) : itemSizeLabel(item.path),
1198
+ extra2: routineDevicesColumn
1199
+ ? compactDeviceCell(extraOf(item, 'devices')) || 'none'
1200
+ : (isRoutines ? undefined : (sources.size > 1 ? item.source : undefined)),
1201
+ targets: [],
1202
+ // Pass the resolved manifest: the picker rebuilds this on every arrow
1203
+ // key, so re-reading ~11 KB of YAML per keystroke is wasted work on top
1204
+ // of being the wrong manifest for a repo target.
1205
+ buildDetail: () => previewFor(kind, item, hookEvents ?? new Map()),
1206
+ })),
1207
+ });
1208
+ // On a TTY the picker's preview pane carries each plugin's bundled skills and
1209
+ // commands. Piped, there is no pane — and the pre-picker output printed those
1210
+ // lines under every row, so a table alone would silently drop what a plugin
1211
+ // actually ships from `agents inspect . --plugins | grep`.
1212
+ if (!isInteractiveTerminal() && items.some(i => i.groups?.length)) {
1213
+ for (const item of items) {
1214
+ if (!item.groups?.length)
1215
+ continue;
1216
+ console.log('\n' + chalk.cyan(item.name));
1217
+ const width = Math.max(...item.groups.map(g => g.label.length));
1218
+ for (const g of item.groups) {
1219
+ const colorFn = PLUGIN_GROUP_COLORS[g.label] ?? chalk.white;
1220
+ // Wrapped, not truncated: routing these through a truncating helper is
1221
+ // what once showed 4 of a 10-command plugin at 80 columns.
1222
+ printWrappedJoined(` ${chalk.gray(g.label.padEnd(width))} `, g.items.map(s => colorFn(s)), ', ');
1223
+ }
725
1224
  }
726
- if (item.groups)
727
- printGroupRows(item.groups);
1225
+ console.log('');
728
1226
  }
729
- console.log('');
730
1227
  }
731
- /** Print a plugin's resource breakdown as aligned `label items` rows under a list entry. */
732
- function printGroupRows(groups) {
733
- if (groups.length === 0)
734
- return;
735
- const width = Math.max(...groups.map(g => g.label.length));
736
- for (const g of groups) {
737
- const colorFn = PLUGIN_GROUP_COLORS[g.label] ?? chalk.white;
738
- const label = chalk.gray(g.label.padEnd(width));
739
- const value = g.items.map((s) => colorFn(s)).join(chalk.gray(', '));
740
- console.log(` ${label} ${value}`);
741
- }
1228
+ /**
1229
+ * The scannable half of a description: everything before the trigger clause, and
1230
+ * only the first sentence of that. 15 of 20 skills in .system lead with their
1231
+ * purpose and then append "Triggers on: …" — in a one-line row that boilerplate
1232
+ * is what survives truncation, so the row says nothing. The full text still
1233
+ * renders in the preview pane.
1234
+ */
1235
+ export function summaryLine(description) {
1236
+ if (!description)
1237
+ return '';
1238
+ const cutAtTrigger = description.split(/\s*(?:Triggers on|Use this skill when|Invoke when)\b/i)[0];
1239
+ // A description that OPENS with the trigger clause splits to an empty head.
1240
+ // Returning that would blank the row while --json still carried the text —
1241
+ // showing less than we have is worse than showing boilerplate.
1242
+ const trimmed = cutAtTrigger.trim() || description.trim();
1243
+ // First sentence, but only when the split leaves something substantial —
1244
+ // "e.g." and friends would otherwise chop a description to a fragment.
1245
+ const firstSentence = trimmed.match(/^.*?[.!?](?=\s+[A-Z(`])/)?.[0];
1246
+ const candidate = firstSentence && firstSentence.length >= 40 ? firstSentence : trimmed;
1247
+ return candidate.replace(/\s+/g, ' ').trim();
1248
+ }
1249
+ /** Compact size for a list row: `18 KB` for a bundle, `413 B` for a single file. */
1250
+ function itemSizeLabel(p) {
1251
+ if (!p)
1252
+ return '';
1253
+ const stat = safeStat(p);
1254
+ if (!stat)
1255
+ return '';
1256
+ if (!stat.isDirectory())
1257
+ return formatBytes(stat.size);
1258
+ const { bytes } = pathSize(p);
1259
+ return formatBytes(bytes);
742
1260
  }
743
1261
  // ─── Detail mode (fuzzy) ─────────────────────────────────────────────────────
744
1262
  async function renderDetail(agent, version, versionHome, kind, query, options) {
@@ -868,6 +1386,13 @@ function summaryResourcesJson(itemsByKind, hookByScript, mcpConfigs, hookInvento
868
1386
  groups: Object.fromEntries((i.groups ?? []).map(g => [g.label, g.items.length])),
869
1387
  })) };
870
1388
  }
1389
+ else if (kind === 'routines') {
1390
+ out[kind] = { ...base, items: items.map(i => ({
1391
+ name: i.name,
1392
+ source: i.source,
1393
+ ...Object.fromEntries(i.extra ?? []),
1394
+ })) };
1395
+ }
871
1396
  else {
872
1397
  out[kind] = { ...base, names: items.map(i => i.name) };
873
1398
  }
@@ -900,8 +1425,54 @@ function collectKind(agent, versionHome, kind) {
900
1425
  }));
901
1426
  case 'plugins':
902
1427
  return pluginItems();
1428
+ case 'routines':
1429
+ return agentRoutineItems(agent);
903
1430
  }
904
1431
  }
1432
+ /**
1433
+ * The routines that dispatch this agent.
1434
+ *
1435
+ * Version-invariant, like the `rules`/`subagents` cases above which also ignore
1436
+ * `versionHome`: a routine names an agent, never an agent@version. `listJobs()`
1437
+ * with no cwd is deliberate — it is the daemon's own view (user + system), so an
1438
+ * unsynced project routine that can never fire is not listed as if it could.
1439
+ */
1440
+ function agentRoutineItems(agent) {
1441
+ const { live } = defaultRoutineLive();
1442
+ const now = new Date();
1443
+ let jobs;
1444
+ try {
1445
+ jobs = listJobs();
1446
+ }
1447
+ catch {
1448
+ return [];
1449
+ }
1450
+ return jobs
1451
+ .filter(job => job.agent === agent)
1452
+ .map(job => {
1453
+ const state = live(job.name);
1454
+ return {
1455
+ name: job.name,
1456
+ source: 'user',
1457
+ path: '',
1458
+ linkTarget: '',
1459
+ description: routineDescription(job, ''),
1460
+ extra: [
1461
+ ['fires', routineFireLabel(job, now)],
1462
+ // listJobs already applied device activation, so job.enabled is the
1463
+ // effective answer here — no need to re-derive it.
1464
+ ['enabled', job.enabled ? 'yes' : 'no'],
1465
+ ['devices', state.devices.length > 0
1466
+ ? state.devices.join(', ')
1467
+ : state.materialized ? 'no device — will not fire' : 'no device allowlist yet'],
1468
+ ['last', state.lastStatus
1469
+ ? `${state.lastStatus}${state.lastAt ? ` · ${compactAge(state.lastAt, now)} ago` : ''}`
1470
+ : 'never run'],
1471
+ ],
1472
+ };
1473
+ })
1474
+ .sort((a, b) => a.name.localeCompare(b.name));
1475
+ }
905
1476
  function pluginItems() {
906
1477
  return discoverPlugins().map(p => pluginToItem(p, 'user'));
907
1478
  }
@@ -985,7 +1556,13 @@ function entriesFromAgentResources(agent, versionHome, kind) {
985
1556
  source: e.scope,
986
1557
  path: e.path,
987
1558
  linkTarget: linkTarget(e.path),
988
- description: readDescription(e.path),
1559
+ // A hook is a shell/Python script with no human description, so the prose
1560
+ // fallback returned code: all 53 under an agent home read "!/usr/bin/env
1561
+ // bash" (the `#` strip eating the shebang), and skipping that line only
1562
+ // promoted `set -euo pipefail`. repoHookItems already hardcodes '' for the
1563
+ // repo path; this makes the agent path agree instead of guessing. The
1564
+ // Hooks view shows firing events in that column.
1565
+ description: kind === 'hooks' ? '' : readDescription(e.path),
989
1566
  }));
990
1567
  }
991
1568
  function skillsFromAgentResources(agent, versionHome) {
@@ -1021,6 +1598,96 @@ function buildDetail(item, kind) {
1021
1598
  out[k] = v;
1022
1599
  return out;
1023
1600
  }
1601
+ /**
1602
+ * The preview pane for one row, refreshed as the selection moves.
1603
+ *
1604
+ * Adaptive on purpose — the sessions picker sets the precedent: a Cursor
1605
+ * session shows Dirs/Repos/Artifacts, a Codex one shows a different set, and
1606
+ * empty fields simply do not render. A hook's useful metadata (what fires it,
1607
+ * whether it is wired) has nothing in common with a skill's (what invokes it,
1608
+ * where it is synced), so each kind contributes its own rows and blanks drop out.
1609
+ */
1610
+ export function previewFor(kind, item, hookManifest) {
1611
+ const out = [];
1612
+ const label = (k, v) => ` ${chalk.gray(k.padEnd(11))}${v}`;
1613
+ out.push(chalk.bold.cyan(item.name) + ' ' + chalk.gray(`${kind.replace(/s$/, '')} · ${item.source}`));
1614
+ if (item.description) {
1615
+ out.push('');
1616
+ for (const line of wrapJoined(' ', item.description.split(/\s+/), ' ', terminalWidth())) {
1617
+ out.push(chalk.white(line));
1618
+ }
1619
+ }
1620
+ const rows = [];
1621
+ // A hook's identity is when it fires — the summary view has always shown this
1622
+ // while the drill-down showed only a size.
1623
+ if (kind === 'hooks') {
1624
+ // Use the manifest the caller already resolved. Re-resolving the CENTRAL
1625
+ // one here made the preview contradict its own row: a repo hook wired by
1626
+ // that repo's agents.yaml showed `PreToolUse(Bash)` in the table and
1627
+ // "not registered" in the pane below it, and the mirror case credited a
1628
+ // central registration to the repo. No `?? loadCentralHookManifest()`
1629
+ // fallback: it would be dead in production and would silently reinstate
1630
+ // exactly that bug the next time a caller forgot the argument.
1631
+ const hook = hookManifest.get(item.name);
1632
+ if (hook) {
1633
+ rows.push(['fires', chalk.yellow(summarizeHook(hook))]);
1634
+ rows.push(['wired', chalk.green('yes') + chalk.gray(' · agents.yaml')]);
1635
+ }
1636
+ else {
1637
+ rows.push(['wired', chalk.gray('no — on disk but not registered')]);
1638
+ }
1639
+ }
1640
+ if (kind === 'skills' || kind === 'commands' || kind === 'subagents') {
1641
+ const fm = readFrontmatter(item.path);
1642
+ if (fm) {
1643
+ const triggers = manifestList(fm.triggers).join(', ');
1644
+ if (triggers)
1645
+ rows.push(['triggers', triggers]);
1646
+ const model = manifestText(fm.model);
1647
+ if (model)
1648
+ rows.push(['model', model]);
1649
+ const tools = manifestList(fm.tools).join(', ');
1650
+ if (tools)
1651
+ rows.push(['tools', tools]);
1652
+ }
1653
+ }
1654
+ if (item.extra)
1655
+ rows.push(...item.extra);
1656
+ const size = itemSizeDetail(item.path);
1657
+ if (size)
1658
+ rows.push(['size', size]);
1659
+ if (item.path)
1660
+ rows.push(['path', chalk.blue(item.path)]);
1661
+ if (rows.length > 0) {
1662
+ out.push('');
1663
+ for (const [k, v] of rows)
1664
+ out.push(label(k, v));
1665
+ }
1666
+ // A plugin's bundled resources, each on its own line rather than a comma run.
1667
+ if (item.groups?.length) {
1668
+ for (const g of item.groups) {
1669
+ out.push('');
1670
+ out.push(' ' + chalk.bold(`${g.label} (${g.items.length})`));
1671
+ for (const entry of g.items.slice(0, 8))
1672
+ out.push(' ' + chalk.green(entry));
1673
+ if (g.items.length > 8)
1674
+ out.push(' ' + chalk.gray(`…${g.items.length - 8} more`));
1675
+ }
1676
+ }
1677
+ return out.join('\n');
1678
+ }
1679
+ /** `18 KB · 3 files` for a bundle, `413 B` for a single file. */
1680
+ function itemSizeDetail(p) {
1681
+ if (!p)
1682
+ return '';
1683
+ const stat = safeStat(p);
1684
+ if (!stat)
1685
+ return '';
1686
+ if (!stat.isDirectory())
1687
+ return formatBytes(stat.size);
1688
+ const { bytes, files } = pathSize(p);
1689
+ return `${formatBytes(bytes)} · ${files} ${files === 1 ? 'file' : 'files'}`;
1690
+ }
1024
1691
  function buildDetailRows(item, kind) {
1025
1692
  const rows = [];
1026
1693
  if (item.path && kind !== 'mcp') {
@@ -1030,7 +1697,7 @@ function buildDetailRows(item, kind) {
1030
1697
  if (stat) {
1031
1698
  if (stat.isDirectory()) {
1032
1699
  const { bytes, files } = pathSize(item.path);
1033
- rows.push(['size', `${formatBytes(bytes)} · ${files} files`]);
1700
+ rows.push(['size', `${formatBytes(bytes)} · ${files} ${files === 1 ? 'file' : 'files'}`]);
1034
1701
  }
1035
1702
  else {
1036
1703
  rows.push(['size', formatBytes(stat.size)]);
@@ -1065,9 +1732,13 @@ function buildDetailRows(item, kind) {
1065
1732
  if (item.groups)
1066
1733
  for (const g of item.groups)
1067
1734
  rows.push([g.label, g.items.join(', ')]);
1068
- if (item.extra)
1069
- rows.push(...item.extra);
1070
1735
  }
1736
+ // Every kind's scalar rows. previewFor already renders `extra` unconditionally,
1737
+ // so gating it on plugins here made the detail view and its own preview pane
1738
+ // disagree about the same item — and dropped a routine's schedule/status from
1739
+ // `--routine <name> --json` entirely.
1740
+ if (item.extra)
1741
+ rows.push(...item.extra);
1071
1742
  return rows;
1072
1743
  }
1073
1744
  /** `system` → `sys`; everything else unchanged. Keeps the tag column narrow. */
@@ -1191,7 +1862,16 @@ export function hookManifestByScript(manifest) {
1191
1862
  }
1192
1863
  /** Build hook rows by enriching the installed hook items with manifest events/predicates. */
1193
1864
  function hookRows(items, byScript) {
1194
- return items.map(item => {
1865
+ // The section shows only the first handful before a `…(+N)` tail, so order
1866
+ // matters: wired hooks first. Alphabetically `00-…_test` sorts next to the
1867
+ // hook it tests, so half the visible rows were test scaffolding with a blank
1868
+ // event column while real registered hooks hid behind the tail.
1869
+ const ordered = [...items].sort((a, b) => {
1870
+ const aw = byScript.has(a.name) ? 0 : 1;
1871
+ const bw = byScript.has(b.name) ? 0 : 1;
1872
+ return aw !== bw ? aw - bw : a.name.localeCompare(b.name);
1873
+ });
1874
+ return ordered.map(item => {
1195
1875
  const hook = byScript.get(item.name);
1196
1876
  return {
1197
1877
  source: item.source,
@@ -1212,6 +1892,152 @@ function pluginRows(items) {
1212
1892
  return { source: item.source, name: item.name, detail, linkTarget: item.linkTarget };
1213
1893
  });
1214
1894
  }
1895
+ /** Statuses that mean the last run did not do its job. */
1896
+ const UNHEALTHY_RUN = new Set(['failed', 'timeout', 'missed', 'blocked']);
1897
+ /**
1898
+ * Health tier for the section ordering. Lower sorts first.
1899
+ *
1900
+ * `printExpandedSection` shows only the first handful before a `…(+N)` tail, so
1901
+ * for a repo with 24 routines the sort IS the section. Same reasoning as
1902
+ * `hookRows` putting wired hooks first: a routine that fires nowhere is exactly
1903
+ * what the reader opened this for, and must never hide behind the tail.
1904
+ */
1905
+ export function routineHealthTier(item) {
1906
+ if (extraOf(item, 'problem'))
1907
+ return 0;
1908
+ const fires = extraOf(item, 'fires');
1909
+ if (fires.includes('expired'))
1910
+ return 5;
1911
+ const devices = extraOf(item, 'devices');
1912
+ if (devices.startsWith('no device —'))
1913
+ return 1;
1914
+ if (extraOf(item, 'enabled').startsWith('no'))
1915
+ return 2;
1916
+ const last = extraOf(item, 'last').split(' ')[0];
1917
+ if (UNHEALTHY_RUN.has(last))
1918
+ return 3;
1919
+ return 4;
1920
+ }
1921
+ /** Build routine rows: `daily at 9:00 AM zion completed · 2h ago`. Broken first. */
1922
+ function routineRows(items) {
1923
+ const ordered = [...items].sort((a, b) => {
1924
+ const at = routineHealthTier(a), bt = routineHealthTier(b);
1925
+ return at !== bt ? at - bt : a.name.localeCompare(b.name);
1926
+ });
1927
+ const nameW = ordered.length > 0 ? Math.max(...ordered.slice(0, 6).map(r => r.name.length)) : 0;
1928
+ // Budget the three cells against what printExpandedSection leaves after the
1929
+ // `[source]` tag and the name column, so the status never truncates to `fai…`.
1930
+ const budget = Math.max(30, terminalWidth() - nameW - 14);
1931
+ const firesW = Math.min(21, Math.max(12, Math.floor(budget * 0.42)));
1932
+ const devicesW = Math.min(14, Math.max(7, Math.floor(budget * 0.22)));
1933
+ return ordered.map(item => {
1934
+ const problem = extraOf(item, 'problem');
1935
+ const fires = extraOf(item, 'fires');
1936
+ // A routine with no `fires` never parsed — the problem IS the whole row. One
1937
+ // that parsed but carries a problem (a duplicate-extension sibling) keeps its
1938
+ // schedule and status, with the problem appended rather than replacing them.
1939
+ if (problem && !fires) {
1940
+ return { source: item.source, name: item.name, linkTarget: item.linkTarget, detail: chalk.red(problem) };
1941
+ }
1942
+ const devices = compactDeviceCell(extraOf(item, 'devices'));
1943
+ const last = extraOf(item, 'last');
1944
+ // Pad the PLAIN strings, then colour. Padding a chalk-wrapped string counts
1945
+ // the escape codes as width and silently steals columns from the next cell.
1946
+ // The trailing problem gets what the three sized cells leave, or it would
1947
+ // blow the row past the terminal and break the alignment this budget exists
1948
+ // to hold — a 147-column row at COLUMNS=100 in testing.
1949
+ const problemW = Math.max(12, budget - firesW - devicesW - stringWidth(last) - 8);
1950
+ const detail = [
1951
+ chalk.gray(truncateToWidth(fires, firesW).padEnd(firesW)),
1952
+ (devices === 'none ⚠' ? chalk.yellow : chalk.gray)(truncateToWidth(devices, devicesW).padEnd(devicesW)),
1953
+ colorLast(last),
1954
+ problem ? chalk.red(truncateToWidth(` ⚠ ${problem}`, problemW)) : '',
1955
+ ].join(' ').trimEnd();
1956
+ return {
1957
+ source: item.source,
1958
+ name: item.name,
1959
+ linkTarget: item.linkTarget,
1960
+ detail,
1961
+ };
1962
+ });
1963
+ }
1964
+ /**
1965
+ * Routine rows for an agent target: `daily at 9:00 AM completed · 2h ago`.
1966
+ * No devices column — an agent view is inherently about this machine.
1967
+ */
1968
+ function agentRoutineRows(items) {
1969
+ const ordered = [...items].sort((a, b) => {
1970
+ const at = routineHealthTier(a), bt = routineHealthTier(b);
1971
+ return at !== bt ? at - bt : a.name.localeCompare(b.name);
1972
+ });
1973
+ return ordered.map(item => ({
1974
+ source: item.source,
1975
+ name: item.name,
1976
+ detail: `${extraOf(item, 'fires').padEnd(20)} ${colorLast(extraOf(item, 'last'))}`,
1977
+ }));
1978
+ }
1979
+ /**
1980
+ * `ok 2h` / `fail 3d` / `never` — the Last column, clamped to the 10-wide cell.
1981
+ *
1982
+ * The picker path pads without truncating, so an 11-char `blocked 3d` would push
1983
+ * the next column; the abbreviations cover only the two commonest statuses, so
1984
+ * the clamp is what actually holds the invariant.
1985
+ */
1986
+ function routineLastCell(item, width = 10) {
1987
+ const last = extraOf(item, 'last');
1988
+ if (!last || last === 'never run')
1989
+ return 'never';
1990
+ const [status, , age] = last.split(' ');
1991
+ const word = status === 'completed' ? 'ok' : status === 'failed' ? 'fail' : status;
1992
+ return truncateToWidth(age ? `${word} ${age}` : word, width);
1993
+ }
1994
+ /**
1995
+ * Trim the long-form `devices` phrasing down to a cell.
1996
+ *
1997
+ * The extra row is written for the detail pane ("no device — will not fire");
1998
+ * a 16-column cell needs the same fact in two words.
1999
+ */
2000
+ function compactDeviceCell(devices) {
2001
+ if (devices.startsWith('no device —'))
2002
+ return 'none ⚠';
2003
+ if (devices.startsWith('no device allowlist yet'))
2004
+ return 'unset';
2005
+ const list = devices.split(', ').filter(Boolean);
2006
+ return list.length > 1 ? `${list[0]} +${list.length - 1}` : (list[0] ?? '');
2007
+ }
2008
+ function colorLast(last) {
2009
+ if (!last || last === 'never run')
2010
+ return chalk.gray('never run');
2011
+ const word = last.split(' ')[0];
2012
+ if (word === 'completed')
2013
+ return chalk.green(last);
2014
+ if (UNHEALTHY_RUN.has(word))
2015
+ return chalk.red(last);
2016
+ return chalk.gray(last);
2017
+ }
2018
+ /**
2019
+ * The one-line warning under the Routines section, or null when it would mislead.
2020
+ *
2021
+ * Silent until some device has materialized a `routines:` list — before that,
2022
+ * every routine is legitimately unpinned and "will not fire" would be noise.
2023
+ */
2024
+ export function routineDarkWarning(items) {
2025
+ const materialized = items.some(i => {
2026
+ const d = extraOf(i, 'devices');
2027
+ return d !== '' && !d.startsWith('no device allowlist yet');
2028
+ });
2029
+ if (!materialized)
2030
+ return null;
2031
+ const dark = items.filter(i => extraOf(i, 'devices').startsWith('no device —')).length;
2032
+ if (dark === 0)
2033
+ return null;
2034
+ // The noun pluralizes on the total ("1 of 2 routines"), the verb on the count
2035
+ // that is actually dark ("... is on no device's allowlist").
2036
+ const noun = items.length === 1 ? 'routine' : 'routines';
2037
+ const verb = dark === 1 ? 'is' : 'are';
2038
+ const pronoun = dark === 1 ? 'it' : 'they';
2039
+ return `${dark} of ${items.length} ${noun} ${verb} on no device's allowlist — ${pronoun} will not fire.`;
2040
+ }
1215
2041
  /** Build MCP rows by joining the installed mcp items with their full configs (transport/url/command). */
1216
2042
  function mcpRows(items, configs) {
1217
2043
  return items.map(item => {
@@ -1220,7 +2046,7 @@ function mcpRows(items, configs) {
1220
2046
  });
1221
2047
  }
1222
2048
  /** Read a repo/agents.yaml `hooks:` section into a name→ManifestHook map (best-effort). */
1223
- function hookManifestFromFile(agentsYamlPath) {
2049
+ export function hookManifestFromFile(agentsYamlPath) {
1224
2050
  try {
1225
2051
  const meta = yaml.parse(fs.readFileSync(agentsYamlPath, 'utf-8'));
1226
2052
  return meta?.hooks ?? {};