@phnx-labs/agents-cli 1.20.74 → 1.20.77

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 (231) hide show
  1. package/CHANGELOG.md +1055 -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 +18 -0
  13. package/dist/commands/exec.js +83 -13
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +286 -15
  26. package/dist/commands/secrets.js +102 -37
  27. package/dist/commands/sessions-browser.js +12 -2
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.d.ts +14 -1
  32. package/dist/commands/sessions-picker.js +184 -16
  33. package/dist/commands/sessions.d.ts +56 -14
  34. package/dist/commands/sessions.js +322 -62
  35. package/dist/commands/setup-computer.js +2 -2
  36. package/dist/commands/ssh.d.ts +13 -0
  37. package/dist/commands/ssh.js +110 -37
  38. package/dist/commands/status.js +10 -2
  39. package/dist/commands/versions.js +15 -6
  40. package/dist/commands/view.d.ts +5 -0
  41. package/dist/commands/view.js +24 -4
  42. package/dist/commands/watchdog.d.ts +7 -2
  43. package/dist/commands/watchdog.js +73 -57
  44. package/dist/index.js +59 -7
  45. package/dist/lib/activity.d.ts +17 -6
  46. package/dist/lib/activity.js +466 -40
  47. package/dist/lib/actor.d.ts +50 -0
  48. package/dist/lib/actor.js +166 -0
  49. package/dist/lib/agent-spec/provider.js +2 -1
  50. package/dist/lib/agent-spec/resolve.js +19 -5
  51. package/dist/lib/agent-spec/types.d.ts +9 -1
  52. package/dist/lib/agents.d.ts +21 -0
  53. package/dist/lib/agents.js +70 -6
  54. package/dist/lib/cloud/codex.d.ts +2 -0
  55. package/dist/lib/cloud/codex.js +14 -3
  56. package/dist/lib/cloud/session-index.d.ts +32 -0
  57. package/dist/lib/cloud/session-index.js +58 -0
  58. package/dist/lib/cloud/store.d.ts +7 -0
  59. package/dist/lib/cloud/store.js +25 -0
  60. package/dist/lib/config-transfer.js +4 -0
  61. package/dist/lib/daemon.d.ts +10 -1
  62. package/dist/lib/daemon.js +114 -11
  63. package/dist/lib/devices/connect.d.ts +15 -1
  64. package/dist/lib/devices/connect.js +15 -1
  65. package/dist/lib/devices/fleet.d.ts +37 -1
  66. package/dist/lib/devices/fleet.js +36 -2
  67. package/dist/lib/devices/health-report.d.ts +38 -0
  68. package/dist/lib/devices/health-report.js +214 -0
  69. package/dist/lib/devices/health.js +4 -1
  70. package/dist/lib/devices/reachability.d.ts +31 -0
  71. package/dist/lib/devices/reachability.js +40 -0
  72. package/dist/lib/devices/registry.d.ts +33 -0
  73. package/dist/lib/devices/registry.js +37 -0
  74. package/dist/lib/devices/resolve-target.d.ts +15 -27
  75. package/dist/lib/devices/resolve-target.js +63 -102
  76. package/dist/lib/devices/sync.d.ts +18 -0
  77. package/dist/lib/devices/sync.js +23 -1
  78. package/dist/lib/devices/tailscale.d.ts +3 -0
  79. package/dist/lib/devices/tailscale.js +1 -0
  80. package/dist/lib/events.d.ts +1 -1
  81. package/dist/lib/exec.d.ts +82 -0
  82. package/dist/lib/exec.js +218 -6
  83. package/dist/lib/feed-post.d.ts +63 -0
  84. package/dist/lib/feed-post.js +204 -0
  85. package/dist/lib/feed.d.ts +7 -1
  86. package/dist/lib/feed.js +96 -6
  87. package/dist/lib/fleet/apply.d.ts +32 -2
  88. package/dist/lib/fleet/apply.js +97 -10
  89. package/dist/lib/fleet/types.d.ts +11 -0
  90. package/dist/lib/fs-walk.d.ts +13 -0
  91. package/dist/lib/fs-walk.js +16 -7
  92. package/dist/lib/heal.d.ts +7 -4
  93. package/dist/lib/heal.js +10 -22
  94. package/dist/lib/hooks.d.ts +1 -0
  95. package/dist/lib/hooks.js +156 -0
  96. package/dist/lib/hosts/dispatch.d.ts +16 -0
  97. package/dist/lib/hosts/dispatch.js +26 -4
  98. package/dist/lib/hosts/passthrough.js +9 -2
  99. package/dist/lib/hosts/reconnect.d.ts +78 -0
  100. package/dist/lib/hosts/reconnect.js +127 -0
  101. package/dist/lib/hosts/registry.d.ts +75 -14
  102. package/dist/lib/hosts/registry.js +205 -30
  103. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  104. package/dist/lib/hosts/remote-cmd.js +32 -8
  105. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  106. package/dist/lib/hosts/remote-session-id.js +84 -0
  107. package/dist/lib/hosts/run-target.d.ts +17 -5
  108. package/dist/lib/hosts/run-target.js +30 -10
  109. package/dist/lib/hosts/session-index.d.ts +18 -1
  110. package/dist/lib/hosts/session-index.js +40 -5
  111. package/dist/lib/hosts/session-marker.d.ts +33 -0
  112. package/dist/lib/hosts/session-marker.js +51 -0
  113. package/dist/lib/hosts/tasks.d.ts +8 -4
  114. package/dist/lib/import.d.ts +2 -0
  115. package/dist/lib/import.js +35 -2
  116. package/dist/lib/menubar/install-menubar.d.ts +29 -0
  117. package/dist/lib/menubar/install-menubar.js +59 -1
  118. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  119. package/dist/lib/menubar/notify-desktop.js +78 -0
  120. package/dist/lib/migrate.d.ts +16 -0
  121. package/dist/lib/migrate.js +36 -0
  122. package/dist/lib/models.d.ts +27 -0
  123. package/dist/lib/models.js +54 -1
  124. package/dist/lib/overdue.d.ts +6 -2
  125. package/dist/lib/overdue.js +10 -36
  126. package/dist/lib/picker.js +4 -1
  127. package/dist/lib/platform/process.d.ts +17 -0
  128. package/dist/lib/platform/process.js +70 -0
  129. package/dist/lib/profiles-presets.js +9 -7
  130. package/dist/lib/profiles.d.ts +31 -0
  131. package/dist/lib/profiles.js +70 -0
  132. package/dist/lib/pty-server.d.ts +2 -10
  133. package/dist/lib/pty-server.js +4 -38
  134. package/dist/lib/registry.d.ts +1 -1
  135. package/dist/lib/registry.js +48 -8
  136. package/dist/lib/rotate.d.ts +18 -0
  137. package/dist/lib/rotate.js +28 -0
  138. package/dist/lib/routine-notify.d.ts +76 -0
  139. package/dist/lib/routine-notify.js +190 -0
  140. package/dist/lib/routines-placement.d.ts +38 -0
  141. package/dist/lib/routines-placement.js +79 -0
  142. package/dist/lib/routines-project.d.ts +97 -0
  143. package/dist/lib/routines-project.js +349 -0
  144. package/dist/lib/routines.d.ts +94 -0
  145. package/dist/lib/routines.js +93 -9
  146. package/dist/lib/runner.d.ts +12 -2
  147. package/dist/lib/runner.js +242 -20
  148. package/dist/lib/sandbox.js +10 -0
  149. package/dist/lib/secrets/account-token.d.ts +20 -0
  150. package/dist/lib/secrets/account-token.js +64 -0
  151. package/dist/lib/secrets/agent.d.ts +74 -4
  152. package/dist/lib/secrets/agent.js +181 -107
  153. package/dist/lib/secrets/bundles.d.ts +54 -6
  154. package/dist/lib/secrets/bundles.js +230 -34
  155. package/dist/lib/secrets/index.d.ts +26 -3
  156. package/dist/lib/secrets/index.js +42 -12
  157. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  158. package/dist/lib/secrets/rc-hygiene.js +154 -0
  159. package/dist/lib/secrets/remote.d.ts +7 -4
  160. package/dist/lib/secrets/remote.js +7 -4
  161. package/dist/lib/secrets/session-store.d.ts +6 -2
  162. package/dist/lib/secrets/session-store.js +65 -15
  163. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  164. package/dist/lib/secrets/sync-commands.js +21 -0
  165. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  166. package/dist/lib/secrets/unlock-hints.js +36 -0
  167. package/dist/lib/self-update.d.ts +23 -2
  168. package/dist/lib/self-update.js +86 -5
  169. package/dist/lib/session/active.d.ts +124 -7
  170. package/dist/lib/session/active.js +390 -56
  171. package/dist/lib/session/cloud.js +2 -1
  172. package/dist/lib/session/db.d.ts +54 -0
  173. package/dist/lib/session/db.js +320 -19
  174. package/dist/lib/session/detached.d.ts +30 -0
  175. package/dist/lib/session/detached.js +92 -0
  176. package/dist/lib/session/discover.d.ts +478 -3
  177. package/dist/lib/session/discover.js +1740 -472
  178. package/dist/lib/session/fork.js +2 -1
  179. package/dist/lib/session/hook-sessions.d.ts +43 -0
  180. package/dist/lib/session/hook-sessions.js +135 -0
  181. package/dist/lib/session/linear.d.ts +6 -0
  182. package/dist/lib/session/linear.js +55 -0
  183. package/dist/lib/session/migrate-targets.d.ts +65 -0
  184. package/dist/lib/session/migrate-targets.js +94 -0
  185. package/dist/lib/session/migrations.d.ts +37 -0
  186. package/dist/lib/session/migrations.js +60 -0
  187. package/dist/lib/session/parse.d.ts +18 -0
  188. package/dist/lib/session/parse.js +141 -32
  189. package/dist/lib/session/pid-registry.d.ts +34 -2
  190. package/dist/lib/session/pid-registry.js +49 -2
  191. package/dist/lib/session/prompt.d.ts +7 -0
  192. package/dist/lib/session/prompt.js +37 -0
  193. package/dist/lib/session/provenance.d.ts +7 -0
  194. package/dist/lib/session/render.d.ts +7 -2
  195. package/dist/lib/session/render.js +40 -26
  196. package/dist/lib/session/short-id.d.ts +17 -0
  197. package/dist/lib/session/short-id.js +20 -0
  198. package/dist/lib/session/state.d.ts +4 -0
  199. package/dist/lib/session/state.js +99 -13
  200. package/dist/lib/session/types.d.ts +25 -1
  201. package/dist/lib/session/types.js +8 -0
  202. package/dist/lib/shims.d.ts +1 -1
  203. package/dist/lib/shims.js +20 -6
  204. package/dist/lib/sqlite.d.ts +3 -2
  205. package/dist/lib/sqlite.js +27 -4
  206. package/dist/lib/staleness/detectors/commands.js +1 -1
  207. package/dist/lib/staleness/detectors/workflows.js +13 -2
  208. package/dist/lib/staleness/writers/commands.js +7 -7
  209. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  210. package/dist/lib/startup/command-registry.d.ts +1 -0
  211. package/dist/lib/startup/command-registry.js +3 -0
  212. package/dist/lib/state.d.ts +8 -4
  213. package/dist/lib/state.js +21 -37
  214. package/dist/lib/teams/agents.d.ts +19 -10
  215. package/dist/lib/teams/agents.js +40 -39
  216. package/dist/lib/types.d.ts +47 -2
  217. package/dist/lib/usage.d.ts +33 -1
  218. package/dist/lib/usage.js +172 -12
  219. package/dist/lib/versions.d.ts +9 -2
  220. package/dist/lib/versions.js +37 -8
  221. package/dist/lib/watchdog/log.d.ts +43 -0
  222. package/dist/lib/watchdog/log.js +69 -0
  223. package/dist/lib/watchdog/routine.d.ts +44 -0
  224. package/dist/lib/watchdog/routine.js +69 -0
  225. package/dist/lib/watchdog/runner.d.ts +51 -7
  226. package/dist/lib/watchdog/runner.js +239 -64
  227. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  228. package/dist/lib/watchdog/watchdog.js +31 -16
  229. package/dist/lib/workflows.d.ts +16 -0
  230. package/dist/lib/workflows.js +110 -1
  231. package/package.json +1 -1
@@ -144,6 +144,15 @@ export interface ExecOptions {
144
144
  * (output is mirrored to the parent's stdout exactly like stdio:'inherit').
145
145
  */
146
146
  captureStdoutTail?: boolean;
147
+ /**
148
+ * Print the run's resolved session id to stdout as a one-line sentinel once the
149
+ * child exits (see hosts/session-marker.ts). Set by the `--host` dispatch so the
150
+ * LAUNCHER can relate the remote-created session back to itself — Claude's id is
151
+ * forced up front, but every other agent coins its own id on the remote box, and
152
+ * this marker is how that id rides the followed log home. Headless-only and inert
153
+ * for interactive runs (no combined log to parse).
154
+ */
155
+ emitSessionId?: boolean;
147
156
  /**
148
157
  * Escape hatch for the interactive tmux spawn-wrap (see shouldWrapInTmux):
149
158
  * when true, spawn the agent directly instead of inside a shared-socket tmux
@@ -188,6 +197,22 @@ export declare function inferredInteractiveWithoutTty(options: Pick<ExecOptions,
188
197
  export declare function shouldTapStdout(interactive: boolean, piped: boolean, capsActive: boolean, captureTail?: boolean): boolean;
189
198
  /** Parse an array of KEY=VALUE strings into an env record. Returns undefined for empty input. */
190
199
  export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
200
+ /**
201
+ * Resolve the launch id a run exports as `AGENT_LAUNCH_ID`.
202
+ *
203
+ * The launch id is the stable correlation key the SessionStart hook records
204
+ * alongside the agent's real session id (terminals/sessions/<pid>.json), so it
205
+ * is what maps a launch to its exact session even when the hook runs under a
206
+ * different pid (tmux pane leaf / cmd.exe wrapper) — and, across an SSH hop, what
207
+ * lets a `--host` launcher resolve the remote-coined id for agents that never
208
+ * accept a forced `--session-id`.
209
+ *
210
+ * ADOPT a caller-supplied `AGENT_LAUNCH_ID` (a `--host` launcher forwards one via
211
+ * `--env` so it controls the key end-to-end); MINT a fresh one otherwise (every
212
+ * local run, which passes none). A malformed inbound value is ignored in favour
213
+ * of a fresh mint — the key must be a real correlation id, never an empty string.
214
+ */
215
+ export declare function resolveLaunchId(envLaunchId: string | undefined): string;
191
216
  /**
192
217
  * Build the process environment for an agent invocation.
193
218
  * Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
@@ -339,6 +364,63 @@ export declare const BUDGET_KILL_EXIT_CODE = 7;
339
364
  export declare const RATE_LIMIT_PATTERNS: RegExp[];
340
365
  /** Return true if the text contains any known rate-limit or overload indicator. */
341
366
  export declare function detectRateLimit(text: string): boolean;
367
+ /**
368
+ * Patterns that indicate an authentication failure — the agent is logged out,
369
+ * its token was revoked, or the session expired. These are the user-visible
370
+ * strings a logged-out agent surfaces (observed across the routine-run corpus).
371
+ * Unlike a rate limit, an auth failure is NOT self-healing by failover — every
372
+ * chain entry on the same account fails identically — so it is classified
373
+ * separately and never triggers a fallback attempt.
374
+ *
375
+ * The bare `401` is deliberately paired with an auth keyword: a plain "401" can
376
+ * appear in legitimate output (an HTTP-status table, a log line), so it only
377
+ * counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
378
+ */
379
+ export declare const AUTH_FAILURE_PATTERNS: RegExp[];
380
+ /**
381
+ * Return true if the text contains any known authentication-failure indicator.
382
+ * Agent-agnostic: matches the user-visible error string wherever it surfaces
383
+ * (stdout tail, a captured error message, a plain-text agent's output).
384
+ */
385
+ export declare function detectAuthFailure(text: string): boolean;
386
+ /**
387
+ * Return true if a stream-json log carries the structural markers of an auth
388
+ * failure. This is the authoritative signal for Claude: a logged-out run emits
389
+ * {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
390
+ * {"type":"assistant",…,"error":"authentication_failed"}
391
+ * {"type":"result","is_error":true,"result":"Failed to authenticate…"}
392
+ * Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
393
+ * reason logic can never catch it — the `error:"authentication_failed"` marker
394
+ * and the `result`+`is_error` text are the reliable signals.
395
+ *
396
+ * Gated on the Claude stream-json shape; other agents don't emit these fields,
397
+ * so callers pass their agent and this returns false for non-claude.
398
+ */
399
+ export declare function detectAuthFailureEvent(logText: string, agent: AgentId): boolean;
400
+ /**
401
+ * Return the first human-readable auth-failure phrase found in the text, for use
402
+ * as a stable, short run `errorMessage` reason. Falls back to null when the only
403
+ * signal was the structural `error:"authentication_failed"` marker with no
404
+ * user-visible string.
405
+ */
406
+ export declare function authFailureReason(text: string): string | null;
407
+ /**
408
+ * Decide whether a run's stream-json log is an authentication failure. The
409
+ * single source of truth for both the foreground and detached run paths.
410
+ *
411
+ * The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
412
+ * authoritative and safe on ANY exit — it catches a logged-out Claude that exits
413
+ * 0 (its `result` event carries `is_error:true` while `terminal_reason` is
414
+ * "completed"). The raw user-visible string (`detectAuthFailure`) is only
415
+ * consulted when the process actually FAILED, as a fallback for a run that died
416
+ * mid-stream before emitting a `result` event. That gate is what prevents a
417
+ * genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
418
+ * routine documenting a login flow) from being misclassified and having its
419
+ * legitimate report suppressed.
420
+ */
421
+ export declare function isAuthFailureFromLog(logText: string, agent: AgentId, opts: {
422
+ processFailed: boolean;
423
+ }): boolean;
342
424
  /** An agent (with optional pinned version) in a fallback chain. */
343
425
  export interface FallbackEntry {
344
426
  agent: AgentId;
package/dist/lib/exec.js CHANGED
@@ -15,10 +15,13 @@ import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion }
15
15
  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
+ import { resolveActor, actorEnv } from './actor.js';
18
19
  import { getShimsDir, getHistoryDir } from './state.js';
19
20
  import { resolveCodexHome } from './codex-home.js';
20
21
  import { readCodexConfiguredModel } from './shims.js';
21
22
  import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
23
+ import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
24
+ import { sessionIdMarkerLine } from './hosts/session-marker.js';
22
25
  import { recordRunName } from './session/run-names.js';
23
26
  import { mailboxDir, isValidMailboxId } from './mailbox.js';
24
27
  import { composeWin32CommandLine } from './platform/index.js';
@@ -215,6 +218,25 @@ export function parseExecEnv(entries) {
215
218
  }
216
219
  return Object.fromEntries(entries.map(parseExecEnvEntry));
217
220
  }
221
+ /**
222
+ * Resolve the launch id a run exports as `AGENT_LAUNCH_ID`.
223
+ *
224
+ * The launch id is the stable correlation key the SessionStart hook records
225
+ * alongside the agent's real session id (terminals/sessions/<pid>.json), so it
226
+ * is what maps a launch to its exact session even when the hook runs under a
227
+ * different pid (tmux pane leaf / cmd.exe wrapper) — and, across an SSH hop, what
228
+ * lets a `--host` launcher resolve the remote-coined id for agents that never
229
+ * accept a forced `--session-id`.
230
+ *
231
+ * ADOPT a caller-supplied `AGENT_LAUNCH_ID` (a `--host` launcher forwards one via
232
+ * `--env` so it controls the key end-to-end); MINT a fresh one otherwise (every
233
+ * local run, which passes none). A malformed inbound value is ignored in favour
234
+ * of a fresh mint — the key must be a real correlation id, never an empty string.
235
+ */
236
+ export function resolveLaunchId(envLaunchId) {
237
+ const inbound = envLaunchId?.trim();
238
+ return inbound ? inbound : randomUUID();
239
+ }
218
240
  /**
219
241
  * Build the process environment for an agent invocation.
220
242
  * Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
@@ -312,14 +334,29 @@ export function buildExecEnv(options) {
312
334
  // iterations share one inbox.
313
335
  if (options.sessionId && isValidMailboxId(options.sessionId)) {
314
336
  result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
337
+ // Full session id for agent-callable tools (`agents feed post`, etc.).
338
+ result.AGENT_SESSION_ID = options.sessionId;
339
+ result.AGENTS_SESSION_ID = options.sessionId;
315
340
  }
316
341
  result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
342
+ // So activity / feed posts stamp the right harness without re-detecting.
343
+ if (options.agent) {
344
+ result.AGENTS_AGENT_NAME = options.agent;
345
+ }
346
+ if (options.cwd) {
347
+ result.AGENTS_CWD = options.cwd;
348
+ }
317
349
  // Export the run's durable name (companion to AGENT_SESSION_ID) so a
318
350
  // SessionStart hook / the agent can associate its transcript with the handle
319
351
  // the user gave the run. Only set when --name was passed.
320
352
  if (options.name) {
321
353
  result.AGENT_SESSION_NAME = options.name;
322
354
  }
355
+ // Actor provenance -- who initiated this run. Rides the env so the whole spawn
356
+ // tree shares one actor, and (for a resolved human) so the agent's own git
357
+ // commits are credited to the person instead of the shared account. options.env
358
+ // (spread last) overrides any of these keys a caller sets explicitly.
359
+ Object.assign(result, actorEnv(resolveActor()));
323
360
  return {
324
361
  ...result,
325
362
  ...options.env,
@@ -666,10 +703,16 @@ export function buildExecCommand(options) {
666
703
  else {
667
704
  cmd.push(...modeFlags);
668
705
  }
669
- // Add print/headless flags only when a prompt is provided. Without a prompt
670
- // the caller wants an interactive REPL -- passing --print would immediately
671
- // wait on stdin and never render the TUI.
672
- if (!interactive && options.headless && template.printFlags) {
706
+ // Add print/headless flags whenever the run RESOLVED headless -- gate on the
707
+ // resolved state (`!interactive`), not the raw `--headless` flag. Headless is
708
+ // inferred from prompt presence (resolveInteractive), and `--headless` defaults
709
+ // to `false` at the CLI layer, so gating on `options.headless` skipped these
710
+ // flags for a bare `agents run <agent> "prompt"`. For antigravity that meant
711
+ // `agy <prompt>` with no `--print`, launching the TUI and dying on `/dev/tty`
712
+ // in any non-terminal shell. `!interactive` is true for both an explicit
713
+ // `--headless` and a prompt-inferred headless run; an interactive run (no
714
+ // prompt, or `--interactive`) still skips them so the CLI opens its REPL.
715
+ if (!interactive && template.printFlags) {
673
716
  cmd.push(...template.printFlags);
674
717
  }
675
718
  // Resume vs. create. With `resume`, emit the agent's NATIVE resume reference:
@@ -842,9 +885,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
842
885
  }
843
886
  // The only flag the bash shim injects (codex); everything else is transparent.
844
887
  const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
888
+ // Mint a launch id and export it as AGENT_LAUNCH_ID so the agent's SessionStart
889
+ // hook records the same id — the join key that maps this launch to its exact
890
+ // session even though the recorded pid here is the cmd.exe wrapper (Windows) or
891
+ // a shell, while the hook runs under the agent descendant. This is the primary
892
+ // attribution path on Windows (no lsof), so the launchId join matters most here.
893
+ const launchId = randomUUID();
845
894
  // mode/effort are required by ExecOptions but unused by buildExecEnv (which only
846
895
  // derives the per-version config-dir env); pass the agent's default to satisfy the type.
847
- const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
896
+ const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
848
897
  const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
849
898
  return new Promise((resolve) => {
850
899
  const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
@@ -860,6 +909,8 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
860
909
  agent,
861
910
  sessionId: extractSessionIdArg(rawArgs),
862
911
  cwd,
912
+ launchId,
913
+ terminalId: process.env.AGENT_TERMINAL_ID,
863
914
  startedAtMs: Date.now(),
864
915
  });
865
916
  }
@@ -1008,6 +1059,12 @@ async function runInTmux(options, executable, args) {
1008
1059
  agent: options.agent,
1009
1060
  sessionId: options.sessionId,
1010
1061
  cwd,
1062
+ // spawnAgent injected AGENT_LAUNCH_ID into options.env before delegating
1063
+ // here; record the same id so the hook (running under the pane-leaf agent
1064
+ // pid) reconciles by launchId. This pane's pid usually IS the agent pid,
1065
+ // but the launchId join is robust even when it isn't.
1066
+ launchId: options.env?.AGENT_LAUNCH_ID,
1067
+ terminalId: process.env.AGENT_TERMINAL_ID,
1011
1068
  tmuxPane: pane,
1012
1069
  startedAtMs: Date.now(),
1013
1070
  });
@@ -1066,6 +1123,36 @@ async function runInTmux(options, executable, args) {
1066
1123
  // Pane still alive → the user detached; keep the session for `agents focus`.
1067
1124
  return { exitCode: 0, stderr: '', stdout: '' };
1068
1125
  }
1126
+ /**
1127
+ * Print the run's resolved session id as a one-line stdout sentinel so a `--host`
1128
+ * launcher can relate the remote-created session back to itself (see the
1129
+ * `emitSessionId` option and hosts/session-marker.ts).
1130
+ *
1131
+ * For Claude the id was forced up front (`options.sessionId`, wired as
1132
+ * `--session-id`), so it's authoritative with no lookup. Every other agent coined
1133
+ * its OWN id, which its SessionStart hook recorded under this run's launchId — the
1134
+ * exact join key `agents sessions --active` uses — so we read it back from the hook
1135
+ * index by launchId (falling back to the child pid the hook may have recorded
1136
+ * under). Nothing to emit when the hook hasn't landed an id (hookless harness):
1137
+ * the launcher simply keeps the un-mapped task, never a fabricated id.
1138
+ */
1139
+ function emitResolvedSessionId(options, launchId, childPid) {
1140
+ let sessionId = options.sessionId;
1141
+ if (!sessionId) {
1142
+ try {
1143
+ sessionId = resolveHookSessionId(loadHookSessionIndex(), {
1144
+ pid: childPid ?? 0,
1145
+ kind: options.agent,
1146
+ launchId,
1147
+ });
1148
+ }
1149
+ catch {
1150
+ /* hook index unreadable — emit nothing rather than a guess */
1151
+ }
1152
+ }
1153
+ if (sessionId)
1154
+ process.stdout.write(sessionIdMarkerLine(sessionId));
1155
+ }
1069
1156
  /**
1070
1157
  * Spawn an agent process and return its exit code plus a tee'd copy of stderr.
1071
1158
  *
@@ -1104,7 +1191,21 @@ async function spawnAgent(options) {
1104
1191
  // timeout. Spend is recorded to the shared ledger in the close handler. The
1105
1192
  // watcher is dormant (and zero-cost) when no caps are configured.
1106
1193
  const cwd = options.cwd || process.cwd();
1107
- const runId = randomUUID();
1194
+ // Resolve the launch id once. It doubles as the budget watcher's run id AND is
1195
+ // exported to the child as AGENT_LAUNCH_ID, so the agent's SessionStart hook
1196
+ // records the SAME id in its own state file (terminals/sessions/<pid>.json).
1197
+ // That id is the join key that reconciles this launch's pid-registry entry
1198
+ // with the hook's authoritative session id even when the hook runs under a
1199
+ // different pid (tmux pane leaf / cmd.exe wrapper) — see pid-registry.ts and
1200
+ // session/hook-sessions.ts. ADOPT a launch id a `--host` launcher already
1201
+ // forwarded (via `--env AGENT_LAUNCH_ID=…`) so ONE correlation key spans the
1202
+ // SSH hop and the launcher can resolve this run's real session id for every
1203
+ // agent, not just Claude (RUSH-2034); mint a fresh one for every local run.
1204
+ // Injected into options.env so every downstream env build (the bare spawn
1205
+ // below AND the tmux env prefix in runInTmux) carries it.
1206
+ const launchId = resolveLaunchId(options.env?.AGENT_LAUNCH_ID);
1207
+ const runId = launchId;
1208
+ options = { ...options, env: { ...options.env, AGENT_LAUNCH_ID: launchId } };
1108
1209
  const watcherState = await setupBudgetWatcher(options, cwd, runId);
1109
1210
  maybeRotate();
1110
1211
  const timer = createTimer('agent.run', {
@@ -1178,6 +1279,8 @@ async function spawnAgent(options) {
1178
1279
  agent: options.agent,
1179
1280
  sessionId: options.sessionId,
1180
1281
  cwd: options.cwd || process.cwd(),
1282
+ launchId,
1283
+ terminalId: process.env.AGENT_TERMINAL_ID,
1181
1284
  tmuxPane: process.env.TMUX_PANE,
1182
1285
  startedAtMs: Date.now(),
1183
1286
  });
@@ -1264,6 +1367,12 @@ async function spawnAgent(options) {
1264
1367
  // Budget kill resolves with a DISTINCT non-zero exit so CI/headless and
1265
1368
  // teams/cloud can tell a budget termination apart from a normal failure.
1266
1369
  const exitCode = budgetKilled ? BUDGET_KILL_EXIT_CODE : (code ?? 0);
1370
+ // Relate the session id back to a `--host` launcher (see the emitSessionId
1371
+ // doc). Claude's id is the one we forced; every other agent coined its own,
1372
+ // which the SessionStart hook recorded under this run's launchId — resolve
1373
+ // and print it as a one-line sentinel that rides the followed log home.
1374
+ if (options.emitSessionId)
1375
+ emitResolvedSessionId(options, launchId, child.pid);
1267
1376
  timer.end({ exitCode, status: budgetKilled ? 'budget_killed' : code === 0 ? 'success' : 'failed' });
1268
1377
  resolve({ exitCode, stderr: stderrBuffer, stdout: stdoutTail });
1269
1378
  });
@@ -1358,6 +1467,109 @@ export const RATE_LIMIT_PATTERNS = [
1358
1467
  export function detectRateLimit(text) {
1359
1468
  return RATE_LIMIT_PATTERNS.some(pattern => pattern.test(text));
1360
1469
  }
1470
+ /**
1471
+ * Patterns that indicate an authentication failure — the agent is logged out,
1472
+ * its token was revoked, or the session expired. These are the user-visible
1473
+ * strings a logged-out agent surfaces (observed across the routine-run corpus).
1474
+ * Unlike a rate limit, an auth failure is NOT self-healing by failover — every
1475
+ * chain entry on the same account fails identically — so it is classified
1476
+ * separately and never triggers a fallback attempt.
1477
+ *
1478
+ * The bare `401` is deliberately paired with an auth keyword: a plain "401" can
1479
+ * appear in legitimate output (an HTTP-status table, a log line), so it only
1480
+ * counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
1481
+ */
1482
+ export const AUTH_FAILURE_PATTERNS = [
1483
+ /OAuth (?:access token has been revoked|session expired)/i,
1484
+ /(?:Please run|run) \/login/i,
1485
+ /\bNot logged in\b/i,
1486
+ /Invalid authentication credentials/i,
1487
+ /Failed to authenticate/i,
1488
+ /organization has (?:disabled|revoked) .*(?:subscription|access)/i,
1489
+ /401\b[^\n]*(?:OAuth|authenticat|credential|Unauthorized)/i,
1490
+ ];
1491
+ /**
1492
+ * Return true if the text contains any known authentication-failure indicator.
1493
+ * Agent-agnostic: matches the user-visible error string wherever it surfaces
1494
+ * (stdout tail, a captured error message, a plain-text agent's output).
1495
+ */
1496
+ export function detectAuthFailure(text) {
1497
+ return AUTH_FAILURE_PATTERNS.some(pattern => pattern.test(text));
1498
+ }
1499
+ /**
1500
+ * Return true if a stream-json log carries the structural markers of an auth
1501
+ * failure. This is the authoritative signal for Claude: a logged-out run emits
1502
+ * {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
1503
+ * {"type":"assistant",…,"error":"authentication_failed"}
1504
+ * {"type":"result","is_error":true,"result":"Failed to authenticate…"}
1505
+ * Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
1506
+ * reason logic can never catch it — the `error:"authentication_failed"` marker
1507
+ * and the `result`+`is_error` text are the reliable signals.
1508
+ *
1509
+ * Gated on the Claude stream-json shape; other agents don't emit these fields,
1510
+ * so callers pass their agent and this returns false for non-claude.
1511
+ */
1512
+ export function detectAuthFailureEvent(logText, agent) {
1513
+ if (agent !== 'claude')
1514
+ return false;
1515
+ const lines = logText.split('\n');
1516
+ for (const line of lines) {
1517
+ const trimmed = line.trim();
1518
+ if (!trimmed || trimmed[0] !== '{')
1519
+ continue;
1520
+ let parsed;
1521
+ try {
1522
+ parsed = JSON.parse(trimmed);
1523
+ }
1524
+ catch {
1525
+ continue;
1526
+ }
1527
+ if (parsed?.error === 'authentication_failed')
1528
+ return true;
1529
+ if (parsed?.type === 'result' &&
1530
+ parsed?.is_error === true &&
1531
+ typeof parsed?.result === 'string' &&
1532
+ detectAuthFailure(parsed.result)) {
1533
+ return true;
1534
+ }
1535
+ }
1536
+ return false;
1537
+ }
1538
+ /**
1539
+ * Return the first human-readable auth-failure phrase found in the text, for use
1540
+ * as a stable, short run `errorMessage` reason. Falls back to null when the only
1541
+ * signal was the structural `error:"authentication_failed"` marker with no
1542
+ * user-visible string.
1543
+ */
1544
+ export function authFailureReason(text) {
1545
+ for (const pattern of AUTH_FAILURE_PATTERNS) {
1546
+ const m = text.match(pattern);
1547
+ if (m)
1548
+ return m[0];
1549
+ }
1550
+ return null;
1551
+ }
1552
+ /**
1553
+ * Decide whether a run's stream-json log is an authentication failure. The
1554
+ * single source of truth for both the foreground and detached run paths.
1555
+ *
1556
+ * The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
1557
+ * authoritative and safe on ANY exit — it catches a logged-out Claude that exits
1558
+ * 0 (its `result` event carries `is_error:true` while `terminal_reason` is
1559
+ * "completed"). The raw user-visible string (`detectAuthFailure`) is only
1560
+ * consulted when the process actually FAILED, as a fallback for a run that died
1561
+ * mid-stream before emitting a `result` event. That gate is what prevents a
1562
+ * genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
1563
+ * routine documenting a login flow) from being misclassified and having its
1564
+ * legitimate report suppressed.
1565
+ */
1566
+ export function isAuthFailureFromLog(logText, agent, opts) {
1567
+ if (detectAuthFailureEvent(logText, agent))
1568
+ return true;
1569
+ if (opts.processFailed && detectAuthFailure(logText))
1570
+ return true;
1571
+ return false;
1572
+ }
1361
1573
  /**
1362
1574
  * Build the prompt handed to the fallback agent when the primary was stopped
1363
1575
  * mid-task by a rate limit.
@@ -0,0 +1,63 @@
1
+ import { type ActivityEvent } from './activity.js';
2
+ import { type PidSessionEntry } from './session/pid-registry.js';
3
+ /** Soft cap so a runaway agent can't flood the activity lane with essays. */
4
+ export declare const STATUS_POST_MAX_CHARS = 500;
5
+ export interface FeedPostInput {
6
+ /** Human-readable progress text (required). Domain-agnostic free text. */
7
+ text: string;
8
+ /** Override session id (escape hatch for scripts/tests). Prefer auto-resolve. */
9
+ sessionId?: string;
10
+ /** Override activity root (tests). */
11
+ activityRoot?: string;
12
+ /** Override env for identity resolution (tests). */
13
+ env?: NodeJS.ProcessEnv;
14
+ /** Override cwd stamp (defaults to process.cwd() / env). */
15
+ cwd?: string;
16
+ /** Fixed timestamp (tests). */
17
+ ts?: string;
18
+ /**
19
+ * Starting pid for registry ancestor walk (defaults to process.ppid so the
20
+ * walk begins at the parent of this CLI process — usually the agent/shell).
21
+ * Tests inject a fake pid here.
22
+ */
23
+ startPid?: number;
24
+ /** Override parent-pid lookup (tests). */
25
+ getParentPid?: (pid: number) => number | undefined;
26
+ /** Override registry read (tests). */
27
+ readEntry?: (pid: number) => PidSessionEntry | undefined;
28
+ /** Override full registry list for launchId match (tests). */
29
+ listEntries?: () => PidSessionEntry[];
30
+ }
31
+ export interface FeedPostResult {
32
+ event: ActivityEvent;
33
+ }
34
+ export interface PostIdentity {
35
+ sessionId: string;
36
+ mailboxId: string;
37
+ host: string;
38
+ runtime: string;
39
+ agent?: string;
40
+ cwd?: string;
41
+ pid?: number;
42
+ launchId?: string;
43
+ terminalId?: string;
44
+ tmuxPane?: string;
45
+ }
46
+ /**
47
+ * Resolve who is posting. Order:
48
+ * 1. Explicit --session flag
49
+ * 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
50
+ * 3. Env AGENT_LAUNCH_ID → match in pid registry
51
+ * 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
52
+ */
53
+ export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
54
+ /** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
55
+ export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
56
+ /** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
57
+ export declare function parentPidOf(pid: number): number | undefined;
58
+ export declare function normalizeStatusText(text: string): string;
59
+ /**
60
+ * Append a `status.posted` milestone for the calling agent.
61
+ * Throws if text is empty or session identity cannot be resolved.
62
+ */
63
+ export declare function postFeedStatus(input: FeedPostInput): FeedPostResult;