@phnx-labs/agents-cli 1.22.43 → 1.22.45

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 +85 -0
  2. package/README.md +14 -12
  3. package/dist/bootstrap.js +3 -6
  4. package/dist/cli/command-registry.d.ts +0 -7
  5. package/dist/cli/command-registry.js +1 -21
  6. package/dist/commands/accounts.d.ts +1 -9
  7. package/dist/commands/accounts.js +12 -90
  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/beta.d.ts +7 -1
  14. package/dist/commands/beta.js +16 -9
  15. package/dist/commands/commands.js +1 -1
  16. package/dist/commands/doctor.js +1 -4
  17. package/dist/commands/events.d.ts +2 -2
  18. package/dist/commands/events.js +6 -5
  19. package/dist/commands/exec.d.ts +1 -1
  20. package/dist/commands/exec.js +8 -4
  21. package/dist/commands/factory.d.ts +1 -1
  22. package/dist/commands/fleet-capture.d.ts +1 -1
  23. package/dist/commands/fleet-capture.js +4 -5
  24. package/dist/commands/harness.js +1 -49
  25. package/dist/commands/hooks.js +1 -1
  26. package/dist/commands/insights.d.ts +1 -1
  27. package/dist/commands/insights.js +85 -156
  28. package/dist/commands/menubar.js +58 -1
  29. package/dist/commands/output.js +2 -2
  30. package/dist/commands/prune.js +5 -4
  31. package/dist/commands/routines.test-fixture.d.ts +86 -0
  32. package/dist/commands/routines.test-fixture.js +345 -0
  33. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  34. package/dist/commands/sessions.test-fixture.js +224 -0
  35. package/dist/commands/setup.js +5 -1
  36. package/dist/commands/share.d.ts +9 -10
  37. package/dist/commands/share.js +68 -50
  38. package/dist/commands/skills.js +1 -1
  39. package/dist/commands/ssh.js +15 -62
  40. package/dist/commands/subagents.js +1 -1
  41. package/dist/commands/view.js +5 -2
  42. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  43. package/dist/lib/analytics/mix-commands.js +14 -40
  44. package/dist/lib/audit/log.d.ts +2 -2
  45. package/dist/lib/audit/log.js +2 -2
  46. package/dist/lib/beta.js +1 -1
  47. package/dist/lib/browser/domain-skills.d.ts +26 -6
  48. package/dist/lib/browser/domain-skills.js +81 -43
  49. package/dist/lib/daemon/daemon.js +11 -0
  50. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  51. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  52. package/dist/lib/daemon-ticks.d.ts +38 -1
  53. package/dist/lib/daemon-ticks.js +52 -5
  54. package/dist/lib/device-config.js +1 -2
  55. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  56. package/dist/lib/devices/discovery-policy.js +3 -3
  57. package/dist/lib/devices/fleet.d.ts +4 -8
  58. package/dist/lib/devices/fleet.js +3 -15
  59. package/dist/lib/devices/health-report.js +3 -3
  60. package/dist/lib/devices/pool.d.ts +0 -6
  61. package/dist/lib/devices/pool.js +0 -6
  62. package/dist/lib/devices/registry.d.ts +0 -15
  63. package/dist/lib/devices/registry.js +0 -9
  64. package/dist/lib/devices/stats-cache.d.ts +12 -0
  65. package/dist/lib/devices/stats-cache.js +24 -5
  66. package/dist/lib/fleet/capture.d.ts +1 -1
  67. package/dist/lib/fleet/manifest.js +1 -1
  68. package/dist/lib/fleet/remote-login.d.ts +2 -2
  69. package/dist/lib/fleet/remote-login.js +3 -4
  70. package/dist/lib/hosts/dispatch.d.ts +2 -0
  71. package/dist/lib/hosts/dispatch.js +8 -1
  72. package/dist/lib/hosts/passthrough.js +0 -1
  73. package/dist/lib/hosts/providers/devices.js +1 -13
  74. package/dist/lib/hosts/ready.d.ts +12 -3
  75. package/dist/lib/hosts/ready.js +27 -12
  76. package/dist/lib/hosts/registry.d.ts +4 -5
  77. package/dist/lib/hosts/registry.js +3 -7
  78. package/dist/lib/installations/versions.js +21 -2
  79. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  80. package/dist/lib/menubar/install-menubar.js +199 -4
  81. package/dist/lib/remote-agents-json.js +1 -7
  82. package/dist/lib/routines-placement.d.ts +1 -1
  83. package/dist/lib/routines-placement.js +1 -1
  84. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  85. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  86. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  87. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  88. package/dist/lib/session/active.d.ts +4 -16
  89. package/dist/lib/session/active.js +2 -6
  90. package/dist/lib/session/db.d.ts +13 -98
  91. package/dist/lib/session/db.js +4 -11
  92. package/dist/lib/session/discover.d.ts +5 -81
  93. package/dist/lib/session/discover.js +5 -5
  94. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  95. package/dist/lib/session/remote/remote-bundle.js +61 -0
  96. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  97. package/dist/lib/session/remote/remote-list.js +607 -0
  98. package/dist/lib/session/remote/remote.d.ts +132 -0
  99. package/dist/lib/session/remote/remote.js +314 -0
  100. package/dist/lib/session/remote/watch.d.ts +101 -0
  101. package/dist/lib/session/remote/watch.js +241 -0
  102. package/dist/lib/session/remote-bundle.d.ts +1 -12
  103. package/dist/lib/session/remote-bundle.js +3 -61
  104. package/dist/lib/session/remote-list.d.ts +1 -148
  105. package/dist/lib/session/remote-list.js +3 -607
  106. package/dist/lib/session/remote.d.ts +1 -132
  107. package/dist/lib/session/remote.js +3 -314
  108. package/dist/lib/session/watch.d.ts +1 -101
  109. package/dist/lib/session/watch.js +3 -242
  110. package/dist/lib/share/config.d.ts +12 -2
  111. package/dist/lib/share/config.js +47 -11
  112. package/dist/lib/share/delete.js +1 -1
  113. package/dist/lib/share/publish.js +1 -1
  114. package/dist/lib/share/worker-template.js +2 -2
  115. package/dist/lib/smart-launch.d.ts +4 -4
  116. package/dist/lib/smart-launch.js +8 -18
  117. package/dist/lib/staleness/index.d.ts +3 -1
  118. package/dist/lib/staleness/index.js +21 -1
  119. package/dist/lib/staleness/types.d.ts +11 -0
  120. package/dist/lib/staleness/writers/commands.js +13 -4
  121. package/dist/lib/staleness/writers/hooks.js +3 -1
  122. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  123. package/dist/lib/staleness/writers/mcp.js +16 -1
  124. package/dist/lib/staleness/writers/rules.js +1 -1
  125. package/dist/lib/staleness/writers/skills.js +3 -1
  126. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  127. package/dist/lib/staleness/writers/subagents.js +18 -1
  128. package/dist/lib/staleness/writers/types.d.ts +10 -0
  129. package/dist/lib/startup/command-registry.d.ts +8 -1
  130. package/dist/lib/startup/command-registry.js +21 -6
  131. package/dist/lib/startup/root-command.d.ts +18 -1
  132. package/dist/lib/startup/root-command.js +18 -1
  133. package/dist/lib/state.js +7 -0
  134. package/dist/lib/teams/placement-probe.js +27 -12
  135. package/dist/lib/teams/scheduler.d.ts +3 -1
  136. package/dist/lib/types.d.ts +1 -1
  137. package/dist/lib/view-types.d.ts +3 -0
  138. package/package.json +2 -3
  139. package/dist/bin/agents +0 -0
  140. package/dist/commands/auth.d.ts +0 -9
  141. package/dist/commands/auth.js +0 -108
  142. package/dist/commands/org.d.ts +0 -6
  143. package/dist/commands/org.js +0 -175
  144. package/dist/commands/serve.d.ts +0 -10
  145. package/dist/commands/serve.js +0 -68
  146. package/dist/commands/trends.d.ts +0 -10
  147. package/dist/commands/trends.js +0 -12
  148. package/dist/lib/entitlement.d.ts +0 -31
  149. package/dist/lib/entitlement.js +0 -137
  150. package/dist/lib/prix-account.d.ts +0 -158
  151. package/dist/lib/prix-account.js +0 -214
  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,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
+ }
@@ -26,6 +26,7 @@ import { registerSetupSecretsCommand } from './setup-secrets.js';
26
26
  import { registerSetupFleetCommand } from './setup-fleet.js';
27
27
  import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
28
28
  import { registerAliasCommand } from './alias.js';
29
+ import { registerBetaCommands } from './beta.js';
29
30
  import { runPreferencesStep } from './setup-preferences.js';
30
31
  import { getConfiguredDefaultProfileName, getProfile, getAutoDetectedProfile, isProfileLaunchableHere } from '../lib/browser/profiles.js';
31
32
  import { listInstalledBrowsers } from '../lib/browser/chrome.js';
@@ -363,7 +364,7 @@ export function registerSetupCommand(program) {
363
364
  .description('Set up agents-cli, or re-open the capability onboarding hub.')
364
365
  .option('-f, --force', 'Re-run setup even if ~/.agents/.system/ already exists (use with caution)')
365
366
  .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents/.system/ yourself)');
366
- // Capability subcommands: `agents setup browser|computer|mine|secrets|fleet|alias`.
367
+ // Capability subcommands: `agents setup browser|computer|mine|secrets|fleet|alias|beta`.
367
368
  // Share/artifact publishing is set up by `agents artifacts setup` (RUSH-2580);
368
369
  // the hub below still offers it as a phase via runShareWizard.
369
370
  registerSetupBrowserCommand(setupCmd);
@@ -373,6 +374,7 @@ export function registerSetupCommand(program) {
373
374
  registerSetupFleetCommand(setupCmd);
374
375
  registerSetupWatchdogCommand(setupCmd);
375
376
  registerAliasCommand(setupCmd);
377
+ registerBetaCommands(setupCmd);
376
378
  setupCmd.command('status')
377
379
  .description('Show setup readiness for core, browser, computer, secrets, fleet, share, watchdog, and preferences.')
378
380
  .option('--json', 'print machine-readable JSON')
@@ -400,6 +402,7 @@ export function registerSetupCommand(program) {
400
402
  agents setup fleet
401
403
  agents setup watchdog
402
404
  agents setup alias add teams
405
+ agents setup beta enable factory
403
406
  `,
404
407
  notes: `
405
408
  What it does:
@@ -416,6 +419,7 @@ export function registerSetupCommand(program) {
416
419
  agents setup fleet # discover Tailscale devices + configure SSH access
417
420
  agents setup watchdog # choose which devices run the daemon watchdog pass
418
421
  agents setup alias add teams # PATH shorthand: teams runs agents teams
422
+ agents setup beta enable factory # opt into preview features
419
423
 
420
424
  To install CLIs from agents.yaml and sync resources into version homes:
421
425
  agents sync --local -y