@phnx-labs/agents-cli 1.22.31 → 1.22.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +8 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/daemon.js +52 -12
  5. package/dist/commands/doctor.d.ts +19 -0
  6. package/dist/commands/doctor.js +119 -17
  7. package/dist/commands/routines.js +164 -36
  8. package/dist/commands/sessions.d.ts +1 -1
  9. package/dist/commands/sessions.js +44 -10
  10. package/dist/commands/update.d.ts +2 -0
  11. package/dist/commands/update.js +148 -0
  12. package/dist/index.js +3 -1
  13. package/dist/lib/catchup.js +4 -1
  14. package/dist/lib/daemon.d.ts +17 -0
  15. package/dist/lib/daemon.js +69 -3
  16. package/dist/lib/devices/doctor-findings.d.ts +7 -2
  17. package/dist/lib/devices/doctor-findings.js +53 -2
  18. package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
  19. package/dist/lib/devices/doctor-overview-cache.js +15 -0
  20. package/dist/lib/devices/fleet-divergence.d.ts +11 -0
  21. package/dist/lib/devices/fleet-divergence.js +6 -0
  22. package/dist/lib/devices/fleet-inventory.js +16 -2
  23. package/dist/lib/drift.d.ts +6 -1
  24. package/dist/lib/drift.js +9 -0
  25. package/dist/lib/hooks/cache.js +20 -1
  26. package/dist/lib/hooks.d.ts +91 -1
  27. package/dist/lib/hooks.js +289 -3
  28. package/dist/lib/hosts/passthrough.js +3 -0
  29. package/dist/lib/installations/index.d.ts +14 -0
  30. package/dist/lib/installations/index.js +14 -0
  31. package/dist/lib/installations/resolve.d.ts +43 -0
  32. package/dist/lib/installations/resolve.js +93 -0
  33. package/dist/lib/installations/store.d.ts +56 -0
  34. package/dist/lib/installations/store.js +196 -0
  35. package/dist/lib/installations/strategies.d.ts +73 -0
  36. package/dist/lib/installations/strategies.js +293 -0
  37. package/dist/lib/installations/types.d.ts +78 -0
  38. package/dist/lib/installations/types.js +8 -0
  39. package/dist/lib/installations/update.d.ts +40 -0
  40. package/dist/lib/installations/update.js +131 -0
  41. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  42. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  43. package/dist/lib/migrate.d.ts +27 -0
  44. package/dist/lib/migrate.js +112 -2
  45. package/dist/lib/routine-context.d.ts +144 -0
  46. package/dist/lib/routine-context.js +268 -0
  47. package/dist/lib/routine-readiness.d.ts +47 -0
  48. package/dist/lib/routine-readiness.js +239 -0
  49. package/dist/lib/routines.d.ts +97 -1
  50. package/dist/lib/routines.js +107 -1
  51. package/dist/lib/runner.d.ts +18 -4
  52. package/dist/lib/runner.js +291 -98
  53. package/dist/lib/scheduler.d.ts +7 -1
  54. package/dist/lib/scheduler.js +5 -2
  55. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  56. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  57. package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
  58. package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
  59. package/dist/lib/self-heal/registry.js +5 -2
  60. package/dist/lib/self-heal/types.d.ts +1 -1
  61. package/dist/lib/session/state.js +4 -1
  62. package/dist/lib/startup/command-registry.d.ts +1 -0
  63. package/dist/lib/startup/command-registry.js +2 -0
  64. package/dist/lib/versions.d.ts +24 -0
  65. package/dist/lib/versions.js +49 -16
  66. package/package.json +2 -2
@@ -0,0 +1,40 @@
1
+ import { type UpdateStrategy } from './strategies.js';
2
+ import type { Installation, UpdateOutcome } from './types.js';
3
+ export interface UpdateInstallationOptions {
4
+ /** `latest` (default), `oldest`, or a concrete release. */
5
+ to?: string;
6
+ onProgress?: (message: string) => void;
7
+ /**
8
+ * Replace the registry-selected strategy. The seam exists so the transaction
9
+ * below can be exercised against a real filesystem without a vendor fetch, and
10
+ * so a track that installs a harness differently (per-installation Cursor
11
+ * isolation) can reuse this orchestration instead of re-implementing it.
12
+ * Omitted in every normal call — `selectUpdateStrategy` is the default.
13
+ */
14
+ strategy?: UpdateStrategy;
15
+ }
16
+ /**
17
+ * Move one frozen installation to a new vendor release, preserving its identity.
18
+ *
19
+ * The transaction is stage → verify → commit → record, with rollback on any
20
+ * failure after the swap:
21
+ *
22
+ * 1. **stage** — fetch the target release somewhere that is not yet live.
23
+ * 2. **verify** — launch the STAGED binary. This is the gate that makes the
24
+ * update safe: a release that cannot start is discarded while
25
+ * the working one is still in place, so the failure mode is
26
+ * "nothing changed", not "the agent no longer runs".
27
+ * 3. **commit** — swap it in, keeping the displaced release until step 4.
28
+ * 4. **record** — re-verify in place, then write the new release into the
29
+ * installation record and drop the rollback material.
30
+ *
31
+ * The installation's `id` and `label` are never touched, so the global default,
32
+ * an isolated default, a project pin, a routine's `version:`, and a profile's
33
+ * `host.version` all keep resolving to this installation across the update —
34
+ * that reference preservation is the whole point of freezing the label.
35
+ *
36
+ * Strategies whose vendor artifact is global (a self-updating binary) report
37
+ * `transactional: false`; for those, step 3 is a no-op and a failed verify is
38
+ * surfaced as a failed update rather than pretended to be reversible.
39
+ */
40
+ export declare function updateInstallation(installation: Installation, options?: UpdateInstallationOptions): Promise<UpdateOutcome>;
@@ -0,0 +1,131 @@
1
+ import * as fs from 'fs';
2
+ import { AGENTS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
3
+ import { emit } from '../events.js';
4
+ import { getBinaryPath, invalidateInstalledVersionsCache, invalidateLiveVersionCache, verifyBinaryLaunches, } from '../versions.js';
5
+ import { assertValidRelease, selectUpdateStrategy, } from './strategies.js';
6
+ import { listInstallations, recordRelease } from './store.js';
7
+ /**
8
+ * Move one frozen installation to a new vendor release, preserving its identity.
9
+ *
10
+ * The transaction is stage → verify → commit → record, with rollback on any
11
+ * failure after the swap:
12
+ *
13
+ * 1. **stage** — fetch the target release somewhere that is not yet live.
14
+ * 2. **verify** — launch the STAGED binary. This is the gate that makes the
15
+ * update safe: a release that cannot start is discarded while
16
+ * the working one is still in place, so the failure mode is
17
+ * "nothing changed", not "the agent no longer runs".
18
+ * 3. **commit** — swap it in, keeping the displaced release until step 4.
19
+ * 4. **record** — re-verify in place, then write the new release into the
20
+ * installation record and drop the rollback material.
21
+ *
22
+ * The installation's `id` and `label` are never touched, so the global default,
23
+ * an isolated default, a project pin, a routine's `version:`, and a profile's
24
+ * `host.version` all keep resolving to this installation across the update —
25
+ * that reference preservation is the whole point of freezing the label.
26
+ *
27
+ * Strategies whose vendor artifact is global (a self-updating binary) report
28
+ * `transactional: false`; for those, step 3 is a no-op and a failed verify is
29
+ * surfaced as a failed update rather than pretended to be reversible.
30
+ */
31
+ export async function updateInstallation(installation, options = {}) {
32
+ const agent = installation.agent;
33
+ if (isAgentHardDeprecated(agent))
34
+ throw new Error(hardDeprecationError(agent));
35
+ const requested = options.to ?? 'latest';
36
+ assertValidRelease(requested);
37
+ const strategy = options.strategy ?? selectUpdateStrategy(agent);
38
+ const ctx = { agent, installation, requested, onProgress: options.onProgress };
39
+ const target = await strategy.resolveTarget(ctx);
40
+ if (target === installation.releaseVersion) {
41
+ options.onProgress?.(`${AGENTS[agent].name}@${installation.label} is already on release ${target}; nothing to update.`);
42
+ return {
43
+ installation,
44
+ strategy: strategy.id,
45
+ fromRelease: installation.releaseVersion,
46
+ toRelease: target,
47
+ unchanged: true,
48
+ alsoUpdated: [],
49
+ };
50
+ }
51
+ let staged = null;
52
+ try {
53
+ staged = await strategy.stage(ctx, target);
54
+ const stagedHealth = await verifyBinaryLaunches(staged.binary, staged.home);
55
+ if (!stagedHealth.ok) {
56
+ throw new Error(`${AGENTS[agent].name} release ${staged.release} was fetched but its binary failed to launch`
57
+ + `${stagedHealth.detail ? ` (${stagedHealth.detail})` : ''}. `
58
+ + `${installation.label} is unchanged and still on ${installation.releaseVersion}.`);
59
+ }
60
+ // The installer may have reported a release the installation already has
61
+ // (a self-updating binary that was already current). Recording it would
62
+ // claim a change that did not happen and append a bogus history entry.
63
+ if (staged.release === installation.releaseVersion) {
64
+ options.onProgress?.(`${AGENTS[agent].name}@${installation.label} is already on release ${staged.release}; nothing to update.`);
65
+ // Deliberately NOT committed: there is no new release to make live, and
66
+ // for a strategy that swaps the version dir a commit here would displace
67
+ // a working tree, discard its rollback material, and skip the live probe —
68
+ // all while reporting that nothing changed. The `finally` clears staging.
69
+ return {
70
+ installation,
71
+ strategy: strategy.id,
72
+ fromRelease: installation.releaseVersion,
73
+ toRelease: staged.release,
74
+ unchanged: true,
75
+ alsoUpdated: [],
76
+ };
77
+ }
78
+ const handles = await strategy.commit(ctx, staged);
79
+ try {
80
+ // Probe what will actually execute — `getBinaryPath` is the same resolver
81
+ // the shims and `agents run` use — not the staging copy probed above.
82
+ const liveBinary = getBinaryPath(agent, installation.label);
83
+ const liveHealth = await verifyBinaryLaunches(liveBinary, staged.home);
84
+ if (!liveHealth.ok) {
85
+ throw new Error(`${AGENTS[agent].name} release ${staged.release} failed to launch after being installed`
86
+ + `${liveHealth.detail ? ` (${liveHealth.detail})` : ''}.`);
87
+ }
88
+ }
89
+ catch (err) {
90
+ // Undo unconditionally. `transactional` describes whether the VENDOR
91
+ // artifact can be put back, not whether this directory can — gating the
92
+ // undo on it left an installer-driven harness with the broken tree live
93
+ // AND the previous one orphaned in rollback material nothing deletes.
94
+ // A strategy with nothing to restore returns a no-op undo.
95
+ handles.undo();
96
+ throw new Error(strategy.transactional
97
+ ? `${err.message} Rolled back to ${installation.releaseVersion}.`
98
+ : `${err.message} The version directory was restored, but ${AGENTS[agent].name}'s installer `
99
+ + `had already replaced the binary it manages globally — repair it with: agents add ${agent}@latest`);
100
+ }
101
+ handles.finalize();
102
+ const updated = recordRelease(installation, staged.release);
103
+ // Several installations of a global-binary harness point at the same file,
104
+ // so the one we just replaced is live for all of them. Recording the release
105
+ // only on the target would leave the others claiming a release that is no
106
+ // longer on disk.
107
+ const alsoUpdated = strategy.sharedBinary
108
+ ? listInstallations(agent)
109
+ .filter((other) => other.label !== installation.label && other.releaseVersion !== staged.release)
110
+ .map((other) => recordRelease(other, staged.release))
111
+ : [];
112
+ invalidateInstalledVersionsCache(agent);
113
+ invalidateLiveVersionCache(agent);
114
+ // A release really was installed, so this is the right event; `installation`
115
+ // says WHICH frozen install received it, since the label no longer equals
116
+ // the release.
117
+ emit('version.install', { agent, version: staged.release, installation: installation.label });
118
+ return {
119
+ installation: updated,
120
+ strategy: strategy.id,
121
+ fromRelease: installation.releaseVersion,
122
+ toRelease: staged.release,
123
+ unchanged: false,
124
+ alsoUpdated,
125
+ };
126
+ }
127
+ finally {
128
+ if (staged?.stagingDir)
129
+ fs.rmSync(staged.stagingDir, { recursive: true, force: true });
130
+ }
131
+ }
@@ -117,6 +117,33 @@ export declare function migrateExtrasExtrasToAgentsExtras(historyDir?: string):
117
117
  * Params default to the real routines dir; injectable for tests.
118
118
  */
119
119
  export declare function migrateRoutineDeviceToDevices(routinesDir?: string): void;
120
+ /**
121
+ * Fold the legacy host-placement `remoteCwd` field into the canonical portable
122
+ * `cwd` (RUSH-2290). Host dispatch used to read `remoteCwd` while a local run
123
+ * inferred its cwd from `repo` — two path semantics for one concept. The runner
124
+ * now resolves every placement from `cwd`, so this idempotently rewrites the
125
+ * field:
126
+ *
127
+ * - `remoteCwd` present, no `cwd` → rename to `cwd`.
128
+ * - both present and equal → drop the duplicate `remoteCwd`.
129
+ * - both present and DIFFERENT → conflict: leave BOTH fields untouched so
130
+ * the migration never silently chooses one; `validateJob`/`doctor` then flag the
131
+ * pair and the routine stays paused rather than running against a guessed path.
132
+ *
133
+ * Idempotent: a file with only `cwd` (already migrated) is skipped.
134
+ */
135
+ export declare function migrateRoutineRemoteCwdToCwd(routinesDir?: string): void;
136
+ /**
137
+ * Pause every currently-active routine whose execution context no longer
138
+ * resolves ready (RUSH-2290). An agent/workflow routine with no project/cwd, a
139
+ * missing directory, or a non-portable path used to fire and fail every tick —
140
+ * the mass auth_failed / untrusted-home storm this ticket exists to stop. After
141
+ * the fold, such a routine is deactivated on THIS device (only), preventing it
142
+ * from being scheduled until `agents routines doctor --all --fix` (or a repair +
143
+ * `resume`) makes it ready. Never materializes a device manifest that does not
144
+ * yet exist, and never touches command routines (they run in the target home).
145
+ */
146
+ export declare function pauseUnreadyEnabledRoutines(): void;
120
147
  /**
121
148
  * Fold the legacy watchdog enable sentinel into the watchdog routine.
122
149
  *
@@ -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;