@phnx-labs/agents-cli 1.22.42 → 1.22.44

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 (256) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +15 -13
  3. package/dist/bootstrap.js +33 -13
  4. package/dist/cli/command-registry.d.ts +0 -8
  5. package/dist/cli/command-registry.js +1 -25
  6. package/dist/commands/alias.d.ts +7 -4
  7. package/dist/commands/alias.js +33 -8
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/auth.js +14 -6
  14. package/dist/commands/beta.d.ts +7 -1
  15. package/dist/commands/beta.js +16 -9
  16. package/dist/commands/commands.js +1 -1
  17. package/dist/commands/computer.js +20 -6
  18. package/dist/commands/daemon.js +1 -1
  19. package/dist/commands/doctor.d.ts +4 -2
  20. package/dist/commands/doctor.js +21 -9
  21. package/dist/commands/events.d.ts +2 -2
  22. package/dist/commands/events.js +6 -5
  23. package/dist/commands/exec.d.ts +1 -1
  24. package/dist/commands/exec.js +11 -6
  25. package/dist/commands/factory.d.ts +1 -1
  26. package/dist/commands/feed.js +1 -30
  27. package/dist/commands/fleet-capture.d.ts +1 -1
  28. package/dist/commands/fleet-capture.js +4 -5
  29. package/dist/commands/harness.js +4 -52
  30. package/dist/commands/hooks.js +1 -1
  31. package/dist/commands/insights.d.ts +1 -1
  32. package/dist/commands/insights.js +3 -2
  33. package/dist/commands/menubar.js +58 -1
  34. package/dist/commands/monitors.js +1 -1
  35. package/dist/commands/org.d.ts +5 -0
  36. package/dist/commands/org.js +83 -29
  37. package/dist/commands/output.js +2 -2
  38. package/dist/commands/profiles.js +2 -2
  39. package/dist/commands/prune.js +5 -4
  40. package/dist/commands/routines.js +43 -25
  41. package/dist/commands/routines.test-fixture.d.ts +86 -0
  42. package/dist/commands/routines.test-fixture.js +345 -0
  43. package/dist/commands/sessions-picker.js +124 -122
  44. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  45. package/dist/commands/sessions.test-fixture.js +224 -0
  46. package/dist/commands/setup.js +9 -1
  47. package/dist/commands/share.d.ts +9 -10
  48. package/dist/commands/share.js +68 -50
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/snapshot.d.ts +1 -1
  51. package/dist/commands/snapshot.js +4 -4
  52. package/dist/commands/ssh.js +15 -62
  53. package/dist/commands/status.d.ts +4 -2
  54. package/dist/commands/status.js +10 -7
  55. package/dist/commands/subagents.js +1 -1
  56. package/dist/commands/sync.d.ts +1 -0
  57. package/dist/commands/sync.js +29 -2
  58. package/dist/commands/view.js +11 -2
  59. package/dist/commands/webhook.js +1 -1
  60. package/dist/lib/accounting/rotate.d.ts +5 -3
  61. package/dist/lib/accounting/rotate.js +36 -7
  62. package/dist/lib/accounting/usage.d.ts +18 -4
  63. package/dist/lib/accounting/usage.js +138 -27
  64. package/dist/lib/agent-spec/agents.d.ts +7 -7
  65. package/dist/lib/agent-spec/agents.js +15 -15
  66. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  67. package/dist/lib/analytics/mix-commands.js +14 -40
  68. package/dist/lib/audit/log.d.ts +2 -2
  69. package/dist/lib/audit/log.js +2 -2
  70. package/dist/lib/beta.js +1 -1
  71. package/dist/lib/browser/domain-skills.d.ts +26 -6
  72. package/dist/lib/browser/domain-skills.js +81 -43
  73. package/dist/lib/cloud/rush.d.ts +0 -15
  74. package/dist/lib/cloud/rush.js +0 -35
  75. package/dist/lib/crabbox/runtimes.d.ts +6 -2
  76. package/dist/lib/crabbox/runtimes.js +21 -5
  77. package/dist/lib/daemon/daemon.js +97 -58
  78. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  79. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  80. package/dist/lib/daemon/runner.d.ts +18 -1
  81. package/dist/lib/daemon/runner.js +105 -11
  82. package/dist/lib/daemon-ticks.d.ts +38 -1
  83. package/dist/lib/daemon-ticks.js +52 -5
  84. package/dist/lib/device-config.js +1 -2
  85. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  86. package/dist/lib/devices/discovery-policy.js +3 -3
  87. package/dist/lib/devices/doctor-findings.js +1 -1
  88. package/dist/lib/devices/fleet.d.ts +4 -8
  89. package/dist/lib/devices/fleet.js +3 -15
  90. package/dist/lib/devices/health-report.js +4 -4
  91. package/dist/lib/devices/pool.d.ts +0 -6
  92. package/dist/lib/devices/pool.js +0 -6
  93. package/dist/lib/devices/registry.d.ts +0 -15
  94. package/dist/lib/devices/registry.js +0 -9
  95. package/dist/lib/devices/stats-cache.d.ts +12 -0
  96. package/dist/lib/devices/stats-cache.js +24 -5
  97. package/dist/lib/drift-sync.d.ts +1 -1
  98. package/dist/lib/drift-sync.js +2 -2
  99. package/dist/lib/drift.d.ts +1 -1
  100. package/dist/lib/exec.js +13 -3
  101. package/dist/lib/fleet/capture.d.ts +1 -1
  102. package/dist/lib/fleet/manifest.js +1 -1
  103. package/dist/lib/fleet/remote-login.d.ts +2 -2
  104. package/dist/lib/fleet/remote-login.js +3 -4
  105. package/dist/lib/format.d.ts +9 -0
  106. package/dist/lib/format.js +16 -0
  107. package/dist/lib/github/pr-mergeable.d.ts +49 -0
  108. package/dist/lib/github/pr-mergeable.js +157 -0
  109. package/dist/lib/github/pr-verdict.d.ts +67 -0
  110. package/dist/lib/github/pr-verdict.js +72 -0
  111. package/dist/lib/harness/adapter.d.ts +6 -5
  112. package/dist/lib/hosts/dispatch.d.ts +2 -0
  113. package/dist/lib/hosts/dispatch.js +8 -1
  114. package/dist/lib/hosts/passthrough.d.ts +10 -0
  115. package/dist/lib/hosts/passthrough.js +28 -7
  116. package/dist/lib/hosts/providers/devices.js +1 -13
  117. package/dist/lib/hosts/ready.d.ts +12 -3
  118. package/dist/lib/hosts/ready.js +27 -12
  119. package/dist/lib/hosts/registry.d.ts +4 -5
  120. package/dist/lib/hosts/registry.js +3 -7
  121. package/dist/lib/hosts/session-index.js +1 -1
  122. package/dist/lib/installations/shims.js +1 -1
  123. package/dist/lib/installations/versions.d.ts +40 -0
  124. package/dist/lib/installations/versions.js +78 -4
  125. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  126. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  127. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  128. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  129. package/dist/lib/menubar/install-menubar.js +218 -12
  130. package/dist/lib/monitors/config.d.ts +2 -2
  131. package/dist/lib/monitors/config.js +3 -2
  132. package/dist/lib/origin-machine.d.ts +18 -0
  133. package/dist/lib/origin-machine.js +34 -0
  134. package/dist/lib/perf/db.js +1 -1
  135. package/dist/lib/perf/spool.js +1 -1
  136. package/dist/lib/prix-account.d.ts +2 -1
  137. package/dist/lib/prix-account.js +2 -1
  138. package/dist/lib/profiles.d.ts +7 -0
  139. package/dist/lib/profiles.js +26 -1
  140. package/dist/lib/remote-agents-json.js +1 -7
  141. package/dist/lib/routines-placement.d.ts +1 -1
  142. package/dist/lib/routines-placement.js +1 -1
  143. package/dist/lib/sandbox.d.ts +24 -0
  144. package/dist/lib/sandbox.js +101 -0
  145. package/dist/lib/scheduling/routines.d.ts +18 -9
  146. package/dist/lib/scheduling/routines.js +16 -8
  147. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  149. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  151. package/dist/lib/secrets/agent.js +9 -1
  152. package/dist/lib/secrets/index.js +1 -1
  153. package/dist/lib/secrets/list-filter.js +1 -1
  154. package/dist/lib/service-manifest.d.ts +20 -0
  155. package/dist/lib/service-manifest.js +34 -0
  156. package/dist/lib/session/active.d.ts +4 -16
  157. package/dist/lib/session/active.js +2 -6
  158. package/dist/lib/session/cloud.js +1 -1
  159. package/dist/lib/session/db.d.ts +13 -98
  160. package/dist/lib/session/db.js +5 -12
  161. package/dist/lib/session/discover.d.ts +5 -81
  162. package/dist/lib/session/discover.js +5 -5
  163. package/dist/lib/session/fork.js +1 -1
  164. package/dist/lib/session/live-metadata.js +1 -1
  165. package/dist/lib/session/origin-machine.d.ts +1 -18
  166. package/dist/lib/session/origin-machine.js +4 -34
  167. package/dist/lib/session/relative-time.d.ts +1 -44
  168. package/dist/lib/session/relative-time.js +4 -120
  169. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  170. package/dist/lib/session/remote/remote-bundle.js +61 -0
  171. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  172. package/dist/lib/session/remote/remote-list.js +607 -0
  173. package/dist/lib/session/remote/remote.d.ts +132 -0
  174. package/dist/lib/session/remote/remote.js +314 -0
  175. package/dist/lib/session/remote/watch.d.ts +101 -0
  176. package/dist/lib/session/remote/watch.js +241 -0
  177. package/dist/lib/session/remote-bundle.d.ts +1 -12
  178. package/dist/lib/session/remote-bundle.js +3 -61
  179. package/dist/lib/session/remote-list.d.ts +1 -148
  180. package/dist/lib/session/remote-list.js +3 -607
  181. package/dist/lib/session/remote.d.ts +1 -132
  182. package/dist/lib/session/remote.js +3 -314
  183. package/dist/lib/session/short-id.d.ts +1 -17
  184. package/dist/lib/session/short-id.js +4 -20
  185. package/dist/lib/session/watch.d.ts +1 -101
  186. package/dist/lib/session/watch.js +3 -242
  187. package/dist/lib/session/width.d.ts +1 -29
  188. package/dist/lib/session/width.js +4 -101
  189. package/dist/lib/share/config.d.ts +12 -2
  190. package/dist/lib/share/config.js +47 -11
  191. package/dist/lib/share/delete.js +1 -1
  192. package/dist/lib/share/publish.js +1 -1
  193. package/dist/lib/share/worker-template.js +2 -2
  194. package/dist/lib/smart-launch.d.ts +4 -4
  195. package/dist/lib/smart-launch.js +8 -18
  196. package/dist/lib/snapshot.d.ts +2 -2
  197. package/dist/lib/snapshot.js +1 -1
  198. package/dist/lib/staleness/index.d.ts +3 -1
  199. package/dist/lib/staleness/index.js +21 -1
  200. package/dist/lib/staleness/types.d.ts +11 -0
  201. package/dist/lib/staleness/writers/commands.js +13 -4
  202. package/dist/lib/staleness/writers/hooks.js +3 -1
  203. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  204. package/dist/lib/staleness/writers/mcp.js +16 -1
  205. package/dist/lib/staleness/writers/rules.js +1 -1
  206. package/dist/lib/staleness/writers/skills.js +3 -1
  207. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  208. package/dist/lib/staleness/writers/subagents.js +18 -1
  209. package/dist/lib/staleness/writers/types.d.ts +10 -0
  210. package/dist/lib/startup/command-registry.d.ts +10 -1
  211. package/dist/lib/startup/command-registry.js +26 -7
  212. package/dist/lib/startup/root-command.d.ts +18 -1
  213. package/dist/lib/startup/root-command.js +18 -1
  214. package/dist/lib/state.js +7 -0
  215. package/dist/lib/teams/placement-probe.js +28 -13
  216. package/dist/lib/teams/scheduler.d.ts +3 -1
  217. package/dist/lib/teams/scheduler.js +1 -1
  218. package/dist/lib/text/relative-time.d.ts +44 -0
  219. package/dist/lib/text/relative-time.js +120 -0
  220. package/dist/lib/text/short-id.d.ts +17 -0
  221. package/dist/lib/text/short-id.js +20 -0
  222. package/dist/lib/text/width.d.ts +29 -0
  223. package/dist/lib/text/width.js +101 -0
  224. package/dist/lib/triggers/handlers.d.ts +7 -0
  225. package/dist/lib/triggers/handlers.js +8 -0
  226. package/dist/lib/triggers/webhook.d.ts +1 -0
  227. package/dist/lib/triggers/webhook.js +2 -1
  228. package/dist/lib/types.d.ts +1 -1
  229. package/dist/lib/view-types.d.ts +7 -0
  230. package/package.json +2 -3
  231. package/dist/bin/agents +0 -0
  232. package/dist/commands/serve.d.ts +0 -10
  233. package/dist/commands/serve.js +0 -68
  234. package/dist/commands/tickets.d.ts +0 -2
  235. package/dist/commands/tickets.js +0 -43
  236. package/dist/commands/trends.d.ts +0 -10
  237. package/dist/commands/trends.js +0 -12
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  240. package/dist/lib/observe-aliases.d.ts +0 -30
  241. package/dist/lib/observe-aliases.js +0 -38
  242. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  243. package/dist/lib/serve/control.d.ts +0 -95
  244. package/dist/lib/serve/control.js +0 -260
  245. package/dist/lib/serve/data.d.ts +0 -81
  246. package/dist/lib/serve/data.js +0 -91
  247. package/dist/lib/serve/page.d.ts +0 -7
  248. package/dist/lib/serve/page.js +0 -140
  249. package/dist/lib/serve/server.d.ts +0 -80
  250. package/dist/lib/serve/server.js +0 -145
  251. package/dist/lib/serve/stream.d.ts +0 -43
  252. package/dist/lib/serve/stream.js +0 -116
  253. package/dist/lib/serve/token.d.ts +0 -35
  254. package/dist/lib/serve/token.js +0 -85
  255. package/dist/lib/tickets/list.d.ts +0 -53
  256. package/dist/lib/tickets/list.js +0 -153
@@ -0,0 +1,86 @@
1
+ import { describe } from 'vitest';
2
+ import { spawnSync, spawn } from 'child_process';
3
+ /**
4
+ * Shared fixture for the routines.*.test.ts suite slices (RUSH-2819).
5
+ *
6
+ * routines.test.ts was one 2,249-line file measured at ~194s of test time —
7
+ * one of the slowest files in CI, serializing an entire fork while every
8
+ * other selected file finished. The suite is split into topical slices so
9
+ * vitest's per-file fork parallelism can spread the subprocess-heavy tests
10
+ * across workers; the helpers each slice shares live here.
11
+ *
12
+ * Every test spawns the real CLI (`node --import tsx src/index.ts routines ...`)
13
+ * against an isolated mkdtemp HOME — no live ~/.agents state, no mocks, no
14
+ * imported writeJob/readJob. Modeled on `routines-webhook.test.ts`.
15
+ */
16
+ export declare const REPO_ROOT: string;
17
+ export declare const TSX_IMPORT: string;
18
+ export declare const CLI_ENTRYPOINT: string;
19
+ export declare const describeRoutines: typeof describe.skip;
20
+ /** Provision an isolated HOME with agents.yaml, .system/.git, and optional routines + device registry. */
21
+ export declare function makeHome(opts?: {
22
+ jobs?: Record<string, unknown>[];
23
+ projectJobs?: Record<string, unknown>[];
24
+ registry?: Record<string, unknown>;
25
+ deviceRoutines?: Record<string, string[]>;
26
+ /** Full mkdtemp prefix override — the daemon harness scopes its leak sweep by it. */
27
+ tmpPrefix?: string;
28
+ }): string;
29
+ /** Run `agents routines <args>` against an isolated HOME. */
30
+ export declare function run(home: string, args: string[], extraEnv?: Record<string, string>, cwd?: string): ReturnType<typeof spawnSync>;
31
+ export declare function readRoutineYaml(home: string, name: string): Record<string, unknown> | null;
32
+ export declare function writeRunMeta(home: string, jobName: string, runId: string, meta: Record<string, unknown>): void;
33
+ export declare function isProcessAlive(pid: number): boolean;
34
+ /**
35
+ * Per-file isolated-daemon harness (RUSH-2367, RUSH-2819). Each slice that
36
+ * spawns a real `__daemon-run` process against an isolated HOME calls this
37
+ * once at module scope and registers its own leak detector — the tracked pid
38
+ * set MUST be private to the file that spawned it, since vitest's per-file
39
+ * fork isolation means the leak sweep only ever needs to answer for what THIS
40
+ * file started.
41
+ *
42
+ * `fileSlug` MUST be unique per test file: it names the mkdtemp prefix
43
+ * `makeDaemonHome` uses, and the CI leak sweep matches ONLY that prefix.
44
+ * The suite slices run in parallel forks, so a sweep over the shared
45
+ * `agents-routines-test-` prefix would catch a sibling file's still-running
46
+ * daemon and kill it mid-test — exactly what failed the first CI run of the
47
+ * RUSH-2819 split (run 32552954164: add's sweep killed a live daemon whose
48
+ * HOME belonged to a concurrently running slice).
49
+ */
50
+ export declare function createDaemonHarness(fileSlug: string): {
51
+ startIsolatedDaemon: (home: string) => {
52
+ child: ReturnType<typeof spawn>;
53
+ pidPromise: Promise<number | null>;
54
+ };
55
+ stopIsolatedDaemon: (child: ReturnType<typeof spawn>) => Promise<void>;
56
+ registerLeakDetector: () => void;
57
+ makeDaemonHome: (opts?: Omit<Parameters<typeof makeHome>[0], 'tmpPrefix'>) => string;
58
+ };
59
+ export declare const baseJob: {
60
+ name: string;
61
+ schedule: string;
62
+ agent: string;
63
+ prompt: string;
64
+ cwd: string;
65
+ enabled: boolean;
66
+ };
67
+ export declare const registry: {
68
+ 'yosemite-s0': {
69
+ name: string;
70
+ platform: string;
71
+ };
72
+ 'mac-mini': {
73
+ name: string;
74
+ platform: string;
75
+ };
76
+ zion: {
77
+ name: string;
78
+ platform: string;
79
+ };
80
+ };
81
+ export declare function readDeviceRoutines(home: string, device: string): string[];
82
+ export declare function writeDeviceRoutines(home: string, device: string, routines: string[]): void;
83
+ /** Write a minimal project YAML so listProjectDefs() sees it. */
84
+ export declare function writeProject(home: string, name: string): void;
85
+ /** Env that points the projects dir at the isolated home. */
86
+ export declare function projectsEnv(home: string): Record<string, string>;
@@ -0,0 +1,345 @@
1
+ import { describe, afterAll } from 'vitest';
2
+ import { spawnSync, spawn, execFileSync } from 'child_process';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import * as yaml from 'yaml';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
8
+ import { createRequire } from 'module';
9
+ /**
10
+ * Shared fixture for the routines.*.test.ts suite slices (RUSH-2819).
11
+ *
12
+ * routines.test.ts was one 2,249-line file measured at ~194s of test time —
13
+ * one of the slowest files in CI, serializing an entire fork while every
14
+ * other selected file finished. The suite is split into topical slices so
15
+ * vitest's per-file fork parallelism can spread the subprocess-heavy tests
16
+ * across workers; the helpers each slice shares live here.
17
+ *
18
+ * Every test spawns the real CLI (`node --import tsx src/index.ts routines ...`)
19
+ * against an isolated mkdtemp HOME — no live ~/.agents state, no mocks, no
20
+ * imported writeJob/readJob. Modeled on `routines-webhook.test.ts`.
21
+ */
22
+ export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
23
+ const require = createRequire(import.meta.url);
24
+ // `--import` takes a module specifier, not a path: a bare Windows path like
25
+ // `D:\a\...\tsx\dist\loader.mjs` is parsed as a URL with protocol 'd:' and the
26
+ // child dies with ERR_UNSUPPORTED_ESM_URL_SCHEME before running the CLI. Same
27
+ // pattern as sessions.test.ts:21.
28
+ export const TSX_IMPORT = pathToFileURL(require.resolve('tsx')).href;
29
+ export const CLI_ENTRYPOINT = path.join(REPO_ROOT, 'src', 'index.ts');
30
+ // win32: subprocess CLI + process-group signals / path spawn assumptions (RUSH-2215).
31
+ export const describeRoutines = process.platform === 'win32' ? describe.skip : describe;
32
+ /** Provision an isolated HOME with agents.yaml, .system/.git, and optional routines + device registry. */
33
+ export function makeHome(opts = {}) {
34
+ const home = fs.mkdtempSync(opts.tmpPrefix ?? path.join(os.tmpdir(), 'agents-routines-test-'));
35
+ const agentsDir = path.join(home, '.agents');
36
+ const routinesDir = path.join(agentsDir, 'routines');
37
+ const projectDir = path.join(home, 'project');
38
+ const projectRoutinesDir = path.join(projectDir, '.agents', 'routines');
39
+ fs.mkdirSync(routinesDir, { recursive: true });
40
+ fs.mkdirSync(projectRoutinesDir, { recursive: true });
41
+ fs.mkdirSync(path.join(projectDir, '.git'), { recursive: true });
42
+ fs.writeFileSync(path.join(agentsDir, 'agents.yaml'), 'agents: {}\n');
43
+ fs.mkdirSync(path.join(agentsDir, '.system', '.git'), { recursive: true });
44
+ for (const job of opts.jobs ?? []) {
45
+ fs.writeFileSync(path.join(routinesDir, `${job.name}.yml`), yaml.stringify(job));
46
+ }
47
+ for (const job of opts.projectJobs ?? []) {
48
+ fs.writeFileSync(path.join(projectRoutinesDir, `${job.name}.yml`), yaml.stringify(job));
49
+ }
50
+ if (opts.registry) {
51
+ const devicesDir = path.join(agentsDir, '.history', 'devices');
52
+ fs.mkdirSync(devicesDir, { recursive: true });
53
+ fs.writeFileSync(path.join(devicesDir, 'registry.json'), JSON.stringify(opts.registry));
54
+ }
55
+ for (const [device, routines] of Object.entries(opts.deviceRoutines ?? {})) {
56
+ writeDeviceRoutines(home, device, routines);
57
+ }
58
+ return home;
59
+ }
60
+ /** Run `agents routines <args>` against an isolated HOME. */
61
+ export function run(home, args, extraEnv = {}, cwd = REPO_ROOT) {
62
+ return spawnSync('node', ['--import', TSX_IMPORT, CLI_ENTRYPOINT, 'routines', ...args], {
63
+ cwd,
64
+ env: {
65
+ ...process.env,
66
+ HOME: home,
67
+ USERPROFILE: home,
68
+ // Point the device registry at this test's home (RUSH-2042): getDevicesDir()
69
+ // reads AGENTS_DEVICES_DIR, and the parent vitest fork exports its own via
70
+ // setup.ts — override it so the child reads the registry makeHome() wrote.
71
+ AGENTS_DEVICES_DIR: path.join(home, '.agents', '.history', 'devices'),
72
+ AGENTS_SKIP_MIGRATION: '1',
73
+ ...extraEnv,
74
+ },
75
+ encoding: 'utf-8',
76
+ timeout: 30_000,
77
+ });
78
+ }
79
+ export function readRoutineYaml(home, name) {
80
+ const p = path.join(home, '.agents', 'routines', `${name}.yml`);
81
+ if (!fs.existsSync(p))
82
+ return null;
83
+ return yaml.parse(fs.readFileSync(p, 'utf-8'));
84
+ }
85
+ export function writeRunMeta(home, jobName, runId, meta) {
86
+ const runDir = path.join(home, '.agents', '.history', 'runs', jobName, runId);
87
+ fs.mkdirSync(runDir, { recursive: true });
88
+ fs.writeFileSync(path.join(runDir, 'meta.json'), JSON.stringify(meta));
89
+ }
90
+ function daemonPidPath(home) {
91
+ return path.join(home, '.agents', '.cache', 'helpers', 'daemon', 'daemon.pid');
92
+ }
93
+ function readDaemonPid(home) {
94
+ const pidPath = daemonPidPath(home);
95
+ if (!fs.existsSync(pidPath))
96
+ return null;
97
+ const raw = fs.readFileSync(pidPath, 'utf-8').trim();
98
+ const pid = parseInt(raw, 10);
99
+ return isNaN(pid) ? null : pid;
100
+ }
101
+ export function isProcessAlive(pid) {
102
+ try {
103
+ process.kill(pid, 0);
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Per-file isolated-daemon harness (RUSH-2367, RUSH-2819). Each slice that
112
+ * spawns a real `__daemon-run` process against an isolated HOME calls this
113
+ * once at module scope and registers its own leak detector — the tracked pid
114
+ * set MUST be private to the file that spawned it, since vitest's per-file
115
+ * fork isolation means the leak sweep only ever needs to answer for what THIS
116
+ * file started.
117
+ *
118
+ * `fileSlug` MUST be unique per test file: it names the mkdtemp prefix
119
+ * `makeDaemonHome` uses, and the CI leak sweep matches ONLY that prefix.
120
+ * The suite slices run in parallel forks, so a sweep over the shared
121
+ * `agents-routines-test-` prefix would catch a sibling file's still-running
122
+ * daemon and kill it mid-test — exactly what failed the first CI run of the
123
+ * RUSH-2819 split (run 32552954164: add's sweep killed a live daemon whose
124
+ * HOME belonged to a concurrently running slice).
125
+ */
126
+ export function createDaemonHarness(fileSlug) {
127
+ const homePrefix = path.join(os.tmpdir(), `agents-routines-${fileSlug}-`);
128
+ /**
129
+ * Every daemon pid spawned via `startIsolatedDaemon` in this file, live for as
130
+ * long as `stopIsolatedDaemon` has not yet reaped it. Backstops the per-test
131
+ * try/finally: this file's `afterAll` (registered via `registerLeakDetector`)
132
+ * asserts the set is empty and force-kills + fails the suite on anything left
133
+ * in it — a leaked real daemon process is exactly the RUSH-2367 bug (three
134
+ * left running for up to 3.5 days on a fleet box, invisible to `agents daemon`
135
+ * because each served its own fixture HOME/registry).
136
+ */
137
+ const trackedDaemonPids = new Set();
138
+ /** Start the real scheduler foreground process against an isolated HOME. */
139
+ function startIsolatedDaemon(home) {
140
+ const child = spawn('node', ['--import', 'tsx', 'src/index.ts', '__daemon-run'], {
141
+ cwd: REPO_ROOT,
142
+ env: {
143
+ ...process.env,
144
+ HOME: home,
145
+ USERPROFILE: home,
146
+ // Without this override the daemon inherits the parent vitest process's real
147
+ // AGENTS_HISTORY_DIR (pointing at ~/.agents/.history). The daemon's SIGTERM
148
+ // sweep then reads live session records from the production history directory
149
+ // and kills real tmux-wrapped processes every five-minute tick (RUSH-2545).
150
+ AGENTS_HISTORY_DIR: path.join(home, '.agents', '.history'),
151
+ AGENTS_SKIP_MIGRATION: '1',
152
+ },
153
+ detached: true,
154
+ stdio: 'ignore',
155
+ });
156
+ if (child.pid)
157
+ trackedDaemonPids.add(child.pid);
158
+ const pidPromise = new Promise((resolve) => {
159
+ const deadline = Date.now() + 15_000;
160
+ const interval = setInterval(() => {
161
+ const pid = readDaemonPid(home);
162
+ if (pid) {
163
+ clearInterval(interval);
164
+ resolve(pid);
165
+ return;
166
+ }
167
+ if (Date.now() >= deadline) {
168
+ clearInterval(interval);
169
+ resolve(null);
170
+ }
171
+ }, 50);
172
+ });
173
+ return { child, pidPromise };
174
+ }
175
+ /** Terminate a daemon process started by startIsolatedDaemon and wait for it to exit. */
176
+ async function stopIsolatedDaemon(child) {
177
+ const pid = child.pid;
178
+ try {
179
+ if (!pid)
180
+ return;
181
+ const closePromise = new Promise((resolve) => {
182
+ if (child.exitCode !== null || child.signalCode !== null) {
183
+ resolve();
184
+ return;
185
+ }
186
+ child.on('close', () => resolve());
187
+ });
188
+ if (child.exitCode !== null || child.signalCode !== null)
189
+ return;
190
+ const signalProcessGroup = process.platform !== 'win32';
191
+ const signal = (sig) => {
192
+ try {
193
+ if (signalProcessGroup) {
194
+ process.kill(-pid, sig);
195
+ }
196
+ else {
197
+ child.kill(sig);
198
+ }
199
+ }
200
+ catch {
201
+ // already gone
202
+ }
203
+ };
204
+ signal('SIGTERM');
205
+ const timer = setTimeout(() => signal('SIGKILL'), 3_000);
206
+ await closePromise;
207
+ clearTimeout(timer);
208
+ }
209
+ finally {
210
+ if (pid)
211
+ trackedDaemonPids.delete(pid);
212
+ }
213
+ }
214
+ /**
215
+ * Suite-level leak detector (RUSH-2367). Every per-test try/finally above
216
+ * already reaps its own daemon on success, failure, or a thrown assertion —
217
+ * but nothing in JS runs if the whole vitest worker is killed externally
218
+ * before reaching `finally`, which is what actually produced three real
219
+ * orphaned daemons found alive on a fleet box for up to 3.5 days: their
220
+ * fixture HOME dirs still existed (the `finally`'s `fs.rmSync` never ran
221
+ * either), so no amount of in-test cleanup logic would have caught it. This
222
+ * is the second line of defense, not a substitute for the self-terminate
223
+ * guard in the daemon itself (`runDaemon`'s state-dir check).
224
+ *
225
+ * Two checks, after every test in this file has run:
226
+ * 1. Always: nothing THIS run spawned via `startIsolatedDaemon` may still be
227
+ * alive — `trackedDaemonPids` is only ever non-empty here if a bug (not
228
+ * an external kill) let one slip past its own test's `finally`.
229
+ * 2. CI only: sweep for any OTHER live `__daemon-run` process whose HOME
230
+ * sits under THIS FILE's unique `agents-routines-<fileSlug>-` prefix —
231
+ * a leak from a previous interrupted run of this same file. Scoped to
232
+ * the per-file prefix because sibling slices run in parallel forks and
233
+ * legitimately have live daemons under their own prefixes. POSIX-only
234
+ * (`/proc`); best-effort and skipped where `/proc` is unavailable
235
+ * (macOS CI legs).
236
+ *
237
+ * Either check force-kills what it finds and fails the suite — a silent
238
+ * "still running, we'll get it next time" is exactly how the original three
239
+ * accumulated.
240
+ */
241
+ function registerLeakDetector() {
242
+ afterAll(() => {
243
+ const leaks = [];
244
+ const killDaemon = (pid) => {
245
+ try {
246
+ process.kill(-pid, 'SIGKILL');
247
+ }
248
+ catch { /* already gone, or never its own group leader */ }
249
+ try {
250
+ process.kill(pid, 'SIGKILL');
251
+ }
252
+ catch { /* already gone */ }
253
+ };
254
+ for (const pid of trackedDaemonPids) {
255
+ if (isProcessAlive(pid)) {
256
+ leaks.push(`pid ${pid} (spawned by this test run, never reaped by its own test)`);
257
+ killDaemon(pid);
258
+ }
259
+ }
260
+ trackedDaemonPids.clear();
261
+ if (process.env.CI && process.platform !== 'win32') {
262
+ const prefix = homePrefix;
263
+ let psOut = '';
264
+ try {
265
+ psOut = execFileSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
266
+ }
267
+ catch { /* ps unavailable — nothing to sweep */ }
268
+ for (const line of psOut.split('\n')) {
269
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
270
+ if (!m)
271
+ continue;
272
+ const tokens = m[2].trim().split(/\s+/);
273
+ if (tokens[tokens.length - 1] !== '__daemon-run')
274
+ continue;
275
+ const pid = parseInt(m[1], 10);
276
+ if (isNaN(pid))
277
+ continue;
278
+ let home = null;
279
+ try {
280
+ const environ = fs.readFileSync(`/proc/${pid}/environ`, 'utf-8');
281
+ const homeVar = environ.split('\0').find((v) => v.startsWith('HOME='));
282
+ home = homeVar ? homeVar.slice(5) : null;
283
+ }
284
+ catch {
285
+ continue;
286
+ } // /proc unreadable (macOS, permissions) — best-effort only
287
+ if (!home || !home.startsWith(prefix))
288
+ continue;
289
+ leaks.push(`pid ${pid} HOME=${home} (leaked from a previous interrupted run of this file)`);
290
+ killDaemon(pid);
291
+ }
292
+ }
293
+ if (leaks.length > 0) {
294
+ throw new Error(`RUSH-2367 leak detector: ${leaks.length} __daemon-run process(es) survived this test file, ` +
295
+ `now force-killed: ${leaks.join('; ')}`);
296
+ }
297
+ });
298
+ }
299
+ /** makeHome under this file's unique prefix — REQUIRED for any home a daemon runs against. */
300
+ function makeDaemonHome(opts = {}) {
301
+ return makeHome({ ...opts, tmpPrefix: homePrefix });
302
+ }
303
+ return { startIsolatedDaemon, stopIsolatedDaemon, registerLeakDetector, makeDaemonHome };
304
+ }
305
+ export const baseJob = {
306
+ name: 'test-job',
307
+ schedule: '0 3 * * *',
308
+ agent: 'claude',
309
+ prompt: 'noop',
310
+ // Agent routines now need an execution anchor to activate (RUSH-2290); home
311
+ // is a valid one and keeps these device/eligibility fixtures ready.
312
+ cwd: '~',
313
+ // Legacy fixture state: tests that specifically cover the new manifest model
314
+ // materialize a device document below.
315
+ enabled: true,
316
+ };
317
+ export const registry = {
318
+ 'yosemite-s0': { name: 'yosemite-s0', platform: 'linux' },
319
+ 'mac-mini': { name: 'mac-mini', platform: 'macos' },
320
+ 'zion': { name: 'zion', platform: 'macos' },
321
+ };
322
+ export function readDeviceRoutines(home, device) {
323
+ const file = path.join(home, '.agents', 'devices', device, 'agents.yaml');
324
+ if (!fs.existsSync(file))
325
+ return [];
326
+ return yaml.parse(fs.readFileSync(file, 'utf-8')).routines ?? [];
327
+ }
328
+ export function writeDeviceRoutines(home, device, routines) {
329
+ const dir = path.join(home, '.agents', 'devices', device);
330
+ fs.mkdirSync(dir, { recursive: true });
331
+ fs.writeFileSync(path.join(dir, 'agents.yaml'), yaml.stringify({ routines }));
332
+ }
333
+ // ---------------------------------------------------------------------------
334
+ // Project-tagging tests (--project / --all-projects / list --json projectGroup)
335
+ // ---------------------------------------------------------------------------
336
+ /** Write a minimal project YAML so listProjectDefs() sees it. */
337
+ export function writeProject(home, name) {
338
+ const dir = path.join(home, '.agents', 'projects');
339
+ fs.mkdirSync(dir, { recursive: true });
340
+ fs.writeFileSync(path.join(dir, `${name}.yaml`), yaml.stringify({ name }));
341
+ }
342
+ /** Env that points the projects dir at the isolated home. */
343
+ export function projectsEnv(home) {
344
+ return { AGENTS_PROJECTS_DIR: path.join(home, '.agents', 'projects') };
345
+ }