@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.
- package/CHANGELOG.md +149 -0
- package/README.md +15 -13
- package/dist/bootstrap.js +33 -13
- package/dist/cli/command-registry.d.ts +0 -8
- package/dist/cli/command-registry.js +1 -25
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +33 -8
- package/dist/commands/apply.d.ts +7 -10
- package/dist/commands/apply.js +15 -35
- package/dist/commands/artifacts.js +4 -5
- package/dist/commands/audit.d.ts +6 -6
- package/dist/commands/audit.js +12 -12
- package/dist/commands/auth.js +14 -6
- package/dist/commands/beta.d.ts +7 -1
- package/dist/commands/beta.js +16 -9
- package/dist/commands/commands.js +1 -1
- package/dist/commands/computer.js +20 -6
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/events.d.ts +2 -2
- package/dist/commands/events.js +6 -5
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +11 -6
- package/dist/commands/factory.d.ts +1 -1
- package/dist/commands/feed.js +1 -30
- package/dist/commands/fleet-capture.d.ts +1 -1
- package/dist/commands/fleet-capture.js +4 -5
- package/dist/commands/harness.js +4 -52
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +3 -2
- package/dist/commands/menubar.js +58 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +5 -0
- package/dist/commands/org.js +83 -29
- package/dist/commands/output.js +2 -2
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +5 -4
- package/dist/commands/routines.js +43 -25
- package/dist/commands/routines.test-fixture.d.ts +86 -0
- package/dist/commands/routines.test-fixture.js +345 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/sessions.test-fixture.d.ts +24 -0
- package/dist/commands/sessions.test-fixture.js +224 -0
- package/dist/commands/setup.js +9 -1
- package/dist/commands/share.d.ts +9 -10
- package/dist/commands/share.js +68 -50
- package/dist/commands/skills.js +1 -1
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/ssh.js +15 -62
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/subagents.js +1 -1
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +29 -2
- package/dist/commands/view.js +11 -2
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +7 -7
- package/dist/lib/agent-spec/agents.js +15 -15
- package/dist/lib/analytics/mix-commands.d.ts +6 -28
- package/dist/lib/analytics/mix-commands.js +14 -40
- package/dist/lib/audit/log.d.ts +2 -2
- package/dist/lib/audit/log.js +2 -2
- package/dist/lib/beta.js +1 -1
- package/dist/lib/browser/domain-skills.d.ts +26 -6
- package/dist/lib/browser/domain-skills.js +81 -43
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +97 -58
- package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
- package/dist/lib/daemon/daemon.test-fixture.js +71 -0
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +105 -11
- package/dist/lib/daemon-ticks.d.ts +38 -1
- package/dist/lib/daemon-ticks.js +52 -5
- package/dist/lib/device-config.js +1 -2
- package/dist/lib/devices/discovery-policy.d.ts +3 -3
- package/dist/lib/devices/discovery-policy.js +3 -3
- package/dist/lib/devices/doctor-findings.js +1 -1
- package/dist/lib/devices/fleet.d.ts +4 -8
- package/dist/lib/devices/fleet.js +3 -15
- package/dist/lib/devices/health-report.js +4 -4
- package/dist/lib/devices/pool.d.ts +0 -6
- package/dist/lib/devices/pool.js +0 -6
- package/dist/lib/devices/registry.d.ts +0 -15
- package/dist/lib/devices/registry.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +12 -0
- package/dist/lib/devices/stats-cache.js +24 -5
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/exec.js +13 -3
- package/dist/lib/fleet/capture.d.ts +1 -1
- package/dist/lib/fleet/manifest.js +1 -1
- package/dist/lib/fleet/remote-login.d.ts +2 -2
- package/dist/lib/fleet/remote-login.js +3 -4
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +6 -5
- package/dist/lib/hosts/dispatch.d.ts +2 -0
- package/dist/lib/hosts/dispatch.js +8 -1
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -7
- package/dist/lib/hosts/providers/devices.js +1 -13
- package/dist/lib/hosts/ready.d.ts +12 -3
- package/dist/lib/hosts/ready.js +27 -12
- package/dist/lib/hosts/registry.d.ts +4 -5
- package/dist/lib/hosts/registry.js +3 -7
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/installations/shims.js +1 -1
- package/dist/lib/installations/versions.d.ts +40 -0
- package/dist/lib/installations/versions.js +78 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/menubar/install-menubar.d.ts +109 -0
- package/dist/lib/menubar/install-menubar.js +218 -12
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/prix-account.d.ts +2 -1
- package/dist/lib/prix-account.js +2 -1
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/remote-agents-json.js +1 -7
- package/dist/lib/routines-placement.d.ts +1 -1
- package/dist/lib/routines-placement.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +16 -8
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/active.d.ts +4 -16
- package/dist/lib/session/active.js +2 -6
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.d.ts +13 -98
- package/dist/lib/session/db.js +5 -12
- package/dist/lib/session/discover.d.ts +5 -81
- package/dist/lib/session/discover.js +5 -5
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote/remote-bundle.js +61 -0
- package/dist/lib/session/remote/remote-list.d.ts +148 -0
- package/dist/lib/session/remote/remote-list.js +607 -0
- package/dist/lib/session/remote/remote.d.ts +132 -0
- package/dist/lib/session/remote/remote.js +314 -0
- package/dist/lib/session/remote/watch.d.ts +101 -0
- package/dist/lib/session/remote/watch.js +241 -0
- package/dist/lib/session/remote-bundle.d.ts +1 -12
- package/dist/lib/session/remote-bundle.js +3 -61
- package/dist/lib/session/remote-list.d.ts +1 -148
- package/dist/lib/session/remote-list.js +3 -607
- package/dist/lib/session/remote.d.ts +1 -132
- package/dist/lib/session/remote.js +3 -314
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/watch.d.ts +1 -101
- package/dist/lib/session/watch.js +3 -242
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/share/config.d.ts +12 -2
- package/dist/lib/share/config.js +47 -11
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/publish.js +1 -1
- package/dist/lib/share/worker-template.js +2 -2
- package/dist/lib/smart-launch.d.ts +4 -4
- package/dist/lib/smart-launch.js +8 -18
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/index.d.ts +3 -1
- package/dist/lib/staleness/index.js +21 -1
- package/dist/lib/staleness/types.d.ts +11 -0
- package/dist/lib/staleness/writers/commands.js +13 -4
- package/dist/lib/staleness/writers/hooks.js +3 -1
- package/dist/lib/staleness/writers/mcp.d.ts +0 -7
- package/dist/lib/staleness/writers/mcp.js +16 -1
- package/dist/lib/staleness/writers/rules.js +1 -1
- package/dist/lib/staleness/writers/skills.js +3 -1
- package/dist/lib/staleness/writers/subagents.d.ts +0 -9
- package/dist/lib/staleness/writers/subagents.js +18 -1
- package/dist/lib/staleness/writers/types.d.ts +10 -0
- package/dist/lib/startup/command-registry.d.ts +10 -1
- package/dist/lib/startup/command-registry.js +26 -7
- package/dist/lib/startup/root-command.d.ts +18 -1
- package/dist/lib/startup/root-command.js +18 -1
- package/dist/lib/state.js +7 -0
- package/dist/lib/teams/placement-probe.js +28 -13
- package/dist/lib/teams/scheduler.d.ts +3 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +8 -0
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +2 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +2 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/serve.d.ts +0 -10
- package/dist/commands/serve.js +0 -68
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/commands/trends.d.ts +0 -10
- package/dist/commands/trends.js +0 -12
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/serve/control.d.ts +0 -95
- package/dist/lib/serve/control.js +0 -260
- package/dist/lib/serve/data.d.ts +0 -81
- package/dist/lib/serve/data.js +0 -91
- package/dist/lib/serve/page.d.ts +0 -7
- package/dist/lib/serve/page.js +0 -140
- package/dist/lib/serve/server.d.ts +0 -80
- package/dist/lib/serve/server.js +0 -145
- package/dist/lib/serve/stream.d.ts +0 -43
- package/dist/lib/serve/stream.js +0 -116
- package/dist/lib/serve/token.d.ts +0 -35
- package/dist/lib/serve/token.js +0 -85
- package/dist/lib/tickets/list.d.ts +0 -53
- 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
|
+
}
|