@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime placement resolution for routines: local / host / fleet / cloud.
|
|
3
|
+
*
|
|
4
|
+
* The scheduler only decides *whether* this machine may fire the job
|
|
5
|
+
* (`devices` + `jobRunsOnThisDevice`). This module decides *where the job
|
|
6
|
+
* body runs* once it has been fired, using `hostStrategy`.
|
|
7
|
+
*
|
|
8
|
+
* Fleet semantics (RUSH-2035 / RUSH-1980): pick exactly one online device per
|
|
9
|
+
* fire. Cross-device double-fire is prevented by requiring a `devices` firing
|
|
10
|
+
* pin for fleet/host/cloud strategies (applied at add/sync time).
|
|
11
|
+
*/
|
|
12
|
+
import { resolveHostStrategy } from './routines.js';
|
|
13
|
+
import { machineId, normalizeHost } from './machine-id.js';
|
|
14
|
+
import { loadDevicesSync } from './devices/registry.js';
|
|
15
|
+
import { planFleetTargets } from './devices/fleet.js';
|
|
16
|
+
/**
|
|
17
|
+
* Pick one online fleet device for `hostStrategy: fleet`.
|
|
18
|
+
*
|
|
19
|
+
* Preference order:
|
|
20
|
+
* 1. This machine, if online and eligible (avoids needless SSH)
|
|
21
|
+
* 2. First eligible online device by name (stable / deterministic)
|
|
22
|
+
*
|
|
23
|
+
* `config.devices` is the *firing* allowlist only (which daemon may fire the
|
|
24
|
+
* job). It is intentionally NOT used as an execution pool filter — otherwise
|
|
25
|
+
* the double-fire pin (`devices: [self]`) would collapse fleet placement to
|
|
26
|
+
* always-local. Control / offline / no-address devices are never chosen.
|
|
27
|
+
*/
|
|
28
|
+
export function pickFleetDevice(_config) {
|
|
29
|
+
let reg;
|
|
30
|
+
try {
|
|
31
|
+
reg = loadDevicesSync();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const planned = planFleetTargets(reg);
|
|
37
|
+
const candidates = planned.filter((t) => !t.skip).map((t) => t.device.name);
|
|
38
|
+
if (candidates.length === 0) {
|
|
39
|
+
// No registry / nothing online: fall back to self so a single-box fleet
|
|
40
|
+
// without a registry entry still runs locally.
|
|
41
|
+
return machineId();
|
|
42
|
+
}
|
|
43
|
+
const self = machineId();
|
|
44
|
+
const selfMatch = candidates.find((n) => normalizeHost(n) === self);
|
|
45
|
+
if (selfMatch)
|
|
46
|
+
return selfMatch;
|
|
47
|
+
return [...candidates].sort((a, b) => a.localeCompare(b))[0] ?? null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve where a fired job's body should execute.
|
|
51
|
+
* Throws a human-readable Error when placement cannot be satisfied.
|
|
52
|
+
*/
|
|
53
|
+
export function resolvePlacementTarget(config) {
|
|
54
|
+
const strategy = resolveHostStrategy(config);
|
|
55
|
+
switch (strategy) {
|
|
56
|
+
case 'local':
|
|
57
|
+
return { mode: 'local' };
|
|
58
|
+
case 'host': {
|
|
59
|
+
if (!config.host || config.host.trim() === '') {
|
|
60
|
+
throw new Error(`Routine '${config.name}' has hostStrategy: host but no host: — set host: or --run-on`);
|
|
61
|
+
}
|
|
62
|
+
// host: pointing at this machine is local execution (no SSH loop).
|
|
63
|
+
if (normalizeHost(config.host) === machineId())
|
|
64
|
+
return { mode: 'local' };
|
|
65
|
+
return { mode: 'host', host: config.host };
|
|
66
|
+
}
|
|
67
|
+
case 'fleet': {
|
|
68
|
+
const picked = pickFleetDevice(config);
|
|
69
|
+
if (!picked) {
|
|
70
|
+
throw new Error(`Routine '${config.name}' hostStrategy: fleet — no eligible online device to place the run`);
|
|
71
|
+
}
|
|
72
|
+
if (normalizeHost(picked) === machineId())
|
|
73
|
+
return { mode: 'local' };
|
|
74
|
+
return { mode: 'host', host: picked };
|
|
75
|
+
}
|
|
76
|
+
case 'cloud':
|
|
77
|
+
return { mode: 'cloud' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-level routine opt-in, source tracking, and user-layer sync.
|
|
3
|
+
*
|
|
4
|
+
* Project YAML under `<project>/.agents/routines/*.yml` is inspection-only by
|
|
5
|
+
* default (a cloned public repo must never auto-fire agent prompts). After an
|
|
6
|
+
* explicit opt-in, routines are materialised into `~/.agents/routines/` with
|
|
7
|
+
* `source:` provenance so the daemon — which only loads user + system layers —
|
|
8
|
+
* can fire them. `syncProjectRoutines` refreshes the user-layer copies when
|
|
9
|
+
* project YAML changes (also invoked on daemon SIGHUP).
|
|
10
|
+
*/
|
|
11
|
+
import { type JobSource } from './routines.js';
|
|
12
|
+
/** Expand `~/…` and resolve to an absolute path. */
|
|
13
|
+
export declare function expandProjectPath(p: string): string;
|
|
14
|
+
/** Store paths home-relative when under $HOME so they travel across machines. */
|
|
15
|
+
export declare function displayProjectPath(abs: string): string;
|
|
16
|
+
/** Project roots currently opted into daemon firing (absolute paths). */
|
|
17
|
+
export declare function listEnabledProjectRoots(): string[];
|
|
18
|
+
/** True when this project root is on the opt-in allowlist. */
|
|
19
|
+
export declare function isProjectRoutinesEnabled(projectRoot: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True when the project's own `agents.yaml` opts into project routines:
|
|
22
|
+
* `routines: { enable: true }`. This is an additional source of opt-in that
|
|
23
|
+
* still requires the project to be present on disk; it does not auto-enable
|
|
24
|
+
* every clone — the project must declare it, and `sync` / `enable-project`
|
|
25
|
+
* still materialises consent into the user layer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function projectAgentsYamlEnablesRoutines(projectRoot: string): boolean;
|
|
28
|
+
/** Resolve the project root (parent of `.agents/`) from a cwd, or null. */
|
|
29
|
+
export declare function resolveProjectRoot(cwd?: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Opt a project root into daemon firing. Returns true when newly added,
|
|
32
|
+
* false when it was already enabled.
|
|
33
|
+
*/
|
|
34
|
+
export declare function enableProjectRoutines(projectRoot: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Remove a project root from the opt-in allowlist. Optionally deletes the
|
|
37
|
+
* user-layer copies that were materialised from it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function disableProjectRoutines(projectRoot: string, opts?: {
|
|
40
|
+
removeSynced?: boolean;
|
|
41
|
+
}): {
|
|
42
|
+
removed: boolean;
|
|
43
|
+
deletedJobs: string[];
|
|
44
|
+
};
|
|
45
|
+
/** Git provenance for a project root (best-effort; never throws). */
|
|
46
|
+
export declare function readProjectGitSource(projectRoot: string): Pick<JobSource, 'repo' | 'branch' | 'commit'>;
|
|
47
|
+
/** List project routine YAML files (name + absolute path). */
|
|
48
|
+
export declare function listProjectRoutineFiles(projectRoot: string): Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
path: string;
|
|
51
|
+
}>;
|
|
52
|
+
export interface SyncProjectResult {
|
|
53
|
+
projectRoot: string;
|
|
54
|
+
synced: string[];
|
|
55
|
+
skipped: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
reason: string;
|
|
58
|
+
}>;
|
|
59
|
+
removed: string[];
|
|
60
|
+
errors: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
error: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Materialise one project's routines into the user layer.
|
|
67
|
+
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
68
|
+
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
69
|
+
* - Removes user copies from this project whose YAML disappeared
|
|
70
|
+
* - Auto-pins `devices` for host/fleet/cloud placement when unset
|
|
71
|
+
*/
|
|
72
|
+
export declare function syncProjectRoutines(projectRoot: string): SyncProjectResult;
|
|
73
|
+
export interface SyncAllResult {
|
|
74
|
+
projects: SyncProjectResult[];
|
|
75
|
+
/** Project roots that were listed but missing on disk. */
|
|
76
|
+
missing: string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sync every project root on the user allowlist (`meta.routines.projects`),
|
|
80
|
+
* plus any explicit `extraRoots`. Project `agents.yaml` `routines.enable`
|
|
81
|
+
* alone never opts a path in — enable-project is required.
|
|
82
|
+
*/
|
|
83
|
+
export declare function syncAllProjectRoutines(opts?: {
|
|
84
|
+
extraRoots?: string[];
|
|
85
|
+
}): SyncAllResult;
|
|
86
|
+
/**
|
|
87
|
+
* Discover project routines at cwd for the setup UX. Returns null when no
|
|
88
|
+
* project `.agents/routines` dir exists.
|
|
89
|
+
*/
|
|
90
|
+
export declare function discoverProjectRoutinesAt(cwd?: string): {
|
|
91
|
+
projectRoot: string;
|
|
92
|
+
files: Array<{
|
|
93
|
+
name: string;
|
|
94
|
+
path: string;
|
|
95
|
+
}>;
|
|
96
|
+
enabled: boolean;
|
|
97
|
+
} | null;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-level routine opt-in, source tracking, and user-layer sync.
|
|
3
|
+
*
|
|
4
|
+
* Project YAML under `<project>/.agents/routines/*.yml` is inspection-only by
|
|
5
|
+
* default (a cloned public repo must never auto-fire agent prompts). After an
|
|
6
|
+
* explicit opt-in, routines are materialised into `~/.agents/routines/` with
|
|
7
|
+
* `source:` provenance so the daemon — which only loads user + system layers —
|
|
8
|
+
* can fire them. `syncProjectRoutines` refreshes the user-layer copies when
|
|
9
|
+
* project YAML changes (also invoked on daemon SIGHUP).
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as yaml from 'yaml';
|
|
15
|
+
import { execFileSync } from 'child_process';
|
|
16
|
+
import { getProjectAgentsDir, getProjectRoutinesDir, readMeta, updateMeta, ensureAgentsDir, } from './state.js';
|
|
17
|
+
import { readJob, writeJob, deleteJob, listJobs, validateJob, resolveHostStrategy, placementRequiresFiringPin, } from './routines.js';
|
|
18
|
+
import { parseOwnerRepoFromRemote } from './registry.js';
|
|
19
|
+
import { machineId } from './machine-id.js';
|
|
20
|
+
import { isSafeSegmentName } from './paths.js';
|
|
21
|
+
/** Expand `~/…` and resolve to an absolute path. */
|
|
22
|
+
export function expandProjectPath(p) {
|
|
23
|
+
const trimmed = p.trim();
|
|
24
|
+
if (trimmed.startsWith('~/') || trimmed === '~') {
|
|
25
|
+
return path.resolve(trimmed.replace(/^~(?=$|[/\\])/, os.homedir()));
|
|
26
|
+
}
|
|
27
|
+
return path.resolve(trimmed);
|
|
28
|
+
}
|
|
29
|
+
/** Store paths home-relative when under $HOME so they travel across machines. */
|
|
30
|
+
export function displayProjectPath(abs) {
|
|
31
|
+
const home = os.homedir();
|
|
32
|
+
const resolved = path.resolve(abs);
|
|
33
|
+
if (resolved === home)
|
|
34
|
+
return '~';
|
|
35
|
+
if (resolved.startsWith(home + path.sep)) {
|
|
36
|
+
return '~' + resolved.slice(home.length);
|
|
37
|
+
}
|
|
38
|
+
return resolved;
|
|
39
|
+
}
|
|
40
|
+
/** Project roots currently opted into daemon firing (absolute paths). */
|
|
41
|
+
export function listEnabledProjectRoots() {
|
|
42
|
+
const meta = readMeta();
|
|
43
|
+
const raw = meta.routines?.projects ?? [];
|
|
44
|
+
const out = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
for (const p of raw) {
|
|
47
|
+
if (typeof p !== 'string' || p.trim() === '')
|
|
48
|
+
continue;
|
|
49
|
+
const abs = expandProjectPath(p);
|
|
50
|
+
if (seen.has(abs))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(abs);
|
|
53
|
+
out.push(abs);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** True when this project root is on the opt-in allowlist. */
|
|
58
|
+
export function isProjectRoutinesEnabled(projectRoot) {
|
|
59
|
+
const abs = expandProjectPath(projectRoot);
|
|
60
|
+
return listEnabledProjectRoots().some((p) => p === abs);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when the project's own `agents.yaml` opts into project routines:
|
|
64
|
+
* `routines: { enable: true }`. This is an additional source of opt-in that
|
|
65
|
+
* still requires the project to be present on disk; it does not auto-enable
|
|
66
|
+
* every clone — the project must declare it, and `sync` / `enable-project`
|
|
67
|
+
* still materialises consent into the user layer.
|
|
68
|
+
*/
|
|
69
|
+
export function projectAgentsYamlEnablesRoutines(projectRoot) {
|
|
70
|
+
const agentsYaml = path.join(expandProjectPath(projectRoot), 'agents.yaml');
|
|
71
|
+
if (!fs.existsSync(agentsYaml))
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = yaml.parse(fs.readFileSync(agentsYaml, 'utf-8'));
|
|
75
|
+
if (!parsed || typeof parsed !== 'object')
|
|
76
|
+
return false;
|
|
77
|
+
const routines = parsed.routines;
|
|
78
|
+
if (!routines || typeof routines !== 'object')
|
|
79
|
+
return false;
|
|
80
|
+
return routines.enable === true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Resolve the project root (parent of `.agents/`) from a cwd, or null. */
|
|
87
|
+
export function resolveProjectRoot(cwd = process.cwd()) {
|
|
88
|
+
const agentsDir = getProjectAgentsDir(cwd);
|
|
89
|
+
if (!agentsDir)
|
|
90
|
+
return null;
|
|
91
|
+
return path.dirname(agentsDir);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Opt a project root into daemon firing. Returns true when newly added,
|
|
95
|
+
* false when it was already enabled.
|
|
96
|
+
*/
|
|
97
|
+
export function enableProjectRoutines(projectRoot) {
|
|
98
|
+
const abs = expandProjectPath(projectRoot);
|
|
99
|
+
const stored = displayProjectPath(abs);
|
|
100
|
+
let added = false;
|
|
101
|
+
updateMeta((meta) => {
|
|
102
|
+
const projects = [...(meta.routines?.projects ?? [])];
|
|
103
|
+
const already = projects.some((p) => expandProjectPath(p) === abs);
|
|
104
|
+
if (already)
|
|
105
|
+
return meta;
|
|
106
|
+
projects.push(stored);
|
|
107
|
+
added = true;
|
|
108
|
+
return {
|
|
109
|
+
...meta,
|
|
110
|
+
routines: {
|
|
111
|
+
...(meta.routines ?? {}),
|
|
112
|
+
projects,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
return added;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Remove a project root from the opt-in allowlist. Optionally deletes the
|
|
120
|
+
* user-layer copies that were materialised from it.
|
|
121
|
+
*/
|
|
122
|
+
export function disableProjectRoutines(projectRoot, opts = {}) {
|
|
123
|
+
const abs = expandProjectPath(projectRoot);
|
|
124
|
+
let removed = false;
|
|
125
|
+
updateMeta((meta) => {
|
|
126
|
+
const projects = meta.routines?.projects ?? [];
|
|
127
|
+
const next = projects.filter((p) => expandProjectPath(p) !== abs);
|
|
128
|
+
if (next.length === projects.length)
|
|
129
|
+
return meta;
|
|
130
|
+
removed = true;
|
|
131
|
+
return {
|
|
132
|
+
...meta,
|
|
133
|
+
routines: {
|
|
134
|
+
...(meta.routines ?? {}),
|
|
135
|
+
projects: next,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const deletedJobs = [];
|
|
140
|
+
if (opts.removeSynced) {
|
|
141
|
+
for (const job of listJobs()) {
|
|
142
|
+
if (job.source?.kind === 'project' && expandProjectPath(job.source.projectPath) === abs) {
|
|
143
|
+
if (deleteJob(job.name))
|
|
144
|
+
deletedJobs.push(job.name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { removed, deletedJobs };
|
|
149
|
+
}
|
|
150
|
+
/** Git provenance for a project root (best-effort; never throws). */
|
|
151
|
+
export function readProjectGitSource(projectRoot) {
|
|
152
|
+
const abs = expandProjectPath(projectRoot);
|
|
153
|
+
const out = {};
|
|
154
|
+
try {
|
|
155
|
+
const remote = execFileSync('git', ['-C', abs, 'remote', 'get-url', 'origin'], {
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
158
|
+
timeout: 5_000,
|
|
159
|
+
}).trim();
|
|
160
|
+
const repo = parseOwnerRepoFromRemote(remote);
|
|
161
|
+
if (repo)
|
|
162
|
+
out.repo = repo;
|
|
163
|
+
}
|
|
164
|
+
catch { /* no origin */ }
|
|
165
|
+
try {
|
|
166
|
+
out.branch = execFileSync('git', ['-C', abs, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
167
|
+
encoding: 'utf-8',
|
|
168
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
169
|
+
timeout: 5_000,
|
|
170
|
+
}).trim();
|
|
171
|
+
if (out.branch === 'HEAD')
|
|
172
|
+
delete out.branch; // detached
|
|
173
|
+
}
|
|
174
|
+
catch { /* not a git repo */ }
|
|
175
|
+
try {
|
|
176
|
+
out.commit = execFileSync('git', ['-C', abs, 'rev-parse', '--short', 'HEAD'], {
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
179
|
+
timeout: 5_000,
|
|
180
|
+
}).trim();
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore */ }
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** List project routine YAML files (name + absolute path). */
|
|
186
|
+
export function listProjectRoutineFiles(projectRoot) {
|
|
187
|
+
const routinesDir = getProjectRoutinesDir(expandProjectPath(projectRoot));
|
|
188
|
+
if (!routinesDir || !fs.existsSync(routinesDir))
|
|
189
|
+
return [];
|
|
190
|
+
return fs.readdirSync(routinesDir)
|
|
191
|
+
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
192
|
+
.map((f) => ({
|
|
193
|
+
name: f.replace(/\.ya?ml$/, ''),
|
|
194
|
+
path: path.join(routinesDir, f),
|
|
195
|
+
}))
|
|
196
|
+
.filter((e) => isSafeSegmentName(e.name));
|
|
197
|
+
}
|
|
198
|
+
function readProjectJobFile(filePath) {
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
201
|
+
const parsed = yaml.parse(content);
|
|
202
|
+
if (!parsed || typeof parsed !== 'object')
|
|
203
|
+
return null;
|
|
204
|
+
if (Object.prototype.hasOwnProperty.call(parsed, 'device'))
|
|
205
|
+
return null;
|
|
206
|
+
return {
|
|
207
|
+
mode: 'auto',
|
|
208
|
+
effort: 'auto',
|
|
209
|
+
timeout: '10m',
|
|
210
|
+
enabled: true,
|
|
211
|
+
...parsed,
|
|
212
|
+
name: parsed.name || path.basename(filePath).replace(/\.ya?ml$/, ''),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Materialise one project's routines into the user layer.
|
|
221
|
+
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
222
|
+
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
223
|
+
* - Removes user copies from this project whose YAML disappeared
|
|
224
|
+
* - Auto-pins `devices` for host/fleet/cloud placement when unset
|
|
225
|
+
*/
|
|
226
|
+
export function syncProjectRoutines(projectRoot) {
|
|
227
|
+
ensureAgentsDir();
|
|
228
|
+
const abs = expandProjectPath(projectRoot);
|
|
229
|
+
const git = readProjectGitSource(abs);
|
|
230
|
+
const source = {
|
|
231
|
+
kind: 'project',
|
|
232
|
+
projectPath: abs,
|
|
233
|
+
...(git.repo ? { repo: git.repo } : {}),
|
|
234
|
+
...(git.branch ? { branch: git.branch } : {}),
|
|
235
|
+
...(git.commit ? { commit: git.commit } : {}),
|
|
236
|
+
};
|
|
237
|
+
const result = {
|
|
238
|
+
projectRoot: abs,
|
|
239
|
+
synced: [],
|
|
240
|
+
skipped: [],
|
|
241
|
+
removed: [],
|
|
242
|
+
errors: [],
|
|
243
|
+
};
|
|
244
|
+
const files = listProjectRoutineFiles(abs);
|
|
245
|
+
const seenNames = new Set();
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
seenNames.add(file.name);
|
|
248
|
+
const job = readProjectJobFile(file.path);
|
|
249
|
+
if (!job) {
|
|
250
|
+
result.errors.push({ name: file.name, error: 'unreadable or invalid YAML' });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const existing = readJob(file.name);
|
|
254
|
+
if (existing) {
|
|
255
|
+
const existingSource = existing.source;
|
|
256
|
+
const fromThisProject = existingSource?.kind === 'project'
|
|
257
|
+
&& expandProjectPath(existingSource.projectPath) === abs;
|
|
258
|
+
if (!fromThisProject) {
|
|
259
|
+
result.skipped.push({
|
|
260
|
+
name: file.name,
|
|
261
|
+
reason: existingSource
|
|
262
|
+
? `user-layer routine already exists from another source (${existingSource.projectPath})`
|
|
263
|
+
: 'user-layer routine already exists (hand-authored); not overwriting without source match',
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// Preserve user-layer devices pin when project YAML omits it (same overlay
|
|
268
|
+
// semantics as listJobs project discovery).
|
|
269
|
+
if (job.devices === undefined && existing.devices && existing.devices.length > 0) {
|
|
270
|
+
job.devices = existing.devices;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Placement that leaves the firing machine must pin devices to avoid
|
|
274
|
+
// every fleet daemon dispatching once.
|
|
275
|
+
const strategy = resolveHostStrategy(job);
|
|
276
|
+
if (placementRequiresFiringPin(strategy) && (!job.devices || job.devices.length === 0)) {
|
|
277
|
+
job.devices = [machineId()];
|
|
278
|
+
}
|
|
279
|
+
// Surface repo for list/display when project has a GitHub origin.
|
|
280
|
+
if (git.repo && !job.repo)
|
|
281
|
+
job.repo = git.repo;
|
|
282
|
+
job.source = source;
|
|
283
|
+
job.name = file.name;
|
|
284
|
+
const errors = validateJob(job);
|
|
285
|
+
if (errors.length > 0) {
|
|
286
|
+
result.errors.push({ name: file.name, error: errors.join('; ') });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
writeJob(job);
|
|
291
|
+
result.synced.push(file.name);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
result.errors.push({ name: file.name, error: err.message });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Drop user-layer copies from this project that no longer exist in project YAML.
|
|
298
|
+
for (const job of listJobs()) {
|
|
299
|
+
if (job.source?.kind !== 'project')
|
|
300
|
+
continue;
|
|
301
|
+
if (expandProjectPath(job.source.projectPath) !== abs)
|
|
302
|
+
continue;
|
|
303
|
+
if (seenNames.has(job.name))
|
|
304
|
+
continue;
|
|
305
|
+
if (deleteJob(job.name))
|
|
306
|
+
result.removed.push(job.name);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Sync every project root on the user allowlist (`meta.routines.projects`),
|
|
312
|
+
* plus any explicit `extraRoots`. Project `agents.yaml` `routines.enable`
|
|
313
|
+
* alone never opts a path in — enable-project is required.
|
|
314
|
+
*/
|
|
315
|
+
export function syncAllProjectRoutines(opts = {}) {
|
|
316
|
+
const roots = new Set(listEnabledProjectRoots());
|
|
317
|
+
for (const r of opts.extraRoots ?? [])
|
|
318
|
+
roots.add(expandProjectPath(r));
|
|
319
|
+
const projects = [];
|
|
320
|
+
const missing = [];
|
|
321
|
+
for (const root of roots) {
|
|
322
|
+
if (!fs.existsSync(root)) {
|
|
323
|
+
missing.push(root);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
projects.push(syncProjectRoutines(root));
|
|
327
|
+
}
|
|
328
|
+
return { projects, missing };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Discover project routines at cwd for the setup UX. Returns null when no
|
|
332
|
+
* project `.agents/routines` dir exists.
|
|
333
|
+
*/
|
|
334
|
+
export function discoverProjectRoutinesAt(cwd = process.cwd()) {
|
|
335
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
336
|
+
if (!projectRoot)
|
|
337
|
+
return null;
|
|
338
|
+
const routinesDir = getProjectRoutinesDir(projectRoot);
|
|
339
|
+
if (!routinesDir || !fs.existsSync(routinesDir))
|
|
340
|
+
return null;
|
|
341
|
+
const files = listProjectRoutineFiles(projectRoot);
|
|
342
|
+
if (files.length === 0)
|
|
343
|
+
return null;
|
|
344
|
+
return {
|
|
345
|
+
projectRoot,
|
|
346
|
+
files,
|
|
347
|
+
enabled: isProjectRoutinesEnabled(projectRoot) || projectAgentsYamlEnablesRoutines(projectRoot),
|
|
348
|
+
};
|
|
349
|
+
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -14,6 +14,29 @@ export interface JobAllowConfig {
|
|
|
14
14
|
sites?: string[];
|
|
15
15
|
dirs?: string[];
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Where a routine's job body executes when the daemon fires it.
|
|
19
|
+
* Distinct from `devices` (which daemon may *fire*) and from the CLI `--host`
|
|
20
|
+
* remote-management passthrough (manage routines *on* another machine).
|
|
21
|
+
*/
|
|
22
|
+
export type HostStrategy = 'local' | 'host' | 'fleet' | 'cloud';
|
|
23
|
+
export declare const HOST_STRATEGIES: readonly HostStrategy[];
|
|
24
|
+
/**
|
|
25
|
+
* Provenance for a routine that was materialised from a project
|
|
26
|
+
* (`.agents/routines/*.yml` synced into the user layer after opt-in).
|
|
27
|
+
*/
|
|
28
|
+
export interface JobSource {
|
|
29
|
+
/** Always `project` today; reserved for future layers. */
|
|
30
|
+
kind: 'project';
|
|
31
|
+
/** Absolute path to the project root that owns the YAML. */
|
|
32
|
+
projectPath: string;
|
|
33
|
+
/** GitHub `owner/repo` when the project has a GitHub origin remote. */
|
|
34
|
+
repo?: string;
|
|
35
|
+
/** Branch at last sync (when known). */
|
|
36
|
+
branch?: string;
|
|
37
|
+
/** Short commit SHA at last sync (when known). */
|
|
38
|
+
commit?: string;
|
|
39
|
+
}
|
|
17
40
|
/** GitHub webhook events a routine can be triggered by. */
|
|
18
41
|
export type GithubTriggerEvent = 'pull_request' | 'push' | 'issue_comment' | 'workflow_run';
|
|
19
42
|
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
@@ -102,15 +125,57 @@ export interface JobConfig {
|
|
|
102
125
|
* `host` says where the dispatched run EXECUTES. CLI flag: `--run-on`
|
|
103
126
|
* (`--host` on routines commands already means "manage routines on that
|
|
104
127
|
* machine" via the remote passthrough).
|
|
128
|
+
*
|
|
129
|
+
* When `hostStrategy` is set, it owns placement semantics; `host` is then
|
|
130
|
+
* only required for `hostStrategy: host` (or when strategy is inferred from
|
|
131
|
+
* a bare `host:` field for back-compat).
|
|
105
132
|
*/
|
|
106
133
|
host?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Where the job body should run when the daemon fires it.
|
|
136
|
+
* - `local` — on the firing machine (default / current behavior)
|
|
137
|
+
* - `host` — on the named `host` over SSH (maps to `--run-on`)
|
|
138
|
+
* - `fleet` — pick one online registered device per run (no cross-device
|
|
139
|
+
* double-fire; the firing pin stays on `devices`)
|
|
140
|
+
* - `cloud` — dispatch via the agent's native cloud provider
|
|
141
|
+
*
|
|
142
|
+
* CLI flag: `--placement` (not `--host`, which is the remote-management
|
|
143
|
+
* passthrough). Omitted strategy falls back to `host` when `host:` is set,
|
|
144
|
+
* otherwise `local`.
|
|
145
|
+
*/
|
|
146
|
+
hostStrategy?: HostStrategy;
|
|
107
147
|
/** Working directory on the host for `host:`-placed runs. */
|
|
108
148
|
remoteCwd?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Provenance for routines materialised from a project
|
|
151
|
+
* (`<project>/.agents/routines/*.yml` → user-layer copy after opt-in).
|
|
152
|
+
* Absent for hand-authored user/system routines.
|
|
153
|
+
*/
|
|
154
|
+
source?: JobSource;
|
|
109
155
|
variables?: Record<string, string>;
|
|
110
156
|
sandbox?: boolean;
|
|
111
157
|
allow?: JobAllowConfig;
|
|
112
158
|
config?: Record<string, unknown>;
|
|
113
159
|
version?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Pin this routine to a signed-in account by identity (its login email, or its
|
|
162
|
+
* account key) instead of rotating. At launch it resolves to whichever
|
|
163
|
+
* installed version currently holds that account and runs pinned — no
|
|
164
|
+
* `balanced` rotation, no usage-read refresh, no failover onto other accounts.
|
|
165
|
+
*
|
|
166
|
+
* This is the durable way to keep concurrent unattended routines off a *shared*
|
|
167
|
+
* Claude OAuth credential: the refresh token is single-use and rotates
|
|
168
|
+
* server-side on every refresh, so two routines running the same account
|
|
169
|
+
* concurrently (on one box or across the fleet) mutually revoke each other —
|
|
170
|
+
* the `401 OAuth access token has been revoked` storm (RUSH-1957). Give each
|
|
171
|
+
* routine (or each device's routines) a distinct account and no run rotates a
|
|
172
|
+
* credential out from under another.
|
|
173
|
+
*
|
|
174
|
+
* Prefer this over `version:`, which pins a version *number* that is GC'd on
|
|
175
|
+
* the next agent upgrade — when the pinned version disappears the routine
|
|
176
|
+
* silently falls back to `balanced` and the stampede returns.
|
|
177
|
+
*/
|
|
178
|
+
account?: string;
|
|
114
179
|
runOnce?: boolean;
|
|
115
180
|
endAt?: string;
|
|
116
181
|
/**
|
|
@@ -147,6 +212,10 @@ export interface RunMeta {
|
|
|
147
212
|
/** The host-task sidecar id backing a `host:` run; the daemon monitor
|
|
148
213
|
* finalizes the run by reconciling it against the remote `.exit`. */
|
|
149
214
|
hostTaskId?: string;
|
|
215
|
+
/** Cloud provider task id when `hostStrategy: cloud` dispatched the run. */
|
|
216
|
+
cloudTaskId?: string;
|
|
217
|
+
/** Cloud provider id when the run was cloud-dispatched. */
|
|
218
|
+
cloudProvider?: string;
|
|
150
219
|
}
|
|
151
220
|
/**
|
|
152
221
|
* Finalize a run record with a terminal status, computing `duration` from
|
|
@@ -165,6 +234,24 @@ export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status']
|
|
|
165
234
|
* catchup/overdue, manual run) gates on this.
|
|
166
235
|
*/
|
|
167
236
|
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'devices'>): boolean;
|
|
237
|
+
/**
|
|
238
|
+
* Resolve the effective host strategy for a job.
|
|
239
|
+
* Bare `host:` without an explicit strategy implies `host` (back-compat with
|
|
240
|
+
* pre-hostStrategy YAML). Otherwise default to `local`.
|
|
241
|
+
*/
|
|
242
|
+
export declare function resolveHostStrategy(config: Pick<JobConfig, 'hostStrategy' | 'host'>): HostStrategy;
|
|
243
|
+
/**
|
|
244
|
+
* Parse a CLI `--placement` value into a HostStrategy, or null when empty.
|
|
245
|
+
* Throws a human-readable Error for unknown values.
|
|
246
|
+
*/
|
|
247
|
+
export declare function parseHostStrategy(raw: string | undefined | null): HostStrategy | null;
|
|
248
|
+
/**
|
|
249
|
+
* Strategies that dispatch the job body off the firing machine. Without a
|
|
250
|
+
* `devices` pin every daemon in the fleet would fire and each would dispatch
|
|
251
|
+
* once — N× duplicate runs. Callers pin to this machine when the user did not
|
|
252
|
+
* set an explicit allowlist.
|
|
253
|
+
*/
|
|
254
|
+
export declare function placementRequiresFiringPin(strategy: HostStrategy): boolean;
|
|
168
255
|
/** Human presentation of a device-affinity mismatch for commands and runner. */
|
|
169
256
|
export interface JobEligibilityResult {
|
|
170
257
|
/** Full human message, e.g. "Job 'NAME' can only run on: a, b". */
|
|
@@ -251,6 +338,13 @@ export declare function parseTimeout(timeout: string): number | null;
|
|
|
251
338
|
export declare function listRuns(jobName: string): RunMeta[];
|
|
252
339
|
/** Get the most recent run for a job, or null if never run. */
|
|
253
340
|
export declare function getLatestRun(jobName: string): RunMeta | null;
|
|
341
|
+
/**
|
|
342
|
+
* Get the most recent COMPLETED run for a job, or null if none has completed.
|
|
343
|
+
* Used to resolve `{last_report}` so a failed run's error text (e.g. an auth
|
|
344
|
+
* login prompt written into report.md) can never be injected into the next
|
|
345
|
+
* run's prompt.
|
|
346
|
+
*/
|
|
347
|
+
export declare function getLatestCompletedRun(jobName: string): RunMeta | null;
|
|
254
348
|
/** Persist run metadata to its run directory as meta.json. */
|
|
255
349
|
export declare function writeRunMeta(meta: RunMeta): void;
|
|
256
350
|
/** Read run metadata from disk. Returns null if missing or corrupt. */
|