@phnx-labs/agents-cli 1.22.35 → 1.22.38

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 (500) hide show
  1. package/CHANGELOG.md +708 -2
  2. package/README.md +83 -43
  3. package/dist/bin/agents +0 -0
  4. package/dist/bootstrap.d.ts +15 -0
  5. package/dist/bootstrap.js +1212 -0
  6. package/dist/commands/accounts.d.ts +4 -11
  7. package/dist/commands/accounts.js +159 -27
  8. package/dist/commands/artifacts-setup.d.ts +53 -0
  9. package/dist/commands/artifacts-setup.js +161 -0
  10. package/dist/commands/artifacts.d.ts +18 -0
  11. package/dist/commands/artifacts.js +58 -0
  12. package/dist/commands/attach.js +17 -4
  13. package/dist/commands/audit.d.ts +8 -8
  14. package/dist/commands/audit.js +57 -47
  15. package/dist/commands/browser.js +17 -11
  16. package/dist/commands/cloud.js +1 -1
  17. package/dist/commands/commands.js +0 -11
  18. package/dist/commands/computer-actions.js +33 -0
  19. package/dist/commands/computer-sessions-picker.d.ts +21 -0
  20. package/dist/commands/computer-sessions-picker.js +141 -0
  21. package/dist/commands/computer.d.ts +16 -0
  22. package/dist/commands/computer.js +37 -0
  23. package/dist/commands/config.d.ts +17 -0
  24. package/dist/commands/config.js +401 -0
  25. package/dist/commands/cp.d.ts +84 -0
  26. package/dist/commands/cp.js +231 -0
  27. package/dist/commands/daemon.js +243 -14
  28. package/dist/commands/detach-core.d.ts +1 -1
  29. package/dist/commands/detach-core.js +12 -2
  30. package/dist/commands/detach.js +1 -1
  31. package/dist/commands/doctor.js +64 -5
  32. package/dist/commands/events.d.ts +24 -12
  33. package/dist/commands/events.js +144 -28
  34. package/dist/commands/exec.js +86 -42
  35. package/dist/commands/feed.d.ts +1 -1
  36. package/dist/commands/feed.js +16 -8
  37. package/dist/commands/focus.d.ts +33 -1
  38. package/dist/commands/focus.js +177 -17
  39. package/dist/commands/funnel.d.ts +1 -1
  40. package/dist/commands/go.js +9 -8
  41. package/dist/commands/harness-wizard.d.ts +1 -0
  42. package/dist/commands/harness-wizard.js +18 -20
  43. package/dist/commands/harness.d.ts +2 -0
  44. package/dist/commands/harness.js +59 -26
  45. package/dist/commands/hooks.js +0 -11
  46. package/dist/commands/inspect.d.ts +94 -1
  47. package/dist/commands/inspect.js +866 -40
  48. package/dist/commands/lease.d.ts +4 -4
  49. package/dist/commands/lease.js +6 -6
  50. package/dist/commands/login.js +0 -13
  51. package/dist/commands/logs.d.ts +2 -2
  52. package/dist/commands/logs.js +2 -2
  53. package/dist/commands/mcp.js +7 -2
  54. package/dist/commands/memory.js +7 -2
  55. package/dist/commands/message.d.ts +1 -1
  56. package/dist/commands/message.js +37 -6
  57. package/dist/commands/models.js +71 -0
  58. package/dist/commands/monitors.d.ts +0 -1
  59. package/dist/commands/monitors.js +199 -16
  60. package/dist/commands/open.d.ts +18 -0
  61. package/dist/commands/open.js +57 -0
  62. package/dist/commands/packages.js +2 -107
  63. package/dist/commands/plugins.js +6 -3
  64. package/dist/commands/profiles.d.ts +1 -0
  65. package/dist/commands/profiles.js +16 -160
  66. package/dist/commands/projects.d.ts +28 -0
  67. package/dist/commands/projects.js +125 -5
  68. package/dist/commands/reconnect.js +9 -3
  69. package/dist/commands/repo.js +42 -1
  70. package/dist/commands/resource-view.d.ts +17 -0
  71. package/dist/commands/resource-view.js +36 -14
  72. package/dist/commands/resume.js +9 -2
  73. package/dist/commands/routines.d.ts +0 -2
  74. package/dist/commands/routines.js +473 -289
  75. package/dist/commands/secrets.js +52 -9
  76. package/dist/commands/sessions-export.d.ts +47 -0
  77. package/dist/commands/sessions-export.js +116 -6
  78. package/dist/commands/sessions-import.d.ts +17 -0
  79. package/dist/commands/sessions-import.js +114 -6
  80. package/dist/commands/sessions-migrate.d.ts +27 -0
  81. package/dist/commands/sessions-migrate.js +50 -9
  82. package/dist/commands/sessions-picker.js +23 -4
  83. package/dist/commands/sessions-resume.d.ts +5 -3
  84. package/dist/commands/sessions-resume.js +30 -8
  85. package/dist/commands/sessions-watch.d.ts +2 -0
  86. package/dist/commands/sessions-watch.js +53 -0
  87. package/dist/commands/sessions.d.ts +66 -3
  88. package/dist/commands/sessions.js +298 -47
  89. package/dist/commands/setup-browser.js +2 -2
  90. package/dist/commands/setup-mine.js +0 -1
  91. package/dist/commands/setup-preferences.js +1 -1
  92. package/dist/commands/setup.d.ts +5 -0
  93. package/dist/commands/setup.js +31 -5
  94. package/dist/commands/share.d.ts +87 -7
  95. package/dist/commands/share.js +320 -58
  96. package/dist/commands/skills.js +0 -11
  97. package/dist/commands/snapshot.js +1 -1
  98. package/dist/commands/ssh.d.ts +18 -0
  99. package/dist/commands/ssh.js +549 -210
  100. package/dist/commands/sync.d.ts +5 -0
  101. package/dist/commands/sync.js +212 -32
  102. package/dist/commands/teams.d.ts +13 -0
  103. package/dist/commands/teams.js +187 -30
  104. package/dist/commands/tickets.d.ts +2 -0
  105. package/dist/commands/tickets.js +43 -0
  106. package/dist/commands/update.js +2 -4
  107. package/dist/commands/usage.js +5 -3
  108. package/dist/commands/versions.js +11 -5
  109. package/dist/commands/view.js +15 -7
  110. package/dist/commands/webhook.d.ts +2 -2
  111. package/dist/commands/webhook.js +7 -7
  112. package/dist/index.d.ts +18 -2
  113. package/dist/index.js +53 -1264
  114. package/dist/lib/account-catalog.d.ts +18 -0
  115. package/dist/lib/account-catalog.js +38 -0
  116. package/dist/lib/account-provider-registry.d.ts +18 -0
  117. package/dist/lib/account-provider-registry.js +59 -0
  118. package/dist/lib/account-registry.d.ts +40 -0
  119. package/dist/lib/account-registry.js +239 -0
  120. package/dist/lib/account-schema.d.ts +44 -0
  121. package/dist/lib/account-schema.js +91 -0
  122. package/dist/lib/account-state-service.d.ts +21 -0
  123. package/dist/lib/account-state-service.js +60 -0
  124. package/dist/lib/activity.d.ts +1 -1
  125. package/dist/lib/activity.js +8 -1
  126. package/dist/lib/add-dir.d.ts +80 -0
  127. package/dist/lib/add-dir.js +241 -0
  128. package/dist/lib/agent-cli-commands.d.ts +11 -0
  129. package/dist/lib/agent-cli-commands.js +30 -0
  130. package/dist/lib/agents.js +44 -36
  131. package/dist/lib/answer-router.js +1 -1
  132. package/dist/lib/audit/log.d.ts +10 -27
  133. package/dist/lib/audit/log.js +20 -33
  134. package/dist/lib/auth-health.d.ts +9 -2
  135. package/dist/lib/auth-health.js +42 -9
  136. package/dist/lib/auto-pull-worker.js +7 -3
  137. package/dist/lib/auto-pull.d.ts +44 -3
  138. package/dist/lib/auto-pull.js +88 -5
  139. package/dist/lib/binary-shadow.d.ts +28 -0
  140. package/dist/lib/binary-shadow.js +121 -0
  141. package/dist/lib/brand.js +9 -3
  142. package/dist/lib/browser/hygiene.d.ts +90 -0
  143. package/dist/lib/browser/hygiene.js +146 -0
  144. package/dist/lib/browser/ipc.d.ts +30 -0
  145. package/dist/lib/browser/ipc.js +93 -4
  146. package/dist/lib/browser/profiles.d.ts +3 -2
  147. package/dist/lib/browser/profiles.js +65 -22
  148. package/dist/lib/browser/service.d.ts +79 -1
  149. package/dist/lib/browser/service.js +231 -14
  150. package/dist/lib/browser/sessions-list.d.ts +22 -2
  151. package/dist/lib/browser/sessions-list.js +54 -6
  152. package/dist/lib/browser/stream.d.ts +2 -0
  153. package/dist/lib/browser/stream.js +1 -0
  154. package/dist/lib/browser/types.d.ts +63 -1
  155. package/dist/lib/byok-usage.d.ts +9 -2
  156. package/dist/lib/byok-usage.js +133 -31
  157. package/dist/lib/cli-entry.d.ts +11 -0
  158. package/dist/lib/cli-entry.js +46 -0
  159. package/dist/lib/cloud/host.d.ts +1 -1
  160. package/dist/lib/cloud/host.js +2 -2
  161. package/dist/lib/cloud/registry.js +1 -1
  162. package/dist/lib/cloud/types.d.ts +1 -1
  163. package/dist/lib/commands.d.ts +2 -0
  164. package/dist/lib/commands.js +2 -0
  165. package/dist/lib/computer/sessions-list.d.ts +150 -0
  166. package/dist/lib/computer/sessions-list.js +363 -0
  167. package/dist/lib/config-keys.d.ts +86 -0
  168. package/dist/lib/config-keys.js +193 -0
  169. package/dist/lib/config-machine-keys.d.ts +12 -0
  170. package/dist/lib/config-machine-keys.js +17 -0
  171. package/dist/lib/config-transfer.js +3 -4
  172. package/dist/lib/crabbox/cli.d.ts +2 -2
  173. package/dist/lib/crabbox/cli.js +3 -3
  174. package/dist/lib/daemon-health.d.ts +22 -0
  175. package/dist/lib/daemon-health.js +33 -3
  176. package/dist/lib/daemon-services.d.ts +50 -0
  177. package/dist/lib/daemon-services.js +147 -0
  178. package/dist/lib/daemon-ticks.d.ts +35 -66
  179. package/dist/lib/daemon-ticks.js +77 -159
  180. package/dist/lib/daemon.d.ts +72 -4
  181. package/dist/lib/daemon.js +792 -202
  182. package/dist/lib/deeplink/register.d.ts +49 -0
  183. package/dist/lib/deeplink/register.js +253 -0
  184. package/dist/lib/deeplink/url.d.ts +34 -0
  185. package/dist/lib/deeplink/url.js +78 -0
  186. package/dist/lib/device-config.d.ts +124 -29
  187. package/dist/lib/device-config.js +346 -141
  188. package/dist/lib/devices/config-migration.d.ts +42 -0
  189. package/dist/lib/devices/config-migration.js +203 -0
  190. package/dist/lib/devices/connect.js +14 -7
  191. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  192. package/dist/lib/devices/doctor-findings.js +20 -1
  193. package/dist/lib/devices/harness-inventory.d.ts +11 -2
  194. package/dist/lib/devices/harness-inventory.js +0 -0
  195. package/dist/lib/devices/health.d.ts +1 -1
  196. package/dist/lib/devices/health.js +1 -1
  197. package/dist/lib/devices/pending.d.ts +22 -1
  198. package/dist/lib/devices/pending.js +67 -2
  199. package/dist/lib/devices/registry.d.ts +0 -25
  200. package/dist/lib/devices/registry.js +2 -83
  201. package/dist/lib/devices/resolve-profile.d.ts +20 -0
  202. package/dist/lib/devices/resolve-profile.js +44 -0
  203. package/dist/lib/devices/rollout-verify.d.ts +101 -0
  204. package/dist/lib/devices/rollout-verify.js +193 -0
  205. package/dist/lib/devices/ssh-config.js +10 -6
  206. package/dist/lib/event-families.d.ts +28 -0
  207. package/dist/lib/event-families.js +159 -0
  208. package/dist/lib/event-stream.d.ts +9 -1
  209. package/dist/lib/event-stream.js +26 -9
  210. package/dist/lib/events.d.ts +9 -1
  211. package/dist/lib/events.js +21 -6
  212. package/dist/lib/exec.d.ts +30 -0
  213. package/dist/lib/exec.js +219 -121
  214. package/dist/lib/feed-broadcast.d.ts +25 -0
  215. package/dist/lib/feed-broadcast.js +33 -0
  216. package/dist/lib/feed.d.ts +1 -1
  217. package/dist/lib/feed.js +12 -2
  218. package/dist/lib/fleet/capture.js +15 -0
  219. package/dist/lib/fleet/manifest.js +9 -0
  220. package/dist/lib/fleet/types.d.ts +9 -0
  221. package/dist/lib/fleet-status.js +10 -8
  222. package/dist/lib/git.d.ts +21 -7
  223. package/dist/lib/git.js +122 -18
  224. package/dist/lib/hosts/dispatch.d.ts +8 -3
  225. package/dist/lib/hosts/dispatch.js +29 -3
  226. package/dist/lib/hosts/logs.d.ts +2 -2
  227. package/dist/lib/hosts/logs.js +2 -2
  228. package/dist/lib/hosts/option.js +1 -1
  229. package/dist/lib/hosts/passthrough.d.ts +2 -3
  230. package/dist/lib/hosts/passthrough.js +10 -24
  231. package/dist/lib/hosts/progress.js +2 -2
  232. package/dist/lib/hosts/providers/devices.d.ts +3 -3
  233. package/dist/lib/hosts/providers/devices.js +18 -12
  234. package/dist/lib/hosts/ready.d.ts +5 -1
  235. package/dist/lib/hosts/ready.js +9 -3
  236. package/dist/lib/hosts/reconcile.d.ts +2 -2
  237. package/dist/lib/hosts/reconcile.js +2 -2
  238. package/dist/lib/hosts/registry.js +14 -9
  239. package/dist/lib/hosts/remote-cmd.js +1 -1
  240. package/dist/lib/hosts/remote-os.d.ts +9 -6
  241. package/dist/lib/hosts/remote-os.js +13 -6
  242. package/dist/lib/hosts/routing-flag.d.ts +23 -0
  243. package/dist/lib/hosts/routing-flag.js +41 -0
  244. package/dist/lib/hosts/run-target.js +1 -1
  245. package/dist/lib/hosts/session-index.js +1 -1
  246. package/dist/lib/hosts/tasks.d.ts +6 -6
  247. package/dist/lib/hosts/tasks.js +4 -4
  248. package/dist/lib/hosts/types.js +1 -2
  249. package/dist/lib/installations/resolve.js +2 -8
  250. package/dist/lib/isolation-boundary-report.js +0 -1
  251. package/dist/lib/linear-cache.d.ts +7 -0
  252. package/dist/lib/linear-cache.js +31 -4
  253. package/dist/lib/linear-project-counts.d.ts +1 -1
  254. package/dist/lib/linear-project-counts.js +2 -3
  255. package/dist/lib/linear-projects.js +1 -1
  256. package/dist/lib/loop.d.ts +2 -2
  257. package/dist/lib/loop.js +1 -1
  258. package/dist/lib/mailbox-target.d.ts +1 -1
  259. package/dist/lib/mailbox-target.js +1 -1
  260. package/dist/lib/manifest.js +7 -4
  261. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  262. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  263. package/dist/lib/menubar/snapshot.d.ts +3 -2
  264. package/dist/lib/menubar/snapshot.js +6 -2
  265. package/dist/lib/migrate-fold.d.ts +33 -0
  266. package/dist/lib/migrate-fold.js +130 -0
  267. package/dist/lib/migrate.d.ts +34 -34
  268. package/dist/lib/migrate.js +106 -128
  269. package/dist/lib/model-tier-overrides.d.ts +1 -1
  270. package/dist/lib/model-tier-overrides.js +15 -2
  271. package/dist/lib/monitors/config.d.ts +17 -3
  272. package/dist/lib/monitors/config.js +52 -18
  273. package/dist/lib/monitors/engine.d.ts +32 -1
  274. package/dist/lib/monitors/engine.js +94 -11
  275. package/dist/lib/monitors/fingerprint.d.ts +46 -0
  276. package/dist/lib/monitors/fingerprint.js +100 -0
  277. package/dist/lib/monitors/remote.d.ts +47 -0
  278. package/dist/lib/monitors/remote.js +81 -0
  279. package/dist/lib/monitors/state.d.ts +39 -0
  280. package/dist/lib/monitors/state.js +56 -0
  281. package/dist/lib/onepassword.d.ts +17 -0
  282. package/dist/lib/onepassword.js +52 -22
  283. package/dist/lib/open-url.js +1 -1
  284. package/dist/lib/picker.d.ts +14 -2
  285. package/dist/lib/picker.js +35 -13
  286. package/dist/lib/placement.d.ts +1 -1
  287. package/dist/lib/placement.js +1 -1
  288. package/dist/lib/pr-land-detach.d.ts +32 -0
  289. package/dist/lib/pr-land-detach.js +79 -0
  290. package/dist/lib/profiles-presets.js +13 -0
  291. package/dist/lib/profiles.d.ts +2 -0
  292. package/dist/lib/profiles.js +44 -1
  293. package/dist/lib/project-focus.d.ts +1 -1
  294. package/dist/lib/project-focus.js +1 -1
  295. package/dist/lib/project-probe.d.ts +4 -1
  296. package/dist/lib/project-probe.js +5 -4
  297. package/dist/lib/project-resources.js +2 -18
  298. package/dist/lib/project-root.d.ts +16 -0
  299. package/dist/lib/project-root.js +20 -2
  300. package/dist/lib/projects.d.ts +20 -1
  301. package/dist/lib/projects.js +78 -1
  302. package/dist/lib/refresh-coordinator.d.ts +24 -0
  303. package/dist/lib/refresh-coordinator.js +52 -0
  304. package/dist/lib/registry.d.ts +2 -2
  305. package/dist/lib/registry.js +1 -1
  306. package/dist/lib/resource-aliases.d.ts +13 -0
  307. package/dist/lib/resource-aliases.js +26 -0
  308. package/dist/lib/resources/types.d.ts +1 -1
  309. package/dist/lib/resources.d.ts +16 -0
  310. package/dist/lib/resources.js +66 -10
  311. package/dist/lib/rotate.d.ts +3 -4
  312. package/dist/lib/rotate.js +4 -5
  313. package/dist/lib/routine-activation.d.ts +22 -2
  314. package/dist/lib/routine-activation.js +63 -16
  315. package/dist/lib/routine-context.d.ts +9 -2
  316. package/dist/lib/routine-context.js +21 -6
  317. package/dist/lib/routines-project.d.ts +58 -48
  318. package/dist/lib/routines-project.js +190 -159
  319. package/dist/lib/routines.d.ts +44 -7
  320. package/dist/lib/routines.js +92 -34
  321. package/dist/lib/run-defaults.d.ts +5 -0
  322. package/dist/lib/run-defaults.js +11 -1
  323. package/dist/lib/runner.d.ts +10 -8
  324. package/dist/lib/runner.js +122 -39
  325. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  326. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  327. package/dist/lib/secrets/agent.d.ts +10 -3
  328. package/dist/lib/secrets/agent.js +43 -8
  329. package/dist/lib/secrets/bundles.d.ts +1 -0
  330. package/dist/lib/secrets/bundles.js +92 -80
  331. package/dist/lib/secrets/index.js +32 -9
  332. package/dist/lib/secrets/install-helper.d.ts +4 -4
  333. package/dist/lib/secrets/install-helper.js +4 -4
  334. package/dist/lib/secrets/push.d.ts +40 -6
  335. package/dist/lib/secrets/push.js +58 -11
  336. package/dist/lib/secrets/read-backoff.d.ts +1 -1
  337. package/dist/lib/secrets/read-backoff.js +1 -1
  338. package/dist/lib/secrets/remote.d.ts +24 -0
  339. package/dist/lib/secrets/remote.js +54 -4
  340. package/dist/lib/self-update.d.ts +108 -0
  341. package/dist/lib/self-update.js +276 -1
  342. package/dist/lib/session/active.d.ts +89 -5
  343. package/dist/lib/session/active.js +165 -10
  344. package/dist/lib/session/actor-sidecar.d.ts +1 -1
  345. package/dist/lib/session/actor-sidecar.js +2 -1
  346. package/dist/lib/session/db.d.ts +176 -2
  347. package/dist/lib/session/db.js +484 -17
  348. package/dist/lib/session/discover.js +27 -4
  349. package/dist/lib/session/hook-sessions.js +2 -2
  350. package/dist/lib/session/host-link.d.ts +2 -2
  351. package/dist/lib/session/host-link.js +2 -2
  352. package/dist/lib/session/pid-registry.d.ts +21 -0
  353. package/dist/lib/session/pid-registry.js +57 -0
  354. package/dist/lib/session/remote-active.js +15 -1
  355. package/dist/lib/session/remote.js +2 -2
  356. package/dist/lib/session/session-cache.d.ts +14 -7
  357. package/dist/lib/session/session-cache.js +51 -9
  358. package/dist/lib/session/state.d.ts +2 -2
  359. package/dist/lib/session/state.js +1 -1
  360. package/dist/lib/session/sync/config.d.ts +10 -10
  361. package/dist/lib/session/sync/config.js +10 -10
  362. package/dist/lib/session/sync/r2.d.ts +34 -0
  363. package/dist/lib/session/sync/r2.js +123 -0
  364. package/dist/lib/session/throughput.d.ts +1 -1
  365. package/dist/lib/session/throughput.js +1 -1
  366. package/dist/lib/session/types.d.ts +23 -1
  367. package/dist/lib/session/types.js +15 -0
  368. package/dist/lib/session/viewing-in.d.ts +1 -1
  369. package/dist/lib/session/viewing-in.js +1 -1
  370. package/dist/lib/session/watch.d.ts +101 -0
  371. package/dist/lib/session/watch.js +242 -0
  372. package/dist/lib/share/analytics.js +1 -1
  373. package/dist/lib/share/capture.d.ts +1 -1
  374. package/dist/lib/share/capture.js +3 -3
  375. package/dist/lib/share/config.d.ts +6 -2
  376. package/dist/lib/share/config.js +6 -5
  377. package/dist/lib/share/delete.js +3 -3
  378. package/dist/lib/share/provision.d.ts +38 -0
  379. package/dist/lib/share/provision.js +51 -1
  380. package/dist/lib/share/publish.d.ts +40 -1
  381. package/dist/lib/share/publish.js +119 -6
  382. package/dist/lib/share/worker-template.d.ts +12 -1
  383. package/dist/lib/share/worker-template.js +83 -7
  384. package/dist/lib/shims.d.ts +17 -3
  385. package/dist/lib/shims.js +98 -26
  386. package/dist/lib/signin-badge.js +3 -2
  387. package/dist/lib/skills.js +2 -0
  388. package/dist/lib/smart-launch.d.ts +8 -8
  389. package/dist/lib/smart-launch.js +80 -59
  390. package/dist/lib/ssh-exec.d.ts +1 -1
  391. package/dist/lib/ssh-exec.js +1 -1
  392. package/dist/lib/staleness/prune.d.ts +88 -0
  393. package/dist/lib/staleness/prune.js +69 -0
  394. package/dist/lib/staleness/registry.d.ts +1 -1
  395. package/dist/lib/staleness/writers/commands.js +34 -2
  396. package/dist/lib/staleness/writers/hooks.js +16 -0
  397. package/dist/lib/staleness/writers/kinds.d.ts +6 -0
  398. package/dist/lib/staleness/writers/skills.js +19 -0
  399. package/dist/lib/staleness/writers/subagents.d.ts +2 -3
  400. package/dist/lib/staleness/writers/subagents.js +0 -4
  401. package/dist/lib/staleness/writers/types.d.ts +32 -0
  402. package/dist/lib/startup/command-registry.d.ts +16 -14
  403. package/dist/lib/startup/command-registry.js +26 -37
  404. package/dist/lib/startup/dev-build.d.ts +10 -0
  405. package/dist/lib/startup/dev-build.js +13 -1
  406. package/dist/lib/startup/spellcheck.d.ts +18 -0
  407. package/dist/lib/startup/spellcheck.js +45 -0
  408. package/dist/lib/state.d.ts +36 -9
  409. package/dist/lib/state.js +96 -46
  410. package/dist/lib/subagents-registry.d.ts +0 -7
  411. package/dist/lib/subagents-registry.js +38 -65
  412. package/dist/lib/subagents.d.ts +0 -33
  413. package/dist/lib/subagents.js +0 -75
  414. package/dist/lib/sync-umbrella.js +1 -1
  415. package/dist/lib/teams/agents.d.ts +41 -1
  416. package/dist/lib/teams/agents.js +187 -9
  417. package/dist/lib/teams/api.d.ts +11 -1
  418. package/dist/lib/teams/api.js +11 -2
  419. package/dist/lib/teams/delivery.d.ts +41 -0
  420. package/dist/lib/teams/delivery.js +60 -0
  421. package/dist/lib/teams/registry.d.ts +14 -0
  422. package/dist/lib/teams/registry.js +51 -1
  423. package/dist/lib/teams/remoteWorktree.d.ts +19 -0
  424. package/dist/lib/teams/remoteWorktree.js +29 -0
  425. package/dist/lib/teams/scheduler.d.ts +1 -1
  426. package/dist/lib/teams/scheduler.js +1 -1
  427. package/dist/lib/teams/worktree.d.ts +24 -0
  428. package/dist/lib/teams/worktree.js +42 -0
  429. package/dist/lib/terminal/backends/vscodium-agent.d.ts +10 -1
  430. package/dist/lib/terminal/backends/vscodium-agent.js +16 -5
  431. package/dist/lib/terminal/engine.d.ts +4 -0
  432. package/dist/lib/terminal/engine.js +8 -2
  433. package/dist/lib/terminal/types.d.ts +21 -2
  434. package/dist/lib/testdata/refresh-coordinator-worker.d.ts +1 -0
  435. package/dist/lib/testdata/refresh-coordinator-worker.js +23 -0
  436. package/dist/lib/tickets/list.d.ts +53 -0
  437. package/dist/lib/tickets/list.js +153 -0
  438. package/dist/lib/tmux/orphan-reap.d.ts +316 -0
  439. package/dist/lib/tmux/orphan-reap.js +638 -0
  440. package/dist/lib/tmux/session.d.ts +68 -3
  441. package/dist/lib/tmux/session.js +96 -5
  442. package/dist/lib/triggers/handlers.d.ts +20 -0
  443. package/dist/lib/triggers/handlers.js +16 -1
  444. package/dist/lib/triggers/webhook.js +10 -0
  445. package/dist/lib/types.d.ts +47 -35
  446. package/dist/lib/usage-fleet.d.ts +32 -0
  447. package/dist/lib/usage-fleet.js +125 -0
  448. package/dist/lib/usage-refresh.d.ts +3 -3
  449. package/dist/lib/usage-refresh.js +19 -16
  450. package/dist/lib/usage.d.ts +51 -47
  451. package/dist/lib/usage.js +145 -153
  452. package/dist/lib/version-duplicates.js +2 -2
  453. package/dist/lib/versions.d.ts +56 -13
  454. package/dist/lib/versions.js +195 -45
  455. package/dist/lib/watchdog/log.d.ts +1 -1
  456. package/dist/lib/watchdog/log.js +3 -3
  457. package/dist/lib/watchdog/rotate.d.ts +6 -6
  458. package/dist/lib/watchdog/rotate.js +6 -6
  459. package/dist/lib/workflows.d.ts +1 -1
  460. package/dist/lib/yaml-io.d.ts +47 -0
  461. package/dist/lib/yaml-io.js +55 -0
  462. package/package.json +2 -1
  463. package/scripts/install-helper.js +2 -2
  464. package/scripts/postinstall.js +23 -20
  465. package/dist/commands/defaults.d.ts +0 -7
  466. package/dist/commands/defaults.js +0 -107
  467. package/dist/commands/export.d.ts +0 -11
  468. package/dist/commands/export.js +0 -215
  469. package/dist/commands/helper.d.ts +0 -12
  470. package/dist/commands/helper.js +0 -87
  471. package/dist/commands/hosts.d.ts +0 -11
  472. package/dist/commands/hosts.js +0 -330
  473. package/dist/commands/lock.d.ts +0 -12
  474. package/dist/commands/lock.js +0 -70
  475. package/dist/commands/pull.d.ts +0 -17
  476. package/dist/commands/pull.js +0 -39
  477. package/dist/commands/push.d.ts +0 -14
  478. package/dist/commands/push.js +0 -30
  479. package/dist/commands/set.d.ts +0 -15
  480. package/dist/commands/set.js +0 -79
  481. package/dist/commands/setup-share.d.ts +0 -17
  482. package/dist/commands/setup-share.js +0 -85
  483. package/dist/commands/wallet.d.ts +0 -20
  484. package/dist/commands/wallet.js +0 -216
  485. package/dist/commands/worktree.d.ts +0 -19
  486. package/dist/commands/worktree.js +0 -272
  487. package/dist/lib/account-labels.d.ts +0 -24
  488. package/dist/lib/account-labels.js +0 -72
  489. package/dist/lib/auto-dispatch-linear.d.ts +0 -18
  490. package/dist/lib/auto-dispatch-linear.js +0 -107
  491. package/dist/lib/auto-dispatch-provider.d.ts +0 -10
  492. package/dist/lib/auto-dispatch-provider.js +0 -30
  493. package/dist/lib/auto-dispatch.d.ts +0 -93
  494. package/dist/lib/auto-dispatch.js +0 -128
  495. package/dist/lib/export.d.ts +0 -72
  496. package/dist/lib/export.js +0 -269
  497. package/dist/lib/lock.d.ts +0 -93
  498. package/dist/lib/lock.js +0 -207
  499. package/dist/lib/wallet/index.d.ts +0 -78
  500. package/dist/lib/wallet/index.js +0 -253
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Reap the helper processes an exited agent leaves behind (RUSH-2521).
3
+ *
4
+ * ## The leak
5
+ *
6
+ * An interactive agent runs as the leaf of a detached tmux pane
7
+ * (`runInTmux` → `createSession`, [`../exec.ts`](../exec.ts)). Anything the
8
+ * harness spawns — MCP servers, background daemons, `exec` helper trees — is a
9
+ * child of that pane. When the agent exits, the kernel SIGHUPs the pane's
10
+ * FOREGROUND process group only. A helper that put itself in its own process
11
+ * group, or that ignores SIGHUP, survives: it reparents to init and keeps its
12
+ * memory forever. Measured on the fleet: one pane holding 2.5 GB of Claude Code
13
+ * background daemons 22 days after its session ended, and 34 orphaned
14
+ * `cgraph-mcp --daemon` processes on a single worker.
15
+ *
16
+ * ## Why attribution is by ENVIRONMENT, not by pid ancestry / tty / pgid
17
+ *
18
+ * Every obvious OS handle is destroyed by exactly the event we are detecting:
19
+ *
20
+ * - **ppid ancestry** — an orphan is reparented to init the instant its parent
21
+ * dies, so the chain back to the pane is gone.
22
+ * - **controlling terminal** — POSIX disassociates the controlling terminal from
23
+ * every process in a session when the session leader exits, so the orphan's
24
+ * `tty` reads `?`. Verified on Linux: an orphan under a dead pane leader shows
25
+ * `tty=?` while its own pane still reports `pane_tty=/dev/pts/6`.
26
+ * - **process group** — the helpers that survive are precisely the ones that
27
+ * left the pane's process group (that is why they survived the SIGHUP).
28
+ * - **POSIX session id** — durable and correct, but not portable: `ps -o sid=`
29
+ * exists on Linux and does not exist on macOS (`ps: sid: keyword not found`),
30
+ * `ps -o sess=` reports 0 there, and `pgrep -s` is Linux-only.
31
+ *
32
+ * What DOES survive all of it is the process environment. `runInTmux` stamps
33
+ * `AGENT_TMUX_SESSION_NAME` into the pane env ([`../exec.ts`](../exec.ts)), and
34
+ * every descendant inherits it — including a reparented one. So a process
35
+ * carrying `AGENT_TMUX_SESSION_NAME=<name>` is attributable to that tmux session
36
+ * with no heuristics at all, and tmux itself is the liveness oracle for whether
37
+ * that session's agent is still running.
38
+ *
39
+ * ## The second tier: harness daemons that scrub the environment
40
+ *
41
+ * Claude Code's background pool (`claude … daemon run`, which owns the
42
+ * `bg-pty-host` / `bg-spare` processes) starts with a clean environment, so tier
43
+ * one cannot see it. It does, however, declare its owner in its own argv:
44
+ * `daemon run --origin transient --spawned-by {"label":"claude",…,"pid":3834601}`.
45
+ * A pool whose declared spawner pid is dead is unowned, so it and its descendants
46
+ * are reaped. That rule lives in {@link DETACHED_HELPER_RULES} rather than an
47
+ * inline `if (agent === 'claude')`, so another harness that detaches helpers the
48
+ * same way is one table entry.
49
+ *
50
+ * ## Safety
51
+ *
52
+ * Nothing is killed without positive proof its owner is gone:
53
+ *
54
+ * - a tmux session that still exists AND whose agent pane process is alive
55
+ * protects every process it owns;
56
+ * - a tmux session with a client attached protects every process it owns,
57
+ * whatever the pane state;
58
+ * - the reaping process itself, its ancestors, pid 1, and every long-lived
59
+ * agents-cli service ({@link isProtectedAgentsService}) are never candidates.
60
+ * - an UNRELIABLE read of tmux's session state (the query threw, timed out, or
61
+ * tmux itself never answered) disables tier 1 for that sweep entirely;
62
+ * - an absent session entry is unknown, even after a reliable read. A tmux
63
+ * server restart can produce the same empty snapshot while marked agents
64
+ * remain alive, so tier 1 requires a present owner with positive death proof.
65
+ * - tier 2 is anchored on the actual claude EXECUTABLE (argv[0]'s basename),
66
+ * never a substring match anywhere in the command line, and excludes any
67
+ * process still structurally part of a LIVE pane leaf's process tree right
68
+ * now (tmux's own `#{pane_pid}`, expanded to its current descendants —
69
+ * independent of whether any of those processes' environment markers were
70
+ * ever readable) or that carries a marker whose own session is
71
+ * live/attached — a live interactive agent, OR a subprocess it spawns
72
+ * (e.g. its own Bash tool invoking `claude --print "…"` as a sub-task),
73
+ * whose argv happens to quote this file's docblock (it contains the exact
74
+ * `daemon run --spawned-by {"pid":...}` shape) must never become a kill
75
+ * seed, and that has to hold even where tier 1's marker is dead (macOS, or
76
+ * any `claude` invocation started outside an agents-cli pane). Two review
77
+ * rounds found this: first that marker-only exclusion left the whole
78
+ * macOS/bare-invocation case open (round 2, closed by the pane-pid check),
79
+ * then that the pane-pid check alone only covered the pane LEAF itself, not
80
+ * its own live children (round 3, closed by expanding the check to the
81
+ * leaf's current descendant set via {@link descendantsOf}). A process that
82
+ * has genuinely reparented away — a real detached daemon, no longer chained
83
+ * to any live leaf by ppid — is deliberately NOT in that set and remains
84
+ * reapable, whatever unrelated live agent happens to also be running.
85
+ *
86
+ * ## Known platform gap — tier 1 is Linux-only today
87
+ *
88
+ * Tier 1's env-marker attribution requires reading another process's
89
+ * environment. `/proc/<pid>/environ` makes that a plain file read on Linux.
90
+ * macOS has no `/proc`; `ps -E` was the fallback, but on modern macOS (verified
91
+ * live on mac-mini 15.6, macOS Sequoia) `ps -E` no longer prints another
92
+ * process's environment at all — only the CALLING process's own env shows up.
93
+ * `readAgentProcesses()` therefore returns every macOS process with
94
+ * `tmuxSession: undefined`, and tier 1's `if (!p.tmuxSession) continue` skips
95
+ * all of them. This is SAFE (tier 1 silently no-ops rather than misattributing
96
+ * anything) but not effective: on macOS, only tier 2 — the detached-helper
97
+ * registry — can reap anything today. A macOS fix needs a different channel
98
+ * entirely (env is inherently unreadable cross-process there without a native
99
+ * helper); it is not a parsing bug {@link parseTmuxSessionMarker} can fix.
100
+ */
101
+ import { execFile } from 'child_process';
102
+ import * as fs from 'fs';
103
+ import { promisify } from 'util';
104
+ const execFileAsync = promisify(execFile);
105
+ /** Ceiling on the `ps` snapshot so a wedged `ps` can never stall the daemon tick. */
106
+ const PS_TIMEOUT_MS = 10_000;
107
+ /**
108
+ * Ceiling on a single tmux pane-owner query. Without this a wedged tmux server
109
+ * hangs `readPaneOwners` forever, which — before this bound existed — could
110
+ * only be told apart from "genuinely no sessions" by the caller giving up and
111
+ * treating it as empty (RUSH-2521 review: exactly the failure mode that let a
112
+ * flaky tick select every marked process on the box as orphaned).
113
+ */
114
+ const TMUX_QUERY_TIMEOUT_MS = 10_000;
115
+ /** Grace between SIGTERM and SIGKILL for a reaped orphan. */
116
+ export const REAP_GRACE_MS = 2_000;
117
+ /** Env var `runInTmux` stamps into the pane, inherited by every descendant. */
118
+ export const TMUX_SESSION_ENV = 'AGENT_TMUX_SESSION_NAME';
119
+ /**
120
+ * Basename of argv[0] — the actual executable a process runs, not whatever
121
+ * text later argv tokens happen to contain. `ps -o args=` gives one opaque
122
+ * command-line string (no reliable per-token quoting to split on), so this
123
+ * takes the first whitespace-delimited run and strips its directory prefix.
124
+ */
125
+ function argv0Basename(args) {
126
+ const m = /^\s*(\S+)/.exec(args);
127
+ const token = m ? m[1] : '';
128
+ const base = token.split(/[\\/]/).pop() ?? '';
129
+ return base.toLowerCase();
130
+ }
131
+ /**
132
+ * Claude Code's background daemon pool. Its argv carries a JSON `--spawned-by`
133
+ * blob naming the claude process that started it, e.g.
134
+ * `claude.exe daemon run --origin transient --spawned-by {"label":"claude","cwd":"/…","pid":3834601}`.
135
+ * The `bg-pty-host` / `bg-spare` workers are its children and are reaped as
136
+ * descendants rather than matched individually — they carry no owner of their own.
137
+ *
138
+ * Anchored on argv[0]'s basename, not a substring match anywhere in the full
139
+ * command line: `daemon\s+run` and `--spawned-by\s+.*"pid"\s*:\s*(\d+)` both
140
+ * appear verbatim in THIS FILE's own docblock above, so a `grep`, `cat`, or
141
+ * pager viewing `orphan-reap.ts` — or a coding agent whose tool-call argv
142
+ * quotes this review's own bug report — matched the old substring-only rule
143
+ * and became a kill seed for its whole process subtree (RUSH-2521 review,
144
+ * reproduced against the exact diff text). Only a process whose real
145
+ * executable is `claude`/`claude.exe` can seed this rule now.
146
+ */
147
+ const CLAUDE_BG_DAEMON = {
148
+ agent: 'claude',
149
+ spawnerPid(args) {
150
+ const exe = argv0Basename(args);
151
+ if (exe !== 'claude' && exe !== 'claude.exe')
152
+ return undefined;
153
+ if (!/\bdaemon\s+run\b/.test(args))
154
+ return undefined;
155
+ const m = /--spawned-by\s+.*?"pid"\s*:\s*(\d+)/.exec(args);
156
+ if (!m)
157
+ return undefined;
158
+ const pid = parseInt(m[1], 10);
159
+ return Number.isFinite(pid) && pid > 1 ? pid : undefined;
160
+ },
161
+ };
162
+ export const DETACHED_HELPER_RULES = [CLAUDE_BG_DAEMON];
163
+ /**
164
+ * Long-lived agents-cli services that must never be reaped, even when they
165
+ * inherited a pane's environment because the CLI invocation that started them
166
+ * happened to run inside an agent pane.
167
+ *
168
+ * This is a safety invariant, not a fallback: the routines daemon runs this very
169
+ * reaper, so killing it would make the reaper delete its own executor, and the
170
+ * secrets broker holds the keychain session every live agent depends on.
171
+ */
172
+ export function isProtectedAgentsService(args) {
173
+ return /\b__daemon-run\b/.test(args)
174
+ || /\bsecrets\s+_agent-run\b/.test(args)
175
+ || /\bMenubarHelper\b/.test(args)
176
+ || /\bAgents CLI\.app\b/.test(args);
177
+ }
178
+ /** Is this pid currently running? Signal-0 probe, matching `platform/process.ts`. */
179
+ function livePid(pid) {
180
+ if (!Number.isFinite(pid) || pid <= 0)
181
+ return false;
182
+ try {
183
+ process.kill(pid, 0);
184
+ return true;
185
+ }
186
+ catch (err) {
187
+ // EPERM means the process exists but belongs to another user — alive.
188
+ return err.code === 'EPERM';
189
+ }
190
+ }
191
+ /**
192
+ * Pure. Select the processes whose owning agent is provably gone.
193
+ *
194
+ * `owners` maps a tmux session name to its liveness. An ABSENT name is never
195
+ * proof that the marked process is orphaned: the tmux server can restart or its
196
+ * socket can disappear while the pane's process tree is still alive. That exact
197
+ * state made the daemon classify every live agent as `tmux-session-gone` and
198
+ * SIGTERM it on startup (RUSH-2603). Tier 1 therefore requires a PRESENT owner
199
+ * whose pane process is confirmed dead and has no attached client. A reliable
200
+ * empty map means "nothing proven dead", not "every marked process is dead".
201
+ * `opts.ownersReliable` still disables tier 1 when any query failed to answer.
202
+ */
203
+ export function selectOrphanProcesses(procs, owners, opts) {
204
+ const isAlive = opts.isAlive ?? livePid;
205
+ const ownersReliable = opts.ownersReliable ?? true;
206
+ const eligible = (p) => p.pid > 1 && !opts.protectedPids.has(p.pid) && !isProtectedAgentsService(p.args);
207
+ /**
208
+ * Every pid STILL reachable, right now, from a live pane leaf through ppid
209
+ * links — not just the leaf itself. A live agent's own child (e.g. a Bash
210
+ * tool spawning `claude --print "…daemon run --spawned-by…"` as a
211
+ * sub-invocation) inherits nothing from `livePanePids` (that set only ever
212
+ * contains the leaf's own pid) unless we walk the tree, and on macOS that
213
+ * child ALSO carries no readable env marker (tier 1 is dead there) — so
214
+ * without this expansion, the "must never become a kill seed, whatever its
215
+ * own argv says" guarantee only held for the leaf itself, not the live
216
+ * agent's actual subprocess tree the same docblock's threat model names
217
+ * (RUSH-2521 review, round 3). A process that has genuinely reparented away
218
+ * (a real detached daemon, ppid now 1 or otherwise no longer chained to the
219
+ * leaf) is NOT in this set and remains reapable — this only protects what
220
+ * is still structurally part of the live agent's own tree right now.
221
+ */
222
+ const livePaneSubtree = opts.livePanePids && opts.livePanePids.size > 0
223
+ ? new Set(descendantsOf(procs, [...opts.livePanePids]))
224
+ : undefined;
225
+ /**
226
+ * A process still owned by a live/attached pane can never seed a kill,
227
+ * whatever its own argv says. Two independent signals, either sufficient:
228
+ * the process is still IN a live pane leaf's process tree right now (tmux's
229
+ * own data, no marker needed), or it carries a marker whose session is
230
+ * live/attached.
231
+ */
232
+ const ownedByLivePane = (p) => {
233
+ if (livePaneSubtree?.has(p.pid))
234
+ return true;
235
+ if (!p.tmuxSession)
236
+ return false;
237
+ const owner = owners.get(p.tmuxSession);
238
+ return !!owner && (owner.agentAlive || owner.attached);
239
+ };
240
+ const out = [];
241
+ const seen = new Set();
242
+ const push = (p, reason) => {
243
+ if (seen.has(p.pid))
244
+ return;
245
+ seen.add(p.pid);
246
+ out.push({ pid: p.pid, args: p.args, reason, tmuxSession: p.tmuxSession });
247
+ };
248
+ // Tier 1 — inherited pane marker with a dead owner. Skipped whole when the
249
+ // owners read is unreliable (see the docstring above).
250
+ if (ownersReliable) {
251
+ for (const p of procs) {
252
+ if (!p.tmuxSession || !eligible(p))
253
+ continue;
254
+ const owner = owners.get(p.tmuxSession);
255
+ if (!owner)
256
+ continue;
257
+ // An attached client or a live agent pane process is a hard exclusion.
258
+ if (owner.attached || owner.agentAlive)
259
+ continue;
260
+ push(p, 'tmux-agent-exited');
261
+ }
262
+ }
263
+ // Tier 2 — a detached helper daemon whose declared spawner is dead, plus the
264
+ // workers it owns. Its own descendants are pulled in because they carry no
265
+ // owner declaration of their own. A candidate seed that still carries a
266
+ // LIVE/attached pane marker is excluded even if its argv matches a rule: it
267
+ // is provably not a detached daemon, whatever text its command line quotes.
268
+ const seeds = [];
269
+ for (const p of procs) {
270
+ if (!eligible(p) || ownedByLivePane(p))
271
+ continue;
272
+ for (const rule of DETACHED_HELPER_RULES) {
273
+ const spawner = rule.spawnerPid(p.args);
274
+ if (spawner === undefined || isAlive(spawner))
275
+ continue;
276
+ seeds.push(p);
277
+ break;
278
+ }
279
+ }
280
+ if (seeds.length > 0) {
281
+ const byPid = new Map(procs.map(p => [p.pid, p]));
282
+ for (const seed of descendantsOf(procs, seeds.map(s => s.pid))) {
283
+ const p = byPid.get(seed);
284
+ if (p && eligible(p))
285
+ push(p, 'detached-helper');
286
+ }
287
+ }
288
+ return out;
289
+ }
290
+ /**
291
+ * Pure. `seeds` plus every process reachable from them through ppid links.
292
+ * Depth is bounded by the table size, so a ppid cycle (which the kernel cannot
293
+ * produce, but a malformed snapshot could) terminates.
294
+ */
295
+ export function descendantsOf(procs, seeds) {
296
+ const children = new Map();
297
+ for (const p of procs) {
298
+ const list = children.get(p.ppid);
299
+ if (list)
300
+ list.push(p.pid);
301
+ else
302
+ children.set(p.ppid, [p.pid]);
303
+ }
304
+ const out = [];
305
+ const seen = new Set();
306
+ const stack = [...seeds];
307
+ while (stack.length > 0) {
308
+ const pid = stack.pop();
309
+ if (seen.has(pid))
310
+ continue;
311
+ seen.add(pid);
312
+ out.push(pid);
313
+ for (const child of children.get(pid) ?? [])
314
+ stack.push(child);
315
+ }
316
+ return out;
317
+ }
318
+ /** Pure. Parse `ps -A -o pid=,ppid=,args=` output. */
319
+ export function parseProcessRows(stdout) {
320
+ const rows = [];
321
+ for (const line of stdout.split('\n')) {
322
+ const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
323
+ if (!m)
324
+ continue;
325
+ const pid = parseInt(m[1], 10);
326
+ const ppid = parseInt(m[2], 10);
327
+ if (!Number.isFinite(pid) || !Number.isFinite(ppid))
328
+ continue;
329
+ rows.push({ pid, ppid, args: m[3].trim() });
330
+ }
331
+ return rows;
332
+ }
333
+ /**
334
+ * Pure. Extract the pane marker from a blob that contains the process
335
+ * environment — a NUL-separated `/proc/<pid>/environ` on Linux, or the
336
+ * space-separated `KEY=VALUE` tail macOS `ps -E` appends after the command.
337
+ * Session names are `[A-Za-z0-9_-]` ({@link ../tmux/session.ts} `VALID_NAME`),
338
+ * so the value ends at the first character outside that set.
339
+ */
340
+ export function parseTmuxSessionMarker(blob) {
341
+ const m = new RegExp(`(?:^|[\\s\\0])${TMUX_SESSION_ENV}=([A-Za-z0-9_-]{1,64})`).exec(blob);
342
+ return m ? m[1] : undefined;
343
+ }
344
+ /** Pure. Parse `tmux list-panes -a -F '#{session_name}\t#{pane_pid}\t#{session_attached}'`. */
345
+ export function parsePaneOwners(stdout, isAlive = livePid) {
346
+ const owners = new Map();
347
+ for (const line of stdout.split('\n')) {
348
+ const parts = line.trim().split('\t');
349
+ if (parts.length < 3)
350
+ continue;
351
+ const name = parts[0];
352
+ if (!name)
353
+ continue;
354
+ const panePid = parseInt(parts[1], 10);
355
+ const attached = parts[2].trim() !== '0';
356
+ const prev = owners.get(name);
357
+ const agentAlive = Number.isFinite(panePid) && isAlive(panePid);
358
+ owners.set(name, {
359
+ agentAlive: (prev?.agentAlive ?? false) || agentAlive,
360
+ attached: (prev?.attached ?? false) || attached,
361
+ });
362
+ }
363
+ return owners;
364
+ }
365
+ /**
366
+ * Pure. Every `#{pane_pid}` tmux reports in the same `list-panes -a` output
367
+ * {@link parsePaneOwners} reads — a pid tmux itself says is a pane's leaf
368
+ * process, independent of whether that process's OWN environment is readable.
369
+ *
370
+ * This is the marker-independent half of pane ownership: {@link parsePaneOwners}
371
+ * answers "is SESSION X owned" by aggregating over every pane a session has;
372
+ * this answers "is PID N itself a pane leaf tmux currently tracks" for exactly
373
+ * one candidate pid, with no dependency on {@link TMUX_SESSION_ENV} ever having
374
+ * been readable for it.
375
+ */
376
+ export function parsePanePids(stdout) {
377
+ const pids = new Set();
378
+ for (const line of stdout.split('\n')) {
379
+ const parts = line.trim().split('\t');
380
+ if (parts.length < 2)
381
+ continue;
382
+ const pid = parseInt(parts[1], 10);
383
+ if (Number.isFinite(pid))
384
+ pids.add(pid);
385
+ }
386
+ return pids;
387
+ }
388
+ /**
389
+ * Snapshot the process table with each process's pane marker.
390
+ *
391
+ * Two readers because no single command works on both platforms: Linux exposes
392
+ * `/proc/<pid>/environ` (one cheap read per pid), while macOS has no `/proc` and
393
+ * instead lets `ps -E` print a process's own-uid environment after its command
394
+ * — though on modern macOS `ps -E` no longer includes it at all (see the
395
+ * "Known platform gap" section in this file's docblock); tier 1 safely no-ops
396
+ * there rather than misattributing anything. Windows has no tmux integration
397
+ * at all, so it returns an empty table rather than pretending to reap.
398
+ *
399
+ * `opts.pids` restricts the `ps` snapshot to an explicit pid allowlist instead
400
+ * of the whole machine (`-A`) — a test-only escape hatch so a reap exercised
401
+ * against real spawned processes can never select or signal a real, unrelated
402
+ * process on a shared box (RUSH-2521 review: the integration tests previously
403
+ * called the machine-wide reap directly, which is safe only by construction of
404
+ * the fixture, not by anything the reaper itself guarantees).
405
+ */
406
+ export async function readAgentProcesses(opts = {}) {
407
+ if (process.platform === 'win32')
408
+ return [];
409
+ const scope = opts.pids && opts.pids.length > 0 ? ['-p', opts.pids.join(',')] : ['-A'];
410
+ const hasProc = fs.existsSync('/proc/self/environ');
411
+ const args = hasProc ? [...scope, '-o', 'pid=,ppid=,args='] : [...scope, '-E', '-o', 'pid=,ppid=,args='];
412
+ let stdout;
413
+ try {
414
+ ({ stdout } = await execFileAsync('ps', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: PS_TIMEOUT_MS }));
415
+ }
416
+ catch {
417
+ return [];
418
+ }
419
+ const rows = parseProcessRows(stdout);
420
+ if (!hasProc) {
421
+ // `ps -E` already appended the environment to the args column.
422
+ for (const row of rows)
423
+ row.tmuxSession = parseTmuxSessionMarker(row.args);
424
+ return rows;
425
+ }
426
+ for (const row of rows) {
427
+ let blob;
428
+ try {
429
+ blob = fs.readFileSync(`/proc/${row.pid}/environ`, 'utf8');
430
+ }
431
+ catch {
432
+ continue; // exited between ps and the read, or not ours to inspect
433
+ }
434
+ row.tmuxSession = parseTmuxSessionMarker(blob);
435
+ }
436
+ return rows;
437
+ }
438
+ /**
439
+ * Read one tmux socket's session-ownership state.
440
+ *
441
+ * Distinguishes "tmux answered: nothing here" from "tmux did not answer" —
442
+ * the two were previously conflated (`.catch(() => null)` then `!res` folded
443
+ * into the same empty-map return as a clean nonzero exit), which meant a
444
+ * missing/wrong-version tmux, a spawn failure, or a wedged server were
445
+ * indistinguishable from a genuinely empty box. A completed process — even a
446
+ * nonzero exit — is a real answer; a rejected promise is not an answer at all
447
+ * (RUSH-2521 review).
448
+ */
449
+ export async function readPaneOwners(socket) {
450
+ // No socket file at all means no server was ever started here — for the
451
+ // shared agents socket this is reliable (tmux unlinks its own socket on
452
+ // exit), so this is a confident, reliable EMPTY read, not an unknown one.
453
+ if (!fs.existsSync(socket))
454
+ return { ok: true, owners: new Map(), panePids: new Set() };
455
+ const { runTmux } = await import('./binary.js');
456
+ try {
457
+ const res = await runTmux({
458
+ socket,
459
+ args: ['list-panes', '-a', '-F', '#{session_name}\t#{pane_pid}\t#{session_attached}'],
460
+ throwOnError: false,
461
+ timeoutMs: TMUX_QUERY_TIMEOUT_MS,
462
+ });
463
+ // A completed run answered, whatever its exit code — tmux itself is
464
+ // telling us there's no server/no sessions on a nonzero exit here.
465
+ if (res.code !== 0)
466
+ return { ok: true, owners: new Map(), panePids: new Set() };
467
+ return { ok: true, owners: parsePaneOwners(res.stdout), panePids: parsePanePids(res.stdout) };
468
+ }
469
+ catch {
470
+ // Threw: tmux missing/unsupported version (assertTmuxAvailable), a spawn
471
+ // failure, or the query timed out. We genuinely do not know.
472
+ return { ok: false, owners: new Map(), panePids: new Set() };
473
+ }
474
+ }
475
+ /**
476
+ * Ownership across every socket that could own a marked process, merged so a
477
+ * session that is live on ANY of them protects its processes. `reliable` is
478
+ * `false` the moment ANY queried socket's read failed — the merged map could
479
+ * then be missing entries for sessions that live there, so a caller MUST NOT
480
+ * treat an absent key in it as proof of anything.
481
+ *
482
+ * The union is load bearing, not defensive: the process table is machine-wide,
483
+ * so a reap driven from a non-default socket (`agents sessions reap --socket`,
484
+ * or a test's temp socket) would otherwise see every real agent's marker with no
485
+ * matching session and classify a whole box's live helpers as orphans.
486
+ */
487
+ async function readAllPaneOwners(socket) {
488
+ const { getDefaultSocketPath } = await import('./paths.js');
489
+ const merged = new Map();
490
+ const panePids = new Set();
491
+ let reliable = true;
492
+ for (const sock of new Set([socket, getDefaultSocketPath()].filter(Boolean))) {
493
+ const read = await readPaneOwners(sock);
494
+ reliable = reliable && read.ok;
495
+ for (const pid of read.panePids)
496
+ panePids.add(pid);
497
+ for (const [name, owner] of read.owners) {
498
+ const prev = merged.get(name);
499
+ merged.set(name, {
500
+ agentAlive: (prev?.agentAlive ?? false) || owner.agentAlive,
501
+ attached: (prev?.attached ?? false) || owner.attached,
502
+ });
503
+ }
504
+ }
505
+ return { owners: merged, panePids, reliable };
506
+ }
507
+ /** Pids the reaper must never signal: itself and its whole ancestor chain. */
508
+ function selfProtectedPids(procs) {
509
+ const byPid = new Map(procs.map(p => [p.pid, p]));
510
+ const out = new Set([process.pid]);
511
+ let cursor = process.ppid;
512
+ // Bounded by the table size — an ancestor walk cannot exceed it.
513
+ for (let i = 0; i <= procs.length && cursor > 1; i += 1) {
514
+ out.add(cursor);
515
+ cursor = byPid.get(cursor)?.ppid ?? 0;
516
+ }
517
+ return out;
518
+ }
519
+ /** SIGTERM every candidate, then SIGKILL whatever ignored it. */
520
+ async function terminate(candidates, graceMs) {
521
+ let signalled = 0;
522
+ for (const c of candidates) {
523
+ try {
524
+ process.kill(c.pid, 'SIGTERM');
525
+ signalled += 1;
526
+ }
527
+ catch { /* already gone */ }
528
+ }
529
+ if (signalled === 0)
530
+ return 0;
531
+ await new Promise(resolve => setTimeout(resolve, graceMs));
532
+ for (const c of candidates) {
533
+ if (!livePid(c.pid))
534
+ continue;
535
+ try {
536
+ process.kill(c.pid, 'SIGKILL');
537
+ }
538
+ catch { /* already gone */ }
539
+ }
540
+ return signalled;
541
+ }
542
+ function describe(c) {
543
+ const where = c.tmuxSession ? ` [${c.tmuxSession}]` : '';
544
+ return `pid ${c.pid}${where} (${c.reason}): ${c.args.slice(0, 120)}`;
545
+ }
546
+ /**
547
+ * Reap every helper process whose owning agent has exited.
548
+ *
549
+ * Called from the daemon's periodic tick and from `agents sessions reap`. Panes
550
+ * are read before processes to take one ownership snapshot. A session absent
551
+ * from that snapshot is unknown and is never sufficient proof for tier 1.
552
+ *
553
+ * `opts.pids` is the test-only process-table scope described on
554
+ * {@link readAgentProcesses} — never set by production callers.
555
+ */
556
+ export async function reapOrphanAgentProcesses(opts = { socket: '' }) {
557
+ const result = { killed: 0, details: [], candidates: [], warnings: [] };
558
+ if (process.platform === 'win32')
559
+ return result;
560
+ const { owners, panePids, reliable } = await readAllPaneOwners(opts.socket);
561
+ if (!reliable) {
562
+ result.warnings.push('tier 1 (pane-marker) sweep skipped this tick: a tmux session query failed to answer');
563
+ }
564
+ const procs = await readAgentProcesses({ pids: opts.pids });
565
+ if (procs.length === 0)
566
+ return result;
567
+ const candidates = selectOrphanProcesses(procs, owners, {
568
+ protectedPids: selfProtectedPids(procs),
569
+ ownersReliable: reliable,
570
+ livePanePids: panePids,
571
+ });
572
+ result.candidates = candidates;
573
+ if (candidates.length === 0)
574
+ return result;
575
+ result.details = candidates.map(describe);
576
+ if (opts.dryRun)
577
+ return result;
578
+ result.killed = await terminate(candidates, opts.graceMs ?? REAP_GRACE_MS);
579
+ return result;
580
+ }
581
+ /**
582
+ * Reap the helper processes belonging to ONE tmux session, on the teardown path.
583
+ *
584
+ * `killSession` calls this so an agent's helpers die with the session that owned
585
+ * them instead of waiting for the next daemon tick. The tmux session is being
586
+ * destroyed by the caller, so ownership needs no liveness check — anything still
587
+ * carrying this session's marker is leftover by definition, PROVIDED the name
588
+ * is unique to the socket in play.
589
+ *
590
+ * `socket` narrows that: a session name is only unique WITHIN one tmux server,
591
+ * so the exact same marker value could legitimately belong to a live, unrelated
592
+ * session on a DIFFERENT socket (e.g. the shared agents socket vs. a test's own
593
+ * temp socket). When `socket` names a non-default socket, this checks the
594
+ * default agents socket for a live/attached session of the same name and backs
595
+ * off entirely if one exists — the caller is tearing down one session, not a
596
+ * same-named one it does not own.
597
+ *
598
+ * `opts.pids` is the test-only process-table scope described on
599
+ * {@link readAgentProcesses} — never set by production callers.
600
+ */
601
+ export async function reapProcessesForTmuxSession(name, socket, opts = {}) {
602
+ const result = { killed: 0, details: [], candidates: [], warnings: [] };
603
+ if (process.platform === 'win32')
604
+ return result;
605
+ const { getDefaultSocketPath } = await import('./paths.js');
606
+ const defaultSocket = getDefaultSocketPath();
607
+ if (socket && socket !== defaultSocket) {
608
+ const elsewhere = await readPaneOwners(defaultSocket);
609
+ const owner = elsewhere.owners.get(name);
610
+ if (owner && (owner.agentAlive || owner.attached)) {
611
+ result.warnings.push(`skipped: "${name}" is a live session on the agents socket, not the one being torn down`);
612
+ return result;
613
+ }
614
+ }
615
+ const procs = await readAgentProcesses({ pids: opts.pids });
616
+ if (procs.length === 0)
617
+ return result;
618
+ const protectedPids = selfProtectedPids(procs);
619
+ const byPid = new Map(procs.map(p => [p.pid, p]));
620
+ const owned = procs.filter(p => p.tmuxSession === name);
621
+ // Descendants too: a helper that scrubbed its own environment (so it carries
622
+ // no marker) is still attributable through the marked process that spawned it.
623
+ const candidates = [];
624
+ for (const pid of descendantsOf(procs, owned.map(p => p.pid))) {
625
+ const p = byPid.get(pid);
626
+ if (!p || p.pid <= 1 || protectedPids.has(p.pid) || isProtectedAgentsService(p.args))
627
+ continue;
628
+ candidates.push({ pid: p.pid, args: p.args, reason: 'tmux-agent-exited', tmuxSession: name });
629
+ }
630
+ result.candidates = candidates;
631
+ if (candidates.length === 0)
632
+ return result;
633
+ result.details = candidates.map(describe);
634
+ if (opts.dryRun)
635
+ return result;
636
+ result.killed = await terminate(candidates, opts.graceMs ?? REAP_GRACE_MS);
637
+ return result;
638
+ }