@phnx-labs/agents-cli 1.20.74 → 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 (199) hide show
  1. package/CHANGELOG.md +893 -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 +31 -3
  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 +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  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.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. package/package.json +1 -1
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';
@@ -312,14 +315,29 @@ export function buildExecEnv(options) {
312
315
  // iterations share one inbox.
313
316
  if (options.sessionId && isValidMailboxId(options.sessionId)) {
314
317
  result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
318
+ // Full session id for agent-callable tools (`agents feed post`, etc.).
319
+ result.AGENT_SESSION_ID = options.sessionId;
320
+ result.AGENTS_SESSION_ID = options.sessionId;
315
321
  }
316
322
  result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
323
+ // So activity / feed posts stamp the right harness without re-detecting.
324
+ if (options.agent) {
325
+ result.AGENTS_AGENT_NAME = options.agent;
326
+ }
327
+ if (options.cwd) {
328
+ result.AGENTS_CWD = options.cwd;
329
+ }
317
330
  // Export the run's durable name (companion to AGENT_SESSION_ID) so a
318
331
  // SessionStart hook / the agent can associate its transcript with the handle
319
332
  // the user gave the run. Only set when --name was passed.
320
333
  if (options.name) {
321
334
  result.AGENT_SESSION_NAME = options.name;
322
335
  }
336
+ // Actor provenance -- who initiated this run. Rides the env so the whole spawn
337
+ // tree shares one actor, and (for a resolved human) so the agent's own git
338
+ // commits are credited to the person instead of the shared account. options.env
339
+ // (spread last) overrides any of these keys a caller sets explicitly.
340
+ Object.assign(result, actorEnv(resolveActor()));
323
341
  return {
324
342
  ...result,
325
343
  ...options.env,
@@ -666,10 +684,16 @@ export function buildExecCommand(options) {
666
684
  else {
667
685
  cmd.push(...modeFlags);
668
686
  }
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) {
687
+ // Add print/headless flags whenever the run RESOLVED headless -- gate on the
688
+ // resolved state (`!interactive`), not the raw `--headless` flag. Headless is
689
+ // inferred from prompt presence (resolveInteractive), and `--headless` defaults
690
+ // to `false` at the CLI layer, so gating on `options.headless` skipped these
691
+ // flags for a bare `agents run <agent> "prompt"`. For antigravity that meant
692
+ // `agy <prompt>` with no `--print`, launching the TUI and dying on `/dev/tty`
693
+ // in any non-terminal shell. `!interactive` is true for both an explicit
694
+ // `--headless` and a prompt-inferred headless run; an interactive run (no
695
+ // prompt, or `--interactive`) still skips them so the CLI opens its REPL.
696
+ if (!interactive && template.printFlags) {
673
697
  cmd.push(...template.printFlags);
674
698
  }
675
699
  // Resume vs. create. With `resume`, emit the agent's NATIVE resume reference:
@@ -842,9 +866,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
842
866
  }
843
867
  // The only flag the bash shim injects (codex); everything else is transparent.
844
868
  const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
869
+ // Mint a launch id and export it as AGENT_LAUNCH_ID so the agent's SessionStart
870
+ // hook records the same id — the join key that maps this launch to its exact
871
+ // session even though the recorded pid here is the cmd.exe wrapper (Windows) or
872
+ // a shell, while the hook runs under the agent descendant. This is the primary
873
+ // attribution path on Windows (no lsof), so the launchId join matters most here.
874
+ const launchId = randomUUID();
845
875
  // mode/effort are required by ExecOptions but unused by buildExecEnv (which only
846
876
  // 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' });
877
+ const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
848
878
  const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
849
879
  return new Promise((resolve) => {
850
880
  const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
@@ -860,6 +890,8 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
860
890
  agent,
861
891
  sessionId: extractSessionIdArg(rawArgs),
862
892
  cwd,
893
+ launchId,
894
+ terminalId: process.env.AGENT_TERMINAL_ID,
863
895
  startedAtMs: Date.now(),
864
896
  });
865
897
  }
@@ -1008,6 +1040,12 @@ async function runInTmux(options, executable, args) {
1008
1040
  agent: options.agent,
1009
1041
  sessionId: options.sessionId,
1010
1042
  cwd,
1043
+ // spawnAgent injected AGENT_LAUNCH_ID into options.env before delegating
1044
+ // here; record the same id so the hook (running under the pane-leaf agent
1045
+ // pid) reconciles by launchId. This pane's pid usually IS the agent pid,
1046
+ // but the launchId join is robust even when it isn't.
1047
+ launchId: options.env?.AGENT_LAUNCH_ID,
1048
+ terminalId: process.env.AGENT_TERMINAL_ID,
1011
1049
  tmuxPane: pane,
1012
1050
  startedAtMs: Date.now(),
1013
1051
  });
@@ -1066,6 +1104,36 @@ async function runInTmux(options, executable, args) {
1066
1104
  // Pane still alive → the user detached; keep the session for `agents focus`.
1067
1105
  return { exitCode: 0, stderr: '', stdout: '' };
1068
1106
  }
1107
+ /**
1108
+ * Print the run's resolved session id as a one-line stdout sentinel so a `--host`
1109
+ * launcher can relate the remote-created session back to itself (see the
1110
+ * `emitSessionId` option and hosts/session-marker.ts).
1111
+ *
1112
+ * For Claude the id was forced up front (`options.sessionId`, wired as
1113
+ * `--session-id`), so it's authoritative with no lookup. Every other agent coined
1114
+ * its OWN id, which its SessionStart hook recorded under this run's launchId — the
1115
+ * exact join key `agents sessions --active` uses — so we read it back from the hook
1116
+ * index by launchId (falling back to the child pid the hook may have recorded
1117
+ * under). Nothing to emit when the hook hasn't landed an id (hookless harness):
1118
+ * the launcher simply keeps the un-mapped task, never a fabricated id.
1119
+ */
1120
+ function emitResolvedSessionId(options, launchId, childPid) {
1121
+ let sessionId = options.sessionId;
1122
+ if (!sessionId) {
1123
+ try {
1124
+ sessionId = resolveHookSessionId(loadHookSessionIndex(), {
1125
+ pid: childPid ?? 0,
1126
+ kind: options.agent,
1127
+ launchId,
1128
+ });
1129
+ }
1130
+ catch {
1131
+ /* hook index unreadable — emit nothing rather than a guess */
1132
+ }
1133
+ }
1134
+ if (sessionId)
1135
+ process.stdout.write(sessionIdMarkerLine(sessionId));
1136
+ }
1069
1137
  /**
1070
1138
  * Spawn an agent process and return its exit code plus a tee'd copy of stderr.
1071
1139
  *
@@ -1104,7 +1172,17 @@ async function spawnAgent(options) {
1104
1172
  // timeout. Spend is recorded to the shared ledger in the close handler. The
1105
1173
  // watcher is dormant (and zero-cost) when no caps are configured.
1106
1174
  const cwd = options.cwd || process.cwd();
1107
- const runId = randomUUID();
1175
+ // Mint the launch id once. It doubles as the budget watcher's run id AND is
1176
+ // exported to the child as AGENT_LAUNCH_ID, so the agent's SessionStart hook
1177
+ // records the SAME id in its own state file (terminals/sessions/<pid>.json).
1178
+ // That id is the join key that reconciles this launch's pid-registry entry
1179
+ // with the hook's authoritative session id even when the hook runs under a
1180
+ // different pid (tmux pane leaf / cmd.exe wrapper) — see pid-registry.ts and
1181
+ // session/hook-sessions.ts. Injected into options.env so every downstream env
1182
+ // build (the bare spawn below AND the tmux env prefix in runInTmux) carries it.
1183
+ const launchId = randomUUID();
1184
+ const runId = launchId;
1185
+ options = { ...options, env: { ...options.env, AGENT_LAUNCH_ID: launchId } };
1108
1186
  const watcherState = await setupBudgetWatcher(options, cwd, runId);
1109
1187
  maybeRotate();
1110
1188
  const timer = createTimer('agent.run', {
@@ -1178,6 +1256,8 @@ async function spawnAgent(options) {
1178
1256
  agent: options.agent,
1179
1257
  sessionId: options.sessionId,
1180
1258
  cwd: options.cwd || process.cwd(),
1259
+ launchId,
1260
+ terminalId: process.env.AGENT_TERMINAL_ID,
1181
1261
  tmuxPane: process.env.TMUX_PANE,
1182
1262
  startedAtMs: Date.now(),
1183
1263
  });
@@ -1264,6 +1344,12 @@ async function spawnAgent(options) {
1264
1344
  // Budget kill resolves with a DISTINCT non-zero exit so CI/headless and
1265
1345
  // teams/cloud can tell a budget termination apart from a normal failure.
1266
1346
  const exitCode = budgetKilled ? BUDGET_KILL_EXIT_CODE : (code ?? 0);
1347
+ // Relate the session id back to a `--host` launcher (see the emitSessionId
1348
+ // doc). Claude's id is the one we forced; every other agent coined its own,
1349
+ // which the SessionStart hook recorded under this run's launchId — resolve
1350
+ // and print it as a one-line sentinel that rides the followed log home.
1351
+ if (options.emitSessionId)
1352
+ emitResolvedSessionId(options, launchId, child.pid);
1267
1353
  timer.end({ exitCode, status: budgetKilled ? 'budget_killed' : code === 0 ? 'success' : 'failed' });
1268
1354
  resolve({ exitCode, stderr: stderrBuffer, stdout: stdoutTail });
1269
1355
  });
@@ -1358,6 +1444,109 @@ export const RATE_LIMIT_PATTERNS = [
1358
1444
  export function detectRateLimit(text) {
1359
1445
  return RATE_LIMIT_PATTERNS.some(pattern => pattern.test(text));
1360
1446
  }
1447
+ /**
1448
+ * Patterns that indicate an authentication failure — the agent is logged out,
1449
+ * its token was revoked, or the session expired. These are the user-visible
1450
+ * strings a logged-out agent surfaces (observed across the routine-run corpus).
1451
+ * Unlike a rate limit, an auth failure is NOT self-healing by failover — every
1452
+ * chain entry on the same account fails identically — so it is classified
1453
+ * separately and never triggers a fallback attempt.
1454
+ *
1455
+ * The bare `401` is deliberately paired with an auth keyword: a plain "401" can
1456
+ * appear in legitimate output (an HTTP-status table, a log line), so it only
1457
+ * counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
1458
+ */
1459
+ export const AUTH_FAILURE_PATTERNS = [
1460
+ /OAuth (?:access token has been revoked|session expired)/i,
1461
+ /(?:Please run|run) \/login/i,
1462
+ /\bNot logged in\b/i,
1463
+ /Invalid authentication credentials/i,
1464
+ /Failed to authenticate/i,
1465
+ /organization has (?:disabled|revoked) .*(?:subscription|access)/i,
1466
+ /401\b[^\n]*(?:OAuth|authenticat|credential|Unauthorized)/i,
1467
+ ];
1468
+ /**
1469
+ * Return true if the text contains any known authentication-failure indicator.
1470
+ * Agent-agnostic: matches the user-visible error string wherever it surfaces
1471
+ * (stdout tail, a captured error message, a plain-text agent's output).
1472
+ */
1473
+ export function detectAuthFailure(text) {
1474
+ return AUTH_FAILURE_PATTERNS.some(pattern => pattern.test(text));
1475
+ }
1476
+ /**
1477
+ * Return true if a stream-json log carries the structural markers of an auth
1478
+ * failure. This is the authoritative signal for Claude: a logged-out run emits
1479
+ * {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
1480
+ * {"type":"assistant",…,"error":"authentication_failed"}
1481
+ * {"type":"result","is_error":true,"result":"Failed to authenticate…"}
1482
+ * Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
1483
+ * reason logic can never catch it — the `error:"authentication_failed"` marker
1484
+ * and the `result`+`is_error` text are the reliable signals.
1485
+ *
1486
+ * Gated on the Claude stream-json shape; other agents don't emit these fields,
1487
+ * so callers pass their agent and this returns false for non-claude.
1488
+ */
1489
+ export function detectAuthFailureEvent(logText, agent) {
1490
+ if (agent !== 'claude')
1491
+ return false;
1492
+ const lines = logText.split('\n');
1493
+ for (const line of lines) {
1494
+ const trimmed = line.trim();
1495
+ if (!trimmed || trimmed[0] !== '{')
1496
+ continue;
1497
+ let parsed;
1498
+ try {
1499
+ parsed = JSON.parse(trimmed);
1500
+ }
1501
+ catch {
1502
+ continue;
1503
+ }
1504
+ if (parsed?.error === 'authentication_failed')
1505
+ return true;
1506
+ if (parsed?.type === 'result' &&
1507
+ parsed?.is_error === true &&
1508
+ typeof parsed?.result === 'string' &&
1509
+ detectAuthFailure(parsed.result)) {
1510
+ return true;
1511
+ }
1512
+ }
1513
+ return false;
1514
+ }
1515
+ /**
1516
+ * Return the first human-readable auth-failure phrase found in the text, for use
1517
+ * as a stable, short run `errorMessage` reason. Falls back to null when the only
1518
+ * signal was the structural `error:"authentication_failed"` marker with no
1519
+ * user-visible string.
1520
+ */
1521
+ export function authFailureReason(text) {
1522
+ for (const pattern of AUTH_FAILURE_PATTERNS) {
1523
+ const m = text.match(pattern);
1524
+ if (m)
1525
+ return m[0];
1526
+ }
1527
+ return null;
1528
+ }
1529
+ /**
1530
+ * Decide whether a run's stream-json log is an authentication failure. The
1531
+ * single source of truth for both the foreground and detached run paths.
1532
+ *
1533
+ * The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
1534
+ * authoritative and safe on ANY exit — it catches a logged-out Claude that exits
1535
+ * 0 (its `result` event carries `is_error:true` while `terminal_reason` is
1536
+ * "completed"). The raw user-visible string (`detectAuthFailure`) is only
1537
+ * consulted when the process actually FAILED, as a fallback for a run that died
1538
+ * mid-stream before emitting a `result` event. That gate is what prevents a
1539
+ * genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
1540
+ * routine documenting a login flow) from being misclassified and having its
1541
+ * legitimate report suppressed.
1542
+ */
1543
+ export function isAuthFailureFromLog(logText, agent, opts) {
1544
+ if (detectAuthFailureEvent(logText, agent))
1545
+ return true;
1546
+ if (opts.processFailed && detectAuthFailure(logText))
1547
+ return true;
1548
+ return false;
1549
+ }
1361
1550
  /**
1362
1551
  * Build the prompt handed to the fallback agent when the primary was stopped
1363
1552
  * 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;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Agent status posts — deliberate progress messages into the activity stream.
3
+ *
4
+ * Surface: `agents feed post <text>` (agent-callable; humans watch via
5
+ * `agents feed` / `agents activity` / `agents events --module activity`).
6
+ *
7
+ * Identity is automatic: session id, agent, cwd, launch/pid/tmux provenance
8
+ * are resolved from the process environment and the per-pid launch registry
9
+ * (`lib/session/pid-registry.ts`). The agent only authors free-form text —
10
+ * no domain-specific flags (tickets, URLs, tracks).
11
+ *
12
+ * Storage: append-only activity log as a `status.posted` milestone. Does NOT
13
+ * open a feed block (blocks remain "needs you" state only).
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import { spawnSync } from 'child_process';
18
+ import { appendActivityEvent, } from './activity.js';
19
+ import { machineId } from './machine-id.js';
20
+ import { isValidMailboxId } from './mailbox.js';
21
+ import { listPidSessionEntries, readPidSessionEntry, } from './session/pid-registry.js';
22
+ /** Soft cap so a runaway agent can't flood the activity lane with essays. */
23
+ export const STATUS_POST_MAX_CHARS = 500;
24
+ /**
25
+ * Resolve who is posting. Order:
26
+ * 1. Explicit --session flag
27
+ * 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
28
+ * 3. Env AGENT_LAUNCH_ID → match in pid registry
29
+ * 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
30
+ */
31
+ export function resolvePostIdentity(input) {
32
+ const env = input.env ?? process.env;
33
+ const readEntry = input.readEntry ?? readPidSessionEntry;
34
+ const listEntries = input.listEntries ?? listPidSessionEntries;
35
+ const getParent = input.getParentPid ?? parentPidOf;
36
+ const envSession = firstValidId([
37
+ input.sessionId,
38
+ env.AGENT_SESSION_ID,
39
+ env.AGENTS_SESSION_ID,
40
+ mailboxIdFromEnv(env),
41
+ ]);
42
+ const launchId = env.AGENT_LAUNCH_ID?.trim() || undefined;
43
+ let registry;
44
+ if (launchId) {
45
+ registry = listEntries().find((e) => e.launchId === launchId);
46
+ }
47
+ if (!registry) {
48
+ const start = input.startPid ?? (typeof process.ppid === 'number' ? process.ppid : undefined);
49
+ if (start && start > 1) {
50
+ registry = walkPidRegistry(start, getParent, readEntry);
51
+ }
52
+ }
53
+ // Prefer env session (explicit + managed run), fill gaps from registry.
54
+ const sessionId = envSession ?? registry?.sessionId;
55
+ if (!sessionId || !isValidMailboxId(sessionId))
56
+ return undefined;
57
+ const mailboxFromEnv = mailboxIdFromEnv(env);
58
+ const mailboxId = mailboxFromEnv && isValidMailboxId(mailboxFromEnv)
59
+ ? mailboxFromEnv
60
+ : sessionId;
61
+ return {
62
+ sessionId,
63
+ mailboxId,
64
+ host: machineIdFromEnv(env),
65
+ runtime: env.AGENTS_RUNTIME?.trim() || 'headless',
66
+ agent: env.AGENTS_AGENT_NAME?.trim()
67
+ || registry?.agent
68
+ || detectAgentKind(env),
69
+ cwd: input.cwd
70
+ ?? (env.AGENTS_CWD?.trim() || registry?.cwd || process.cwd()),
71
+ pid: registry?.pid,
72
+ launchId: launchId || registry?.launchId,
73
+ terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId,
74
+ tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane,
75
+ };
76
+ }
77
+ function firstValidId(candidates) {
78
+ for (const raw of candidates) {
79
+ const id = (raw ?? '').trim();
80
+ if (id && isValidMailboxId(id))
81
+ return id;
82
+ }
83
+ return undefined;
84
+ }
85
+ function mailboxIdFromEnv(env) {
86
+ const dir = env.AGENTS_MAILBOX_DIR?.trim();
87
+ if (!dir)
88
+ return undefined;
89
+ const base = path.basename(dir.replace(/[/\\]+$/, ''));
90
+ return base || undefined;
91
+ }
92
+ /** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
93
+ export function walkPidRegistry(startPid, getParent, readEntry) {
94
+ let pid = startPid;
95
+ const seen = new Set();
96
+ let firstHit;
97
+ for (let i = 0; i < 16 && pid && pid > 1 && !seen.has(pid); i++) {
98
+ seen.add(pid);
99
+ const entry = readEntry(pid);
100
+ if (entry) {
101
+ if (entry.sessionId && isValidMailboxId(entry.sessionId))
102
+ return entry;
103
+ if (!firstHit)
104
+ firstHit = entry;
105
+ }
106
+ pid = getParent(pid);
107
+ }
108
+ // Entry without sessionId still carries agent/cwd/launchId — usable when
109
+ // session id comes from env.
110
+ return firstHit;
111
+ }
112
+ /** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
113
+ export function parentPidOf(pid) {
114
+ if (!Number.isInteger(pid) || pid <= 1)
115
+ return undefined;
116
+ if (process.platform === 'linux') {
117
+ try {
118
+ const status = fs.readFileSync(`/proc/${pid}/status`, 'utf8');
119
+ const m = status.match(/^PPid:\s*(\d+)/m);
120
+ if (m) {
121
+ const pp = Number(m[1]);
122
+ return Number.isInteger(pp) && pp > 0 ? pp : undefined;
123
+ }
124
+ }
125
+ catch {
126
+ /* fall through */
127
+ }
128
+ }
129
+ try {
130
+ const r = spawnSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
131
+ encoding: 'utf8',
132
+ timeout: 1000,
133
+ });
134
+ if (r.status === 0) {
135
+ const pp = Number((r.stdout || '').trim());
136
+ return Number.isInteger(pp) && pp > 0 ? pp : undefined;
137
+ }
138
+ }
139
+ catch {
140
+ /* best-effort */
141
+ }
142
+ return undefined;
143
+ }
144
+ export function normalizeStatusText(text) {
145
+ const collapsed = text.replace(/\s+/g, ' ').trim();
146
+ if (!collapsed)
147
+ return '';
148
+ if (collapsed.length <= STATUS_POST_MAX_CHARS)
149
+ return collapsed;
150
+ return `${collapsed.slice(0, STATUS_POST_MAX_CHARS - 1)}…`;
151
+ }
152
+ /**
153
+ * Append a `status.posted` milestone for the calling agent.
154
+ * Throws if text is empty or session identity cannot be resolved.
155
+ */
156
+ export function postFeedStatus(input) {
157
+ const detail = normalizeStatusText(input.text);
158
+ if (!detail) {
159
+ throw new Error('Status text is empty. Usage: agents feed post "what just happened"');
160
+ }
161
+ const identity = resolvePostIdentity(input);
162
+ if (!identity) {
163
+ throw new Error('No session id. Run from an agents-cli session '
164
+ + '(AGENT_SESSION_ID / AGENTS_MAILBOX_DIR / pid registry), or pass --session <id>.');
165
+ }
166
+ const event = {
167
+ ts: input.ts ?? new Date().toISOString(),
168
+ event: 'status.posted',
169
+ sessionId: identity.sessionId,
170
+ mailboxId: identity.mailboxId,
171
+ host: identity.host,
172
+ runtime: identity.runtime,
173
+ cwd: identity.cwd,
174
+ agent: identity.agent,
175
+ tool: 'feed.post',
176
+ detail,
177
+ ...(identity.pid !== undefined ? { pid: identity.pid } : {}),
178
+ ...(identity.launchId ? { launchId: identity.launchId } : {}),
179
+ ...(identity.terminalId ? { terminalId: identity.terminalId } : {}),
180
+ ...(identity.tmuxPane ? { tmuxPane: identity.tmuxPane } : {}),
181
+ };
182
+ appendActivityEvent(event, input.activityRoot);
183
+ return {
184
+ event: {
185
+ v: 1,
186
+ tier: 'milestone',
187
+ ...event,
188
+ },
189
+ };
190
+ }
191
+ function machineIdFromEnv(env) {
192
+ const raw = env.AGENTS_SYNC_MACHINE_ID || undefined;
193
+ if (raw) {
194
+ return raw.split('.')[0].trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-') || 'unknown';
195
+ }
196
+ return machineId();
197
+ }
198
+ function detectAgentKind(env) {
199
+ if (env.CLAUDECODE === '1')
200
+ return 'claude';
201
+ if (env.CODEX_CI || env.CODEX_HOME)
202
+ return 'codex';
203
+ return 'agent';
204
+ }
@@ -11,6 +11,30 @@ import type { DeviceProfile } from '../devices/registry.js';
11
11
  import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
12
12
  /** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
13
13
  export declare function agentIdOf(spec: string): string;
14
+ /**
15
+ * The explicit pinned version of a spec, or undefined for an id-level spec.
16
+ * `claude@2.1.170` -> `2.1.170`; `claude`, `claude@latest`, `claude@oldest`, and
17
+ * `claude@all` all return undefined (the label channels install-latest / are
18
+ * expanded upstream, so they diff at id granularity, not per-version).
19
+ */
20
+ export declare function pinnedVersion(spec: string): string | undefined;
21
+ /** True when any spec in the roster pins an explicit version (needs a version probe). */
22
+ export declare function rosterNeedsVersions(desired: DeviceDesired[]): boolean;
23
+ /**
24
+ * Expand any `<agent>@all` spec into one pinned spec per version installed on the
25
+ * source, so `--agent claude@all` replicates THIS machine's exact version set.
26
+ * `versionsOf` returns the source's installed versions for an agent id. Every
27
+ * other spec passes through unchanged; the result is de-duplicated in order.
28
+ * Throws if `@all` names an agent with no installed versions here (nothing to
29
+ * replicate — a clear misconfig, not a silent no-op).
30
+ */
31
+ export declare function expandAllSpecs(specs: string[], versionsOf: (id: string) => string[]): string[];
32
+ /**
33
+ * Parse `agents view --json` (the all-agents array form) into a map of agent id
34
+ * -> installed version strings. Tolerant: returns undefined on any parse failure
35
+ * so a version-pinned spec falls back to id-level presence rather than crashing.
36
+ */
37
+ export declare function parseInstalledVersions(stdout: string): Record<string, string[]> | undefined;
14
38
  /** Source-side auth availability, computed once from `snapshotAuth`. */
15
39
  export interface SourceAuth {
16
40
  /** Agent ids the source has a readable, propagatable credential file for. */
@@ -37,8 +61,14 @@ export interface DiffContext {
37
61
  }
38
62
  /** Pure: desired vs probed -> per-device diff + flat action list. */
39
63
  export declare function diffFleet(desired: DeviceDesired[], probes: Map<string, DeviceProbe>, ctx: DiffContext): FleetPlan;
40
- /** Probe one device: reachability + agents-cli version + installed agent ids. */
41
- export declare function probeDevice(device: DeviceProfile): DeviceProbe;
64
+ export interface ProbeOptions {
65
+ /** Also fetch per-agent installed versions (one extra `agents view --json`
66
+ * round-trip). Enable only when the plan has a version-pinned spec. */
67
+ withVersions?: boolean;
68
+ }
69
+ /** Probe one device: reachability + agents-cli version + installed agent ids
70
+ * (and, when `withVersions`, the installed version strings per agent). */
71
+ export declare function probeDevice(device: DeviceProfile, opts?: ProbeOptions): DeviceProbe;
42
72
  export interface ApplyStep {
43
73
  kind: FleetAction['kind'];
44
74
  ok: boolean;