@phnx-labs/agents-cli 1.20.73 → 1.20.76

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 (209) hide show
  1. package/CHANGELOG.md +989 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +31 -1
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/export.d.ts +11 -0
  15. package/dist/commands/export.js +215 -0
  16. package/dist/commands/feed.d.ts +7 -1
  17. package/dist/commands/feed.js +47 -4
  18. package/dist/commands/go.d.ts +9 -2
  19. package/dist/commands/go.js +16 -6
  20. package/dist/commands/harness.d.ts +14 -0
  21. package/dist/commands/harness.js +145 -0
  22. package/dist/commands/import.js +87 -9
  23. package/dist/commands/inspect.js +5 -2
  24. package/dist/commands/profiles.d.ts +20 -0
  25. package/dist/commands/profiles.js +68 -21
  26. package/dist/commands/repo.js +14 -0
  27. package/dist/commands/routines.js +15 -2
  28. package/dist/commands/secrets.js +44 -21
  29. package/dist/commands/sessions-browser.js +1 -1
  30. package/dist/commands/sessions-export.js +19 -5
  31. package/dist/commands/sessions-migrate.d.ts +29 -0
  32. package/dist/commands/sessions-migrate.js +596 -0
  33. package/dist/commands/sessions-picker.js +16 -1
  34. package/dist/commands/sessions.d.ts +36 -13
  35. package/dist/commands/sessions.js +210 -40
  36. package/dist/commands/setup-computer.js +2 -2
  37. package/dist/commands/setup.js +6 -1
  38. package/dist/commands/ssh.js +71 -35
  39. package/dist/commands/status.js +10 -2
  40. package/dist/commands/versions.js +51 -11
  41. package/dist/commands/view.d.ts +5 -0
  42. package/dist/commands/view.js +99 -31
  43. package/dist/commands/watchdog.d.ts +7 -2
  44. package/dist/commands/watchdog.js +73 -57
  45. package/dist/index.js +59 -7
  46. package/dist/lib/activity.d.ts +14 -4
  47. package/dist/lib/activity.js +17 -5
  48. package/dist/lib/actor.d.ts +50 -0
  49. package/dist/lib/actor.js +166 -0
  50. package/dist/lib/agent-spec/provider.js +2 -1
  51. package/dist/lib/agent-spec/resolve.js +19 -5
  52. package/dist/lib/agent-spec/types.d.ts +9 -1
  53. package/dist/lib/agents.d.ts +56 -0
  54. package/dist/lib/agents.js +133 -7
  55. package/dist/lib/cloud/codex.d.ts +2 -0
  56. package/dist/lib/cloud/codex.js +14 -3
  57. package/dist/lib/cloud/session-index.d.ts +32 -0
  58. package/dist/lib/cloud/session-index.js +58 -0
  59. package/dist/lib/cloud/store.d.ts +7 -0
  60. package/dist/lib/cloud/store.js +25 -0
  61. package/dist/lib/config-transfer.d.ts +16 -0
  62. package/dist/lib/config-transfer.js +60 -0
  63. package/dist/lib/daemon.js +18 -2
  64. package/dist/lib/devices/connect.d.ts +15 -1
  65. package/dist/lib/devices/connect.js +15 -1
  66. package/dist/lib/devices/fleet.d.ts +21 -0
  67. package/dist/lib/devices/fleet.js +26 -0
  68. package/dist/lib/devices/health-report.d.ts +38 -0
  69. package/dist/lib/devices/health-report.js +214 -0
  70. package/dist/lib/devices/health.js +4 -1
  71. package/dist/lib/devices/reachability.d.ts +31 -0
  72. package/dist/lib/devices/reachability.js +40 -0
  73. package/dist/lib/devices/registry.d.ts +33 -0
  74. package/dist/lib/devices/registry.js +37 -0
  75. package/dist/lib/devices/resolve-target.d.ts +15 -27
  76. package/dist/lib/devices/resolve-target.js +63 -102
  77. package/dist/lib/devices/sync.d.ts +18 -0
  78. package/dist/lib/devices/sync.js +23 -1
  79. package/dist/lib/devices/tailscale.d.ts +3 -0
  80. package/dist/lib/devices/tailscale.js +1 -0
  81. package/dist/lib/events.d.ts +1 -1
  82. package/dist/lib/exec.d.ts +66 -0
  83. package/dist/lib/exec.js +195 -6
  84. package/dist/lib/export.d.ts +72 -0
  85. package/dist/lib/export.js +269 -0
  86. package/dist/lib/feed-post.d.ts +63 -0
  87. package/dist/lib/feed-post.js +204 -0
  88. package/dist/lib/fleet/apply.d.ts +32 -2
  89. package/dist/lib/fleet/apply.js +97 -10
  90. package/dist/lib/fleet/types.d.ts +11 -0
  91. package/dist/lib/fs-walk.d.ts +13 -0
  92. package/dist/lib/fs-walk.js +16 -7
  93. package/dist/lib/hooks.d.ts +1 -0
  94. package/dist/lib/hooks.js +156 -0
  95. package/dist/lib/hosts/dispatch.d.ts +7 -0
  96. package/dist/lib/hosts/dispatch.js +2 -0
  97. package/dist/lib/hosts/passthrough.js +2 -0
  98. package/dist/lib/hosts/reconnect.d.ts +78 -0
  99. package/dist/lib/hosts/reconnect.js +127 -0
  100. package/dist/lib/hosts/registry.d.ts +75 -14
  101. package/dist/lib/hosts/registry.js +205 -30
  102. package/dist/lib/hosts/remote-cmd.js +1 -0
  103. package/dist/lib/hosts/run-target.d.ts +13 -5
  104. package/dist/lib/hosts/run-target.js +26 -10
  105. package/dist/lib/hosts/session-index.d.ts +18 -1
  106. package/dist/lib/hosts/session-index.js +37 -2
  107. package/dist/lib/hosts/session-marker.d.ts +33 -0
  108. package/dist/lib/hosts/session-marker.js +51 -0
  109. package/dist/lib/hosts/tasks.d.ts +8 -4
  110. package/dist/lib/import.d.ts +25 -0
  111. package/dist/lib/import.js +107 -2
  112. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  113. package/dist/lib/isolation-boundary-report.js +35 -0
  114. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  115. package/dist/lib/menubar/install-menubar.js +45 -1
  116. package/dist/lib/migrate.d.ts +16 -0
  117. package/dist/lib/migrate.js +36 -0
  118. package/dist/lib/models.d.ts +27 -0
  119. package/dist/lib/models.js +54 -1
  120. package/dist/lib/overdue.d.ts +7 -2
  121. package/dist/lib/overdue.js +18 -11
  122. package/dist/lib/picker.js +4 -1
  123. package/dist/lib/platform/process.d.ts +17 -0
  124. package/dist/lib/platform/process.js +70 -0
  125. package/dist/lib/profiles-presets.js +9 -7
  126. package/dist/lib/profiles.d.ts +31 -0
  127. package/dist/lib/profiles.js +70 -0
  128. package/dist/lib/project-resources.js +20 -5
  129. package/dist/lib/pty-server.d.ts +2 -10
  130. package/dist/lib/pty-server.js +4 -38
  131. package/dist/lib/registry.d.ts +1 -1
  132. package/dist/lib/registry.js +48 -8
  133. package/dist/lib/rotate.d.ts +18 -0
  134. package/dist/lib/rotate.js +28 -0
  135. package/dist/lib/routines.d.ts +26 -0
  136. package/dist/lib/routines.js +19 -2
  137. package/dist/lib/runner.js +104 -7
  138. package/dist/lib/secrets/agent.d.ts +66 -1
  139. package/dist/lib/secrets/agent.js +154 -90
  140. package/dist/lib/secrets/bundles.d.ts +34 -0
  141. package/dist/lib/secrets/bundles.js +179 -19
  142. package/dist/lib/secrets/index.d.ts +15 -1
  143. package/dist/lib/secrets/index.js +21 -3
  144. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  145. package/dist/lib/secrets/rc-hygiene.js +154 -0
  146. package/dist/lib/secrets/remote.d.ts +7 -4
  147. package/dist/lib/secrets/remote.js +7 -4
  148. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  149. package/dist/lib/secrets/sync-commands.js +21 -0
  150. package/dist/lib/self-update.d.ts +23 -2
  151. package/dist/lib/self-update.js +86 -5
  152. package/dist/lib/session/active.d.ts +114 -7
  153. package/dist/lib/session/active.js +323 -52
  154. package/dist/lib/session/cloud.js +2 -1
  155. package/dist/lib/session/db.d.ts +48 -0
  156. package/dist/lib/session/db.js +238 -14
  157. package/dist/lib/session/detached.d.ts +30 -0
  158. package/dist/lib/session/detached.js +92 -0
  159. package/dist/lib/session/discover.d.ts +465 -2
  160. package/dist/lib/session/discover.js +1648 -468
  161. package/dist/lib/session/fork.js +2 -1
  162. package/dist/lib/session/hook-sessions.d.ts +43 -0
  163. package/dist/lib/session/hook-sessions.js +135 -0
  164. package/dist/lib/session/linear.d.ts +6 -0
  165. package/dist/lib/session/linear.js +55 -0
  166. package/dist/lib/session/migrate-targets.d.ts +65 -0
  167. package/dist/lib/session/migrate-targets.js +94 -0
  168. package/dist/lib/session/migrations.d.ts +37 -0
  169. package/dist/lib/session/migrations.js +60 -0
  170. package/dist/lib/session/parse.d.ts +18 -0
  171. package/dist/lib/session/parse.js +130 -30
  172. package/dist/lib/session/pid-registry.d.ts +34 -2
  173. package/dist/lib/session/pid-registry.js +49 -2
  174. package/dist/lib/session/render.d.ts +7 -2
  175. package/dist/lib/session/render.js +22 -10
  176. package/dist/lib/session/short-id.d.ts +17 -0
  177. package/dist/lib/session/short-id.js +20 -0
  178. package/dist/lib/session/state.js +6 -2
  179. package/dist/lib/shims.d.ts +27 -1
  180. package/dist/lib/shims.js +88 -6
  181. package/dist/lib/sqlite.d.ts +3 -2
  182. package/dist/lib/sqlite.js +27 -4
  183. package/dist/lib/staleness/detectors/commands.js +1 -1
  184. package/dist/lib/staleness/detectors/workflows.js +13 -2
  185. package/dist/lib/staleness/writers/commands.js +7 -7
  186. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  187. package/dist/lib/startup/command-registry.d.ts +2 -0
  188. package/dist/lib/startup/command-registry.js +5 -0
  189. package/dist/lib/state.d.ts +1 -1
  190. package/dist/lib/state.js +15 -36
  191. package/dist/lib/teams/agents.d.ts +19 -10
  192. package/dist/lib/teams/agents.js +40 -39
  193. package/dist/lib/types.d.ts +48 -2
  194. package/dist/lib/uninstall.js +1 -45
  195. package/dist/lib/usage.d.ts +33 -1
  196. package/dist/lib/usage.js +172 -12
  197. package/dist/lib/versions.d.ts +22 -2
  198. package/dist/lib/versions.js +98 -10
  199. package/dist/lib/watchdog/log.d.ts +43 -0
  200. package/dist/lib/watchdog/log.js +69 -0
  201. package/dist/lib/watchdog/routine.d.ts +44 -0
  202. package/dist/lib/watchdog/routine.js +69 -0
  203. package/dist/lib/watchdog/runner.d.ts +51 -7
  204. package/dist/lib/watchdog/runner.js +239 -64
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +31 -16
  207. package/dist/lib/workflows.d.ts +16 -0
  208. package/dist/lib/workflows.js +110 -1
  209. package/package.json +1 -1
@@ -4,7 +4,9 @@
4
4
  * Each preset bundles a host CLI, API base URL, default model, and provider
5
5
  * name so users can `agents profiles add kimi` without manual configuration.
6
6
  */
7
- // Model IDs verified against openrouter.ai/api/v1/models on 2026-04-20.
7
+ // Model IDs verified against openrouter.ai/api/v1/models on 2026-07-31
8
+ // (spark presets corrected from the never-served meta/claude-spark-1.1 to the
9
+ // live meta/muse-spark-1.1, confirmed on both OpenRouter and OpenCode id:meta).
8
10
  // Presets target the top-ranked open-source model per provider based on
9
11
  // SWE-bench Verified, LiveCodeBench, HumanEval, and Chatbot Arena rankings.
10
12
  //
@@ -100,18 +102,18 @@ export const PRESETS = [
100
102
  },
101
103
  {
102
104
  name: 'claude-spark',
103
- description: 'Meta Claude Spark 1.1 via OpenRouter inside Claude Code (open alternative). Model: meta/claude-spark-1.1 — free via opencode, now usable in Claude Code UI. HEADLESS-SAFE. For open-claude spark usage.',
105
+ description: 'Meta Muse Spark 1.1 via OpenRouter inside Claude Code (open alternative). Model: meta/muse-spark-1.1 — now usable in the Claude Code UI. HEADLESS-SAFE. For open-claude spark usage.',
104
106
  ...OPENROUTER_AUTH,
105
107
  env: {
106
108
  ANTHROPIC_BASE_URL: OPENROUTER_BASE,
107
- ANTHROPIC_MODEL: 'meta/claude-spark-1.1',
108
- ANTHROPIC_SMALL_FAST_MODEL: 'meta/claude-spark-1.1',
109
+ ANTHROPIC_MODEL: 'meta/muse-spark-1.1',
110
+ ANTHROPIC_SMALL_FAST_MODEL: 'meta/muse-spark-1.1',
109
111
  },
110
112
  },
111
113
  // ----- OpenCode CLI (open-claude harness) -----
112
114
  {
113
115
  name: 'opencode',
114
- description: 'OpenCode default — uses your configured model via opencode auth. Run `opencode auth` to login, then `agents run opencode --model meta/claude-spark-1.1 "prompt"` for spark usage.',
116
+ description: 'OpenCode default — uses your configured model via opencode auth. Run `opencode auth` to login, then `agents run opencode --model meta/muse-spark-1.1 "prompt"` for spark usage.',
115
117
  provider: 'opencode',
116
118
  host: 'opencode',
117
119
  authEnvVar: 'OPENCODE_API_KEY',
@@ -120,13 +122,13 @@ export const PRESETS = [
120
122
  },
121
123
  {
122
124
  name: 'opencode-spark',
123
- description: 'Meta Claude Spark 1.1 via OpenCode (free, headless-safe). Pinned to meta/claude-spark-1.1 — best for open-claude usage with opencode harness.',
125
+ description: 'Meta Muse Spark 1.1 via OpenCode (free, headless-safe). Pinned to meta/muse-spark-1.1 — best for open-claude usage with opencode harness.',
124
126
  provider: 'opencode',
125
127
  host: 'opencode',
126
128
  authEnvVar: 'OPENCODE_API_KEY',
127
129
  authOptional: true,
128
130
  env: {
129
- OPENCODE_MODEL: 'meta/claude-spark-1.1',
131
+ OPENCODE_MODEL: 'meta/muse-spark-1.1',
130
132
  },
131
133
  },
132
134
  {
@@ -19,6 +19,13 @@ export interface Profile {
19
19
  envVar: string;
20
20
  keychainItem: string;
21
21
  };
22
+ /**
23
+ * When true, the host manages its own login and the keychain token named by
24
+ * `auth` is optional: if it is not stored, no auth env var is injected and the
25
+ * run proceeds on the host's own credentials (e.g. `opencode auth`). Without
26
+ * this flag a missing keychain item is a hard error at exec time.
27
+ */
28
+ authOptional?: boolean;
22
29
  description?: string;
23
30
  preset?: string;
24
31
  provider?: string;
@@ -92,6 +99,30 @@ export declare function profileSummary(profile: Profile): ProfileSummary;
92
99
  * for the OpenRouter key exactly once.
93
100
  */
94
101
  export declare function profileFromPreset(profileName: string, preset: Preset, version?: string): Profile;
102
+ /** Return the model-override env var for a host (known hosts mapped; else `<HOST>_MODEL`). */
103
+ export declare function modelEnvKeyForHost(host: AgentId): string;
104
+ /** Return the base-URL env var for a host, or null when the host has no known override var. */
105
+ export declare function baseUrlEnvKeyForHost(host: AgentId): string | null;
106
+ /** Return the auth-token env var for a host, or null when unknown. */
107
+ export declare function authEnvKeyForHost(host: AgentId): string | null;
108
+ /** Options for {@link profileFromHostModel}. */
109
+ export interface HostModelOptions {
110
+ version?: string;
111
+ /** Base URL for OpenAI/Anthropic-compatible hosts (claude, codex). Ignored for hosts without a known var. */
112
+ baseUrl?: string;
113
+ /** Provider label + keychain namespace; only needed when the host requires a token. */
114
+ provider?: string;
115
+ /** Env var the host reads its auth token from; pair with `provider` to attach keychain auth. */
116
+ authEnvVar?: string;
117
+ description?: string;
118
+ }
119
+ /**
120
+ * Build a custom-harness profile from a host CLI + model in one shot, without a
121
+ * preset. The model lands on the host's model env var ({@link modelEnvKeyForHost});
122
+ * auth is attached only when both `provider` and `authEnvVar` are supplied
123
+ * (hosts that manage their own login — e.g. opencode — need neither).
124
+ */
125
+ export declare function profileFromHostModel(name: string, host: AgentId, model: string, opts?: HostModelOptions): Profile;
95
126
  /**
96
127
  * Resolve a profile into the env block that should be injected into the
97
128
  * spawned agent process. Reads the token from keychain at exec time so the
@@ -223,11 +223,75 @@ export function profileFromPreset(profileName, preset, version) {
223
223
  envVar: preset.authEnvVar,
224
224
  keychainItem: keychainItemName(preset.provider),
225
225
  },
226
+ authOptional: preset.authOptional,
226
227
  description: preset.description,
227
228
  preset: preset.name,
228
229
  provider: preset.provider,
229
230
  };
230
231
  }
232
+ /**
233
+ * Env var each host CLI reads to override its model. Mirror of the read-side
234
+ * `MODEL_ENV_KEYS` above, keyed by host so a one-shot `--host <agent> --model
235
+ * <id>` writes the model onto the var that host actually honors.
236
+ */
237
+ const MODEL_ENV_KEY_BY_HOST = {
238
+ claude: 'ANTHROPIC_MODEL',
239
+ opencode: 'OPENCODE_MODEL',
240
+ grok: 'GROK_MODEL',
241
+ gemini: 'GEMINI_MODEL',
242
+ codex: 'OPENAI_MODEL',
243
+ };
244
+ /** Base-URL env var per host, for the OpenAI/Anthropic-compatible hosts where it applies. */
245
+ const BASE_URL_ENV_KEY_BY_HOST = {
246
+ claude: 'ANTHROPIC_BASE_URL',
247
+ codex: 'OPENAI_BASE_URL',
248
+ };
249
+ /** Return the model-override env var for a host (known hosts mapped; else `<HOST>_MODEL`). */
250
+ export function modelEnvKeyForHost(host) {
251
+ return MODEL_ENV_KEY_BY_HOST[host] ?? `${host.toUpperCase()}_MODEL`;
252
+ }
253
+ /** Return the base-URL env var for a host, or null when the host has no known override var. */
254
+ export function baseUrlEnvKeyForHost(host) {
255
+ return BASE_URL_ENV_KEY_BY_HOST[host] ?? null;
256
+ }
257
+ /** Env var each host reads its auth token from, for custom-endpoint (`--auth-provider`) harnesses. */
258
+ const AUTH_ENV_KEY_BY_HOST = {
259
+ claude: 'ANTHROPIC_AUTH_TOKEN',
260
+ codex: 'OPENAI_API_KEY',
261
+ gemini: 'GEMINI_API_KEY',
262
+ grok: 'XAI_API_KEY',
263
+ opencode: 'OPENCODE_API_KEY',
264
+ };
265
+ /** Return the auth-token env var for a host, or null when unknown. */
266
+ export function authEnvKeyForHost(host) {
267
+ return AUTH_ENV_KEY_BY_HOST[host] ?? null;
268
+ }
269
+ /**
270
+ * Build a custom-harness profile from a host CLI + model in one shot, without a
271
+ * preset. The model lands on the host's model env var ({@link modelEnvKeyForHost});
272
+ * auth is attached only when both `provider` and `authEnvVar` are supplied
273
+ * (hosts that manage their own login — e.g. opencode — need neither).
274
+ */
275
+ export function profileFromHostModel(name, host, model, opts = {}) {
276
+ const env = { [modelEnvKeyForHost(host)]: model };
277
+ if (opts.baseUrl) {
278
+ const key = baseUrlEnvKeyForHost(host);
279
+ if (key)
280
+ env[key] = opts.baseUrl;
281
+ }
282
+ const profile = {
283
+ name,
284
+ host: { agent: host, version: opts.version },
285
+ env,
286
+ description: opts.description ?? `Custom harness: ${host} + ${model}`,
287
+ provider: opts.provider ?? host,
288
+ };
289
+ if (opts.provider && opts.authEnvVar) {
290
+ profile.auth = { envVar: opts.authEnvVar, keychainItem: keychainItemName(opts.provider) };
291
+ profile.authOptional = false;
292
+ }
293
+ return profile;
294
+ }
231
295
  /**
232
296
  * Resolve a profile into the env block that should be injected into the
233
297
  * spawned agent process. Reads the token from keychain at exec time so the
@@ -236,6 +300,12 @@ export function profileFromPreset(profileName, preset, version) {
236
300
  export function resolveProfileEnv(profile) {
237
301
  const env = { ...profile.env };
238
302
  if (profile.auth) {
303
+ // Optional auth (host manages its own login) with no stored token: inject
304
+ // nothing and let the host use its own credentials. Only required auth
305
+ // hard-fails on a missing keychain item.
306
+ if (profile.authOptional && !hasKeychainToken(profile.auth.keychainItem)) {
307
+ return env;
308
+ }
239
309
  const token = getKeychainToken(profile.auth.keychainItem);
240
310
  env[profile.auth.envVar] = token;
241
311
  }
@@ -41,7 +41,7 @@ function loadProjectManifest(agentRoot) {
41
41
  return null;
42
42
  if (!raw.paths.every((p) => typeof p === 'string' && p.length > 0))
43
43
  return null;
44
- return raw;
44
+ return { ...raw, paths: raw.paths.map(toPosixRel) };
45
45
  }
46
46
  catch {
47
47
  return null;
@@ -106,10 +106,22 @@ function projectEntries(projectAgentsDir, kind) {
106
106
  return [];
107
107
  }
108
108
  }
109
+ /**
110
+ * Manifest paths are persisted to `.agents-managed.json`, which lives in the
111
+ * version-controlled project dir and therefore travels between machines. Store
112
+ * them POSIX-style: `path.join` yields `skills\myskill` on Windows, and a
113
+ * manifest carrying that would silently fail to match — and so fail to clean up
114
+ * its managed files — when the same project is synced on macOS or Linux.
115
+ * Normalizing on both write and read also repairs manifests written by earlier
116
+ * Windows builds.
117
+ */
118
+ function toPosixRel(rel) {
119
+ return rel.replace(/\\/g, '/');
120
+ }
109
121
  function record(kind, name, relPaths, result, manifestPaths) {
110
122
  result.synced.push(`${kind}/${name}`);
111
123
  for (const rel of relPaths)
112
- manifestPaths.add(rel);
124
+ manifestPaths.add(toPosixRel(rel));
113
125
  }
114
126
  function skip(dest, projectRoot, result) {
115
127
  const rel = path.relative(projectRoot, dest);
@@ -209,8 +221,8 @@ function syncProjectSubagents(agent, version, projectAgentsDir, projectRoot, age
209
221
  return;
210
222
  const all = readProjectSubagents(projectAgentsDir);
211
223
  const dir = target.dir(projectRoot);
212
- const targetDirRel = path.relative(agentRoot, dir);
213
- const hadManagedTarget = manifest?.paths.some((rel) => rel === targetDirRel || rel.startsWith(targetDirRel + path.sep)) ?? false;
224
+ const targetDirRel = toPosixRel(path.relative(agentRoot, dir));
225
+ const hadManagedTarget = manifest?.paths.some((rel) => rel === targetDirRel || rel.startsWith(targetDirRel + '/')) ?? false;
214
226
  for (const sub of all.values()) {
215
227
  const occupied = target.occupied(dir, sub.name);
216
228
  const existing = occupied.find((entry) => pathExists(entry.path));
@@ -234,7 +246,10 @@ function syncProjectSubagents(agent, version, projectAgentsDir, projectRoot, age
234
246
  if (target.finalize && (syncedNames.length > 0 || hadManagedTarget)) {
235
247
  target.finalize(dir, syncedNames);
236
248
  for (const entry of target.finalizeOccupied?.(dir) ?? []) {
237
- manifestPaths.add(path.relative(agentRoot, entry.path));
249
+ // Same normalization as record(): this is the one manifest write that
250
+ // doesn't funnel through it (the parent subagent index, e.g. Kimi's
251
+ // agents/_agents-cli.yaml).
252
+ manifestPaths.add(toPosixRel(path.relative(agentRoot, entry.path)));
238
253
  }
239
254
  }
240
255
  }
@@ -8,16 +8,8 @@
8
8
  *
9
9
  * Protocol: newline-delimited JSON over ~/.agents/.system/pty.sock
10
10
  */
11
- /**
12
- * Capture a stable identifier for a process at the moment it was started.
13
- * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
14
- * still occupies the PID we observed at spawn time.
15
- *
16
- * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
17
- * macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
18
- * Returns null on any error so callers can skip the guard rather than crash.
19
- */
20
- export declare function captureProcessStartTime(pid: number): string | null;
11
+ import { captureProcessStartTime } from './platform/index.js';
12
+ export { captureProcessStartTime };
21
13
  /**
22
14
  * Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
23
15
  * finishes — that line drives completion detection in the exec/read flow.
@@ -12,46 +12,12 @@ import * as net from 'net';
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import * as crypto from 'crypto';
15
- import { execFileSync } from 'child_process';
16
15
  import { fileURLToPath } from 'url';
17
16
  import { getPtyDir as getPtyDirRoot } from './state.js';
18
- import { isAlive } from './platform/index.js';
19
- /**
20
- * Capture a stable identifier for a process at the moment it was started.
21
- * Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
22
- * still occupies the PID we observed at spawn time.
23
- *
24
- * Linux: field 22 of /proc/<pid>/stat (starttime in clock ticks since boot).
25
- * macOS: output of `ps -o lstart= -p <pid>` (start time in human format).
26
- * Returns null on any error so callers can skip the guard rather than crash.
27
- */
28
- export function captureProcessStartTime(pid) {
29
- if (!pid || pid <= 0)
30
- return null;
31
- try {
32
- if (process.platform === 'linux') {
33
- const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
34
- // The comm field (#2) is wrapped in parens and may contain spaces, so
35
- // split off everything after the last `)` to get a clean field list.
36
- const lastParen = stat.lastIndexOf(')');
37
- if (lastParen < 0)
38
- return null;
39
- const tail = stat.slice(lastParen + 2);
40
- const fields = tail.split(' ');
41
- // After comm we are at field 3; starttime is field 22, so index 19 here.
42
- return fields[19] || null;
43
- }
44
- const out = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
45
- encoding: 'utf-8',
46
- stdio: ['ignore', 'pipe', 'ignore'],
47
- });
48
- const trimmed = out.trim();
49
- return trimmed.length > 0 ? trimmed : null;
50
- }
51
- catch {
52
- return null;
53
- }
54
- }
17
+ import { isAlive, captureProcessStartTime } from './platform/index.js';
18
+ // Re-exported for the existing importers of this module; the implementation is
19
+ // in platform/process.ts, shared with teams/agents.ts so the two cannot drift.
20
+ export { captureProcessStartTime };
55
21
  // --- Constants ---
56
22
  const IS_WINDOWS = process.platform === 'win32';
57
23
  const SENTINEL = '__AGENTS_PTY_DONE__';
@@ -17,7 +17,7 @@ export declare function getEnabledRegistries(type: RegistryType): Array<{
17
17
  }>;
18
18
  /** Add or update a registry configuration in agents.yaml. */
19
19
  export declare function setRegistry(type: RegistryType, name: string, config: Partial<RegistryConfig>): void;
20
- /** Remove a user-configured registry. Returns false if it did not exist. */
20
+ /** Remove a user-configured or seeded registry. Returns false if it did not exist. */
21
21
  export declare function removeRegistry(type: RegistryType, name: string): boolean;
22
22
  /** Search MCP registries for servers matching a query string. */
23
23
  export declare function searchMcpRegistries(query: string, options?: {
@@ -8,7 +8,7 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import { createHash } from 'crypto';
11
- import { DEFAULT_REGISTRIES } from './types.js';
11
+ import { DEFAULT_REGISTRIES, SEEDED_REGISTRIES } from './types.js';
12
12
  import { readMeta, writeMeta } from './state.js';
13
13
  import { discoverSkillsFromRepo } from './skills.js';
14
14
  const UNSAFE_PACKAGE_SPEC_CHARS = /[;&|`$\s\x00-\x1f\x7f]/;
@@ -26,13 +26,38 @@ export function validatedPyPISpec(spec) {
26
26
  }
27
27
  return spec;
28
28
  }
29
+ /**
30
+ * Seeded presets offered for `type`, minus any the user has removed.
31
+ *
32
+ * These are resolved here rather than written into agents.yaml. Seeding used to
33
+ * happen on the state READ path and persisted the entry — but agents.yaml is
34
+ * git-tracked in the user's DotAgents repo, so that write left the working tree
35
+ * dirty and every `agents repo pull` aborted with "Working tree has uncommitted
36
+ * changes", naming neither the file nor the cause. Since every `agents`
37
+ * invocation reads state, the dirt returned the instant it was cleared, and the
38
+ * loop could not be escaped through the CLI at all (RUSH-1925).
39
+ *
40
+ * `seededPresets` is the removal tombstone: a key listed there means the user
41
+ * removed that preset, so it is not offered again. Users who were seeded under
42
+ * the old behaviour have the key listed *and* the entry in their own
43
+ * `registries:` block, which still wins below — so nothing disappears for them.
44
+ */
45
+ function offeredSeeds(type, meta) {
46
+ const removed = new Set(meta.seededPresets || []);
47
+ const offered = {};
48
+ for (const [name, config] of Object.entries(SEEDED_REGISTRIES[type] || {})) {
49
+ if (!removed.has(`${type}.${name}`))
50
+ offered[name] = { ...config };
51
+ }
52
+ return offered;
53
+ }
29
54
  /** Get all registries of a given type, merging defaults with user overrides. */
30
55
  export function getRegistries(type) {
31
56
  const meta = readMeta();
32
57
  const defaultRegs = DEFAULT_REGISTRIES[type] || {};
33
58
  const userRegs = meta.registries?.[type] || {};
34
59
  // Merge defaults with user config (user overrides defaults)
35
- return { ...defaultRegs, ...userRegs };
60
+ return { ...defaultRegs, ...offeredSeeds(type, meta), ...userRegs };
36
61
  }
37
62
  /** Get only the enabled registries of a given type. */
38
63
  export function getEnabledRegistries(type) {
@@ -50,19 +75,34 @@ export function setRegistry(type, name, config) {
50
75
  if (!meta.registries[type]) {
51
76
  meta.registries[type] = {};
52
77
  }
53
- const existing = meta.registries[type][name] || DEFAULT_REGISTRIES[type]?.[name];
78
+ // Seeded presets are resolved in memory and never materialized in agents.yaml
79
+ // (see offeredSeeds), so a partial update — `registry disable`, `enable`, or
80
+ // `config --api-key` — has nothing stored to merge with. Without the
81
+ // SEEDED_REGISTRIES fallback it would persist only the changed fields and drop
82
+ // `url`, and because userRegs wins over the in-memory seed in getRegistries the
83
+ // preset would stay broken even after re-enabling.
84
+ const existing = meta.registries[type][name]
85
+ || DEFAULT_REGISTRIES[type]?.[name]
86
+ || SEEDED_REGISTRIES[type]?.[name];
54
87
  meta.registries[type][name] = { ...existing, ...config };
55
88
  writeMeta(meta);
56
89
  }
57
- /** Remove a user-configured registry. Returns false if it did not exist. */
90
+ /** Remove a user-configured or seeded registry. Returns false if it did not exist. */
58
91
  export function removeRegistry(type, name) {
59
92
  const meta = readMeta();
60
- if (meta.registries?.[type]?.[name]) {
93
+ const inUserConfig = !!meta.registries?.[type]?.[name];
94
+ // A seeded preset has no entry in agents.yaml until the user edits it, so
95
+ // removal is recorded as a tombstone in seededPresets instead of a deletion.
96
+ const isOfferedSeed = !!offeredSeeds(type, meta)[name];
97
+ if (!inUserConfig && !isOfferedSeed)
98
+ return false;
99
+ if (inUserConfig)
61
100
  delete meta.registries[type][name];
62
- writeMeta(meta);
63
- return true;
101
+ if (SEEDED_REGISTRIES[type]?.[name]) {
102
+ meta.seededPresets = [...new Set([...(meta.seededPresets || []), `${type}.${name}`])];
64
103
  }
65
- return false;
104
+ writeMeta(meta);
105
+ return true;
66
106
  }
67
107
  /**
68
108
  * Cap every registry network call. Without this a slow or unreachable registry
@@ -126,6 +126,24 @@ export declare function pickBalancedCandidate(candidates: RotateCandidate[]): Ro
126
126
  */
127
127
  export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null): RotateResult | null;
128
128
  export declare function collectRunCandidates(agent: AgentId): Promise<RotateCandidate[]>;
129
+ /**
130
+ * Resolve an account identity to the installed version slot that holds it, over
131
+ * an already-collected candidate list. Pure — no I/O — so it is unit-tested
132
+ * directly. Matches, case-insensitively, against a candidate's login `email`
133
+ * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
134
+ * Returns null when nothing matches, so the caller can fall back and warn.
135
+ */
136
+ export declare function matchAccountVersion(candidates: RotateCandidate[], account: string): string | null;
137
+ /**
138
+ * Resolve a routine's `account:` pin (login email or account key) to the
139
+ * installed version currently holding that account. Thin I/O wrapper over
140
+ * {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
141
+ * no signed-in version matches. Pinning a routine to a distinct account is the
142
+ * durable cure for the shared-single-use-refresh-token revocation storm
143
+ * (RUSH-1957): the pinned run never rotates and never lands on another
144
+ * routine's credential.
145
+ */
146
+ export declare function resolveAccountVersion(agent: AgentId, account: string): Promise<string | null>;
129
147
  /**
130
148
  * Pick a healthy version for `agent` using weighted random by remaining
131
149
  * capacity. See `pickBalancedCandidate` for algorithm details.
@@ -316,6 +316,34 @@ export async function collectRunCandidates(agent) {
316
316
  };
317
317
  });
318
318
  }
319
+ /**
320
+ * Resolve an account identity to the installed version slot that holds it, over
321
+ * an already-collected candidate list. Pure — no I/O — so it is unit-tested
322
+ * directly. Matches, case-insensitively, against a candidate's login `email`
323
+ * (the usual form) or its `accountKey`, and only ever returns a signed-in slot.
324
+ * Returns null when nothing matches, so the caller can fall back and warn.
325
+ */
326
+ export function matchAccountVersion(candidates, account) {
327
+ const needle = account.trim().toLowerCase();
328
+ if (!needle)
329
+ return null;
330
+ const match = candidates.find((c) => c.signedIn &&
331
+ (c.email?.toLowerCase() === needle || c.accountKey?.toLowerCase() === needle));
332
+ return match?.version ?? null;
333
+ }
334
+ /**
335
+ * Resolve a routine's `account:` pin (login email or account key) to the
336
+ * installed version currently holding that account. Thin I/O wrapper over
337
+ * {@link collectRunCandidates} + {@link matchAccountVersion}; returns null when
338
+ * no signed-in version matches. Pinning a routine to a distinct account is the
339
+ * durable cure for the shared-single-use-refresh-token revocation storm
340
+ * (RUSH-1957): the pinned run never rotates and never lands on another
341
+ * routine's credential.
342
+ */
343
+ export async function resolveAccountVersion(agent, account) {
344
+ const candidates = await collectRunCandidates(agent);
345
+ return matchAccountVersion(candidates, account);
346
+ }
319
347
  /**
320
348
  * Pick a healthy version for `agent` using weighted random by remaining
321
349
  * capacity. See `pickBalancedCandidate` for algorithm details.
@@ -111,6 +111,25 @@ export interface JobConfig {
111
111
  allow?: JobAllowConfig;
112
112
  config?: Record<string, unknown>;
113
113
  version?: string;
114
+ /**
115
+ * Pin this routine to a signed-in account by identity (its login email, or its
116
+ * account key) instead of rotating. At launch it resolves to whichever
117
+ * installed version currently holds that account and runs pinned — no
118
+ * `balanced` rotation, no usage-read refresh, no failover onto other accounts.
119
+ *
120
+ * This is the durable way to keep concurrent unattended routines off a *shared*
121
+ * Claude OAuth credential: the refresh token is single-use and rotates
122
+ * server-side on every refresh, so two routines running the same account
123
+ * concurrently (on one box or across the fleet) mutually revoke each other —
124
+ * the `401 OAuth access token has been revoked` storm (RUSH-1957). Give each
125
+ * routine (or each device's routines) a distinct account and no run rotates a
126
+ * credential out from under another.
127
+ *
128
+ * Prefer this over `version:`, which pins a version *number* that is GC'd on
129
+ * the next agent upgrade — when the pinned version disappears the routine
130
+ * silently falls back to `balanced` and the stampede returns.
131
+ */
132
+ account?: string;
114
133
  runOnce?: boolean;
115
134
  endAt?: string;
116
135
  /**
@@ -251,6 +270,13 @@ export declare function parseTimeout(timeout: string): number | null;
251
270
  export declare function listRuns(jobName: string): RunMeta[];
252
271
  /** Get the most recent run for a job, or null if never run. */
253
272
  export declare function getLatestRun(jobName: string): RunMeta | null;
273
+ /**
274
+ * Get the most recent COMPLETED run for a job, or null if none has completed.
275
+ * Used to resolve `{last_report}` so a failed run's error text (e.g. an auth
276
+ * login prompt written into report.md) can never be injected into the next
277
+ * run's prompt.
278
+ */
279
+ export declare function getLatestCompletedRun(jobName: string): RunMeta | null;
254
280
  /** Persist run metadata to its run directory as meta.json. */
255
281
  export declare function writeRunMeta(meta: RunMeta): void;
256
282
  /** Read run metadata from disk. Returns null if missing or corrupt. */
@@ -536,8 +536,11 @@ export function resolveJobPrompt(config) {
536
536
  prompt = prompt.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
537
537
  }
538
538
  }
539
- // Last report (special handling)
540
- const latestRun = getLatestRun(config.name);
539
+ // Last report (special handling). Only a COMPLETED run's report is injected —
540
+ // a failed run's report.md is the agent's error text (e.g. a login prompt on
541
+ // an auth failure), and feeding that into the next prompt poisons every
542
+ // subsequent run until a human intervenes.
543
+ const latestRun = getLatestCompletedRun(config.name);
541
544
  if (latestRun) {
542
545
  const reportPath = path.join(getJobRunsDir(config.name), latestRun.runId, 'report.md');
543
546
  if (fs.existsSync(reportPath)) {
@@ -595,6 +598,20 @@ export function getLatestRun(jobName) {
595
598
  const runs = listRuns(jobName);
596
599
  return runs.length > 0 ? runs[runs.length - 1] : null;
597
600
  }
601
+ /**
602
+ * Get the most recent COMPLETED run for a job, or null if none has completed.
603
+ * Used to resolve `{last_report}` so a failed run's error text (e.g. an auth
604
+ * login prompt written into report.md) can never be injected into the next
605
+ * run's prompt.
606
+ */
607
+ export function getLatestCompletedRun(jobName) {
608
+ const runs = listRuns(jobName);
609
+ for (let i = runs.length - 1; i >= 0; i--) {
610
+ if (runs[i].status === 'completed')
611
+ return runs[i];
612
+ }
613
+ return null;
614
+ }
598
615
  /** Persist run metadata to its run directory as meta.json. */
599
616
  export function writeRunMeta(meta) {
600
617
  ensureAgentsDir();