@phnx-labs/agents-cli 1.22.57 → 1.22.59

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 (152) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/README.md +29 -0
  3. package/dist/bootstrap.js +39 -1
  4. package/dist/commands/accounts.js +7 -3
  5. package/dist/commands/apply.js +10 -2
  6. package/dist/commands/fork.d.ts +23 -10
  7. package/dist/commands/fork.js +115 -58
  8. package/dist/commands/monitors.js +198 -23
  9. package/dist/commands/prune.js +5 -3
  10. package/dist/commands/routines.d.ts +8 -0
  11. package/dist/commands/routines.js +57 -3
  12. package/dist/commands/routines.test-fixture.js +5 -0
  13. package/dist/commands/send.d.ts +2 -1
  14. package/dist/commands/send.js +7 -5
  15. package/dist/commands/sessions-picker.d.ts +11 -0
  16. package/dist/commands/sessions-picker.js +16 -0
  17. package/dist/commands/sessions-stats.js +37 -5
  18. package/dist/commands/sessions.js +40 -5
  19. package/dist/commands/share.d.ts +14 -0
  20. package/dist/commands/share.js +43 -2
  21. package/dist/commands/ssh.js +12 -1
  22. package/dist/commands/status.js +1 -1
  23. package/dist/commands/sync.js +83 -7
  24. package/dist/commands/traces.js +7 -0
  25. package/dist/commands/versions.js +12 -4
  26. package/dist/commands/view.js +7 -2
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +6 -1
  29. package/dist/lib/account-registry.d.ts +5 -1
  30. package/dist/lib/account-registry.js +47 -14
  31. package/dist/lib/accounting/capacity.d.ts +18 -7
  32. package/dist/lib/accounting/capacity.js +19 -8
  33. package/dist/lib/accounting/usage-sync.d.ts +29 -1
  34. package/dist/lib/accounting/usage-sync.js +76 -2
  35. package/dist/lib/accounting/usage.js +7 -1
  36. package/dist/lib/auth-mint.d.ts +11 -1
  37. package/dist/lib/auth-mint.js +21 -6
  38. package/dist/lib/auto-pull-worker.js +7 -2
  39. package/dist/lib/browser/ipc.d.ts +8 -0
  40. package/dist/lib/browser/ipc.js +87 -0
  41. package/dist/lib/browser/service.d.ts +19 -0
  42. package/dist/lib/browser/service.js +96 -11
  43. package/dist/lib/browser/sessions-list.js +10 -1
  44. package/dist/lib/cloud/rush.d.ts +7 -0
  45. package/dist/lib/cloud/rush.js +29 -1
  46. package/dist/lib/daemon/daemon.d.ts +22 -0
  47. package/dist/lib/daemon/daemon.js +39 -0
  48. package/dist/lib/daemon/runner.d.ts +3 -0
  49. package/dist/lib/daemon/runner.js +86 -45
  50. package/dist/lib/daemon/session-index-service.js +9 -1
  51. package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
  52. package/dist/lib/daemon/usage-sync-service.js +14 -8
  53. package/dist/lib/daemon-services.js +1 -1
  54. package/dist/lib/daemon-ticks.d.ts +15 -0
  55. package/dist/lib/daemon-ticks.js +26 -0
  56. package/dist/lib/device-config.d.ts +5 -1
  57. package/dist/lib/device-config.js +2 -2
  58. package/dist/lib/devices/connect.d.ts +17 -8
  59. package/dist/lib/devices/connect.js +31 -14
  60. package/dist/lib/devices/health.js +5 -1
  61. package/dist/lib/devices/pool.d.ts +25 -2
  62. package/dist/lib/devices/pool.js +32 -2
  63. package/dist/lib/devices/stats-cache.d.ts +0 -6
  64. package/dist/lib/devices/stats-cache.js +2 -9
  65. package/dist/lib/doctor-diff.d.ts +14 -0
  66. package/dist/lib/doctor-diff.js +120 -9
  67. package/dist/lib/fleet/manifest.d.ts +17 -0
  68. package/dist/lib/fleet/manifest.js +26 -0
  69. package/dist/lib/git.d.ts +38 -0
  70. package/dist/lib/git.js +58 -0
  71. package/dist/lib/hooks/install.d.ts +27 -11
  72. package/dist/lib/hooks/install.js +42 -17
  73. package/dist/lib/hosts/ready.d.ts +8 -0
  74. package/dist/lib/hosts/ready.js +13 -2
  75. package/dist/lib/hosts/reconnect.d.ts +52 -203
  76. package/dist/lib/hosts/reconnect.js +64 -284
  77. package/dist/lib/installations/migrate.d.ts +6 -120
  78. package/dist/lib/installations/migrate.js +27 -259
  79. package/dist/lib/installations/shims.d.ts +13 -95
  80. package/dist/lib/installations/shims.js +22 -139
  81. package/dist/lib/installations/store.js +1 -1
  82. package/dist/lib/installations/versions.d.ts +43 -133
  83. package/dist/lib/installations/versions.js +94 -206
  84. package/dist/lib/monitors/config.d.ts +71 -3
  85. package/dist/lib/monitors/config.js +100 -12
  86. package/dist/lib/monitors/pid-watch.d.ts +35 -0
  87. package/dist/lib/monitors/pid-watch.js +45 -0
  88. package/dist/lib/monitors/remote.d.ts +18 -0
  89. package/dist/lib/monitors/remote.js +11 -0
  90. package/dist/lib/permissions.js +7 -2
  91. package/dist/lib/plugins/plugins.d.ts +17 -3
  92. package/dist/lib/plugins/plugins.js +84 -9
  93. package/dist/lib/plugins/skills.d.ts +8 -1
  94. package/dist/lib/plugins/skills.js +18 -2
  95. package/dist/lib/pty-server.d.ts +14 -0
  96. package/dist/lib/pty-server.js +49 -5
  97. package/dist/lib/refresh.d.ts +9 -0
  98. package/dist/lib/refresh.js +3 -1
  99. package/dist/lib/routine-readiness.d.ts +15 -1
  100. package/dist/lib/routine-readiness.js +41 -0
  101. package/dist/lib/sandbox.d.ts +4 -1
  102. package/dist/lib/sandbox.js +30 -1
  103. package/dist/lib/secrets/agent.d.ts +80 -225
  104. package/dist/lib/secrets/agent.js +139 -401
  105. package/dist/lib/secrets/bundles.d.ts +73 -222
  106. package/dist/lib/secrets/bundles.js +168 -467
  107. package/dist/lib/secrets/drivers/rush.js +5 -0
  108. package/dist/lib/secrets/reaper.d.ts +28 -70
  109. package/dist/lib/secrets/reaper.js +30 -85
  110. package/dist/lib/secrets/remote.d.ts +42 -129
  111. package/dist/lib/secrets/remote.js +55 -173
  112. package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
  113. package/dist/lib/self-heal/checks/install-staging.js +96 -0
  114. package/dist/lib/self-heal/registry.js +2 -0
  115. package/dist/lib/self-heal/types.d.ts +1 -1
  116. package/dist/lib/self-update.d.ts +65 -0
  117. package/dist/lib/self-update.js +138 -0
  118. package/dist/lib/session/active.d.ts +13 -1
  119. package/dist/lib/session/active.js +2 -0
  120. package/dist/lib/session/cloud.js +5 -0
  121. package/dist/lib/session/db.d.ts +51 -6
  122. package/dist/lib/session/db.js +266 -20
  123. package/dist/lib/session/fork.d.ts +45 -26
  124. package/dist/lib/session/fork.js +32 -95
  125. package/dist/lib/session/tool-calls.d.ts +43 -1
  126. package/dist/lib/session/tool-calls.js +74 -44
  127. package/dist/lib/session/tool-store.d.ts +33 -2
  128. package/dist/lib/session/tool-store.js +56 -3
  129. package/dist/lib/smart-launch.d.ts +6 -0
  130. package/dist/lib/smart-launch.js +5 -2
  131. package/dist/lib/staleness/writers/plugins.js +5 -2
  132. package/dist/lib/staleness/writers/sources.d.ts +5 -0
  133. package/dist/lib/staleness/writers/sources.js +2 -1
  134. package/dist/lib/staleness/writers/subagents.js +13 -3
  135. package/dist/lib/state.d.ts +7 -4
  136. package/dist/lib/state.js +7 -4
  137. package/dist/lib/subagents.js +8 -2
  138. package/dist/lib/sync-status.d.ts +22 -0
  139. package/dist/lib/sync-status.js +27 -0
  140. package/dist/lib/sync-umbrella.d.ts +9 -0
  141. package/dist/lib/sync-umbrella.js +21 -2
  142. package/dist/lib/teams/scheduler.d.ts +10 -0
  143. package/dist/lib/teams/scheduler.js +8 -0
  144. package/dist/lib/traces/insights.d.ts +47 -14
  145. package/dist/lib/traces/insights.js +92 -21
  146. package/dist/lib/traces/phenotype.d.ts +23 -3
  147. package/dist/lib/traces/phenotype.js +72 -24
  148. package/dist/lib/traces/sync.d.ts +128 -6
  149. package/dist/lib/traces/sync.js +294 -35
  150. package/dist/lib/traces/worker-template.js +154 -1
  151. package/dist/lib/view-types.d.ts +12 -0
  152. package/package.json +2 -2
@@ -16,6 +16,7 @@ import { capableAgents, isCapable } from '../capabilities.js';
16
16
  import { getUserSkillsDir, getSkillsDir as getSystemSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, getTrashSkillsDir } from '../state.js';
17
17
  import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
18
18
  import { listCommandSkillsInVersion } from '../command-skills.js';
19
+ import { pluginSkillDirs, listPluginSkillNames } from '../staleness/writers/sources.js';
19
20
  import { emit } from '../feed/events.js';
20
21
  const HOME = os.homedir();
21
22
  /** User-scoped skills dir (~/.agents/skills/). Used for installs. */
@@ -352,7 +353,14 @@ export function listCentralSkills() {
352
353
  }
353
354
  /**
354
355
  * Resolve a skill name to its source directory. Searches user dir first,
355
- * then system dir, then extra repos. Returns null if no source has a SKILL.md.
356
+ * then system dir, then extra repos, then plugin-bundled skills
357
+ * (`plugins/<plugin>/skills/<name>` under any trusted base). Returns null if no
358
+ * source has a SKILL.md.
359
+ *
360
+ * Crediting plugins here is load-bearing (PHNX-3185): a skill materialized from
361
+ * `plugins/design/skills/design` has a real source, so it must resolve — else
362
+ * `versionSkillMatches` finds no source and the skill reads as drifted, and
363
+ * {@link diffVersionSkills} classes it an orphan `prune cleanup` would delete.
356
364
  */
357
365
  export function resolveSkillSourcePath(skillName) {
358
366
  for (const dir of [getUserSkillsDir(), getSystemSkillsDir()]) {
@@ -365,6 +373,11 @@ export function resolveSkillSourcePath(skillName) {
365
373
  if (fs.existsSync(path.join(candidate, 'SKILL.md')))
366
374
  return candidate;
367
375
  }
376
+ for (const skillsDir of pluginSkillDirs()) {
377
+ const candidate = path.join(skillsDir, skillName);
378
+ if (fs.existsSync(path.join(candidate, 'SKILL.md')))
379
+ return candidate;
380
+ }
368
381
  return null;
369
382
  }
370
383
  /**
@@ -443,7 +456,10 @@ function versionSkillMatches(agent, version, skillName) {
443
456
  * Compare a version home's skills against central. Returns the reconciliation diff.
444
457
  */
445
458
  export function diffVersionSkills(agent, version) {
446
- const available = new Set(listAllSkills());
459
+ // Plugin-provided skills (`plugins/<plugin>/skills`) are legitimate sources too, so a
460
+ // skill materialized from a plugin is NOT an orphan (PHNX-3185). Without this
461
+ // the detector offered to delete the entire plugin-installed skill library.
462
+ const available = new Set([...listAllSkills(), ...listPluginSkillNames({ agent })]);
447
463
  // Goose and other native ~/.agents/skills consumers read central storage
448
464
  // directly. They intentionally have no per-version copy to diff, so every
449
465
  // available central skill is already current for every supported version.
@@ -21,6 +21,20 @@ export { captureProcessStartTime };
21
21
  * (the authoritative code comes from node-pty's onExit).
22
22
  */
23
23
  export declare function buildSentinelCommand(shell: string, command: string): string;
24
+ /**
25
+ * Turn a native-binding load failure into an actionable, platform-aware message.
26
+ *
27
+ * The binding is `@homebridge/node-pty-prebuilt-multiarch`'s `pty.node`. On Linux
28
+ * it is baked into the npm tarball and always present; on macOS/Windows it is
29
+ * fetched per host + Node ABI at install time by the package's `prebuild-install`
30
+ * postinstall. The common failure is a Node runtime whose ABI has no published
31
+ * prebuild (the PHNX-2740 darwin-arm64 case): `prebuild-install` finds nothing,
32
+ * the build-from-source fallback also fails, and the loader throws a bare
33
+ * `Cannot find module '.../pty.node'` / MODULE_NOT_FOUND with no clue what to do.
34
+ *
35
+ * Pure (returns the lines) so it can be unit-tested without spawning the server.
36
+ */
37
+ export declare function describeNativePtyLoadFailure(err: unknown): string[];
24
38
  /**
25
39
  * Resolve the IPC endpoint for a given platform + PTY scratch dir. Pure so both
26
40
  * branches are testable without stubbing process.platform.
@@ -88,6 +88,43 @@ export function buildSentinelCommand(shell, command) {
88
88
  }
89
89
  return `${command}; echo "${SENTINEL}:$?"`;
90
90
  }
91
+ /**
92
+ * Turn a native-binding load failure into an actionable, platform-aware message.
93
+ *
94
+ * The binding is `@homebridge/node-pty-prebuilt-multiarch`'s `pty.node`. On Linux
95
+ * it is baked into the npm tarball and always present; on macOS/Windows it is
96
+ * fetched per host + Node ABI at install time by the package's `prebuild-install`
97
+ * postinstall. The common failure is a Node runtime whose ABI has no published
98
+ * prebuild (the PHNX-2740 darwin-arm64 case): `prebuild-install` finds nothing,
99
+ * the build-from-source fallback also fails, and the loader throws a bare
100
+ * `Cannot find module '.../pty.node'` / MODULE_NOT_FOUND with no clue what to do.
101
+ *
102
+ * Pure (returns the lines) so it can be unit-tested without spawning the server.
103
+ */
104
+ export function describeNativePtyLoadFailure(err) {
105
+ const detail = err instanceof Error ? (err.stack || err.message) : String(err);
106
+ const abi = process.versions.modules;
107
+ const nodeVersion = process.version;
108
+ const platform = `${process.platform}-${process.arch}`;
109
+ const lines = [
110
+ `agents pty could not load its native terminal binding (@homebridge/node-pty-prebuilt-multiarch).`,
111
+ ` platform: ${platform} node: ${nodeVersion} (ABI ${abi})`,
112
+ ``,
113
+ `The prebuilt binary for this platform + Node ABI is missing. On macOS/Windows`,
114
+ `it is downloaded at install time; a Node version newer than any published`,
115
+ `prebuild, or an install that skipped the postinstall, leaves it absent.`,
116
+ ``,
117
+ `Fix it by reinstalling so the native binding is fetched or built:`,
118
+ ` npm rebuild @homebridge/node-pty-prebuilt-multiarch # rebuild for this Node`,
119
+ ` # or reinstall the CLI: npm i -g @phnx-labs/agents-cli`,
120
+ `If your Node (ABI ${abi}) is newer than the shipped prebuilds, install an LTS`,
121
+ `Node and retry, or ensure a C++ toolchain is present for the source build.`,
122
+ ``,
123
+ `Underlying error:`,
124
+ ...detail.split('\n').map(l => ` ${l}`),
125
+ ];
126
+ return lines;
127
+ }
91
128
  /** Get the PTY helper directory, creating it if needed. */
92
129
  function getPtyDir() {
93
130
  const dir = getPtyDirRoot();
@@ -181,9 +218,16 @@ export async function runPtyServer() {
181
218
  let nodePty;
182
219
  let XtermTerminal;
183
220
  try {
184
- // The Homebridge multiarch fork of node-pty: API-identical (same 1.x N-API
185
- // codebase) but ships prebuilt binaries for Linux glibc + musl, x64 + arm64
186
- // (plus macOS/Windows), so no compiler is needed on Linux/Alpine/arm64.
221
+ // The Homebridge multiarch fork of node-pty (API-identical to the 1.x N-API
222
+ // upstream). Its npm tarball BAKES IN the Linux prebuilds (glibc + musl, every
223
+ // arch + Node ABI), so Linux never needs a compiler or a network fetch. The
224
+ // darwin-arm64 / win32 binaries are NOT in the tarball — they are downloaded
225
+ // per host+Node-ABI at install time by the package's own `prebuild-install`
226
+ // postinstall (gated by bun's `trustedDependencies`). That fetch fails when the
227
+ // running Node ABI is newer than any published prebuild (e.g. 0.13.1 shipped no
228
+ // darwin-arm64 above Node 24 / ABI 137, so Node 25/26 fell through to a
229
+ // build-from-source that also failed — PHNX-2740), leaving a raw MODULE_NOT_FOUND.
230
+ // Fail loud with the platform/ABI and a concrete remediation instead.
187
231
  nodePty = await import('@homebridge/node-pty-prebuilt-multiarch');
188
232
  // Handle ESM default export
189
233
  if (nodePty.default?.spawn)
@@ -205,8 +249,8 @@ export async function runPtyServer() {
205
249
  catch { }
206
250
  }
207
251
  catch (err) {
208
- console.error('node-pty (@homebridge/node-pty-prebuilt-multiarch) is required for PTY support.');
209
- console.error('Install: bun add @homebridge/node-pty-prebuilt-multiarch');
252
+ for (const line of describeNativePtyLoadFailure(err))
253
+ console.error(line);
210
254
  process.exit(1);
211
255
  }
212
256
  try {
@@ -40,5 +40,14 @@ export interface RefreshOptions {
40
40
  export interface RefreshResult {
41
41
  /** User-facing sentences, one per refused resource, prefixed with the agent. */
42
42
  declined: string[];
43
+ /**
44
+ * The exact (agent, version) pairs this refresh reconciled — the set a
45
+ * post-reconcile verification must re-check for residual drift, so it never
46
+ * flags a version the reconcile never targeted (PHNX-3186).
47
+ */
48
+ reconciled: Array<{
49
+ agent: AgentId;
50
+ version: string;
51
+ }>;
43
52
  }
44
53
  export declare function refresh(options?: RefreshOptions): Promise<RefreshResult>;
@@ -58,6 +58,7 @@ export async function refresh(options = {}) {
58
58
  // Resources this pass refused to write, surfaced by the caller. An empty
59
59
  // synced list cannot also mean "declined and here is why" (RUSH-2700).
60
60
  const declined = [];
61
+ const reconciled = [];
61
62
  migratePromptcutsToRoot(agentsDir, quiet);
62
63
  const manifest = readManifest(agentsDir);
63
64
  if (!manifest) {
@@ -176,6 +177,7 @@ export async function refresh(options = {}) {
176
177
  // Pass the already-built `available` so each version does not re-scan
177
178
  // resource trees (RUSH-2320 #5).
178
179
  const syncResult = syncResourcesToVersion(agentId, ver, selection, { available, ...(forceFullSync ? { force: true } : {}) });
180
+ reconciled.push({ agent: agentId, version: ver });
179
181
  if (syncResult.commands)
180
182
  kinds.add('commands');
181
183
  if (syncResult.skills)
@@ -353,5 +355,5 @@ export async function refresh(options = {}) {
353
355
  for (const reason of declined)
354
356
  log(` ${chalk.yellow(reason)}`);
355
357
  }
356
- return { declined };
358
+ return { declined, reconciled };
357
359
  }
@@ -10,7 +10,21 @@
10
10
  * authentication and Codex's native workspace-trust record before activation.
11
11
  */
12
12
  import type { JobConfig } from './scheduling/routines.js';
13
- import { type RoutineReadinessResult } from './routine-context.js';
13
+ import { type RoutineReadinessResult, type RoutineReadiness } from './routine-context.js';
14
+ /**
15
+ * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
16
+ * exact (agent, version) a routine has resolved to run, and return an
17
+ * `agent_auth_failed` blocker when that account is provably signed out — so the
18
+ * daemon records a terminal `blocked` run (with the re-login repair) instead of
19
+ * spawning a doomed run that 401s and burns a session (PHNX-3415).
20
+ *
21
+ * Cache-only (no network, no prompt — the daemon refreshes the cache
22
+ * periodically and `fleet ping` writes it), and fails OPEN on a missing or
23
+ * non-blocking verdict so a stale/absent probe never wedges a routine. It is
24
+ * checked AFTER version rotation resolves the account (`launch.chain[0]`), so it
25
+ * judges the identity the run will actually use, never a rotated-past dead pin.
26
+ */
27
+ export declare function fireTimeAuthReadiness(agent: string, version: string): RoutineReadiness | null;
14
28
  /**
15
29
  * Evaluate whether a routine is ready to activate on this box. `probeAgent`
16
30
  * defaults to "is a version of the routine's agent resolvable" via
@@ -14,6 +14,8 @@ import * as path from 'path';
14
14
  import * as TOML from 'smol-toml';
15
15
  import { resolveJobExecutionContext, resolveHostStrategy } from './scheduling/routines.js';
16
16
  import { evaluateRoutineReadiness } from './routine-context.js';
17
+ import { readAuthHealth } from './auth-health.js';
18
+ import { machineId } from './machine-id.js';
17
19
  import { getVersionHomePath, isVersionInstalled, resolveVersion } from './installations/versions.js';
18
20
  import { probeLocalFleetAuth } from './auth-health.js';
19
21
  import { resolveHostRunTarget } from './hosts/run-target.js';
@@ -21,6 +23,45 @@ import { hostIdentityArgs, sshTargetFor } from './hosts/types.js';
21
23
  import { probeHost } from './hosts/ready.js';
22
24
  import { sshExec, shellQuote } from './ssh-exec.js';
23
25
  import { encodePowershell, powershellQuote, POWERSHELL_PROGRESS_SILENCE } from './hosts/remote-cmd.js';
26
+ /**
27
+ * Verdicts that make a FIRE-TIME auth preflight block the run: the last live
28
+ * probe found the account either server-rejected (`revoked`) or with no
29
+ * credential at all (`unconfigured` — the "Please run /login" / "no account
30
+ * signed in" case, which is the most common way a routine's dispatch account
31
+ * goes dead). Everything else fails OPEN: `rate_limited` is still authenticated,
32
+ * `expired` self-heals on the next refresh, `unverified` means signed-in but no
33
+ * live probe endpoint (codex/grok), and `error` is indeterminate — none of those
34
+ * should stop a fire. This is deliberately BROADER than {@link isDeadVerdict}
35
+ * (display-only, `revoked` alone): a signed-out account must block a fire, not
36
+ * just paint a red cell.
37
+ */
38
+ function fireBlockingAuthVerdict(verdict) {
39
+ return verdict === 'revoked' || verdict === 'unconfigured';
40
+ }
41
+ /**
42
+ * Fire-time auth preflight: read the daemon-warmed auth-health cache for the
43
+ * exact (agent, version) a routine has resolved to run, and return an
44
+ * `agent_auth_failed` blocker when that account is provably signed out — so the
45
+ * daemon records a terminal `blocked` run (with the re-login repair) instead of
46
+ * spawning a doomed run that 401s and burns a session (PHNX-3415).
47
+ *
48
+ * Cache-only (no network, no prompt — the daemon refreshes the cache
49
+ * periodically and `fleet ping` writes it), and fails OPEN on a missing or
50
+ * non-blocking verdict so a stale/absent probe never wedges a routine. It is
51
+ * checked AFTER version rotation resolves the account (`launch.chain[0]`), so it
52
+ * judges the identity the run will actually use, never a rotated-past dead pin.
53
+ */
54
+ export function fireTimeAuthReadiness(agent, version) {
55
+ const health = readAuthHealth(machineId(), agent, version);
56
+ if (!health || !fireBlockingAuthVerdict(health.verdict))
57
+ return null;
58
+ const who = health.account ? ` (${health.account})` : '';
59
+ return {
60
+ code: 'agent_auth_failed',
61
+ message: `the ${agent}${who} account is signed out — the last auth probe returned '${health.verdict}', so this run would fail authentication`,
62
+ repair: `agents run ${agent}@${version} -- login`,
63
+ };
64
+ }
24
65
  /**
25
66
  * Evaluate whether a routine is ready to activate on this box. `probeAgent`
26
67
  * defaults to "is a version of the routine's agent resolvable" via
@@ -7,6 +7,7 @@
7
7
  * access to explicitly allowed paths.
8
8
  */
9
9
  import type { JobConfig } from './scheduling/routines.js';
10
+ import type { AgentId } from './types.js';
10
11
  /**
11
12
  * Absolute path to this host's `gh` config directory (`hosts.yml` + `config.yml`),
12
13
  * or null when the host has never run `gh auth login`. Prefer `GH_CONFIG_DIR` when
@@ -28,7 +29,9 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
28
29
  */
29
30
  export declare function getJobHomePath(name: string): string;
30
31
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
31
- export declare function prepareJobHome(config: JobConfig): string;
32
+ export declare function prepareJobHome(config: JobConfig, version?: string): string;
33
+ /** Link only the selected harness login into the disposable routine HOME. */
34
+ export declare function linkVersionAuth(overlayHome: string, agent: AgentId, version?: string): void;
32
35
  /**
33
36
  * Link this host's `gh` config directory into the disposable overlay so
34
37
  * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import { getRoutinesDir, getUserAgentsDir } from './state.js';
13
13
  import { safeJoin } from './paths.js';
14
14
  import { createLink } from './platform/index.js';
15
+ import { getVersionHomePath } from './installations/versions.js';
15
16
  function resolveRealHome() {
16
17
  const home = os.homedir();
17
18
  try {
@@ -132,7 +133,7 @@ export function getJobHomePath(name) {
132
133
  return path.join(safeJoin(getRoutinesDir(), name), 'home');
133
134
  }
134
135
  /** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
135
- export function prepareJobHome(config) {
136
+ export function prepareJobHome(config, version) {
136
137
  const overlayHome = getJobHomePath(config.name);
137
138
  cleanJobHome(config.name);
138
139
  fs.mkdirSync(overlayHome, { recursive: true });
@@ -143,6 +144,7 @@ export function prepareJobHome(config) {
143
144
  }
144
145
  else if (config.agent === 'codex') {
145
146
  generateCodexConfig(overlayHome, config);
147
+ linkVersionAuth(overlayHome, 'codex', version);
146
148
  }
147
149
  else if (config.agent === 'cursor') {
148
150
  generateCursorConfig(overlayHome);
@@ -165,6 +167,33 @@ export function prepareJobHome(config) {
165
167
  }
166
168
  return overlayHome;
167
169
  }
170
+ /** Link only the selected harness login into the disposable routine HOME. */
171
+ export function linkVersionAuth(overlayHome, agent, version) {
172
+ if (!version)
173
+ return;
174
+ const versionHome = getVersionHomePath(agent, version);
175
+ const pairs = agent === 'claude'
176
+ ? [
177
+ [path.join(versionHome, '.claude', '.claude.json'), path.join(overlayHome, '.claude', '.claude.json')],
178
+ [path.join(versionHome, '.claude', '.credentials.json'), path.join(overlayHome, '.claude', '.credentials.json')],
179
+ ]
180
+ : agent === 'codex'
181
+ ? [[path.join(versionHome, '.codex', 'auth.json'), path.join(overlayHome, '.codex', 'auth.json')]]
182
+ : [];
183
+ for (const [source, target] of pairs) {
184
+ if (!fs.existsSync(source))
185
+ continue;
186
+ fs.mkdirSync(path.dirname(target), { recursive: true });
187
+ try {
188
+ fs.rmSync(target, { force: true });
189
+ }
190
+ catch { /* absent */ }
191
+ try {
192
+ createLink(source, target);
193
+ }
194
+ catch { /* the harness fails auth loudly if linking is unavailable */ }
195
+ }
196
+ }
168
197
  /**
169
198
  * Link this host's `gh` config directory into the disposable overlay so
170
199
  * `$HOME/.config/gh` resolves even when `GH_CONFIG_DIR` is unset. Mirrors