@phnx-labs/agents-cli 1.22.43 → 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 (164) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +14 -12
  3. package/dist/bootstrap.js +8 -6
  4. package/dist/cli/command-registry.d.ts +0 -5
  5. package/dist/cli/command-registry.js +1 -17
  6. package/dist/commands/apply.d.ts +7 -10
  7. package/dist/commands/apply.js +15 -35
  8. package/dist/commands/artifacts.js +4 -5
  9. package/dist/commands/audit.d.ts +6 -6
  10. package/dist/commands/audit.js +12 -12
  11. package/dist/commands/auth.js +7 -3
  12. package/dist/commands/beta.d.ts +7 -1
  13. package/dist/commands/beta.js +16 -9
  14. package/dist/commands/commands.js +1 -1
  15. package/dist/commands/doctor.js +1 -4
  16. package/dist/commands/events.d.ts +2 -2
  17. package/dist/commands/events.js +6 -5
  18. package/dist/commands/exec.d.ts +1 -1
  19. package/dist/commands/exec.js +8 -4
  20. package/dist/commands/factory.d.ts +1 -1
  21. package/dist/commands/fleet-capture.d.ts +1 -1
  22. package/dist/commands/fleet-capture.js +4 -5
  23. package/dist/commands/harness.js +1 -49
  24. package/dist/commands/hooks.js +1 -1
  25. package/dist/commands/insights.d.ts +1 -1
  26. package/dist/commands/insights.js +3 -2
  27. package/dist/commands/menubar.js +58 -1
  28. package/dist/commands/org.d.ts +5 -0
  29. package/dist/commands/org.js +82 -29
  30. package/dist/commands/output.js +2 -2
  31. package/dist/commands/prune.js +5 -4
  32. package/dist/commands/routines.test-fixture.d.ts +86 -0
  33. package/dist/commands/routines.test-fixture.js +345 -0
  34. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  35. package/dist/commands/sessions.test-fixture.js +224 -0
  36. package/dist/commands/setup.js +5 -1
  37. package/dist/commands/share.d.ts +9 -10
  38. package/dist/commands/share.js +68 -50
  39. package/dist/commands/skills.js +1 -1
  40. package/dist/commands/ssh.js +15 -62
  41. package/dist/commands/subagents.js +1 -1
  42. package/dist/commands/view.js +5 -2
  43. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  44. package/dist/lib/analytics/mix-commands.js +14 -40
  45. package/dist/lib/audit/log.d.ts +2 -2
  46. package/dist/lib/audit/log.js +2 -2
  47. package/dist/lib/beta.js +1 -1
  48. package/dist/lib/browser/domain-skills.d.ts +26 -6
  49. package/dist/lib/browser/domain-skills.js +81 -43
  50. package/dist/lib/daemon/daemon.js +11 -0
  51. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  52. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  53. package/dist/lib/daemon-ticks.d.ts +38 -1
  54. package/dist/lib/daemon-ticks.js +52 -5
  55. package/dist/lib/device-config.js +1 -2
  56. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  57. package/dist/lib/devices/discovery-policy.js +3 -3
  58. package/dist/lib/devices/fleet.d.ts +4 -8
  59. package/dist/lib/devices/fleet.js +3 -15
  60. package/dist/lib/devices/health-report.js +3 -3
  61. package/dist/lib/devices/pool.d.ts +0 -6
  62. package/dist/lib/devices/pool.js +0 -6
  63. package/dist/lib/devices/registry.d.ts +0 -15
  64. package/dist/lib/devices/registry.js +0 -9
  65. package/dist/lib/devices/stats-cache.d.ts +12 -0
  66. package/dist/lib/devices/stats-cache.js +24 -5
  67. package/dist/lib/fleet/capture.d.ts +1 -1
  68. package/dist/lib/fleet/manifest.js +1 -1
  69. package/dist/lib/fleet/remote-login.d.ts +2 -2
  70. package/dist/lib/fleet/remote-login.js +3 -4
  71. package/dist/lib/hosts/dispatch.d.ts +2 -0
  72. package/dist/lib/hosts/dispatch.js +8 -1
  73. package/dist/lib/hosts/passthrough.js +0 -1
  74. package/dist/lib/hosts/providers/devices.js +1 -13
  75. package/dist/lib/hosts/ready.d.ts +12 -3
  76. package/dist/lib/hosts/ready.js +27 -12
  77. package/dist/lib/hosts/registry.d.ts +4 -5
  78. package/dist/lib/hosts/registry.js +3 -7
  79. package/dist/lib/installations/versions.js +21 -2
  80. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  81. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  82. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  83. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  84. package/dist/lib/menubar/install-menubar.js +199 -4
  85. package/dist/lib/prix-account.d.ts +2 -1
  86. package/dist/lib/prix-account.js +2 -1
  87. package/dist/lib/remote-agents-json.js +1 -7
  88. package/dist/lib/routines-placement.d.ts +1 -1
  89. package/dist/lib/routines-placement.js +1 -1
  90. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  94. package/dist/lib/session/active.d.ts +4 -16
  95. package/dist/lib/session/active.js +2 -6
  96. package/dist/lib/session/db.d.ts +13 -98
  97. package/dist/lib/session/db.js +4 -11
  98. package/dist/lib/session/discover.d.ts +5 -81
  99. package/dist/lib/session/discover.js +5 -5
  100. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  101. package/dist/lib/session/remote/remote-bundle.js +61 -0
  102. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  103. package/dist/lib/session/remote/remote-list.js +607 -0
  104. package/dist/lib/session/remote/remote.d.ts +132 -0
  105. package/dist/lib/session/remote/remote.js +314 -0
  106. package/dist/lib/session/remote/watch.d.ts +101 -0
  107. package/dist/lib/session/remote/watch.js +241 -0
  108. package/dist/lib/session/remote-bundle.d.ts +1 -12
  109. package/dist/lib/session/remote-bundle.js +3 -61
  110. package/dist/lib/session/remote-list.d.ts +1 -148
  111. package/dist/lib/session/remote-list.js +3 -607
  112. package/dist/lib/session/remote.d.ts +1 -132
  113. package/dist/lib/session/remote.js +3 -314
  114. package/dist/lib/session/watch.d.ts +1 -101
  115. package/dist/lib/session/watch.js +3 -242
  116. package/dist/lib/share/config.d.ts +12 -2
  117. package/dist/lib/share/config.js +47 -11
  118. package/dist/lib/share/delete.js +1 -1
  119. package/dist/lib/share/publish.js +1 -1
  120. package/dist/lib/share/worker-template.js +2 -2
  121. package/dist/lib/smart-launch.d.ts +4 -4
  122. package/dist/lib/smart-launch.js +8 -18
  123. package/dist/lib/staleness/index.d.ts +3 -1
  124. package/dist/lib/staleness/index.js +21 -1
  125. package/dist/lib/staleness/types.d.ts +11 -0
  126. package/dist/lib/staleness/writers/commands.js +13 -4
  127. package/dist/lib/staleness/writers/hooks.js +3 -1
  128. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  129. package/dist/lib/staleness/writers/mcp.js +16 -1
  130. package/dist/lib/staleness/writers/rules.js +1 -1
  131. package/dist/lib/staleness/writers/skills.js +3 -1
  132. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  133. package/dist/lib/staleness/writers/subagents.js +18 -1
  134. package/dist/lib/staleness/writers/types.d.ts +10 -0
  135. package/dist/lib/startup/command-registry.d.ts +7 -1
  136. package/dist/lib/startup/command-registry.js +17 -5
  137. package/dist/lib/startup/root-command.d.ts +18 -1
  138. package/dist/lib/startup/root-command.js +18 -1
  139. package/dist/lib/state.js +7 -0
  140. package/dist/lib/teams/placement-probe.js +27 -12
  141. package/dist/lib/teams/scheduler.d.ts +3 -1
  142. package/dist/lib/types.d.ts +1 -1
  143. package/dist/lib/view-types.d.ts +3 -0
  144. package/package.json +2 -3
  145. package/dist/bin/agents +0 -0
  146. package/dist/commands/serve.d.ts +0 -10
  147. package/dist/commands/serve.js +0 -68
  148. package/dist/commands/trends.d.ts +0 -10
  149. package/dist/commands/trends.js +0 -12
  150. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  151. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  152. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  153. package/dist/lib/serve/control.d.ts +0 -95
  154. package/dist/lib/serve/control.js +0 -260
  155. package/dist/lib/serve/data.d.ts +0 -81
  156. package/dist/lib/serve/data.js +0 -91
  157. package/dist/lib/serve/page.d.ts +0 -7
  158. package/dist/lib/serve/page.js +0 -140
  159. package/dist/lib/serve/server.d.ts +0 -80
  160. package/dist/lib/serve/server.js +0 -145
  161. package/dist/lib/serve/stream.d.ts +0 -43
  162. package/dist/lib/serve/stream.js +0 -116
  163. package/dist/lib/serve/token.d.ts +0 -35
  164. package/dist/lib/serve/token.js +0 -85
@@ -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
+ }
@@ -0,0 +1,24 @@
1
+ import { describe } from 'vitest';
2
+ /**
3
+ * Shared fixture for the sessions.*.test.ts suite slices (RUSH-2819).
4
+ *
5
+ * sessions.test.ts was one 2,600-line file measured at 172s — the single
6
+ * slowest file in CI, serializing an entire fork while every other selected
7
+ * file finished. The suite is split into topical slices so vitest's per-file
8
+ * fork parallelism can spread the subprocess-heavy tests across workers; the
9
+ * helpers each slice shares live here.
10
+ */
11
+ export declare const describeLive: typeof describe.skip;
12
+ export declare const repoRoot: string;
13
+ export declare const cliEntry: string;
14
+ export declare const tsxLoaderUrl: string;
15
+ export declare function writeUpdateCache(tempHome: string): void;
16
+ export declare function writeClaudeSession(tempHome: string, projectKey: string, sessionId: string, cwd: string, content: string, timestamp: string): void;
17
+ export declare function writeCodexSession(tempHome: string, sessionId: string, cwd: string, prompt: string, timestamp: string): void;
18
+ export declare function writeGeminiSession(tempHome: string, sessionId: string, cwd: string, prompt: string, timestamp: string, version?: string): void;
19
+ export declare function writeOpenClawSetup(tempHome: string, version?: string): string;
20
+ export declare function runAgents(args: string[], cwd: string, home: string, envOverrides?: Record<string, string>): import("node:child_process").SpawnSyncReturns<string>;
21
+ export declare function outputOf(result: {
22
+ stdout: string;
23
+ stderr: string;
24
+ }): string;
@@ -0,0 +1,224 @@
1
+ import { describe } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as crypto from 'crypto';
5
+ import { spawnSync } from 'child_process';
6
+ import { createRequire } from 'node:module';
7
+ import { pathToFileURL } from 'node:url';
8
+ /**
9
+ * Shared fixture for the sessions.*.test.ts suite slices (RUSH-2819).
10
+ *
11
+ * sessions.test.ts was one 2,600-line file measured at 172s — the single
12
+ * slowest file in CI, serializing an entire fork while every other selected
13
+ * file finished. The suite is split into topical slices so vitest's per-file
14
+ * fork parallelism can spread the subprocess-heavy tests across workers; the
15
+ * helpers each slice shares live here.
16
+ */
17
+ // win32: Claude projects path joins absolute cwd with colons (RUSH-2215).
18
+ export const describeLive = process.platform === 'win32' ? describe.skip : describe;
19
+ export const repoRoot = process.cwd();
20
+ export const cliEntry = path.join(repoRoot, 'src', 'index.ts');
21
+ // Run the CLI as `node --import <tsx loader> src/index.ts`: spawning `node`
22
+ // (always on PATH, no .cmd shell launcher) with the tsx ESM loader resolved to
23
+ // an absolute file URL keeps tsx loadable regardless of the spawn cwd (which we
24
+ // point at the project dir). Avoids both the Windows `tsx.cmd`-needs-a-shell
25
+ // problem and shell:true arg-concatenation (which would split multi-word query
26
+ // args like "prompt text").
27
+ export const tsxLoaderUrl = pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href;
28
+ export function writeUpdateCache(tempHome) {
29
+ const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf-8'));
30
+ fs.mkdirSync(path.join(tempHome, '.agents', '.cache'), { recursive: true });
31
+ fs.writeFileSync(path.join(tempHome, '.agents', '.cache', '.update-check'), JSON.stringify({ lastCheck: Date.now(), latestVersion: packageJson.version }), 'utf-8');
32
+ // ensureInitialized() checks for ~/.agents/.system/.git to confirm setup.
33
+ fs.mkdirSync(path.join(tempHome, '.agents', '.system', '.git'), { recursive: true });
34
+ }
35
+ export function writeClaudeSession(tempHome, projectKey, sessionId, cwd, content, timestamp) {
36
+ fs.mkdirSync(cwd, { recursive: true });
37
+ const sessionsDir = path.join(tempHome, '.claude', 'projects', projectKey);
38
+ fs.mkdirSync(sessionsDir, { recursive: true });
39
+ fs.writeFileSync(path.join(sessionsDir, `${sessionId}.jsonl`), JSON.stringify({
40
+ type: 'user',
41
+ timestamp,
42
+ cwd,
43
+ sessionId,
44
+ version: '2.1.110',
45
+ gitBranch: 'main',
46
+ message: { role: 'user', content },
47
+ }) + '\n', 'utf-8');
48
+ }
49
+ export function writeCodexSession(tempHome, sessionId, cwd, prompt, timestamp) {
50
+ fs.mkdirSync(cwd, { recursive: true });
51
+ const sessionsDir = path.join(tempHome, '.codex', 'sessions', '2026', '04', '17');
52
+ fs.mkdirSync(sessionsDir, { recursive: true });
53
+ const filePath = path.join(sessionsDir, `rollout-${timestamp.replace(/[:.]/g, '-')}-${sessionId}.jsonl`);
54
+ const lines = [
55
+ JSON.stringify({
56
+ timestamp,
57
+ type: 'session_meta',
58
+ payload: {
59
+ id: sessionId,
60
+ timestamp,
61
+ cwd,
62
+ originator: 'codex_cli_rs',
63
+ cli_version: '0.113.0',
64
+ source: 'cli',
65
+ },
66
+ }),
67
+ JSON.stringify({
68
+ timestamp,
69
+ type: 'response_item',
70
+ payload: {
71
+ type: 'message',
72
+ role: 'developer',
73
+ content: [{ type: 'input_text', text: '<permissions instructions>\nFilesystem sandboxing.\n</permissions instructions>' }],
74
+ },
75
+ }),
76
+ JSON.stringify({
77
+ timestamp,
78
+ type: 'response_item',
79
+ payload: {
80
+ type: 'message',
81
+ role: 'user',
82
+ content: [{ type: 'input_text', text: '<environment_context>\n <cwd>/tmp/project</cwd>\n <shell>zsh</shell>\n</environment_context>' }],
83
+ },
84
+ }),
85
+ JSON.stringify({
86
+ timestamp,
87
+ type: 'response_item',
88
+ payload: {
89
+ type: 'message',
90
+ role: 'user',
91
+ content: [{ type: 'input_text', text: '<collaboration_mode># Collaboration Mode: Default\n</collaboration_mode>' }],
92
+ },
93
+ }),
94
+ JSON.stringify({
95
+ timestamp,
96
+ type: 'response_item',
97
+ payload: {
98
+ type: 'message',
99
+ role: 'user',
100
+ content: [{ type: 'input_text', text: '# AGENTS.md instructions for /tmp/project\n\n<INSTRUCTIONS>\nDo work.\n</INSTRUCTIONS>' }],
101
+ },
102
+ }),
103
+ JSON.stringify({
104
+ timestamp,
105
+ type: 'response_item',
106
+ payload: {
107
+ type: 'message',
108
+ role: 'user',
109
+ content: [{ type: 'input_text', text: prompt }],
110
+ },
111
+ }),
112
+ JSON.stringify({
113
+ timestamp,
114
+ type: 'response_item',
115
+ payload: {
116
+ type: 'message',
117
+ role: 'assistant',
118
+ content: [{ type: 'output_text', text: 'Looking into it now.' }],
119
+ },
120
+ }),
121
+ ];
122
+ fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
123
+ }
124
+ export function writeGeminiSession(tempHome, sessionId, cwd, prompt, timestamp, version = '0.29.5') {
125
+ const versionHome = path.join(tempHome, '.agents', '.history', 'versions', 'gemini', version, 'home');
126
+ const geminiHome = path.join(versionHome, '.gemini');
127
+ const projectHash = crypto.createHash('sha256').update(cwd).digest('hex');
128
+ const chatsDir = path.join(geminiHome, 'tmp', projectHash, 'chats');
129
+ fs.mkdirSync(cwd, { recursive: true });
130
+ fs.mkdirSync(chatsDir, { recursive: true });
131
+ fs.mkdirSync(path.dirname(path.join(tempHome, '.gemini')), { recursive: true });
132
+ const activeGeminiHome = path.join(tempHome, '.gemini');
133
+ if (!fs.existsSync(activeGeminiHome)) {
134
+ fs.symlinkSync(geminiHome, activeGeminiHome);
135
+ }
136
+ fs.writeFileSync(path.join(geminiHome, 'projects.json'), JSON.stringify({ projects: [cwd] }), 'utf-8');
137
+ fs.writeFileSync(path.join(chatsDir, `session-${timestamp.replace(/[:.]/g, '-')}-${sessionId.slice(0, 8)}.json`), JSON.stringify({
138
+ sessionId,
139
+ projectHash,
140
+ startTime: timestamp,
141
+ lastUpdated: timestamp,
142
+ messages: [
143
+ {
144
+ id: `${sessionId}-user`,
145
+ timestamp,
146
+ type: 'user',
147
+ content: [{ text: prompt }],
148
+ },
149
+ {
150
+ id: `${sessionId}-assistant`,
151
+ timestamp,
152
+ type: 'gemini',
153
+ content: 'Investigating now.',
154
+ model: 'gemini-3-flash-preview',
155
+ tokens: { total: 1234 },
156
+ },
157
+ ],
158
+ }, null, 2), 'utf-8');
159
+ }
160
+ export function writeOpenClawSetup(tempHome, version = '2026.3.8') {
161
+ const managedHome = path.join(tempHome, '.agents', '.history', 'versions', 'openclaw', version, 'home', '.openclaw');
162
+ fs.mkdirSync(managedHome, { recursive: true });
163
+ const activeHome = path.join(tempHome, '.openclaw');
164
+ fs.mkdirSync(path.dirname(activeHome), { recursive: true });
165
+ if (!fs.existsSync(activeHome)) {
166
+ fs.symlinkSync(managedHome, activeHome);
167
+ }
168
+ const managedWorkspace = path.join(managedHome, 'sergey');
169
+ fs.mkdirSync(managedWorkspace, { recursive: true });
170
+ fs.writeFileSync(path.join(managedHome, 'openclaw.json'), JSON.stringify({
171
+ agents: {
172
+ list: [
173
+ { id: 'sergey', workspace: path.join(activeHome, 'sergey') },
174
+ ],
175
+ },
176
+ }, null, 2), 'utf-8');
177
+ const binDir = path.join(tempHome, 'bin');
178
+ fs.mkdirSync(binDir, { recursive: true });
179
+ const openclawBin = path.join(binDir, 'openclaw');
180
+ fs.writeFileSync(openclawBin, `#!/bin/sh
181
+ if [ "$1" = "--version" ]; then
182
+ echo "openclaw/${version}"
183
+ exit 0
184
+ fi
185
+
186
+ if [ "$1" = "channels" ] && [ "$2" = "status" ]; then
187
+ echo "- Telegram sergey (Sergey): enabled, configured, running, out:2h ago, mode:polling, token:config"
188
+ exit 0
189
+ fi
190
+
191
+ if [ "$1" = "cron" ] && [ "$2" = "list" ]; then
192
+ echo "ID NAME SCHEDULE NEXT LAST STATUS TARGET AGENT MODEL"
193
+ echo "12345678-1234-1234-1234-123456789abc sergey-hourly cron */30 * * * * in 7h 48m ago ok isolated sergey -"
194
+ exit 0
195
+ fi
196
+
197
+ exit 1
198
+ `, 'utf-8');
199
+ fs.chmodSync(openclawBin, 0o755);
200
+ return path.join(activeHome, 'sergey');
201
+ }
202
+ export function runAgents(args, cwd, home, envOverrides = {}) {
203
+ return spawnSync('node', ['--import', tsxLoaderUrl, cliEntry, ...args], {
204
+ cwd,
205
+ env: {
206
+ ...process.env,
207
+ HOME: home,
208
+ // os.homedir() (used via homeDir() in discovery) reads USERPROFILE on
209
+ // Windows and ignores HOME, so set both to redirect the home to tempHome.
210
+ USERPROFILE: home,
211
+ PATH: `${path.join(home, 'bin')}${path.delimiter}${process.env.PATH || ''}`,
212
+ // Some fixtures place files at $HOME/.agents/versions/<agent>/<ver>/ as
213
+ // legacy / synthetic state. The bootstrap-time migration would otherwise
214
+ // move those into ~/.agents-system/, breaking workspace-scoped lookups.
215
+ AGENTS_SKIP_MIGRATION: '1',
216
+ NODE_NO_WARNINGS: '1',
217
+ ...envOverrides,
218
+ },
219
+ encoding: 'utf-8',
220
+ });
221
+ }
222
+ export function outputOf(result) {
223
+ return `${result.stdout}${result.stderr}`;
224
+ }