@phnx-labs/agents-cli 1.22.31 → 1.22.33
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 +72 -0
- package/README.md +8 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/routines.js +164 -36
- package/dist/commands/sessions-browser.js +2 -2
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +66 -22
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +3 -1
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +112 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/session/team-filter.d.ts +11 -0
- package/dist/lib/session/team-filter.js +10 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
package/dist/lib/migrate.js
CHANGED
|
@@ -13,8 +13,9 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
13
13
|
import { machineId } from './machine-id.js';
|
|
14
14
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
15
15
|
import { createLink } from './platform/index.js';
|
|
16
|
-
import { migrateLegacyRoutineActivation, setJobEnabled } from './routines.js';
|
|
17
|
-
import { addEnabledRoutinesOnUpgrade } from './routine-activation.js';
|
|
16
|
+
import { migrateLegacyRoutineActivation, setJobEnabled, listJobs, validateJob } from './routines.js';
|
|
17
|
+
import { addEnabledRoutinesOnUpgrade, enabledRoutineNames, replaceEnabledRoutines } from './routine-activation.js';
|
|
18
|
+
import { evaluateActivationReadiness } from './routine-readiness.js';
|
|
18
19
|
import { DAEMON_TICK_ROUTINE_NAMES } from './daemon-ticks.js';
|
|
19
20
|
const HOME = process.env.HOME ?? os.homedir();
|
|
20
21
|
const USER_DIR = path.join(HOME, '.agents');
|
|
@@ -1987,6 +1988,108 @@ export function migrateRoutineDeviceToDevices(routinesDir) {
|
|
|
1987
1988
|
console.error(`Migrated ${migrated} routine${migrated === 1 ? '' : 's'}: device → devices`);
|
|
1988
1989
|
}
|
|
1989
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Fold the legacy host-placement `remoteCwd` field into the canonical portable
|
|
1993
|
+
* `cwd` (RUSH-2290). Host dispatch used to read `remoteCwd` while a local run
|
|
1994
|
+
* inferred its cwd from `repo` — two path semantics for one concept. The runner
|
|
1995
|
+
* now resolves every placement from `cwd`, so this idempotently rewrites the
|
|
1996
|
+
* field:
|
|
1997
|
+
*
|
|
1998
|
+
* - `remoteCwd` present, no `cwd` → rename to `cwd`.
|
|
1999
|
+
* - both present and equal → drop the duplicate `remoteCwd`.
|
|
2000
|
+
* - both present and DIFFERENT → conflict: leave BOTH fields untouched so
|
|
2001
|
+
* the migration never silently chooses one; `validateJob`/`doctor` then flag the
|
|
2002
|
+
* pair and the routine stays paused rather than running against a guessed path.
|
|
2003
|
+
*
|
|
2004
|
+
* Idempotent: a file with only `cwd` (already migrated) is skipped.
|
|
2005
|
+
*/
|
|
2006
|
+
export function migrateRoutineRemoteCwdToCwd(routinesDir) {
|
|
2007
|
+
const dir = routinesDir ?? path.join(USER_DIR, 'routines');
|
|
2008
|
+
if (!fs.existsSync(dir))
|
|
2009
|
+
return;
|
|
2010
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
2011
|
+
let migrated = 0;
|
|
2012
|
+
let conflicts = 0;
|
|
2013
|
+
for (const file of files) {
|
|
2014
|
+
const filePath = path.join(dir, file);
|
|
2015
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
2016
|
+
let doc;
|
|
2017
|
+
try {
|
|
2018
|
+
doc = yaml.parse(raw);
|
|
2019
|
+
if (!doc || typeof doc !== 'object')
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
if (!('remoteCwd' in doc))
|
|
2026
|
+
continue;
|
|
2027
|
+
const remote = doc.remoteCwd;
|
|
2028
|
+
if (typeof remote !== 'string' || !remote.trim()) {
|
|
2029
|
+
// A malformed legacy value is not something to fold — drop it and move on.
|
|
2030
|
+
delete doc.remoteCwd;
|
|
2031
|
+
atomicWriteFileSync(filePath, yaml.stringify(doc));
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
if ('cwd' in doc) {
|
|
2035
|
+
if (doc.cwd === remote) {
|
|
2036
|
+
delete doc.remoteCwd; // duplicate — dedupe to the canonical field
|
|
2037
|
+
atomicWriteFileSync(filePath, yaml.stringify(doc));
|
|
2038
|
+
migrated++;
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
conflicts++; // leave both fields; validateJob/doctor pause the conflict
|
|
2042
|
+
}
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
delete doc.remoteCwd;
|
|
2046
|
+
doc.cwd = remote;
|
|
2047
|
+
atomicWriteFileSync(filePath, yaml.stringify(doc));
|
|
2048
|
+
migrated++;
|
|
2049
|
+
}
|
|
2050
|
+
if (migrated > 0) {
|
|
2051
|
+
console.error(`Migrated ${migrated} routine${migrated === 1 ? '' : 's'}: remoteCwd → cwd`);
|
|
2052
|
+
}
|
|
2053
|
+
if (conflicts > 0) {
|
|
2054
|
+
console.error(`${conflicts} routine${conflicts === 1 ? '' : 's'} have conflicting remoteCwd/cwd — left paused for manual repair (migration_conflict)`);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Pause every currently-active routine whose execution context no longer
|
|
2059
|
+
* resolves ready (RUSH-2290). An agent/workflow routine with no project/cwd, a
|
|
2060
|
+
* missing directory, or a non-portable path used to fire and fail every tick —
|
|
2061
|
+
* the mass auth_failed / untrusted-home storm this ticket exists to stop. After
|
|
2062
|
+
* the fold, such a routine is deactivated on THIS device (only), preventing it
|
|
2063
|
+
* from being scheduled until `agents routines doctor --all --fix` (or a repair +
|
|
2064
|
+
* `resume`) makes it ready. Never materializes a device manifest that does not
|
|
2065
|
+
* yet exist, and never touches command routines (they run in the target home).
|
|
2066
|
+
*/
|
|
2067
|
+
export function pauseUnreadyEnabledRoutines() {
|
|
2068
|
+
const enabled = enabledRoutineNames();
|
|
2069
|
+
if (enabled === null)
|
|
2070
|
+
return; // no manifest yet — nothing activated to pause
|
|
2071
|
+
const enabledSet = new Set(enabled);
|
|
2072
|
+
const paused = [];
|
|
2073
|
+
for (const job of listJobs()) {
|
|
2074
|
+
if (!enabledSet.has(job.name))
|
|
2075
|
+
continue;
|
|
2076
|
+
let ready = true;
|
|
2077
|
+
try {
|
|
2078
|
+
ready = validateJob(job).length === 0 && evaluateActivationReadiness(job).ready;
|
|
2079
|
+
}
|
|
2080
|
+
catch {
|
|
2081
|
+
ready = true; // never pause a routine because readiness itself threw
|
|
2082
|
+
}
|
|
2083
|
+
if (!ready)
|
|
2084
|
+
paused.push(job.name);
|
|
2085
|
+
}
|
|
2086
|
+
if (paused.length === 0)
|
|
2087
|
+
return;
|
|
2088
|
+
const pausedSet = new Set(paused);
|
|
2089
|
+
replaceEnabledRoutines(enabled.filter((name) => !pausedSet.has(name)));
|
|
2090
|
+
console.error(`Paused ${paused.length} routine${paused.length === 1 ? '' : 's'} with an unresolved execution context ` +
|
|
2091
|
+
`(run 'agents routines doctor --all' to see why): ${paused.join(', ')}`);
|
|
2092
|
+
}
|
|
1990
2093
|
/**
|
|
1991
2094
|
* Fold the legacy watchdog enable sentinel into the watchdog routine.
|
|
1992
2095
|
*
|
|
@@ -2212,6 +2315,8 @@ export async function runMigration() {
|
|
|
2212
2315
|
migrateExtrasExtrasToAgentsExtras();
|
|
2213
2316
|
// Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
|
|
2214
2317
|
migrateRoutineDeviceToDevices();
|
|
2318
|
+
// Fold legacy host-placement `remoteCwd` into the canonical portable `cwd`.
|
|
2319
|
+
migrateRoutineRemoteCwdToCwd();
|
|
2215
2320
|
migrateLegacyRoutineActivation();
|
|
2216
2321
|
// These routines replace daemon timers that were always active. Devices with
|
|
2217
2322
|
// an existing activation manifest must retain that behavior after upgrade.
|
|
@@ -2220,6 +2325,11 @@ export async function runMigration() {
|
|
|
2220
2325
|
// who opted in under the old build stays opted in after upgrading. After the
|
|
2221
2326
|
// routine rewrites above so the routines dir is in its canonical shape.
|
|
2222
2327
|
migrateWatchdogSentinelToRoutine();
|
|
2328
|
+
// Deactivate any routine whose execution context no longer resolves ready, so
|
|
2329
|
+
// an anchor-less agent/workflow routine cannot keep firing-and-failing after the
|
|
2330
|
+
// fold (RUSH-2290). Runs AFTER the tick/watchdog routines are added so those
|
|
2331
|
+
// (command/home) routines are evaluated in their final shape.
|
|
2332
|
+
pauseUnreadyEnabledRoutines();
|
|
2223
2333
|
// Symlink repair runs LAST so it can find the post-move version homes.
|
|
2224
2334
|
repairAgentConfigSymlinks();
|
|
2225
2335
|
// Repair self-referential node_modules/.bin/<cli> symlinks (the droid
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine execution context + readiness resolution.
|
|
3
|
+
*
|
|
4
|
+
* A scheduled routine has to run *somewhere*. This module is the single,
|
|
5
|
+
* target-aware answer to "which directory does this routine's run land in, and
|
|
6
|
+
* is the chosen harness able to start there?" — computed for the eventual
|
|
7
|
+
* execution TARGET, never from the daemon process's own cwd.
|
|
8
|
+
*
|
|
9
|
+
* Two layers, both pure of global state (every input is injected, so a test
|
|
10
|
+
* exercises the real code path against real temp directories rather than a mock):
|
|
11
|
+
*
|
|
12
|
+
* - {@link resolveRoutineExecutionContext} — resolve the working directory from
|
|
13
|
+
* the routine's singular `project` anchor and/or portable `cwd`, following the
|
|
14
|
+
* locked resolution table (see below), and verify the structural + filesystem
|
|
15
|
+
* readiness of that directory (existence, portability, writability, cloud
|
|
16
|
+
* portability). This layer owns the *context* readiness codes.
|
|
17
|
+
* - {@link evaluateRoutineReadiness} — take a resolved context and layer the
|
|
18
|
+
* *harness/target* readiness codes (agent installed, Codex workspace trust,
|
|
19
|
+
* live auth, target reachability) via injected probes.
|
|
20
|
+
*
|
|
21
|
+
* Resolution table (target `$HOME` = the execution device's home):
|
|
22
|
+
*
|
|
23
|
+
* | project | cwd | resolved dir | readiness |
|
|
24
|
+
* |---------|----------------|-------------------------|-----------|
|
|
25
|
+
* | usable | — | project base | continue |
|
|
26
|
+
* | usable | relative | base + cwd (inside base)| continue if inside base + exists |
|
|
27
|
+
* | rootless| relative | $HOME + cwd | continue if exists |
|
|
28
|
+
* | — | relative | $HOME + cwd | continue if exists |
|
|
29
|
+
* | — | ~/… | $HOME-relative | continue if exists |
|
|
30
|
+
* | — | abs under home | normalized to ~/… | continue |
|
|
31
|
+
* | — | abs outside home| local-pinned only | pause (cwd_not_portable) for host/fleet/cloud |
|
|
32
|
+
* | named+unusable | — | no fallback | pause (project_path_missing) |
|
|
33
|
+
* | — | — (agent/workflow) | no implicit home | pause (execution_context_missing) |
|
|
34
|
+
* | — | — (command) | $HOME | continue (housekeeping) |
|
|
35
|
+
*/
|
|
36
|
+
/** Stable, machine-readable readiness codes. A routine is activated only when ready. */
|
|
37
|
+
export type RoutineReadinessCode = 'project_not_found' | 'project_path_missing' | 'cwd_missing' | 'cwd_not_directory' | 'cwd_not_portable' | 'execution_context_missing' | 'cloud_context_unsupported' | 'workspace_not_writable' | 'codex_workspace_untrusted' | 'agent_unavailable' | 'agent_auth_failed' | 'target_unreachable' | 'placement_unsupported' | 'migration_conflict';
|
|
38
|
+
export interface RoutineReadiness {
|
|
39
|
+
code: RoutineReadinessCode;
|
|
40
|
+
/** Human-readable one-line explanation of the failing check. */
|
|
41
|
+
message: string;
|
|
42
|
+
/** A single safe command that repairs the blocker, when one exists. */
|
|
43
|
+
repair?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Where the routine body executes — mirrors {@link HostStrategy} placement. */
|
|
46
|
+
export type PlacementMode = 'local' | 'host' | 'fleet' | 'cloud';
|
|
47
|
+
/**
|
|
48
|
+
* What the caller resolved about the routine's singular `project` anchor.
|
|
49
|
+
* `undefined` (the field on the input) means the routine names no project.
|
|
50
|
+
*/
|
|
51
|
+
export type ProjectResolution = {
|
|
52
|
+
defined: false;
|
|
53
|
+
}
|
|
54
|
+
/** Defined project; `base` is its portable base dir (`~/…` or absolute), or
|
|
55
|
+
* undefined for a rootless Linear-imported project with no checkout. */
|
|
56
|
+
| {
|
|
57
|
+
defined: true;
|
|
58
|
+
base?: string;
|
|
59
|
+
};
|
|
60
|
+
export type RoutineKind = 'agent' | 'workflow' | 'command';
|
|
61
|
+
/** A filesystem probe against the execution TARGET. */
|
|
62
|
+
export interface ContextFsProbe {
|
|
63
|
+
exists(absPath: string): boolean;
|
|
64
|
+
isDirectory(absPath: string): boolean;
|
|
65
|
+
isWritable(absPath: string): boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface ExecutionContextInput {
|
|
68
|
+
/** Routine name (for messages only). */
|
|
69
|
+
name?: string;
|
|
70
|
+
/** Singular execution anchor (`JobConfig.project`). */
|
|
71
|
+
project?: string;
|
|
72
|
+
/** Portable execution directory (`JobConfig.cwd`). */
|
|
73
|
+
cwd?: string;
|
|
74
|
+
/** Exactly one of agent/workflow/command determines the fallback rules. */
|
|
75
|
+
kind: RoutineKind;
|
|
76
|
+
/** Placement of the run — governs portability enforcement and cloud rules. */
|
|
77
|
+
mode: PlacementMode;
|
|
78
|
+
/** Execution target's absolute `$HOME`. Local: `os.homedir()`; remote: the target home. */
|
|
79
|
+
targetHome: string;
|
|
80
|
+
/** Resolution of the `project` anchor; omit when the routine names no project. */
|
|
81
|
+
projectResolution?: ProjectResolution;
|
|
82
|
+
/**
|
|
83
|
+
* Filesystem probe for the target, present only when this process can inspect
|
|
84
|
+
* it (a local run, or add/edit/doctor invoked on the target box). Absent for a
|
|
85
|
+
* remote/cloud target we cannot reach — then only structural + portability
|
|
86
|
+
* checks run (existence is deferred, never assumed).
|
|
87
|
+
*/
|
|
88
|
+
probe?: ContextFsProbe;
|
|
89
|
+
}
|
|
90
|
+
export interface ResolvedExecutionContext {
|
|
91
|
+
project?: string;
|
|
92
|
+
/** `config.cwd` echoed for the run record. */
|
|
93
|
+
requestedCwd?: string;
|
|
94
|
+
/** Portable resolved cwd for the run record: `~/…` when under target home, else absolute. */
|
|
95
|
+
resolvedCwd?: string;
|
|
96
|
+
/** The resolved cwd expanded to an absolute path on the target. Undefined when unresolved. */
|
|
97
|
+
absoluteCwd?: string;
|
|
98
|
+
targetHome: string;
|
|
99
|
+
ready: boolean;
|
|
100
|
+
/** Present when `ready` is false. */
|
|
101
|
+
readiness?: RoutineReadiness;
|
|
102
|
+
}
|
|
103
|
+
/** Expand a leading `~`/`$HOME` against the target home; pass other values through. */
|
|
104
|
+
export declare function expandTargetHome(home: string, p: string): string;
|
|
105
|
+
/** Rewrite an absolute path under the target home to its portable `~/…` form; pass others through. */
|
|
106
|
+
export declare function toTargetPortable(home: string, abs: string): string;
|
|
107
|
+
/** True for a bare relative path (not absolute, not home-anchored). */
|
|
108
|
+
export declare function isBareRelative(p: string): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the working directory a routine's run lands in and verify its
|
|
111
|
+
* structural + filesystem readiness for the given placement. Pure of global
|
|
112
|
+
* state — every dependency (target home, project resolution, filesystem probe)
|
|
113
|
+
* is injected.
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveRoutineExecutionContext(input: ExecutionContextInput): ResolvedExecutionContext;
|
|
116
|
+
/** Injected harness/target probes for {@link evaluateRoutineReadiness}. */
|
|
117
|
+
export interface HarnessReadinessProbes {
|
|
118
|
+
/** Is the resolved agent+version installed on the target? */
|
|
119
|
+
agentInstalled?(): boolean;
|
|
120
|
+
/** Is the absolute execution dir a trusted Codex workspace? (Codex agent only.) */
|
|
121
|
+
codexTrusted?(absoluteCwd: string): boolean;
|
|
122
|
+
/** Live auth verdict for the resolved account/version. `ok:false` → agent_auth_failed. */
|
|
123
|
+
authOk?(): {
|
|
124
|
+
ok: boolean;
|
|
125
|
+
reason?: string;
|
|
126
|
+
};
|
|
127
|
+
/** Is the execution target reachable? (host/fleet/cloud placement only.) */
|
|
128
|
+
targetReachable?(): boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface RoutineReadinessResult {
|
|
131
|
+
context: ResolvedExecutionContext;
|
|
132
|
+
ready: boolean;
|
|
133
|
+
readiness?: RoutineReadiness;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Layer the harness/target readiness codes onto a resolved execution context.
|
|
137
|
+
* Context blockers short-circuit (no point probing auth for a routine that has
|
|
138
|
+
* no directory to run in). Every probe is optional and injected; an omitted
|
|
139
|
+
* probe is treated as "not applicable / passes" so a caller only pays for the
|
|
140
|
+
* checks it wires up.
|
|
141
|
+
*/
|
|
142
|
+
export declare function evaluateRoutineReadiness(context: ResolvedExecutionContext, probes?: HarnessReadinessProbes, opts?: {
|
|
143
|
+
agent?: string;
|
|
144
|
+
}): RoutineReadinessResult;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routine execution context + readiness resolution.
|
|
3
|
+
*
|
|
4
|
+
* A scheduled routine has to run *somewhere*. This module is the single,
|
|
5
|
+
* target-aware answer to "which directory does this routine's run land in, and
|
|
6
|
+
* is the chosen harness able to start there?" — computed for the eventual
|
|
7
|
+
* execution TARGET, never from the daemon process's own cwd.
|
|
8
|
+
*
|
|
9
|
+
* Two layers, both pure of global state (every input is injected, so a test
|
|
10
|
+
* exercises the real code path against real temp directories rather than a mock):
|
|
11
|
+
*
|
|
12
|
+
* - {@link resolveRoutineExecutionContext} — resolve the working directory from
|
|
13
|
+
* the routine's singular `project` anchor and/or portable `cwd`, following the
|
|
14
|
+
* locked resolution table (see below), and verify the structural + filesystem
|
|
15
|
+
* readiness of that directory (existence, portability, writability, cloud
|
|
16
|
+
* portability). This layer owns the *context* readiness codes.
|
|
17
|
+
* - {@link evaluateRoutineReadiness} — take a resolved context and layer the
|
|
18
|
+
* *harness/target* readiness codes (agent installed, Codex workspace trust,
|
|
19
|
+
* live auth, target reachability) via injected probes.
|
|
20
|
+
*
|
|
21
|
+
* Resolution table (target `$HOME` = the execution device's home):
|
|
22
|
+
*
|
|
23
|
+
* | project | cwd | resolved dir | readiness |
|
|
24
|
+
* |---------|----------------|-------------------------|-----------|
|
|
25
|
+
* | usable | — | project base | continue |
|
|
26
|
+
* | usable | relative | base + cwd (inside base)| continue if inside base + exists |
|
|
27
|
+
* | rootless| relative | $HOME + cwd | continue if exists |
|
|
28
|
+
* | — | relative | $HOME + cwd | continue if exists |
|
|
29
|
+
* | — | ~/… | $HOME-relative | continue if exists |
|
|
30
|
+
* | — | abs under home | normalized to ~/… | continue |
|
|
31
|
+
* | — | abs outside home| local-pinned only | pause (cwd_not_portable) for host/fleet/cloud |
|
|
32
|
+
* | named+unusable | — | no fallback | pause (project_path_missing) |
|
|
33
|
+
* | — | — (agent/workflow) | no implicit home | pause (execution_context_missing) |
|
|
34
|
+
* | — | — (command) | $HOME | continue (housekeeping) |
|
|
35
|
+
*/
|
|
36
|
+
import * as path from 'path';
|
|
37
|
+
// --- target-aware path helpers (do NOT use project-root.ts's local-HOME-bound
|
|
38
|
+
// forms: resolution must root at the execution target's home, not this box's) ---
|
|
39
|
+
/**
|
|
40
|
+
* Path flavour of the EXECUTION TARGET, inferred from its own home string.
|
|
41
|
+
*
|
|
42
|
+
* The target home belongs to whichever machine will run the routine, which need
|
|
43
|
+
* not be this one — a Windows box can schedule onto a Linux target. Joining with
|
|
44
|
+
* the LOCAL separator therefore built `\home\user\svc` for a POSIX target (and
|
|
45
|
+
* would build `C:/Users/x/svc` the other way), so these helpers key off the home
|
|
46
|
+
* path's shape instead of `process.platform`. Same-platform behaviour is
|
|
47
|
+
* unchanged; only the cross-platform case is fixed.
|
|
48
|
+
*/
|
|
49
|
+
function targetPath(home) {
|
|
50
|
+
return /^[A-Za-z]:[\\/]/.test(home) || home.includes('\\') ? path.win32 : path.posix;
|
|
51
|
+
}
|
|
52
|
+
/** Expand a leading `~`/`$HOME` against the target home; pass other values through. */
|
|
53
|
+
export function expandTargetHome(home, p) {
|
|
54
|
+
if (p === '~' || p === '$HOME')
|
|
55
|
+
return home;
|
|
56
|
+
const tp = targetPath(home);
|
|
57
|
+
if (p.startsWith('~/'))
|
|
58
|
+
return tp.join(home, p.slice(2));
|
|
59
|
+
if (p.startsWith('$HOME/'))
|
|
60
|
+
return tp.join(home, p.slice('$HOME/'.length));
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
/** Rewrite an absolute path under the target home to its portable `~/…` form; pass others through. */
|
|
64
|
+
export function toTargetPortable(home, abs) {
|
|
65
|
+
const tp = targetPath(home);
|
|
66
|
+
const rel = tp.relative(home, abs);
|
|
67
|
+
if (rel === '')
|
|
68
|
+
return '~';
|
|
69
|
+
if (!rel.startsWith('..') && !tp.isAbsolute(rel))
|
|
70
|
+
return '~/' + rel.split(tp.sep).join('/');
|
|
71
|
+
return abs;
|
|
72
|
+
}
|
|
73
|
+
/** True for a bare relative path (not absolute, not home-anchored). */
|
|
74
|
+
export function isBareRelative(p) {
|
|
75
|
+
return !path.isAbsolute(p) && !p.startsWith('~') && !p.startsWith('$HOME');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* True when `child` is `base` or strictly beneath it (no `..` escape).
|
|
79
|
+
*
|
|
80
|
+
* Compares in the BASE's own path flavour (see {@link targetPath}) — both
|
|
81
|
+
* arguments are target-side paths, and comparing a POSIX pair with Windows
|
|
82
|
+
* semantics (or the reverse) answers about the wrong filesystem.
|
|
83
|
+
*/
|
|
84
|
+
function isInside(baseAbs, childAbs) {
|
|
85
|
+
const tp = targetPath(baseAbs);
|
|
86
|
+
const rel = tp.relative(baseAbs, childAbs);
|
|
87
|
+
return rel === '' || (!rel.startsWith('..') && !tp.isAbsolute(rel));
|
|
88
|
+
}
|
|
89
|
+
function pause(ctx, readiness) {
|
|
90
|
+
return { ...ctx, ready: false, readiness };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the working directory a routine's run lands in and verify its
|
|
94
|
+
* structural + filesystem readiness for the given placement. Pure of global
|
|
95
|
+
* state — every dependency (target home, project resolution, filesystem probe)
|
|
96
|
+
* is injected.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveRoutineExecutionContext(input) {
|
|
99
|
+
const { project, cwd, kind, mode, targetHome, projectResolution, probe } = input;
|
|
100
|
+
const requestedCwd = cwd;
|
|
101
|
+
const base = { project, requestedCwd, targetHome };
|
|
102
|
+
const hasProjectBinding = projectResolution?.defined === true;
|
|
103
|
+
// Finalize a resolved portable dir: run the cloud/portability/filesystem gates
|
|
104
|
+
// and return either a ready context or a paused one.
|
|
105
|
+
const finalize = (portable, missingCode) => {
|
|
106
|
+
const absoluteCwd = expandTargetHome(targetHome, portable);
|
|
107
|
+
const ctx = { ...base, resolvedCwd: portable, absoluteCwd };
|
|
108
|
+
// Cloud: a filesystem-only cwd (no project/repo binding) has no provider
|
|
109
|
+
// repository to map onto. A project binding selects the provider repo.
|
|
110
|
+
if (mode === 'cloud' && !hasProjectBinding) {
|
|
111
|
+
return pause(ctx, {
|
|
112
|
+
code: 'cloud_context_unsupported',
|
|
113
|
+
message: `a bare cwd has no cloud repository to run in — bind a project/repo or run '${input.name ?? 'this routine'}' locally`,
|
|
114
|
+
repair: `agents routines edit ${input.name ?? '<name>'} --project-anchor <name>`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (probe) {
|
|
118
|
+
if (!probe.exists(absoluteCwd)) {
|
|
119
|
+
return pause(ctx, {
|
|
120
|
+
code: missingCode,
|
|
121
|
+
message: missingCode === 'project_path_missing'
|
|
122
|
+
? `project base directory does not exist on the target: ${portable}`
|
|
123
|
+
: `execution directory does not exist on the target: ${portable}`,
|
|
124
|
+
repair: `mkdir -p ${portable}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!probe.isDirectory(absoluteCwd)) {
|
|
128
|
+
return pause(ctx, {
|
|
129
|
+
code: 'cwd_not_directory',
|
|
130
|
+
message: `execution path is not a directory: ${portable}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (!probe.isWritable(absoluteCwd)) {
|
|
134
|
+
return pause(ctx, {
|
|
135
|
+
code: 'workspace_not_writable',
|
|
136
|
+
message: `execution directory is not writable: ${portable}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ...ctx, ready: true };
|
|
141
|
+
};
|
|
142
|
+
// 1. Explicit project anchor.
|
|
143
|
+
if (project !== undefined) {
|
|
144
|
+
if (!projectResolution || projectResolution.defined === false) {
|
|
145
|
+
return pause(base, {
|
|
146
|
+
code: 'project_not_found',
|
|
147
|
+
message: `project '${project}' is not defined`,
|
|
148
|
+
repair: `agents projects add ${project} --root <path>`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const projBase = projectResolution.base;
|
|
152
|
+
if (projBase) {
|
|
153
|
+
// Usable base path.
|
|
154
|
+
if (cwd === undefined) {
|
|
155
|
+
return finalize(projBase, 'project_path_missing');
|
|
156
|
+
}
|
|
157
|
+
if (isBareRelative(cwd)) {
|
|
158
|
+
const baseAbs = expandTargetHome(targetHome, projBase);
|
|
159
|
+
// Target-side join: `path.resolve` would use this host's separator and,
|
|
160
|
+
// for a target home this host does not consider absolute, prepend the
|
|
161
|
+
// local process cwd.
|
|
162
|
+
const joinedAbs = targetPath(targetHome).resolve(baseAbs, cwd);
|
|
163
|
+
if (!isInside(baseAbs, joinedAbs)) {
|
|
164
|
+
return pause(base, {
|
|
165
|
+
code: 'cwd_not_portable',
|
|
166
|
+
message: `cwd '${cwd}' escapes the project base '${projBase}' — a project-relative cwd must stay inside it`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return finalize(toTargetPortable(targetHome, joinedAbs), 'cwd_missing');
|
|
170
|
+
}
|
|
171
|
+
// Absolute or home-anchored cwd alongside a project: fall through to the
|
|
172
|
+
// cwd-first handling below (the project base is not the anchor then).
|
|
173
|
+
}
|
|
174
|
+
else if (cwd === undefined) {
|
|
175
|
+
// Rootless project (Linear import), no cwd: there is nothing to run in.
|
|
176
|
+
return pause(base, {
|
|
177
|
+
code: 'project_path_missing',
|
|
178
|
+
message: `project '${project}' has no checkout path — give it a cwd (anchored at the target home) or set the project's root`,
|
|
179
|
+
repair: `agents routines edit ${input.name ?? '<name>'} --cwd <path>`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Rootless project + a cwd, or usable project + a non-relative cwd: the cwd
|
|
183
|
+
// itself is the anchor. A bare relative cwd anchors at the target home.
|
|
184
|
+
}
|
|
185
|
+
// 2. cwd without a (usable relative-anchoring) project.
|
|
186
|
+
if (cwd !== undefined) {
|
|
187
|
+
if (cwd.startsWith('~') || cwd.startsWith('$HOME')) {
|
|
188
|
+
const abs = expandTargetHome(targetHome, cwd);
|
|
189
|
+
return finalize(toTargetPortable(targetHome, abs), 'cwd_missing');
|
|
190
|
+
}
|
|
191
|
+
if (path.isAbsolute(cwd)) {
|
|
192
|
+
const resolved = path.resolve(cwd);
|
|
193
|
+
if (isInside(targetHome, resolved)) {
|
|
194
|
+
// Normalize an absolute-under-home path to its portable form on save.
|
|
195
|
+
return finalize(toTargetPortable(targetHome, resolved), 'cwd_missing');
|
|
196
|
+
}
|
|
197
|
+
// Absolute path outside the target home: only a local-pinned routine can
|
|
198
|
+
// use it; host/fleet/cloud placement cannot carry a non-portable path.
|
|
199
|
+
if (mode === 'local') {
|
|
200
|
+
return finalize(resolved, 'cwd_missing');
|
|
201
|
+
}
|
|
202
|
+
return pause(base, {
|
|
203
|
+
code: 'cwd_not_portable',
|
|
204
|
+
message: `absolute cwd '${cwd}' is outside the target home and cannot travel to ${mode} placement — use a home-relative path`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Bare relative cwd (no usable project base): anchor at the target home,
|
|
208
|
+
// in the target's own path flavour (see the joinedAbs note above).
|
|
209
|
+
return finalize(toTargetPortable(targetHome, targetPath(targetHome).resolve(targetHome, cwd)), 'cwd_missing');
|
|
210
|
+
}
|
|
211
|
+
// 3. Neither field.
|
|
212
|
+
if (kind === 'command') {
|
|
213
|
+
// Command routines are deterministic housekeeping — the target home is a
|
|
214
|
+
// safe implicit cwd, so a version-check / notify routine keeps working.
|
|
215
|
+
return finalize(toTargetPortable(targetHome, targetHome), 'cwd_missing');
|
|
216
|
+
}
|
|
217
|
+
return pause(base, {
|
|
218
|
+
code: 'execution_context_missing',
|
|
219
|
+
message: `routine '${input.name ?? ''}' has no project or cwd — an agent/workflow routine needs an explicit execution directory`,
|
|
220
|
+
repair: `agents routines edit ${input.name ?? '<name>'} --project-anchor <name> # or --cwd <path>`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Layer the harness/target readiness codes onto a resolved execution context.
|
|
225
|
+
* Context blockers short-circuit (no point probing auth for a routine that has
|
|
226
|
+
* no directory to run in). Every probe is optional and injected; an omitted
|
|
227
|
+
* probe is treated as "not applicable / passes" so a caller only pays for the
|
|
228
|
+
* checks it wires up.
|
|
229
|
+
*/
|
|
230
|
+
export function evaluateRoutineReadiness(context, probes = {}, opts = {}) {
|
|
231
|
+
if (!context.ready) {
|
|
232
|
+
return { context, ready: false, readiness: context.readiness };
|
|
233
|
+
}
|
|
234
|
+
if (probes.targetReachable && !probes.targetReachable()) {
|
|
235
|
+
return withBlocker(context, {
|
|
236
|
+
code: 'target_unreachable',
|
|
237
|
+
message: 'the execution target is not reachable',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (probes.agentInstalled && !probes.agentInstalled()) {
|
|
241
|
+
return withBlocker(context, {
|
|
242
|
+
code: 'agent_unavailable',
|
|
243
|
+
message: `no usable version of ${opts.agent ?? 'the agent'} is installed on the target`,
|
|
244
|
+
repair: opts.agent ? `agents add ${opts.agent}@<version>` : undefined,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (probes.codexTrusted && context.absoluteCwd && !probes.codexTrusted(context.absoluteCwd)) {
|
|
248
|
+
return withBlocker(context, {
|
|
249
|
+
code: 'codex_workspace_untrusted',
|
|
250
|
+
message: `Codex will not start in an untrusted workspace: ${context.resolvedCwd}`,
|
|
251
|
+
repair: `trust the workspace (add it to Codex's trusted projects) — the routine never uses --skip-git-repo-check`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (probes.authOk) {
|
|
255
|
+
const verdict = probes.authOk();
|
|
256
|
+
if (!verdict.ok) {
|
|
257
|
+
return withBlocker(context, {
|
|
258
|
+
code: 'agent_auth_failed',
|
|
259
|
+
message: `the selected account failed a live auth check${verdict.reason ? `: ${verdict.reason}` : ''}`,
|
|
260
|
+
repair: opts.agent ? `agents run ${opts.agent} -- login` : 'log the account back in',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { context, ready: true };
|
|
265
|
+
}
|
|
266
|
+
function withBlocker(context, readiness) {
|
|
267
|
+
return { context: { ...context, ready: false, readiness }, ready: false, readiness };
|
|
268
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation readiness for a routine, composed from the target-aware execution
|
|
3
|
+
* context ({@link resolveJobExecutionContext}) plus the harness/target checks a
|
|
4
|
+
* caller can perform on this box (agent installed). This is the gate `add`,
|
|
5
|
+
* `edit`, `doctor`, and `resume` all run before activating a routine: ready →
|
|
6
|
+
* active, any proven blocker → saved paused with a stable code + repair command.
|
|
7
|
+
*
|
|
8
|
+
* Structural context readiness is synchronous for the scheduler. Interactive
|
|
9
|
+
* add/edit/doctor/resume additionally call the live variant below, which probes
|
|
10
|
+
* authentication and Codex's native workspace-trust record before activation.
|
|
11
|
+
*/
|
|
12
|
+
import type { JobConfig } from './routines.js';
|
|
13
|
+
import { type RoutineReadinessResult } from './routine-context.js';
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate whether a routine is ready to activate on this box. `probeAgent`
|
|
16
|
+
* defaults to "is a version of the routine's agent resolvable" via
|
|
17
|
+
* {@link resolveVersion}; pass a stub in tests to exercise the availability path
|
|
18
|
+
* without an installed harness.
|
|
19
|
+
*/
|
|
20
|
+
export declare function evaluateActivationReadiness(config: JobConfig, deps?: {
|
|
21
|
+
probeAgent?: (agent: string) => boolean;
|
|
22
|
+
}): RoutineReadinessResult;
|
|
23
|
+
/** One-line human summary of a blocked readiness result, with its repair. */
|
|
24
|
+
export declare function formatReadinessBlocker(result: RoutineReadinessResult): string;
|
|
25
|
+
interface RemoteProjectDefinition {
|
|
26
|
+
name: string;
|
|
27
|
+
root?: string;
|
|
28
|
+
defaultPath?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Parse the target HOME sentinel and the project catalog returned by one SSH call. */
|
|
31
|
+
export declare function parseRemoteProjectSnapshot(stdout: string): {
|
|
32
|
+
home: string;
|
|
33
|
+
projects: RemoteProjectDefinition[];
|
|
34
|
+
} | undefined;
|
|
35
|
+
/** Build a target-native probe that creates and removes a file in the workspace. */
|
|
36
|
+
export declare function buildRemoteWorkspaceProbe(cwd: string, windows: boolean): string;
|
|
37
|
+
/** A successful probe must contain the exact requested postcondition, not merely exit zero. */
|
|
38
|
+
export declare function probeOutputHasSentinel(stdout: string, sentinel: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Interactive setup/repair readiness. Unlike the scheduler's deterministic
|
|
41
|
+
* structural gate, this completes a real local auth request and reads Codex's
|
|
42
|
+
* native trust record before add/edit/resume can activate the definition.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluateActivationReadinessLive(config: JobConfig): Promise<RoutineReadinessResult>;
|
|
45
|
+
/** Resolve and probe the actual SSH target used by a host-placed routine. */
|
|
46
|
+
export declare function evaluateHostActivationReadiness(config: JobConfig): Promise<RoutineReadinessResult>;
|
|
47
|
+
export {};
|