@phnx-labs/agents-cli 1.20.49 → 1.20.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/exec.js CHANGED
@@ -16,6 +16,7 @@ import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
17
17
  import { sanitizeProcessEnv } from './secrets/bundles.js';
18
18
  import { getShimsDir } from './state.js';
19
+ import { readCodexConfiguredModel } from './shims.js';
19
20
  import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
20
21
  import { recordRunName } from './session/run-names.js';
21
22
  import { mailboxDir, isValidMailboxId } from './mailbox.js';
@@ -610,18 +611,25 @@ export function buildExecCommand(options) {
610
611
  else if (options.sessionId && options.agent === 'claude') {
611
612
  cmd.push('--session-id', options.sessionId);
612
613
  }
613
- // Add model (only if explicitly provided by user)
614
- if (options.model && template.modelFlag) {
614
+ // Add model. Prefer the user's explicit --model. Otherwise, for Codex, fall
615
+ // back to the model configured in the user's active ~/.codex/config.toml:
616
+ // Codex runs under a per-version CODEX_HOME (see buildExecEnv) that may not
617
+ // carry that setting, so without this it silently defaults to gpt-5.3-codex,
618
+ // which a ChatGPT-tier account can't use (HTTP 400). Forwarding keeps the
619
+ // user's default model setup for both `agents run` and `agents teams`.
620
+ const effectiveModel = options.model
621
+ ?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined);
622
+ if (effectiveModel && template.modelFlag) {
615
623
  const effectiveVersion = options.version || resolveVersion(options.agent, options.cwd || process.cwd());
616
624
  if (effectiveVersion) {
617
- const resolved = resolveModel(options.agent, effectiveVersion, options.model);
625
+ const resolved = resolveModel(options.agent, effectiveVersion, effectiveModel);
618
626
  if (resolved.warning) {
619
627
  process.stderr.write(`[agents] ${resolved.warning}\n`);
620
628
  }
621
629
  cmd.push(template.modelFlag, resolved.forwarded);
622
630
  }
623
631
  else {
624
- cmd.push(template.modelFlag, options.model);
632
+ cmd.push(template.modelFlag, effectiveModel);
625
633
  }
626
634
  }
627
635
  // Add JSON output flags if requested
@@ -91,6 +91,20 @@ export async function maybeRunOnHost(command, allArgs) {
91
91
  const spec = REMOTE_PASSTHROUGH[command];
92
92
  if (!spec)
93
93
  return false;
94
+ // Placement, not routing: `teams add`/`teams create` read `--device`/`--devices`
95
+ // (and `--host`/`--hosts`) as WHERE to place a teammate / the team pool — the
96
+ // command itself always runs locally on the orchestrator. Bail before the
97
+ // generic teams routing below so those flags reach the local action. Every
98
+ // other teams subcommand (`status`/`logs`/`stop`/…) keeps `--host` routing.
99
+ // Find the subcommand = the first non-flag token AFTER `teams` (robust to any
100
+ // leading global flags), then bail for the add/create aliases.
101
+ if (command === 'teams') {
102
+ const teamsIdx = allArgs.indexOf('teams');
103
+ const sub = teamsIdx >= 0 ? allArgs.slice(teamsIdx + 1).find((a) => !a.startsWith('-')) : undefined;
104
+ if (sub === 'add' || sub === 'a' || sub === 'create' || sub === 'c' || sub === 'new') {
105
+ return false;
106
+ }
107
+ }
94
108
  // `--device` is a first-class alias of `--host` (mirrors `agents run`); the
95
109
  // device registry is the source of truth for machine identity. Reject a
96
110
  // conflicting pair rather than silently preferring one — same rule as run.
@@ -101,6 +115,11 @@ export async function maybeRunOnHost(command, allArgs) {
101
115
  process.exitCode = 1;
102
116
  return true;
103
117
  }
118
+ // `--devices` / `--hosts` fan out to every registered device locally; don't
119
+ // let a per-host passthrough turn it into a cascading remote fan-out.
120
+ const fleetFlag = allArgs.includes('--devices') || allArgs.includes('--hosts');
121
+ if (fleetFlag)
122
+ return false;
104
123
  const hostName = hostFlag ?? deviceFlag;
105
124
  if (!hostName)
106
125
  return false;
@@ -142,7 +161,17 @@ export async function maybeRunOnHost(command, allArgs) {
142
161
  }
143
162
  return true;
144
163
  }
145
- const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, resolveRemoteOsSync(host.name));
164
+ // Doctor commands probe the agent CLIs; remote POSIX login shells often don't
165
+ // have the agents shims on PATH, which produces false "not installed" negatives.
166
+ // Bootstrap PATH with the canonical shim locations before the remote command.
167
+ // Windows is skipped: PowerShell usually has the shim dir via the install
168
+ // profile, and single-quoted env values would not expand $HOME/$PATH.
169
+ const isDoctorCommand = command === 'doctor' || (command === 'teams' && forwarded[1] === 'doctor');
170
+ const remoteOs = resolveRemoteOsSync(host.name);
171
+ const env = isDoctorCommand && !/^win/i.test((remoteOs ?? '').trim())
172
+ ? { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' }
173
+ : undefined;
174
+ const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, remoteOs, env);
146
175
  const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
147
176
  if (code === 255) {
148
177
  console.error(chalk.red(`${host.name}: unreachable over SSH (asleep, offline, or host key changed?).`) +
@@ -13,6 +13,37 @@
13
13
  * toward `maxPollMs` while the job is idle, so a quiet long-running follow no
14
14
  * longer spawns thousands of ssh processes per hour on the laptop.
15
15
  */
16
+ /**
17
+ * Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
18
+ * appends remote bytes into the local mirror forever; a team can spin 10+ chatty
19
+ * remote teammates, so the orchestrator must keep a bounded window (the full log
20
+ * always lives on the host). The teams remote path (agents.ts readNewEvents) writes
21
+ * its own append into each teammate's `stdout.log`, then truncates that file to its
22
+ * trailing `REMOTE_MIRROR_MAX_BYTES` — the parser has already consumed the bytes
23
+ * (status/digest updated via lastReadPos), so trailing-tail history is dead weight.
24
+ */
25
+ export declare const REMOTE_MIRROR_MAX_BYTES: number;
26
+ /**
27
+ * Pull the new bytes of a remote log since `offset` in ONE ssh round-trip.
28
+ *
29
+ * The teams remote-teammate monitor calls this each poll to advance its offset-tail
30
+ * cursor into the host's log, mirroring only the delta into the local `stdout.log`
31
+ * the stream-json parser consumes. Byte-exact (raw Buffer, no UTF-8 decode) so a
32
+ * multibyte character split at the `tail -c` boundary neither drifts the offset nor
33
+ * renders as U+FFFD — the same discipline `fetchProgress` uses. `bytes.length` is
34
+ * the exact wire count; `newOffset` is `offset + bytes.length`.
35
+ *
36
+ * Returns null on a transient ssh failure (the caller retries next poll without
37
+ * advancing). `remoteLog` is a $HOME-prefixed path with a safe basename; it's
38
+ * shell-quoted defensively even so.
39
+ */
40
+ export declare function pullRemoteLogDelta(target: string, opts: {
41
+ remoteLog: string;
42
+ offset: number;
43
+ }): {
44
+ bytes: Buffer;
45
+ newOffset: number;
46
+ } | null;
16
47
  export interface FollowOptions {
17
48
  remoteLog: string;
18
49
  remoteExit: string;
@@ -19,6 +19,41 @@ import { localLogPath } from './tasks.js';
19
19
  function sleep(ms) {
20
20
  return new Promise((resolve) => setTimeout(resolve, ms));
21
21
  }
22
+ /**
23
+ * Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
24
+ * appends remote bytes into the local mirror forever; a team can spin 10+ chatty
25
+ * remote teammates, so the orchestrator must keep a bounded window (the full log
26
+ * always lives on the host). The teams remote path (agents.ts readNewEvents) writes
27
+ * its own append into each teammate's `stdout.log`, then truncates that file to its
28
+ * trailing `REMOTE_MIRROR_MAX_BYTES` — the parser has already consumed the bytes
29
+ * (status/digest updated via lastReadPos), so trailing-tail history is dead weight.
30
+ */
31
+ export const REMOTE_MIRROR_MAX_BYTES = 512 * 1024;
32
+ /**
33
+ * Pull the new bytes of a remote log since `offset` in ONE ssh round-trip.
34
+ *
35
+ * The teams remote-teammate monitor calls this each poll to advance its offset-tail
36
+ * cursor into the host's log, mirroring only the delta into the local `stdout.log`
37
+ * the stream-json parser consumes. Byte-exact (raw Buffer, no UTF-8 decode) so a
38
+ * multibyte character split at the `tail -c` boundary neither drifts the offset nor
39
+ * renders as U+FFFD — the same discipline `fetchProgress` uses. `bytes.length` is
40
+ * the exact wire count; `newOffset` is `offset + bytes.length`.
41
+ *
42
+ * Returns null on a transient ssh failure (the caller retries next poll without
43
+ * advancing). `remoteLog` is a $HOME-prefixed path with a safe basename; it's
44
+ * shell-quoted defensively even so.
45
+ */
46
+ export function pullRemoteLogDelta(target, opts) {
47
+ // remoteLog is a dispatch-generated `$HOME/.agents/.cache/hosts/<hex>.log` path —
48
+ // interpolate UNQUOTED so the remote shell expands `$HOME` (shellQuote would
49
+ // single-quote it into a literal `$HOME`, and the tail would find nothing). The
50
+ // hex basename is injection-safe, matching fetchProgress below.
51
+ const remote = `tail -c +${opts.offset + 1} ${opts.remoteLog} 2>/dev/null`;
52
+ const res = sshExecRaw(target, remote, { timeoutMs: 20000, multiplex: true });
53
+ if (res.code === null)
54
+ return null; // ssh itself failed / timed out
55
+ return { bytes: res.stdout, newOffset: opts.offset + res.stdout.length };
56
+ }
22
57
  /**
23
58
  * Build the per-task sentinel that separates the log tail from the exit-file
24
59
  * contents in one combined fetch. The task id (8 hex chars) makes collision with
@@ -44,7 +44,7 @@ export declare const HOST_ROUTING_SPECS: StripSpec[];
44
44
  * does not exist). Anything else — including an unknown/absent OS — keeps the
45
45
  * POSIX form, so linux/macos are byte-for-byte unchanged.
46
46
  */
47
- export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string): string;
47
+ export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string, env?: Record<string, string>): string;
48
48
  /** The two remote shell dialects we build commands for. */
49
49
  export type RemoteShell = 'posix' | 'powershell';
50
50
  /**
@@ -61,13 +61,23 @@ export const HOST_ROUTING_SPECS = [
61
61
  * does not exist). Anything else — including an unknown/absent OS — keeps the
62
62
  * POSIX form, so linux/macos are byte-for-byte unchanged.
63
63
  */
64
- export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os) {
64
+ export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os, env) {
65
65
  if (remoteShellFor(os) === 'powershell') {
66
- return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd });
66
+ return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd, env });
67
67
  }
68
68
  const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
69
69
  const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
70
- return `bash -lc ${shellQuote(withCwd)}`;
70
+ if (!env || Object.keys(env).length === 0) {
71
+ return `bash -lc ${shellQuote(withCwd)}`;
72
+ }
73
+ // Prepend env exports so the remote command sees the shims dir even when the
74
+ // login shell hasn't sourced the interactive rc files that usually add it.
75
+ // Values are double-quoted (not single-quoted) so remote variables like
76
+ // $HOME and $PATH are expanded by the login shell.
77
+ const exports = Object.entries(env)
78
+ .map(([k, v]) => `export ${shellQuote(k)}="${v.replace(/[\\"]/g, '\\$&')}"`)
79
+ .join('; ');
80
+ return `bash -lc ${shellQuote(`${exports}; ${withCwd}`)}`;
71
81
  }
72
82
  /**
73
83
  * Pick the remote shell dialect from a recorded OS/platform string. A Windows
@@ -203,6 +203,23 @@ export declare function removeVersionedAlias(agent: AgentId, version: string): b
203
203
  * Check if a versioned alias exists (the on-disk artifact for this platform).
204
204
  */
205
205
  export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
206
+ /**
207
+ * Read the user's configured Codex model from their active `~/.codex/config.toml`.
208
+ *
209
+ * Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
210
+ * pinned to a version whose home config lacks a top-level `model` key silently
211
+ * falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
212
+ * ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
213
+ * before doing any work. The user's model preference lives in whichever
214
+ * version-home was active when they set it (`~/.codex` symlinks to it), so it is
215
+ * NOT visible to a run pinned to a different version. Forwarding it via `--model`
216
+ * keeps the user's "default model setup" regardless of the active version-home,
217
+ * and is concurrency-safe (no file writes) when fanning out many parallel runs.
218
+ *
219
+ * Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
220
+ * a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
221
+ */
222
+ export declare function readCodexConfiguredModel(): string | undefined;
206
223
  /**
207
224
  * Switch the agent's config symlink to point to a specific version.
208
225
  * e.g., ~/.claude -> ~/.agents/versions/claude/2.0.65/home/.claude/
package/dist/lib/shims.js CHANGED
@@ -1008,6 +1008,35 @@ function getAgentConfigPath(agent) {
1008
1008
  const home = process.env.AGENTS_REAL_HOME || os.homedir();
1009
1009
  return agentConfig.configDir.replace(os.homedir(), home);
1010
1010
  }
1011
+ /**
1012
+ * Read the user's configured Codex model from their active `~/.codex/config.toml`.
1013
+ *
1014
+ * Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
1015
+ * pinned to a version whose home config lacks a top-level `model` key silently
1016
+ * falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
1017
+ * ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
1018
+ * before doing any work. The user's model preference lives in whichever
1019
+ * version-home was active when they set it (`~/.codex` symlinks to it), so it is
1020
+ * NOT visible to a run pinned to a different version. Forwarding it via `--model`
1021
+ * keeps the user's "default model setup" regardless of the active version-home,
1022
+ * and is concurrency-safe (no file writes) when fanning out many parallel runs.
1023
+ *
1024
+ * Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
1025
+ * a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
1026
+ */
1027
+ export function readCodexConfiguredModel() {
1028
+ try {
1029
+ const cfg = path.join(getAgentConfigPath('codex'), 'config.toml');
1030
+ const text = fs.readFileSync(cfg, 'utf-8');
1031
+ // Only trust keys before the first [table]; a `model` under [profile.x] is
1032
+ // not the default the CLI uses at top level.
1033
+ const topLevel = text.split(/^\s*\[/m)[0];
1034
+ return topLevel.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1];
1035
+ }
1036
+ catch {
1037
+ return undefined;
1038
+ }
1039
+ }
1011
1040
  /**
1012
1041
  * Get the path to the version's config directory.
1013
1042
  * e.g., ~/.agents/versions/claude/2.0.65/home/.claude/
@@ -92,6 +92,21 @@ export interface SignInAdvisory {
92
92
  * probe). Never flips the authoritative installed/ready column.
93
93
  */
94
94
  export declare function resolveSignInAdvisory(installed: boolean, running: boolean, probeSignedIn: boolean): SignInAdvisory;
95
+ /** One row of `agents teams doctor --json` output. */
96
+ export interface TeamsDoctorEntry {
97
+ installed: boolean;
98
+ path: string | null;
99
+ error: string | null;
100
+ signedIn: boolean | null;
101
+ running: boolean;
102
+ }
103
+ /**
104
+ * Collect the same data `agents teams doctor` prints: per-agent install status,
105
+ * launch health, and advisory sign-in state. Kept in one place so `agents doctor
106
+ * --devices` can run it locally or compare it against remote JSON without
107
+ * duplicating the probe logic.
108
+ */
109
+ export declare function collectTeamsDoctorData(): Promise<Record<string, TeamsDoctorEntry>>;
95
110
  /** Resolve and cache the base directory where teammate process data is stored. */
96
111
  export declare function getAgentsDir(): Promise<string>;
97
112
  /**
@@ -131,6 +146,17 @@ export declare class AgentProcess {
131
146
  cloudBranch: string | null;
132
147
  worktreeName: string | null;
133
148
  worktreePath: string | null;
149
+ hostName: string | null;
150
+ hostTarget: string | null;
151
+ repoPath: string | null;
152
+ remotePid: number | null;
153
+ remoteLog: string | null;
154
+ remoteExit: string | null;
155
+ remoteLogOffset: number;
156
+ remotePollSnapshot: {
157
+ alive: boolean;
158
+ exit: string | null;
159
+ } | null;
134
160
  private eventsCache;
135
161
  private lastReadPos;
136
162
  private baseDir;
@@ -175,7 +201,35 @@ export declare class AgentProcess {
175
201
  * Falls back to null when none are available.
176
202
  */
177
203
  private getLatestEventTime;
204
+ /**
205
+ * For a distributed (remote-host) teammate, pull NEW bytes of the host's log
206
+ * into the LOCAL mirror the parser consumes, advance the remote offset, and
207
+ * resolve terminal status from the remote `.exit` sentinel. Runs BEFORE the
208
+ * local read in readNewEvents(), so the existing stream-json parse path then
209
+ * runs unchanged over the freshly-mirrored bytes.
210
+ *
211
+ * Uses a per-wave batched snapshot (remotePollSnapshot) when the supervisor's
212
+ * one-ssh-per-host pre-pass populated it; otherwise falls back to its own
213
+ * round-trips so a bare `teams status`/`teams logs` is still correct.
214
+ */
215
+ private syncRemoteMirror;
178
216
  readNewEvents(): Promise<void>;
217
+ /**
218
+ * Truncate the local mirror to its trailing REMOTE_MIRROR_MAX_BYTES and reset
219
+ * lastReadPos to the new (smaller) size so the parser doesn't re-read the kept
220
+ * tail. Only trims when over the cap — a normal-length log is untouched.
221
+ */
222
+ private capMirrorToTail;
223
+ /** Cap on the in-memory event backlog kept per remote teammate. */
224
+ private static readonly REMOTE_EVENTS_MAX;
225
+ /**
226
+ * Drop the oldest cached events for a remote teammate once past the cap. The
227
+ * status path only needs recent events (last N messages, recentToolCalls,
228
+ * terminal status) and the getDelta cursor filters by timestamp, so a bounded
229
+ * recent window preserves the digest while bounding the heap. Terminal status
230
+ * is already latched onto `this.status`, so trimming can't lose it.
231
+ */
232
+ private capEventsCache;
179
233
  saveMeta(): Promise<void>;
180
234
  static loadFromDisk(agentId: string, baseDir?: string | null): Promise<AgentProcess | null>;
181
235
  isProcessAlive(): boolean;
@@ -246,12 +300,51 @@ export declare class AgentManager {
246
300
  */
247
301
  rescanFromDisk(): Promise<number>;
248
302
  private loadExistingAgents;
249
- 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): Promise<AgentProcess>;
303
+ 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>;
250
304
  /**
251
305
  * Actually spawn the OS process for a teammate. Extracted from spawn() so
252
306
  * staged teammates can be launched later by startReady().
253
307
  */
254
308
  private launchProcess;
309
+ /**
310
+ * Dispatch a distributed teammate onto its host over SSH — the remote-host
311
+ * analog of launchProcess(). Symmetric to the cloud path: no local process; the
312
+ * lifecycle lives on the host and is polled (isProcessAlive/readNewEvents over
313
+ * SSH via the remote `.exit` sentinel + offset-tailed log).
314
+ *
315
+ * When the team uses worktrees (agent.worktreeName set), a git worktree is first
316
+ * created ON THE HOST off the freshly-fetched default branch; the teammate runs
317
+ * there. Otherwise it runs in the host repo path directly.
318
+ */
319
+ private launchRemoteProcess;
320
+ /**
321
+ * Resolve a scheduler-picked device to host placement fields on an unpinned
322
+ * teammate at LAUNCH time (the same resolution `teams add --device` runs, minus
323
+ * the fatal `die()` — a scheduling failure here is per-teammate, not per-add).
324
+ * Sets hostName/hostTarget/repoPath + persists, so the subsequent
325
+ * launchRemoteProcess dispatches over SSH. Mirrors the `add`-time pin path:
326
+ * resolve device → reject Windows (POSIX-only) → ssh target → ensure the repo
327
+ * is present on the host from the team's --repo (ensureRemoteRepo).
328
+ */
329
+ private resolveScheduledPlacement;
330
+ /**
331
+ * Place an UNPINNED, non-cloud teammate onto the team pool via the cascade
332
+ * (least-loaded), if the team declares one. A no-op for a pinned teammate
333
+ * (hostName already set from `--device`), a cloud teammate, or a poolless team —
334
+ * leaving hostName null so the local spawn runs unchanged. Shared by spawn()
335
+ * (immediate add-launch) and startReady() (staged launch) so an unpinned pool
336
+ * teammate schedules identically no matter how it was fired.
337
+ */
338
+ private maybeSchedulePlacement;
339
+ /**
340
+ * One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
341
+ * The supervisor calls this each wave BEFORE listByTask() so the per-teammate
342
+ * isProcessAlive()/readNewEvents() consume a cached snapshot instead of each
343
+ * issuing its own SSH handshake — avoiding N round-trips per wave at 10+ remote
344
+ * teammates. Groups by hostTarget and, for each host, checks every teammate's
345
+ * `.exit` + `kill -0` in a single ssh call over the shared ControlMaster socket.
346
+ */
347
+ prefetchRemoteStatus(taskName: string): Promise<void>;
255
348
  /**
256
349
  * Fire any pending teammates in the given team whose `after` deps have all
257
350
  * completed. Returns the list of teammates just launched. Repeatable:
@@ -266,6 +359,20 @@ export declare class AgentManager {
266
359
  * exec path (src/lib/exec.ts). The team runner just supplies prompt + mode
267
360
  * and reads stream-json events off stdout.
268
361
  */
362
+ /**
363
+ * Build the `agents run …` argv AFTER the `agents` binary — the flags + prompt
364
+ * scaffolding shared by the LOCAL launch (buildCommand, which prefixes
365
+ * process.execPath + the agents CLI path) and the REMOTE launch
366
+ * (launchRemoteProcess, which prefixes `agents` on the host via dispatch). Kept
367
+ * in one place so the PROMPT_SUFFIX / CLAUDE_PLAN_MODE_PREFIX scaffolding and the
368
+ * flag set can never drift between the two backends.
369
+ *
370
+ * `cwd` is intentionally NOT emitted here: the local path passes it as
371
+ * `--cwd`/`--add-dir` (below), while the remote path `cd`s into the host cwd
372
+ * before invoking `agents`. `sessionId` is likewise local-only (the remote run
373
+ * mints its own session on the host).
374
+ */
375
+ private buildRunArgv;
269
376
  private buildCommand;
270
377
  get(agentId: string): Promise<AgentProcess | null>;
271
378
  /**