@phnx-labs/agents-cli 1.22.33 → 1.22.35

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 (81) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +9 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/accounts.js +3 -3
  5. package/dist/commands/browser-sessions-picker.d.ts +16 -0
  6. package/dist/commands/browser-sessions-picker.js +179 -0
  7. package/dist/commands/browser.js +9 -4
  8. package/dist/commands/hosts.js +1 -5
  9. package/dist/commands/inspect.js +174 -41
  10. package/dist/commands/message.d.ts +6 -1
  11. package/dist/commands/message.js +60 -3
  12. package/dist/commands/sessions.d.ts +54 -4
  13. package/dist/commands/sessions.js +252 -46
  14. package/dist/commands/share.d.ts +18 -0
  15. package/dist/commands/share.js +108 -0
  16. package/dist/commands/ssh.js +17 -5
  17. package/dist/commands/teams.d.ts +28 -0
  18. package/dist/commands/teams.js +148 -13
  19. package/dist/commands/upgrade.d.ts +7 -0
  20. package/dist/commands/upgrade.js +10 -0
  21. package/dist/commands/watchdog.d.ts +2 -0
  22. package/dist/commands/watchdog.js +112 -27
  23. package/dist/index.js +51 -59
  24. package/dist/lib/agents.js +6 -0
  25. package/dist/lib/browser/sessions-list.d.ts +81 -0
  26. package/dist/lib/browser/sessions-list.js +179 -4
  27. package/dist/lib/codex-policy.d.ts +9 -1
  28. package/dist/lib/codex-policy.js +17 -2
  29. package/dist/lib/daemon.js +45 -5
  30. package/dist/lib/devices/connect.d.ts +33 -0
  31. package/dist/lib/devices/connect.js +61 -3
  32. package/dist/lib/devices/doctor-findings.d.ts +4 -2
  33. package/dist/lib/devices/doctor-findings.js +4 -2
  34. package/dist/lib/exec.js +65 -8
  35. package/dist/lib/help.d.ts +3 -2
  36. package/dist/lib/help.js +4 -0
  37. package/dist/lib/hosts/dispatch.d.ts +2 -32
  38. package/dist/lib/hosts/dispatch.js +6 -61
  39. package/dist/lib/hosts/tasks.d.ts +7 -0
  40. package/dist/lib/hosts/tasks.js +9 -0
  41. package/dist/lib/mailbox-target.d.ts +27 -0
  42. package/dist/lib/mailbox-target.js +21 -0
  43. package/dist/lib/mcp.d.ts +10 -0
  44. package/dist/lib/mcp.js +21 -2
  45. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  46. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  47. package/dist/lib/migrate.d.ts +11 -0
  48. package/dist/lib/migrate.js +40 -0
  49. package/dist/lib/project-key.d.ts +17 -0
  50. package/dist/lib/project-key.js +26 -0
  51. package/dist/lib/project-root.d.ts +47 -0
  52. package/dist/lib/project-root.js +68 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  54. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  55. package/dist/lib/secrets/agent.d.ts +9 -2
  56. package/dist/lib/secrets/agent.js +52 -7
  57. package/dist/lib/secrets/reaper.d.ts +24 -3
  58. package/dist/lib/secrets/reaper.js +55 -6
  59. package/dist/lib/session/discover.js +12 -0
  60. package/dist/lib/session/render.d.ts +2 -0
  61. package/dist/lib/session/render.js +1 -1
  62. package/dist/lib/share/delete.d.ts +93 -0
  63. package/dist/lib/share/delete.js +127 -0
  64. package/dist/lib/shims.js +48 -4
  65. package/dist/lib/startup/command-registry.d.ts +1 -0
  66. package/dist/lib/startup/command-registry.js +11 -3
  67. package/dist/lib/startup/root-command.d.ts +3 -0
  68. package/dist/lib/startup/root-command.js +10 -0
  69. package/dist/lib/teams/agents.d.ts +136 -6
  70. package/dist/lib/teams/agents.js +324 -58
  71. package/dist/lib/teams/worktree.d.ts +39 -2
  72. package/dist/lib/teams/worktree.js +60 -4
  73. package/dist/lib/types.d.ts +11 -0
  74. package/dist/lib/versions.js +2 -2
  75. package/dist/lib/watchdog/history.d.ts +20 -0
  76. package/dist/lib/watchdog/history.js +46 -0
  77. package/dist/lib/watchdog/log.d.ts +16 -1
  78. package/dist/lib/watchdog/log.js +82 -2
  79. package/dist/lib/watchdog/runner.d.ts +12 -0
  80. package/dist/lib/watchdog/runner.js +20 -0
  81. package/package.json +1 -1
package/dist/lib/shims.js CHANGED
@@ -236,6 +236,43 @@ function codexShimLaunchArgs() {
236
236
  ...codexPolicyArgs('edit'),
237
237
  ].map(shellQuote).join(' ');
238
238
  }
239
+ /**
240
+ * The `exec` tail for a generated shim. For most agents this is a plain
241
+ * `exec "$BINARY"<launchArgs> "$@"`.
242
+ *
243
+ * Codex is special: its `workspace-write` sandbox hardcodes any `.agents/` (and
244
+ * `.codex/`) directory read-only, but agents-cli keeps every worktree at
245
+ * `<repo>/.agents/worktrees/<slug>`, so an in-repo build under a static
246
+ * shim would hit `EROFS`. The `agents run codex` path fixes this by adding
247
+ * `<repo>/.agents` to the profile's `workspace_roots` (see codexEditWritableRoots
248
+ * / repoAgentsDirForCwd), but a static shim has no cwd at generation time. So the
249
+ * shim resolves the repo's `.agents` from `$PWD` at RUN time — worktree-aware,
250
+ * mirroring repoAgentsDirForCwd — and passes it via Codex's own `--add-dir`
251
+ * (verified to compose with the `agents-edit` profile). The resolution is inline
252
+ * bash because the shim is the launch hot path and must not spawn Node to compute
253
+ * one directory. `--add-dir` is added only when the resolved `.agents` exists.
254
+ */
255
+ function shimExecTail(agent, launchArgs) {
256
+ if (agent !== 'codex')
257
+ return `exec "$BINARY"${launchArgs} "$@"`;
258
+ return `_repo_agents=""
259
+ case "$PWD" in
260
+ */.agents/worktrees/*) _repo_agents="\${PWD%%/.agents/worktrees/*}/.agents" ;;
261
+ *)
262
+ _d="$PWD"
263
+ # Stop before $HOME so a dotfiles repo at $HOME is not treated as the project
264
+ # root (mirrors repoRootForCwd's home exclusion in project-key.ts).
265
+ while [ -n "$_d" ] && [ "$_d" != "/" ] && [ "$_d" != "$HOME" ]; do
266
+ if [ -e "$_d/.git" ]; then _repo_agents="$_d/.agents"; break; fi
267
+ _d=$(dirname "$_d")
268
+ done
269
+ ;;
270
+ esac
271
+ if [ -n "$_repo_agents" ] && [ -d "$_repo_agents" ]; then
272
+ exec "$BINARY"${launchArgs} --add-dir "$_repo_agents" "$@"
273
+ fi
274
+ exec "$BINARY"${launchArgs} "$@"`;
275
+ }
239
276
  function getAgentsBinForGeneratedShim() {
240
277
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
241
278
  }
@@ -707,7 +744,7 @@ if [ "\$LAUNCH_SKIP" = "0" ]; then
707
744
  "\$AGENTS_BIN" sync --agent "\$AGENT" --agent-version "\$VERSION" --launch --cwd "\$PWD" --quiet 2>/dev/null || true
708
745
  fi
709
746
 
710
- exec "$BINARY"${launchArgs} "$@"
747
+ ${shimExecTail(agent, launchArgs)}
711
748
  `;
712
749
  }
713
750
  /**
@@ -933,7 +970,7 @@ function assertSafeVersion(version) {
933
970
  * KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
934
971
  * The colocated test `shims.isolation-capability.test.ts` enforces this.
935
972
  */
936
- export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode', 'muse'];
973
+ export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'cursor', 'grok', 'kimi', 'opencode', 'muse'];
937
974
  /**
938
975
  * Whether an agent supports a clean `--isolated` install — i.e. its config
939
976
  * location can be redirected by an env var so the isolated copy stays fully
@@ -999,7 +1036,14 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
999
1036
  export XDG_CONFIG_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.config"
1000
1037
  export XDG_DATA_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
1001
1038
  `
1002
- : '';
1039
+ : agent === 'cursor'
1040
+ ? `
1041
+ # Cursor: no config-dir env var. Its OAuth token (the login gate) lives at
1042
+ # $XDG_CONFIG_HOME/cursor/auth.json, so pin XDG_CONFIG_HOME at the version home
1043
+ # to isolate each account's login for direct aliases (parity with buildExecEnv).
1044
+ export XDG_CONFIG_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.config"
1045
+ `
1046
+ : '';
1003
1047
  const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
1004
1048
  // Resolve the binary the same way the main shim does (see generateShimScript).
1005
1049
  // Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
@@ -1087,7 +1131,7 @@ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
1087
1131
  fi
1088
1132
  ${managedEnv}
1089
1133
 
1090
- exec "$BINARY"${launchArgs} "$@"
1134
+ ${shimExecTail(agent, launchArgs)}
1091
1135
  `;
1092
1136
  }
1093
1137
  /**
@@ -98,6 +98,7 @@ export declare const loadPush: ModuleLoader;
98
98
  export declare const loadRepo: ModuleLoader;
99
99
  export declare const loadSetup: ModuleLoader;
100
100
  export declare const loadUninstall: ModuleLoader;
101
+ export declare const loadUpgrade: ModuleLoader;
101
102
  export declare const loadSessions: ModuleLoader;
102
103
  export declare const loadTeams: ModuleLoader;
103
104
  export declare const loadCloud: ModuleLoader;
@@ -19,6 +19,9 @@
19
19
  * `cleanup` subcommand to it), in that order — see commands/prune.ts.
20
20
  */
21
21
  import { Command } from 'commander';
22
+ import { readFileSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { configureRootCommand } from './root-command.js';
22
25
  // One loader per command module. Each dynamically imports the module and hands
23
26
  // back its register function. Kept as named consts so src/index.ts can compose
24
27
  // them into the exact main-branch registration order for the slow path.
@@ -98,6 +101,7 @@ export const loadPush = async () => (await import('../../commands/push.js')).reg
98
101
  export const loadRepo = async () => (await import('../../commands/repo.js')).registerRepoCommands;
99
102
  export const loadSetup = async () => (await import('../../commands/setup.js')).registerSetupCommand;
100
103
  export const loadUninstall = async () => (await import('../../commands/uninstall.js')).registerUninstallCommands;
104
+ export const loadUpgrade = async () => (await import('../../commands/upgrade.js')).registerUpgradeCommand;
101
105
  export const loadSessions = async () => (await import('../../commands/sessions.js')).registerSessionsCommands;
102
106
  export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
103
107
  export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
@@ -247,6 +251,7 @@ export const COMMAND_LOADERS = {
247
251
  repo: [loadRepo],
248
252
  setup: [loadSetup],
249
253
  uninstall: [loadUninstall],
254
+ upgrade: [loadUpgrade],
250
255
  sessions: [loadSessions],
251
256
  // Observe-umbrella alias of sessions --active (same lazy module).
252
257
  roster: [loadSessions],
@@ -263,6 +268,9 @@ export const COMMAND_LOADERS = {
263
268
  mailbox: [loadMailboxes],
264
269
  serve: [loadServe],
265
270
  share: [loadShare],
271
+ // `unshare` is a top-level convenience alias of `share delete` (see
272
+ // commands/share.ts) — same module, registered as its own program.command().
273
+ unshare: [loadShare],
266
274
  audit: [loadAudit],
267
275
  webhook: [loadWebhook],
268
276
  funnel: [loadFunnel],
@@ -272,7 +280,7 @@ export const COMMAND_LOADERS = {
272
280
  /**
273
281
  * Top-level names that {@link COMMAND_LOADERS} does not carry because they are
274
282
  * registered inline in src/index.ts — closures over entry-point state (the
275
- * deprecated aliases and tombstones) plus the internal/upgrade commands. They are
283
+ * deprecated aliases and tombstones) plus the internal command. They are
276
284
  * real commands, so anything that asks "does this command exist?" must count them.
277
285
  */
278
286
  const INLINE_COMMAND_NAMES = [
@@ -284,7 +292,6 @@ const INLINE_COMMAND_NAMES = [
284
292
  'resources', // tombstone -> view --merged
285
293
  'hq', // tombstone
286
294
  '_internal',
287
- 'upgrade',
288
295
  ];
289
296
  /**
290
297
  * Every top-level command name the CLI answers to — the loader table plus the
@@ -318,7 +325,8 @@ export function isKnownTopLevelCommand(name) {
318
325
  * are closures over entry-point state that src/index.ts registers directly.
319
326
  */
320
327
  export async function buildFullCommandTree() {
321
- const program = new Command();
328
+ const packageJson = JSON.parse(readFileSync(fileURLToPath(new URL('../../../package.json', import.meta.url)), 'utf8'));
329
+ const program = configureRootCommand(new Command(), 'agents', packageJson.version);
322
330
  const done = new Set();
323
331
  for (const loaders of Object.values(COMMAND_LOADERS)) {
324
332
  for (const loader of loaders) {
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ /** Configure the public root surface shared by the live CLI and reference generator. */
3
+ export declare function configureRootCommand(program: Command, name: string, version: string): Command;
@@ -0,0 +1,10 @@
1
+ /** Configure the public root surface shared by the live CLI and reference generator. */
2
+ export function configureRootCommand(program, name, version) {
3
+ return program
4
+ .name(name)
5
+ .description('Environment manager for AI agents')
6
+ .version(version)
7
+ .option('--verbose', 'Show startup self-heal details on stderr')
8
+ .helpOption('-h, --help', 'Show help')
9
+ .addHelpCommand(false);
10
+ }
@@ -15,6 +15,45 @@ export declare enum AgentStatus {
15
15
  FAILED = "failed",
16
16
  STOPPED = "stopped"
17
17
  }
18
+ /**
19
+ * The statuses a teammate can never leave — its process has run and finished
20
+ * (or been stopped). Everything else (pending, running) is still live work.
21
+ *
22
+ * This is the ONLY set that retention (cleanupOldAgents) may reap: a `pending`
23
+ * teammate has not launched yet and a `running` one is doing work, so deleting
24
+ * either is data loss. Treating "not running" as "completed" was the RUSH-2356
25
+ * bug — it swept live `pending` `--after` teammates past the 50-record cap.
26
+ */
27
+ export declare const TERMINAL_STATUSES: ReadonlySet<AgentStatus>;
28
+ /** True when a teammate has reached a terminal (completed/failed/stopped) status. */
29
+ export declare function isTerminalStatus(status: AgentStatus): boolean;
30
+ /**
31
+ * One remote teammate's liveness, resolved by a single host probe. Three states,
32
+ * kept distinct on purpose (RUSH-2366):
33
+ * - alive=true → process still running.
34
+ * - exitFilePresent=true → the `.exit` sentinel exists;
35
+ * `exit` is its (possibly empty, mid-write) contents.
36
+ * - alive=false && !exitFilePresent ("GONE") → the process is gone AND the
37
+ * wrapper never recorded a sentinel — it was killed / the box died. There is
38
+ * no exit code coming, so this MUST resolve terminal instead of "running
39
+ * forever". Collapsing GONE into the empty-`.exit` case is exactly the bug
40
+ * that left a dead `--device` teammate RUNNING indefinitely.
41
+ */
42
+ export interface RemoteLivenessSnapshot {
43
+ alive: boolean;
44
+ exit: string | null;
45
+ exitFilePresent: boolean;
46
+ }
47
+ /**
48
+ * The per-teammate shell that emits `<id> <ALIVE|EXITED|GONE> <codeOrEmpty>`.
49
+ * Shared by the batched prefetch (many teammates, one round-trip) and the
50
+ * direct single-teammate probe, so both classify liveness identically.
51
+ * `exitFile` is interpolated UNQUOTED so `$HOME` in the dispatch path expands on
52
+ * the remote shell (shellQuote would defeat the `[ -f ]` test).
53
+ */
54
+ export declare function remoteLivenessSnippet(id: string, exitFile: string, pid: number): string;
55
+ /** Parse one `<STATE> <codeOrEmpty>` reading into a snapshot. */
56
+ export declare function parseRemoteLivenessState(state: string, code: string | undefined): RemoteLivenessSnapshot;
18
57
  /** Task type label for Software Factory workflows. Drives planner fan-out. Optional — teammates without a task_type work exactly as before. */
19
58
  export type TaskType = 'plan' | 'implement' | 'test' | 'review' | 'bugfix' | 'docs';
20
59
  export declare const VALID_TASK_TYPES: readonly TaskType[];
@@ -162,10 +201,7 @@ export declare class AgentProcess {
162
201
  remoteLog: string | null;
163
202
  remoteExit: string | null;
164
203
  remoteLogOffset: number;
165
- remotePollSnapshot: {
166
- alive: boolean;
167
- exit: string | null;
168
- } | null;
204
+ remotePollSnapshot: RemoteLivenessSnapshot | null;
169
205
  private eventsCache;
170
206
  private lastReadPos;
171
207
  private baseDir;
@@ -231,6 +267,13 @@ export declare class AgentProcess {
231
267
  * --active --local` take ~4.3s on a box with 30 completed teammates.
232
268
  */
233
269
  private syncRemoteMirror;
270
+ /**
271
+ * One-shot direct liveness probe for a single remote teammate — the fallback
272
+ * used outside a batched supervisor wave (a bare `teams status`, `mgr.get()`
273
+ * for `teams resume`). Returns null on a transient ssh failure so the caller
274
+ * leaves the teammate RUNNING rather than reaping it on a dropped connection.
275
+ */
276
+ private probeRemoteLiveness;
234
277
  /** Reset the local stdout cursor for a newly truncated resume log. */
235
278
  resetLogReadPosition(): number;
236
279
  /** Restore the cursor when a resume transaction puts the prior log back. */
@@ -263,6 +306,24 @@ export declare class AgentProcess {
263
306
  saveMeta(): Promise<void>;
264
307
  static loadFromDisk(agentId: string, baseDir?: string | null): Promise<AgentProcess | null>;
265
308
  isProcessAlive(): boolean;
309
+ /**
310
+ * Read just the persisted status + completion time from meta.json, without
311
+ * reconstructing the whole teammate. Returns null when there is no readable
312
+ * record on disk. Used to detect that ANOTHER process (a `teams stop`, a
313
+ * sibling supervisor) has already moved this teammate to a terminal status.
314
+ */
315
+ private readDiskStatus;
316
+ /**
317
+ * If this in-memory teammate is still non-terminal but disk already shows a
318
+ * terminal status, adopt the disk state. Returns true when it did.
319
+ *
320
+ * This is the guard against the stale-manager race (RUSH-2366): a long-lived
321
+ * supervisor holding a teammate as `running` must never re-persist that stale
322
+ * `running` over a `stopped`/`failed`/`completed` another process just wrote
323
+ * (e.g. an explicit `teams stop` in a separate CLI invocation). A terminal
324
+ * status is a one-way latch, so disk-terminal always wins over memory-running.
325
+ */
326
+ private adoptDiskTerminalIfNewer;
266
327
  /**
267
328
  * @param opts.skipRemote A `--local` caller (RUSH-2118): a distributed
268
329
  * teammate is never dialed — its in-memory state (already loaded from
@@ -333,6 +394,13 @@ export declare class AgentManager {
333
394
  */
334
395
  private localOnly;
335
396
  private constructorAgentsDir;
397
+ /**
398
+ * One-shot memo of the last `validateAddPreconditions` result, so the
399
+ * command-layer pre-worktree call and spawn()'s own call don't each pay a
400
+ * full `listAll()` status refresh (a round of SSH probes on a `--device`
401
+ * team). Consumed by the first matching call — see that method.
402
+ */
403
+ private validatedAdd;
336
404
  constructor(maxAgents?: number, agentsDir?: string | null, defaultMode?: Mode | null, filterByCwd?: string | null, cleanupAgeDays?: number, localOnly?: boolean);
337
405
  private initialize;
338
406
  private doInitialize;
@@ -350,11 +418,66 @@ export declare class AgentManager {
350
418
  * manager is alive — the supervisor loop calls this each wave so
351
419
  * dynamically-added teammates get picked up.
352
420
  *
353
- * Does not modify or re-load agents already in the cache; that path is
354
- * covered by updateStatusFromProcess() which re-reads stdout.log.
421
+ * For a teammate ALREADY cached, refreshes it only when disk has latched it
422
+ * terminal while the cache still holds it non-terminal — the case where
423
+ * another process (e.g. `agents teams stop` in a separate CLI invocation)
424
+ * moved it to `stopped`/`failed` and this long-lived manager would otherwise
425
+ * never see it and re-persist a stale `running` (RUSH-2366). A still-live
426
+ * cached teammate is left untouched; updateStatusFromProcess() owns that path.
355
427
  */
356
428
  rescanFromDisk(): Promise<number>;
357
429
  private loadExistingAgents;
430
+ /**
431
+ * Validate an add's name uniqueness and `--after` dependency graph, without
432
+ * any side effects. Throws a user-facing error on: a duplicate name, `--after`
433
+ * without `--name`, an unknown dependency, or a cycle. Returns the cleaned
434
+ * (whitespace-filtered) `after` list.
435
+ *
436
+ * Extracted from spawn() so the command layer can run it BEFORE creating a
437
+ * worktree — a rejected add must not leave an orphan `agents/<name>` branch
438
+ * that then breaks the retry with `fatal: a branch ... already exists`
439
+ * (RUSH-2356). spawn() calls it too, so validation lives in exactly one place.
440
+ *
441
+ * The result is cached for exactly ONE subsequent call with the same
442
+ * arguments, which spawn() then consumes. `listByTask()` → `listAll()`
443
+ * refreshes every sibling's status, and on a `--device` team that is a full
444
+ * round of SSH liveness probes — running it twice per `teams add` would
445
+ * double that cost for no gain, since the second pass reads the same snapshot
446
+ * and cannot catch anything the first missed. The cache is single-use so any
447
+ * later spawn (a `teams start --watch` supervisor launching staged teammates)
448
+ * still validates against fresh state and still rejects a duplicate name.
449
+ */
450
+ validateAddPreconditions(taskName: string, name: string | null, after: string[]): Promise<string[]>;
451
+ /**
452
+ * Does any LIVE teammate — in any team — already own `worktreeName`?
453
+ *
454
+ * A RAW disk scan: no status probing, no cache, no `listAll()`. The caller is
455
+ * the `teams add` failure path, where the manager's own status refresh can be
456
+ * the very thing that threw (`cleanupOldAgents()` → `listAll()` →
457
+ * `updateStatusFromProcess()` runs AFTER the staged record is saved), so a
458
+ * check that re-entered that machinery would throw again and answer nothing.
459
+ *
460
+ * `teams add` asks this before removing a worktree, to tell an ORPHAN from
461
+ * someone's live checkout (RUSH-2356). Two deliberate scoping choices:
462
+ *
463
+ * - **Any team, not just the one being added to.** Worktree names are global
464
+ * to the repo but records are per-team, so a same-named worktree owned by
465
+ * another team's teammate must also block the removal.
466
+ * - **Non-terminal records only.** A completed/failed/stopped teammate's
467
+ * worktree was already cleaned up at `teams stop`, and its record lingers
468
+ * until retention reaps it — counting those would leave a genuine orphan
469
+ * branch stranded forever, which is the bug this all exists to fix.
470
+ * - **Fails CLOSED.** This guards a `git worktree remove --force`, so the two
471
+ * errors are not symmetric: a false "claimed" strands an orphan branch that
472
+ * a human can delete, while a false "unclaimed" deletes a live agent's
473
+ * checkout and its uncommitted work. Only `ENOENT` proves absence — no
474
+ * agents dir means no records, and a record with no `meta.json` is not a
475
+ * record. Any other failure (EACCES, EIO, half-written or invalid JSON,
476
+ * a race with a writer) means we could not READ the records, which is not
477
+ * the same as there being none, so it answers `true`. This is deliberate
478
+ * asymmetry, not defensive coding: the caller acts destructively on `false`.
479
+ */
480
+ isWorktreeClaimed(worktreeName: string): Promise<boolean>;
358
481
  spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null, profileName?: string | null, hostName?: string | null, hostTarget?: string | null, repoPath?: string | null): Promise<AgentProcess>;
359
482
  /**
360
483
  * Resume a STOPPED teammate (completed / failed / stopped) by re-entering its
@@ -481,6 +604,13 @@ export declare class AgentManager {
481
604
  }>;
482
605
  listAll(): Promise<AgentProcess[]>;
483
606
  listRunning(): Promise<AgentProcess[]>;
607
+ /**
608
+ * Teammates that have reached a terminal status (completed/failed/stopped) —
609
+ * the ONLY records retention may reap. A `pending` teammate has not launched
610
+ * and a `running` one is working, so neither is "completed"; classifying them
611
+ * as such let cleanupOldAgents sweep live `pending` `--after` teammates past
612
+ * the cap (RUSH-2356). Filter on `isTerminalStatus`, never `!== RUNNING`.
613
+ */
484
614
  listCompleted(): Promise<AgentProcess[]>;
485
615
  listByTask(taskName: string): Promise<AgentProcess[]>;
486
616
  listByParentSession(parentSessionId: string): Promise<AgentProcess[]>;