@phnx-labs/agents-cli 1.20.76 → 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 (85) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/exec.js +58 -16
  4. package/dist/commands/routines.js +271 -13
  5. package/dist/commands/secrets.js +59 -17
  6. package/dist/commands/sessions-browser.js +11 -1
  7. package/dist/commands/sessions-picker.d.ts +14 -1
  8. package/dist/commands/sessions-picker.js +168 -15
  9. package/dist/commands/sessions.d.ts +23 -4
  10. package/dist/commands/sessions.js +128 -38
  11. package/dist/commands/ssh.d.ts +13 -0
  12. package/dist/commands/ssh.js +39 -2
  13. package/dist/index.js +2 -1
  14. package/dist/lib/activity.d.ts +5 -4
  15. package/dist/lib/activity.js +450 -36
  16. package/dist/lib/cloud/session-index.js +2 -2
  17. package/dist/lib/daemon.d.ts +10 -1
  18. package/dist/lib/daemon.js +99 -12
  19. package/dist/lib/devices/fleet.d.ts +16 -1
  20. package/dist/lib/devices/fleet.js +10 -2
  21. package/dist/lib/events.d.ts +1 -1
  22. package/dist/lib/exec.d.ts +16 -0
  23. package/dist/lib/exec.js +27 -4
  24. package/dist/lib/feed.d.ts +7 -1
  25. package/dist/lib/feed.js +96 -6
  26. package/dist/lib/heal.d.ts +7 -4
  27. package/dist/lib/heal.js +10 -22
  28. package/dist/lib/hosts/dispatch.d.ts +9 -0
  29. package/dist/lib/hosts/dispatch.js +24 -4
  30. package/dist/lib/hosts/passthrough.js +7 -2
  31. package/dist/lib/hosts/remote-cmd.d.ts +11 -0
  32. package/dist/lib/hosts/remote-cmd.js +31 -8
  33. package/dist/lib/hosts/remote-session-id.d.ts +45 -0
  34. package/dist/lib/hosts/remote-session-id.js +84 -0
  35. package/dist/lib/hosts/run-target.d.ts +4 -0
  36. package/dist/lib/hosts/run-target.js +5 -1
  37. package/dist/lib/hosts/session-index.js +3 -3
  38. package/dist/lib/menubar/install-menubar.d.ts +9 -0
  39. package/dist/lib/menubar/install-menubar.js +14 -0
  40. package/dist/lib/menubar/notify-desktop.d.ts +44 -0
  41. package/dist/lib/menubar/notify-desktop.js +78 -0
  42. package/dist/lib/overdue.d.ts +4 -5
  43. package/dist/lib/overdue.js +8 -41
  44. package/dist/lib/routine-notify.d.ts +76 -0
  45. package/dist/lib/routine-notify.js +190 -0
  46. package/dist/lib/routines-placement.d.ts +38 -0
  47. package/dist/lib/routines-placement.js +79 -0
  48. package/dist/lib/routines-project.d.ts +97 -0
  49. package/dist/lib/routines-project.js +349 -0
  50. package/dist/lib/routines.d.ts +68 -0
  51. package/dist/lib/routines.js +74 -7
  52. package/dist/lib/runner.d.ts +12 -2
  53. package/dist/lib/runner.js +150 -25
  54. package/dist/lib/sandbox.js +10 -0
  55. package/dist/lib/secrets/account-token.d.ts +20 -0
  56. package/dist/lib/secrets/account-token.js +64 -0
  57. package/dist/lib/secrets/agent.d.ts +9 -4
  58. package/dist/lib/secrets/agent.js +30 -20
  59. package/dist/lib/secrets/bundles.d.ts +20 -6
  60. package/dist/lib/secrets/bundles.js +70 -34
  61. package/dist/lib/secrets/index.d.ts +11 -2
  62. package/dist/lib/secrets/index.js +21 -9
  63. package/dist/lib/secrets/session-store.d.ts +6 -2
  64. package/dist/lib/secrets/session-store.js +65 -15
  65. package/dist/lib/secrets/unlock-hints.d.ts +27 -0
  66. package/dist/lib/secrets/unlock-hints.js +36 -0
  67. package/dist/lib/session/active.d.ts +10 -0
  68. package/dist/lib/session/active.js +67 -4
  69. package/dist/lib/session/db.d.ts +6 -0
  70. package/dist/lib/session/db.js +83 -6
  71. package/dist/lib/session/discover.d.ts +13 -1
  72. package/dist/lib/session/discover.js +91 -3
  73. package/dist/lib/session/parse.js +11 -2
  74. package/dist/lib/session/prompt.d.ts +7 -0
  75. package/dist/lib/session/prompt.js +37 -0
  76. package/dist/lib/session/provenance.d.ts +7 -0
  77. package/dist/lib/session/render.js +18 -16
  78. package/dist/lib/session/state.d.ts +4 -0
  79. package/dist/lib/session/state.js +93 -11
  80. package/dist/lib/session/types.d.ts +25 -1
  81. package/dist/lib/session/types.js +8 -0
  82. package/dist/lib/state.d.ts +7 -3
  83. package/dist/lib/state.js +20 -5
  84. package/dist/lib/types.d.ts +10 -0
  85. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,167 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.77
4
+
5
+ - **Interactive `agents run --host` now tracks the real session for every agent,
6
+ not just Claude.** Codex, Kimi, Grok, and Gemini coin their own session id and
7
+ reject a caller-supplied one, so an interactive host run of any of them showed a
8
+ stale/absent id locally — `agents sessions` couldn't surface it and a dropped
9
+ link couldn't auto-reconnect it (RUSH-2033 fixed only the Claude `--session-id`
10
+ path). The launcher now forwards one correlation key it controls
11
+ (`AGENT_LAUNCH_ID`); the remote `agents run` adopts that key
12
+ (`resolveLaunchId`), so its SessionStart hook records the agent's real session id
13
+ under it. After the stream the launcher does one ssh read of the remote hook
14
+ record, resolves the real id by launch id (`resolveRemoteSessionId` /
15
+ `pickRemoteSessionId`), registers it in the local session index, and reconnects
16
+ against it on a dropped link. Claude still forces its own id up front and is
17
+ unchanged. Source: `apps/cli/src/lib/hosts/remote-session-id.ts`,
18
+ `resolveLaunchId` in `apps/cli/src/lib/exec.ts`, and the interactive `--host`
19
+ branch in `apps/cli/src/commands/exec.ts`. (RUSH-2034)
20
+
21
+ - **Project routines can opt into daemon firing with source tracking, sync, and
22
+ host placement (RUSH-2035).** Project YAML under `<project>/.agents/routines/*.yml`
23
+ stays inspection-only until `agents routines enable-project` (with interactive /
24
+ `--yes` approval) records the project on `meta.routines.projects` and materialises
25
+ copies into `~/.agents/routines/` with a `source:` block (`projectPath`, git
26
+ `repo`/`branch`/`commit`). `agents routines sync` (and daemon start/SIGHUP reload)
27
+ refreshes those copies when project YAML changes; `disable-project` / `projects`
28
+ manage the allowlist. New `hostStrategy: local|host|fleet|cloud` (CLI `--placement`)
29
+ chooses where the job body runs: local, a named `--run-on` host, one online fleet
30
+ device per fire (no cross-device double-fire — off-box strategies auto-pin
31
+ `devices`), or the agent's native cloud provider. `--host` remains the remote-
32
+ management passthrough. List/JSON surfaces source repo/branch and strategy. Source:
33
+ `apps/cli/src/lib/routines.ts`, `routines-project.ts`, `routines-placement.ts`,
34
+ `runner.ts`, `daemon.ts`, `commands/routines.ts`, `docs/03-routines.md`.
35
+
36
+ - **Factory interactive launches default to `--mode auto` (RUSH-2038).** The Factory VS Code extension no longer inherits the CLI's `plan` default for interactive terminal launches. Codex, Claude, Gemini, Cursor, OpenCode, and Antigravity now start in `auto` (writable-but-gated) when opened from Factory without an explicit mode, so the agent can edit files instead of stalling in a read-only sandbox. Source: `apps/factory/src/core/agents.ts`.
37
+
38
+ - **Codex approval blocks now notify you.** A headless or terminal Codex agent
39
+ blocked on an approval prompt used to stall silently — the feed/notification path
40
+ only fired for Claude. Codex emits `PermissionRequest` (not Claude's
41
+ `Notification`), which the `feed-publish` hook now handles: it publishes an
42
+ approval-class block with a high cost-of-delay and a `deny` safe-default, so the
43
+ blocked agent surfaces on `agents feed` and `agents feed --dispatch` pages the
44
+ phone as urgent. A Codex approval card clears once the approved tool runs, via a
45
+ matcher-less `PostToolUse` clear hook registered **for Codex only** — so Claude's
46
+ card lifetime (its `permission_prompt`/`idle_prompt`/`elicitation_dialog`
47
+ notification blocks persist until `Stop`/`SessionEnd`) and per-tool overhead are
48
+ exactly as before. The other feed hooks are now registered for Codex too, not
49
+ Claude only. The Factory extension bridges the same waiting state to an
50
+ edge-triggered VS Code notification with a "Focus terminal" action. Claude's path
51
+ is unchanged. Source: `FEED_PUBLISH_HOOK_SCRIPT` / `ensureFeedPublishHook` in
52
+ `apps/cli/src/lib/feed.ts`, `apps/factory/src/core/waitingNotifier.ts`.
53
+ (RUSH-2039)
54
+
55
+ - **`agents fleet ping` now completes within ~15 s per device and ~30 s total, even when several fleet devices are offline or slow (RUSH-2041).** The per-device remote auth probe timeout was lowered from 60 s to 15 s (matching the `fleet status` version-probe budget, which is enough for the ~8 s provider-fetch inside the local auth probe). `fanOutDevices` gained an optional `perDeviceTimeoutMs` that races each probe against a deadline and records it as `failed: timed out` instead of hanging. `runFleetPing` now also wraps the entire fan-out in a 30 s hard cap so the command can never outlast a reasonable budget. Offline devices are now reported promptly as failed/timed-out rather than left hanging in the spinner. Source: `apps/cli/src/lib/devices/fleet.ts` (`fanOutDevices`, `FanOutDeviceOptions`), `apps/cli/src/commands/ssh.ts` (`probeRemoteAuth`, `runFleetPing`).
56
+
57
+ - **`agents sessions` surfaces checklist progress in every list/preview (RUSH-2045).** The picker preview, `--active` rows (local + cross-machine), flat `doing` cell, and metadata-only previews now show compact `✓done/total · current step` from `SessionMeta.todos` / `ActiveSession.todos`, plus the originating prompt and a directories-touched activity line. Active/cross-machine rows also show label + clickable project/ticket alongside the agent short id. Covers interactive, headless, teams, and sub-agent sessions that share the preview infra. Source: `apps/cli/src/commands/sessions-picker.ts`, `apps/cli/src/commands/sessions.ts`.
58
+
59
+ - **Checklist completions emit a feed event (RUSH-2046).** When an agent marks a
60
+ task-checklist item done, the `11-activity-log.py` hook now appends a
61
+ `task.completed` milestone to the session activity log (and `checklist.created`
62
+ the first time a checklist appears), so completions show in `agents feed` and the
63
+ unified `agents events` stream with the item subject and running `N/M`. Detection
64
+ folds the transcript across harnesses — Claude `TaskUpdate`/`TodoWrite`, Grok
65
+ `todo_write`, Codex `update_plan` — so a completion is recognized regardless of
66
+ which agent produced it. Source: `apps/cli/src/lib/activity.ts` (incl. the embedded
67
+ hook), `apps/cli/src/lib/events.ts`, `apps/cli/src/commands/feed.ts`.
68
+
69
+ - **Actor provenance now survives the SSH hop.** A run dispatched to another host
70
+ (`agents run --host`, a remote `agents teams` supervisor, or any `--host`
71
+ passthrough) used to drop the resolved actor at the SSH boundary, so the remote
72
+ re-resolved it from the *originating* box's `SSH_CONNECTION` and mis-credited the
73
+ work to the shared machine or `UNRESOLVED@<host>`. The dispatch layer now forwards
74
+ `AGENTS_ACTOR*` / `GIT_*` across the wire (POSIX `export` and Windows `$env:`
75
+ alike), so the remote inherits the origin identity instead of re-resolving. A
76
+ caller-supplied env value still wins on collision (mirrors `buildExecEnv`).
77
+ Source: `withActorEnv` in `apps/cli/src/lib/hosts/dispatch.ts`, wired into
78
+ `launchDetached` / `runInteractiveOnHost` and the `--host` passthrough. (RUSH-2028)
79
+
80
+ - **A Linux-driven release now auto-discovers its macOS sign host instead of
81
+ hardcoding `mac-mini`.** `scripts/remote-sign-mac.sh` previously defaulted
82
+ `SIGN_HOST` to `mac-mini`, so a release from a Linux box failed outright whenever
83
+ that one appliance was offline — the recurring reason a release stalled and a
84
+ human had to finish it by hand. With `SIGN_HOST` unset the script now reads
85
+ `agents devices list --json`, keeps the reachable/online macOS devices, and picks
86
+ the first that answers `ssh` in preference order `mac-mini` → `zion` → any other
87
+ online Mac. `mac-mini` stays first because it signs headlessly (no Touch ID);
88
+ `zion` (the interactive Mac) is the fallback. An explicit `SIGN_HOST=<host>` still
89
+ pins one and skips discovery, and when no reachable Mac qualifies the script fails
90
+ with the ordered list it tried rather than hanging on a dead host. Source:
91
+ `apps/cli/scripts/remote-sign-mac.sh`.
92
+
93
+ - **An agent launch never raises a Touch ID sheet.** On macOS, starting an agent
94
+ terminal or firing a routine could pop several biometric prompts in a row, because
95
+ each keychain read runs in its own helper process and the biometric assertion never
96
+ reuses across processes. Two causes: `interactiveUnlock` defaulted to true whenever
97
+ an agent name was present, which let an agent-initiated read fall through the
98
+ `agentOnly` guard; and `isHeadlessSecretsContext` recognized the `headless` and
99
+ `teams` runtimes but not `terminal`, which is what an interactive run sets. Agent
100
+ launches now resolve broker-only and a locked bundle fails fast naming
101
+ `agents secrets unlock <bundle>`; `AGENTS_SECRETS_NO_PROMPT=1` is no longer needed
102
+ as a workaround. `agents secrets get/export/exec` typed in a **plain shell** still
103
+ prompts — it carries no `AGENTS_RUNTIME`, so the guard does not apply. Run beneath
104
+ an agent it refuses, because there the agent is the caller. This narrows the
105
+ agent-triggered approval added in RUSH-2032, which is unreleased.
106
+
107
+ - **`release.sh` now borrows the npm token from a primary device when the local box
108
+ has none, so a Linux-driven release stops asking a human to approve a token.**
109
+ Token resolution was env → local `npmjs.com` bundle → *die*. On a fleet box whose
110
+ own keychain holds no npm token, that dead end pushed agents to hand-move a
111
+ credential between machines (and correctly get gated on it). A third step now
112
+ resolves the bundle **ephemerally from a primary device over SSH** —
113
+ `agents secrets exec npmjs.com --host <host>`, which resolves on the remote and
114
+ injects into the run only, never storing the token locally. It tries `SECRET_HOST`
115
+ first, then `zion`, then `mac-mini`, and fails with the list it tried if none
116
+ answer. Combined with the sign-host auto-discovery, a Linux box can now cut a full
117
+ release end-to-end given a reachable Mac for signing and any reachable device that
118
+ holds the npm token. Source: `apps/cli/scripts/release.sh`.
119
+
120
+ - **Branded, actionable daemon notifications on the routine lifecycle (RUSH-2030).**
121
+ Daemon desktop notifications (overdue routines, config heal, the no-credential
122
+ warning) now route through the `MenubarHelper.app` companion instead of raw
123
+ AppleScript, so they carry the agents-cli mark rather than the generic Script
124
+ Editor icon; they degrade to `osascript`/`notify-send` only when the helper is
125
+ not installed. The daemon also notifies when a routine **starts** and
126
+ **finishes** (success/failure, with the report's first line or the error reason
127
+ folded in), suppressing command-housekeeping start/success pings to avoid spam.
128
+ Clicking a finish notification opens the run report/log; start/overdue open the
129
+ runs folder. Source: `apps/cli/src/lib/menubar/notify-desktop.ts`,
130
+ `apps/cli/src/lib/routine-notify.ts`, `apps/cli/src/lib/daemon.ts`,
131
+ `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
132
+
133
+ - **`agents secrets status` now suggests which bundles to unlock.** It reads the existing `secrets.get` audit events and surfaces bundles you keep getting a Touch ID prompt for — read from the keychain (not served silently by the broker/session) 3+ times in the last 7 days and not currently held — with a ready `agents secrets unlock <name>` command. `never`/no-ACL bundles (which never prompt) are excluded, and the hint is best-effort so it never breaks `status`. Source: `apps/cli/src/lib/secrets/unlock-hints.ts`, `apps/cli/src/commands/secrets.ts`.
134
+
135
+ - **`agents sessions <id>` with a short/partial id resolves by id only — no more
136
+ "Multiple sessions match" from fuzzy content.** A complete UUID already resolved
137
+ by id, but a bare hex short-id like `d3470b57` was not caught by
138
+ `isCompleteSessionId`, so it fell through to the ranked content search and
139
+ surfaced every transcript that merely MENTIONED the string (a resume prompt
140
+ echoes the parent id into the body of many later sessions) — a real view id
141
+ returned a list of unrelated sessions. Any id-shaped query — complete id OR hex
142
+ short-id/prefix (`looksLikeSessionId`) — now resolves through the index by id in
143
+ both `resolveSessionQuery` and the `renderOneSession` content-widen gate, and
144
+ reports "no session found" when nothing matches instead of content-searching.
145
+ Free-text phrases keep the ranked search path. Source:
146
+ `apps/cli/src/commands/sessions.ts`.
147
+
148
+ - **`agents sessions --active` attributes the initiating device for SSH-launched
149
+ sessions.** A session started by ssh'ing into a box (common for tmux-hosted
150
+ runs) used to render as `local` with no origin, because the tmux discovery path
151
+ stamped a `transport:'local'` placeholder that made provenance enrichment skip
152
+ it. Enrichment now probes the pane process's env and upgrades the row to `ssh`
153
+ with the real origin, then resolves the SSH client IP against the device
154
+ registry into `provenance.origin` (`{ device, user? }`). Both the flat listing
155
+ and the interactive browser read `ssh←<device>` (e.g. `ssh←zion`); an
156
+ unregistered IP stays bare `ssh`. Answers "which box launched this session"
157
+ without scraping `ps`/`who`/`tailscale`. Source:
158
+ `apps/cli/src/lib/session/active.ts`, `apps/cli/src/lib/session/provenance.ts`,
159
+ `apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions-browser.ts`.
160
+
161
+ - **`agents sessions --include user` / `--first` / `--last` now count genuine user turns, not harness-injected scaffolding (#1550).** A Claude session opened with a `!`-prefix command (e.g. `j <dir>`) stores `<bash-input>`/`<bash-stdout>` as `role=user` records, and `<system-reminder>`/`<task-notification>`/`<command-*>`/hook-feedback/skill bodies land the same way — so `--include user --first 3` returned the jump command and its shell output before the real ask, and every consumer of `--include user` (the `verify-work-complete` Stop hook's "original request" self-audit, `session-recall`) inherited the noise. `parseSession` now flags these injected `role=user` events `_synthetic` at its central post-parse chokepoint via one shared classifier (`isSyntheticUserMessage`), so turn slicing (`applyTurnSlice`) and role filtering (`roleOfEvent`) skip them; they stay in the default/`--markdown` stream for full fidelity. Claude-specific in practice — Codex/Gemini/OpenCode/Grok/Kimi/Rush route shell output to `tool_result`, never to `role=user`, and Droid's `<system-reminder>` was already dropped — but the classifier is cross-harness by construction. Source: `apps/cli/src/lib/session/{prompt,parse,render,types}.ts`.
162
+
163
+ - **Routine/daemon Claude runs authenticate the rotation-pinned account via its own long-lived setup-token — fixes fleet-wide daily logout.** Claude Code's interactive OAuth session uses single-use *rotating* refresh tokens: when one fleet machine refreshes, the server invalidates that account's token on every other machine, so unattended boxes 401 and drop (Claude Code #25609/#56339). A `claude setup-token` is a 1-year, non-rotating token that sidesteps this. The daemon now injects every `CLAUDE_CODE_OAUTH_TOKEN_<account>` present in the `claude` bundle (not just the one ambient token), and a routine spawn selects the token matching the account its version-home is pinned to (`runner.ts` `buildRoutineSpawnEnv` → `resolveAccountSetupToken`), so each unattended account authenticates with its own setup-token instead of the rotating interactive session. Works on macOS too, where the prior drop-based path was inert. Inert (no behavior change) until per-account setup-tokens are stored in the no-ACL `claude` bundle. Interactive `agents run` and remote `--host` dispatch are unchanged (out of scope; noted for follow-up). Source: `apps/cli/src/lib/secrets/account-token.ts`, `apps/cli/src/lib/runner.ts`, `apps/cli/src/lib/daemon.ts`, `apps/cli/src/lib/sandbox.ts`.
164
+
3
165
  ## 1.20.76
4
166
 
5
167
  - **The routines daemon can read a `never`/no-ACL secrets bundle headlessly again — fixes a false "no Claude credential" alert.** The headless secrets guard (`readAndResolveBundleEnv`'s `agentOnly` branch) threw for every keychain-backed bundle absent from the broker, but a `never`/no-ACL bundle carries no biometry ACL — its reads raise no Touch ID sheet, so blocking it served no purpose. That wrongly blocked the automation-only `claude` bundle the routines daemon reads at startup (`readDaemonClaudeOAuthToken`), leaving every scheduled Claude routine token-less, and — on the new auth-failure alert path — firing "no Claude credential" on each daemon start even when the bundle was configured correctly. The guard now exempts `never`/no-ACL bundles (policy learned via a prompt-less metadata read), matching the existing file-backend exemption. Source: `apps/cli/src/lib/secrets/bundles.ts`.
package/dist/bin/agents CHANGED
Binary file
@@ -20,8 +20,9 @@ import { sshResolve } from '../lib/hosts/ssh-config.js';
20
20
  import * as fs from 'fs';
21
21
  import * as path from 'path';
22
22
  import * as os from 'os';
23
- import { randomUUID } from 'crypto';
24
23
  import { spawnSync } from 'child_process';
24
+ import { randomUUID } from 'crypto';
25
+ import { isSessionTrackedAgent } from '../lib/session/types.js';
25
26
  /** Distinguish a terminal account-picker marker from an explicit @version pin. */
26
27
  export function parseRunAccountPickerRequest(agentSpec) {
27
28
  const requested = agentSpec.endsWith('@');
@@ -877,7 +878,7 @@ export function registerRunCommand(program) {
877
878
  process.exit(1);
878
879
  }
879
880
  const hostName = hostGiven[0];
880
- const { resolveHostRunTarget, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
881
+ const { resolveHostRunTarget, resolveHostSessionId, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
881
882
  const { runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
882
883
  const { registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
883
884
  const { RUN_OPTION_REJECT_MESSAGES } = await import('../lib/hosts/remote-cmd.js');
@@ -1035,10 +1036,21 @@ export function registerRunCommand(program) {
1035
1036
  process.exit(1);
1036
1037
  }
1037
1038
  // Mirror the local path (lib/exec.ts): only Claude accepts a forced
1038
- // `--session-id`. Generating it here lets us register the run in the
1039
- // local index and makes it resumable by id. On resume the remote
1040
- // session keeps its existing id don't mint a new one.
1041
- const hostSessionId = runAgent === 'claude' && !resumeId ? randomUUID() : undefined;
1039
+ // `--session-id`. Adopt the caller's id when present; otherwise mint
1040
+ // one here. Registering that same id keeps the local index aligned
1041
+ // with the remote agent. On resume, don't mint a new one.
1042
+ const hostSessionId = resolveHostSessionId(runAgent, resumeId, options.sessionId);
1043
+ // For every OTHER agent the remote coins its own id, which we can't
1044
+ // know up front. Forward a launch id we control as AGENT_LAUNCH_ID:
1045
+ // the remote `agents run` adopts it (exec.ts resolveLaunchId) and its
1046
+ // SessionStart hook records the real id under that exact key, so after
1047
+ // the stream we resolve the id by one ssh read of the remote hook
1048
+ // record — the same launch-id join used locally (RUSH-2034). Not
1049
+ // needed for Claude (id forced) or resume (id already known).
1050
+ const correlationLaunchId = !hostSessionId && !resumeId && isSessionTrackedAgent(runAgent) ? randomUUID() : undefined;
1051
+ const hostEnv = correlationLaunchId
1052
+ ? [...options.env, `AGENT_LAUNCH_ID=${correlationLaunchId}`]
1053
+ : options.env;
1042
1054
  if (hostSessionId) {
1043
1055
  registerInteractiveHostSession({
1044
1056
  cwd: process.cwd(),
@@ -1058,7 +1070,7 @@ export function registerRunCommand(program) {
1058
1070
  mode: options.mode,
1059
1071
  model: options.model,
1060
1072
  effort: options.effort,
1061
- env: options.env,
1073
+ env: hostEnv,
1062
1074
  addDir: hostAddDirs,
1063
1075
  json: options.json,
1064
1076
  verbose: options.verbose,
@@ -1074,14 +1086,41 @@ export function registerRunCommand(program) {
1074
1086
  forceInteractive: options.interactive,
1075
1087
  copyCreds: hostCopyCreds,
1076
1088
  });
1089
+ // Resolve a non-Claude agent's REAL remote session id now the run has
1090
+ // booted (its hook has fired on the peer): one ssh read of the remote
1091
+ // hook record, keyed by the launch id we forwarded. Register it so the
1092
+ // run shows in `agents sessions` and can be reconnected/focused —
1093
+ // closing the non-Claude gap RUSH-2033 left. Best-effort: an
1094
+ // unreachable host or a not-yet-landed record leaves the run un-mapped
1095
+ // rather than mis-mapped.
1096
+ let resolvedRemoteId;
1097
+ if (correlationLaunchId) {
1098
+ const { resolveRemoteSessionId } = await import('../lib/hosts/remote-session-id.js');
1099
+ const { sshTargetFor } = await import('../lib/hosts/types.js');
1100
+ try {
1101
+ resolvedRemoteId = resolveRemoteSessionId(sshTargetFor(host), correlationLaunchId);
1102
+ }
1103
+ catch {
1104
+ /* ssh read is best-effort — keep the run un-mapped, never guess */
1105
+ }
1106
+ if (resolvedRemoteId) {
1107
+ registerInteractiveHostSession({
1108
+ cwd: process.cwd(),
1109
+ host: host.name,
1110
+ agent: runAgent,
1111
+ sessionId: resolvedRemoteId,
1112
+ name: options.name,
1113
+ });
1114
+ }
1115
+ }
1077
1116
  // A network drop kills the local ssh client (exit 255) but the remote
1078
1117
  // agent survives in its detached tmux session. With a known session id
1079
- // (Claude, or a resumed run) and a tmux-hosted run, re-attach the live
1080
- // pane automatically instead of exiting the user never has to notice
1081
- // the drop and `agents sessions focus` by hand. `raw` runs aren't tmux
1082
- // wrapped, so there is nothing to reconnect to. Non-Claude id capture is
1083
- // tracked in RUSH-2007.
1084
- const reconnectId = hostSessionId ?? resumeId;
1118
+ // (Claude's forced id, a resumed run, or a non-Claude id we just
1119
+ // resolved from the remote hook record) and a tmux-hosted run,
1120
+ // re-attach the live pane automatically instead of exiting the user
1121
+ // never has to notice the drop and `agents sessions focus` by hand.
1122
+ // `raw` runs aren't tmux wrapped, so there is nothing to reconnect to.
1123
+ const reconnectId = hostSessionId ?? resolvedRemoteId ?? resumeId;
1085
1124
  if (reconnectId && !isRaw) {
1086
1125
  const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
1087
1126
  if (exitCode === SSH_CONN_FAILURE) {
@@ -1127,6 +1166,7 @@ export function registerRunCommand(program) {
1127
1166
  remoteCwd: hostCwd,
1128
1167
  name: options.name,
1129
1168
  resume: resumeId,
1169
+ sessionId: options.sessionId,
1130
1170
  follow: options.follow !== false,
1131
1171
  passthroughArgs,
1132
1172
  copyCreds: hostCopyCreds,
@@ -1883,11 +1923,13 @@ export function registerRunCommand(program) {
1883
1923
  else {
1884
1924
  const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, {
1885
1925
  caller: `agent ${agent}`,
1926
+ agent,
1886
1927
  keys: secretsKeysSubset,
1887
1928
  allowExpired: options.allowExpired,
1888
- // A headless/background run (routine, teammate, detached) must not
1889
- // pop a Touch ID sheet nobody can answer resolve broker-only and
1890
- // fail fast with an actionable error. Interactive runs still prompt.
1929
+ // The harness identity scopes any cached grant. It no longer lets the
1930
+ // requesting agent wait for interactive approvalan agent launch
1931
+ // resolves broker-only and fails fast naming
1932
+ // `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock).
1891
1933
  agentOnly: isHeadlessSecretsContext(),
1892
1934
  });
1893
1935
  const entries = describeBundle(bundle);