@phnx-labs/agents-cli 1.20.77 → 1.20.78

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 (70) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +2 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.d.ts +9 -0
  5. package/dist/commands/doctor.js +143 -9
  6. package/dist/commands/exec.js +29 -3
  7. package/dist/commands/repo.js +8 -0
  8. package/dist/commands/routines.js +4 -2
  9. package/dist/commands/secrets.js +8 -2
  10. package/dist/commands/sessions-browser.d.ts +45 -3
  11. package/dist/commands/sessions-browser.js +118 -12
  12. package/dist/commands/sessions-export.d.ts +3 -0
  13. package/dist/commands/sessions-export.js +13 -10
  14. package/dist/commands/sessions-picker.d.ts +15 -1
  15. package/dist/commands/sessions-picker.js +58 -4
  16. package/dist/commands/sessions.d.ts +95 -1
  17. package/dist/commands/sessions.js +224 -26
  18. package/dist/commands/ssh.js +9 -1
  19. package/dist/index.js +3 -5
  20. package/dist/lib/agents.d.ts +0 -21
  21. package/dist/lib/agents.js +0 -37
  22. package/dist/lib/auto-pull.d.ts +16 -8
  23. package/dist/lib/auto-pull.js +23 -28
  24. package/dist/lib/daemon.d.ts +9 -41
  25. package/dist/lib/daemon.js +16 -117
  26. package/dist/lib/devices/fleet-divergence.d.ts +101 -0
  27. package/dist/lib/devices/fleet-divergence.js +188 -0
  28. package/dist/lib/devices/fleet-inventory.d.ts +19 -0
  29. package/dist/lib/devices/fleet-inventory.js +57 -0
  30. package/dist/lib/devices/health-report.d.ts +10 -2
  31. package/dist/lib/devices/health-report.js +32 -1
  32. package/dist/lib/git.d.ts +13 -0
  33. package/dist/lib/git.js +102 -4
  34. package/dist/lib/hosts/remote-cmd.js +2 -0
  35. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
  36. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  38. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  39. package/dist/lib/runner.js +29 -26
  40. package/dist/lib/sandbox.js +0 -16
  41. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  42. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  43. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  44. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  45. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  46. package/dist/lib/secrets/agent.d.ts +2 -0
  47. package/dist/lib/secrets/agent.js +26 -14
  48. package/dist/lib/secrets/bundles.d.ts +1 -1
  49. package/dist/lib/secrets/bundles.js +16 -8
  50. package/dist/lib/secrets/scope.d.ts +26 -0
  51. package/dist/lib/secrets/scope.js +29 -0
  52. package/dist/lib/secrets/session-store.d.ts +10 -0
  53. package/dist/lib/secrets/session-store.js +64 -11
  54. package/dist/lib/session/active.d.ts +7 -0
  55. package/dist/lib/session/active.js +38 -3
  56. package/dist/lib/session/db.d.ts +37 -0
  57. package/dist/lib/session/db.js +112 -4
  58. package/dist/lib/session/digest.js +126 -21
  59. package/dist/lib/session/discover.d.ts +15 -2
  60. package/dist/lib/session/discover.js +19 -11
  61. package/dist/lib/session/origin-machine.d.ts +18 -0
  62. package/dist/lib/session/origin-machine.js +34 -0
  63. package/dist/lib/session/state.d.ts +1 -0
  64. package/dist/lib/session/state.js +1 -1
  65. package/dist/lib/session/sync/config.js +2 -2
  66. package/dist/lib/smart-launch.d.ts +86 -0
  67. package/dist/lib/smart-launch.js +172 -0
  68. package/package.json +1 -1
  69. package/dist/lib/secrets/account-token.d.ts +0 -20
  70. package/dist/lib/secrets/account-token.js +0 -64
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.78
4
+
5
+ - **`agents sessions <uuid>` now resolves a remote session exactly, across the
6
+ fleet.** A full session id absent from the local disk used to fall back to an
7
+ FTS content search — and because a UUID appears verbatim in other sessions'
8
+ transcripts (a watchdog `/continue <uuid>` reference), that surfaced a list of
9
+ unrelated "matches" instead of the one session, which actually lived on another
10
+ machine. A UUID is now treated as an identifier: on a local miss the CLI fans
11
+ the id lookup out to the online fleet (the existing `gatherRemoteList` SSH
12
+ sweep), and when exactly one machine holds it, renders that session's summary
13
+ from the owning peer via `runOnPeer` (instead of `Session transcript not
14
+ available`). Same id on more than one box surfaces a machine-labeled conflict to
15
+ disambiguate with `--device <host>`; a UUID found nowhere prints a clear "no
16
+ session on this machine" message. There is **no** fuzzy/content fallback for a
17
+ UUID anywhere — the peer's `--json` answer id-resolves too, so a content
18
+ mentioner can never masquerade as the session. `--local` still restricts the
19
+ lookup to the local machine, and a peer already answering a parent's sweep
20
+ (`AGENTS_SESSIONS_LOCAL=1`) never re-fans-out. Source:
21
+ `resolveSessionAcrossFleet` / `fleetHitsById` / `shouldFanOutForId` in
22
+ `apps/cli/src/commands/sessions.ts` (wired into `renderOneSession`), and the
23
+ id-only `--json` resolution at the sessions listing seam. (RUSH-2024)
24
+
25
+ - **`agents doctor --devices` now detects cross-device harness divergence (RUSH-2027).** The umbrella fleet diagnostic compares each registered device's installed harness inventory — resources (commands, skills, hooks, rules, mcp, permissions, subagents, plugins, promptcuts, workflows), per-agent installed versions, and `.agents`/`.system` config-repo state (branch, HEAD, dirty) — against the local machine as the baseline, and flags anything present on one box but missing on another. A plugin like `swarm` installed on `zion` but absent on `yosemite-s0` now surfaces as a clear warning (`yosemite-s0 is missing plugin 'swarm' (present on zion)`) instead of only being discovered at runtime as `Unknown command: /swarm:run`. Agent-version gaps (`yosemite-s0 is missing claude@2.1.220`) and diverged config repos are reported too. Read-only by default — it never installs or syncs; `--json` carries a stable `fleet` divergence block for the VS Code extension to consume. `agents fleet status` gained the same per-device divergence warning in its rollup. Every device's top-level `doctor --json` now emits a `fleet` inventory field so the comparison needs no extra probe. Source: `apps/cli/src/lib/devices/fleet-divergence.ts` (comparator), `apps/cli/src/lib/devices/fleet-inventory.ts` (`collectLocalFleetInventory`), `apps/cli/src/commands/doctor.ts` (`runDevicesDoctor`, `renderFleetDivergence`, `--json` `fleet` field), `apps/cli/src/lib/devices/health-report.ts` (`buildFleetHealthReport` divergence warning), `apps/cli/src/lib/git.ts` (`readRepoState`).
26
+
27
+ - **`agents doctor` now shows repo-behind notices; they no longer appear on stderr during normal commands (RUSH-2048).** `printPendingUpdateNotices()` — which wrote "agents-cli: ~/.agents/ is N commits behind origin/main" to stderr on every CLI invocation — is replaced by `readRepoBehindMarkers()`, which returns the same data without printing. `agents doctor` reads these markers and renders a "Repo updates" section showing which repos are behind and the `agents repo pull <alias>` fix command. `agents doctor --json` emits a `repos` array so menubar helpers and other consumers can read the same data. Markers persist on disk until the next background fetch overwrites them, so the notice stays visible until the user acts. Source: `apps/cli/src/lib/auto-pull.ts`, `apps/cli/src/commands/doctor.ts`, `apps/cli/src/index.ts`.
28
+
29
+ - **Session affinity data + host affinity resolver (RUSH-2049).** Sessions index
30
+ persists `machine` (schema v18) so affinity can `GROUP BY machine`.
31
+ `queryAffinityRollup` returns launch counts by device (and harness/joint for
32
+ analytics). Host affinity sampling lives in `smart-launch.ts` as
33
+ `resolveDeviceAffinity` / `applyDeviceAutoToOptions` (weight ∝ launches^α;
34
+ online hosts with no history still explore at weight 1). Account pick stays
35
+ the existing balanced strategy (live session/week rate-limit windows).
36
+ **User-facing host pick shipped as `--device auto` / `--host auto` in
37
+ RUSH-2059** (not a public `--smart` flag and not harness auto-pick). Source:
38
+ `apps/cli/src/lib/session/db.ts`, `origin-machine.ts`, `smart-launch.ts`.
39
+
40
+ - **`agents sessions --all` now widens every non-status filter, not just the
41
+ directory (RUSH-2055).** `--all` used to only drop the current-project scope; it
42
+ now also drops the 30-day window cap, so one flag means "all values for every
43
+ non-status filter" — all directories AND all time. `--active` still composes as a
44
+ status filter, and `-a` / `--device` / `--since` still narrow their own axis (an
45
+ explicit `--since` overrides the all-time default). Applies to both the bare
46
+ listing and `--active`. Source: `apps/cli/src/commands/sessions-browser.ts`.
47
+
48
+ - **Device affinity is `--device auto` (not `--smart` / harness `auto`) (RUSH-2059).**
49
+ Host pick from 14d usage affinity is a special value on the existing host flags:
50
+ `agents run claude --device auto` or `--host auto`. The harness is always the
51
+ agent you type — never auto-selected. Deprecated hidden `--smart` maps to
52
+ `--device auto` for one release. Extension New Agent unpinned launches use
53
+ `--device auto`. Banner: `device=auto → <host> (affinity …) · accounts=balanced`.
54
+ Source: `apps/cli/src/commands/exec.ts`, `smart-launch.ts`,
55
+ `apps/factory/src/core/agents.ts`.
56
+
57
+ - **`agents sessions --active` no longer hides most of your running sessions.** On a TTY, `--active` opens the interactive browser, and the browser resolved "running" two ways that both dropped live sessions: its live scan called the local-only `getActiveSessions()` instead of the fleet sweep the static view uses, and it treated running as an *intersection* with the transcript index (`pool.filter(r => live.has(r.id))`) rather than a source of rows. Together they meant every session on another machine was invisible, as was any local one the index didn't already carry — a fleet with 32 live sessions across 7 machines showed 4. The browser now shares one gather with the static view (`gatherActiveSessions`) and folds live sessions the index lacks in as their own rows, keyed by session id, cloud task id, or `machine:pid` so two id-less sessions never collapse into one. Picking a row that has no session id yet reports where the process is instead of trying to open a transcript that doesn't exist. Source: `apps/cli/src/commands/sessions.ts` (`gatherActiveSessions`, `isIdlessLiveRow`), `apps/cli/src/commands/sessions-browser.ts` (`liveRowKey`, `indexLiveRows`, `liveSessionToMeta`, `mergeLiveIntoPool`).
58
+ - **The session browser now shows which program each running session is in.** A new host column names the terminal or editor hosting the session — `codium`, `ghostty`, `tmux`, or `tmux→ghostty` when a tmux session is being watched through another app (a bare `tmux` means it is running detached) — so a session in the list can actually be found. The column is live-only and appears just in the running view, since transcript metadata carries no host. The id column also truncates now, so a row named by a 7-digit pid can no longer shunt every later column out of alignment. Source: `apps/cli/src/commands/sessions.ts` (`liveHostLabel`, `formatPickerLabel`, `PickerColumns.showHost`).
59
+
60
+ - **The routines daemon holds no Claude credential and injects no token.** A
61
+ scheduled or daemon-fired Claude run now authenticates exactly like an
62
+ interactive `agents run claude` on the same machine: through the rotation-pinned
63
+ account's own `CLAUDE_CONFIG_DIR` login (`.credentials.json`), which Claude Code
64
+ refreshes per-device. The daemon previously read a token from the `claude`
65
+ secrets bundle and injected it into every routine spawn — first as one ambient
66
+ `CLAUDE_CODE_OAUTH_TOKEN` (RUSH-1759), then also as per-account
67
+ `CLAUDE_CODE_OAUTH_TOKEN_<account>` setup-tokens — which shadowed each account's
68
+ own on-disk login and made the daemon a second, competing credential store. Both
69
+ paths are removed, along with the sandbox `ENV_ALLOWLIST` entry that forwarded
70
+ them; a sandboxed routine now strips `CLAUDE_CODE_OAUTH_TOKEN` from its
71
+ environment and falls through to the per-account login. A box whose interactive
72
+ login has expired is skipped up front by the auth-health preflight with a
73
+ `re-login required` hint instead of running on an injected fallback — log in once
74
+ on that box (`agents run claude`) to restore it; no daemon restart is needed. This
75
+ keeps the daemon out of the credential entirely, which is what avoids the
76
+ fleet-wide rotation logout (a shared/injected token was the cause, not the fix).
77
+ Removed: `readDaemonClaudeOAuthToken` / `readDaemonClaudeBundleEnv` /
78
+ `buildDetachedDaemonEnv` (`daemon.ts`), `resolveAccountSetupToken` and
79
+ `apps/cli/src/lib/secrets/account-token.ts`, `claudeHomeHasOwnCredential`
80
+ (`agents.ts`). Source: `apps/cli/src/lib/daemon.ts`, `runner.ts`, `sandbox.ts`,
81
+ `agents.ts`.
82
+
83
+ - **`agents sessions` no longer over-counts test results from arbitrary stdout.** The catch-up digest scraped any `\d+ pass`-shaped substring anywhere in a command's output, so a `442 passwords generated` log, a `git status: 442 files` line, or a `442 passes/sec` benchmark was reported as `Tests ✓ tests 442 pass`. It also treated any command merely containing a runner token as a test run, so npm-script sub-targets like `bun test:setup`, `npm run test:watch`, or `pnpm test:ci` were counted. Test-run classification now matches only real invocations (`bun/npm/yarn/pnpm test` bare, `vitest`, `jest`, `mocha`, `pytest`, `go test`, `cargo test`, `tsc`) and rejects `:sub-target` scripts, and pass/fail counts are read only from each runner's authoritative summary construct — vitest's ` Tests N passed` / ` Tests N failed | M passed` row, jest's `Tests:` line, pytest's `=== N passed[, M failed] in Xs ===` rule, bun's ` N pass`/` N fail` block closed by `Ran N tests`, and mocha's ` N passing`/` N failing`. A verdict is reported only when a real summary matched, so an ambiguous blob now shows nothing instead of a fabricated pass count. Source: `apps/cli/src/lib/session/digest.ts` (`TEST_RUNNERS` classification with `(?![:\w-])` guard, new `parseSummaryLine`, `parseTestOutput`); consumed by `apps/cli/src/lib/session/render.ts` (`renderTestsLine`) and `apps/cli/src/commands/sessions-picker.ts`.
84
+
85
+ - **Readable `Dirs:` line in `agents sessions`.** The session preview's touched-directories line no longer renders raw Claude project-slugs (`-home-me--agents-…`) or nested worktree paths. Paths under a git worktree collapse to `⧉ <slug>/<remainder>`; a Claude project-slug is matched in slug space (its cwd/`.`-encoding is lossy, so it is never decoded to a fake path) — a slug worktree shows `⧉ <name>` and a slug pointing at the session's own cwd (internal projects-storage scratch) is dropped; real paths still relativize against the session cwd and home (`~`). Source: `apps/cli/src/commands/sessions-picker.ts`.
86
+
87
+ - **`release.sh` is now a zero-config, self-routing release — runnable from any fleet box with an empty environment.** No routing/secret environment variables: `SIGN_HOST`, `SECRET_HOST`, `SIGN_HOST_REPO`, `FORCE_REMOTE_SIGN`, the `PREFERRED_SIGN_HOSTS` list, the `zion` fallback, and the `agents devices` fleet discovery are all gone. The release has three self-selected homes: git/gh orchestration on the invoking box, the Linux test suite on a **dynamic crabbox** (`scripts/sandbox.sh` selects an available Hetzner VM for the repo's `.crabbox.yaml` profile or warms one — never a hardcoded instance), and build + sign + notarize + `npm publish` + computer-helper on the **`mac-mini` home base** (the one hardcoded name, `RELEASE_HOME_BASE`). The script detects its own host (`scutil --get LocalHostName` / `hostname -s`) and runs the privileged phase on the home base — locally if already there, else over ssh — always by checking out the `v<version>` tag into a throwaway worktree and running **that worktree's** `release.sh --home-base-phase`, so the publishing script is the one carried by the release tag, never the home base's stale on-disk checkout; the worktree is removed on exit on success or failure. The npm token is resolved on the home base and never borrowed to the trigger box. A new shared `scripts/headless-sign-context.sh` factors the headless keychain-unlock + `AGENTS_SECRETS_PASSPHRASE` preamble (no Touch ID) used by both the on-home-base publish and `remote-sign-mac.sh`. A phase tracker (`[n/N]`, N=6 for a normal release, 4 for a catch-up publish) labels each phase with the box it runs on and a ✓/✗ result; a crabbox test failure prints the failing tests + the captured log path and halts before any PR/publish. Idempotency/catch-up/tree-verification guards are preserved. Source: `apps/cli/scripts/release.sh`, `apps/cli/scripts/remote-sign-mac.sh`, `apps/cli/scripts/headless-sign-context.sh`.
88
+
89
+ - **`agents secrets unlock` now grants globally, so one Touch ID actually covers everything.** An unlock was silently scoped to the ambient `AGENTS_AGENT_NAME`: typed in a plain shell it was stored under a literal `cli` harness, while a read from inside an agent looked under *its* harness (`claude`, `codex`, …). The two never met, so a valid 7-day grant was invisible to every agent for its whole life — `agents secrets exec <bundle>` reported "not unlocked in the secrets agent" while the bundle sat unexpired in the store, and each miss cost another Touch ID or blocked a headless run outright. An unlock with no `--for` is now a global grant that every harness and a plain shell can read; `--for <agent>` still narrows it to one harness, and readers resolve own-harness → global so a narrow grant wins where it applies. The broker's in-memory store and the durable session store share one scope chain, so behavior is identical before and after a daemon restart. Grants already written under the old `cli` scope migrate to global on the next broker start — an unlock you already paid Touch ID for keeps working across the upgrade instead of going unreadable. Source: `apps/cli/src/lib/secrets/scope.ts` (`GLOBAL_HARNESS`, `bundleScopeChain`), `apps/cli/src/lib/secrets/agent.ts` (`get` handler), `apps/cli/src/lib/secrets/session-store.ts` (`resolveSession`, `cli`→global migration), `apps/cli/src/lib/secrets/bundles.ts` (`readAndResolveBundleEnv`), `apps/cli/src/commands/secrets.ts` (`unlock --for`).
90
+
91
+ - **`agents sessions export <id>` now resolves a short id the same way `sessions
92
+ <id>` does — by id only, never fuzzy content.** The id-only fix landed for the
93
+ `sessions` view but `sessions export` still gated its index lookup on
94
+ `isCompleteSessionId`, so a bare hex short-id like `d3470b57` absent from the
95
+ discovered pool skipped the index and fell through to the text query — bundling
96
+ every transcript that merely MENTIONED the id into the export. The one canonical
97
+ id-shaped test, `looksLikeSessionId`, now lives beside `isCompleteSessionId` in
98
+ `lib/session/discover.ts` and is shared: `sessions export` resolves any id-shaped
99
+ selector through the index (exact -> prefix -> `findSessionsById`) and reports
100
+ "No session with id …" on a miss instead of shipping the mentioner. Source:
101
+ `apps/cli/src/lib/session/discover.ts`, `apps/cli/src/commands/sessions-export.ts`.
102
+
103
+ - **Resolve a Claude transcript across every version home, not just the live `~/.claude`.** A session launched under an earlier agent version keeps its transcript under that version's home; resolving only the `~/.claude` symlink (which repoints to the newest installed version) meant that installing a new version silently hid every still-running older-version session — no `sessionFile`, so `agents sessions` rendered it `unknown` and the watchdog skipped it as "no activity timestamp". `findClaudeSessionFile` now searches all version-home project roots via `getAgentSessionDirs`, newest mtime winning. Source: `apps/cli/src/lib/session/active.ts`.
104
+
3
105
  ## 1.20.77
4
106
 
5
107
  - **Interactive `agents run --host` now tracks the real session for every agent,
package/README.md CHANGED
@@ -443,6 +443,8 @@ agents hosts check gpu-box # reachable? which agents-cli version?
443
443
  agents run claude --host gpu-box "profile this build" # headless: follows live by default
444
444
  agents run claude --host gpu-box # no prompt → interactive TTY over SSH (tmux-backed)
445
445
  agents run claude --host gpu-box --copy-creds "fix auth" # copy local runtime creds + Claude token, shred after
446
+ agents run claude --device auto "…" # affinity-pick host from 14d usage (harness stays claude)
447
+ agents run claude --host auto "…" # same — auto is a host value, not a harness name
446
448
  agents hosts ps # list dispatched runs + terminal status
447
449
  agents hosts stop <id> # terminate a hung/detached run (alias: kill)
448
450
  agents logs --host gpu-box # pick a dispatched run — concise summary by default
package/dist/bin/agents CHANGED
Binary file
@@ -22,6 +22,7 @@
22
22
  * apply pending sync.
23
23
  */
24
24
  import type { Command } from 'commander';
25
+ import { type FleetDivergenceReport } from '../lib/devices/fleet-divergence.js';
25
26
  export declare function wrapLine(prefix: string, text: string, width?: number): string[];
26
27
  /**
27
28
  * Windows-only advisory lines. When the effective PowerShell execution policy
@@ -33,4 +34,12 @@ export declare function wrapLine(prefix: string, text: string, width?: number):
33
34
  * PowerShell.
34
35
  */
35
36
  export declare function execPolicyWarningLines(platform: NodeJS.Platform, policy: string | null): string[];
37
+ /**
38
+ * Render the cross-device divergence section for `agents doctor --devices`
39
+ * (RUSH-2027): a clean all-clear when the fleet agrees, otherwise the specific
40
+ * gaps — a resource/version present here but missing on a box, or a diverged
41
+ * config repo — grouped by device, each a plain-language line. Devices that
42
+ * couldn't be compared (offline / older CLI) are named so the readout is honest.
43
+ */
44
+ export declare function renderFleetDivergence(report: FleetDivergenceReport): string[];
36
45
  export declare function registerDoctorCommand(program: Command): void;
@@ -4,6 +4,8 @@ import { addHostOption } from '../lib/hosts/option.js';
4
4
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
5
5
  import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
6
6
  import { fanOutDevices } from '../lib/devices/fleet.js';
7
+ import { compareFleetInventories } from '../lib/devices/fleet-divergence.js';
8
+ import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
7
9
  import { resolveHost } from '../lib/hosts/registry.js';
8
10
  import { sshExecAsync } from '../lib/ssh-exec.js';
9
11
  import { sshTargetFor } from '../lib/hosts/types.js';
@@ -24,6 +26,7 @@ import { heal, healChangedAnything } from '../lib/heal.js';
24
26
  import { blocksLocalScripts, getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
25
27
  import { scanUserRcFiles, rcSecretWarningLines } from '../lib/secrets/rc-hygiene.js';
26
28
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
29
+ import { readRepoBehindMarkers } from '../lib/auto-pull.js';
27
30
  import * as fs from 'fs';
28
31
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
29
32
  // ─── overview mode (no target) ────────────────────────────────────────────────
@@ -61,7 +64,7 @@ function printWrappedLine(prefix, text) {
61
64
  for (const line of wrapLine(prefix, text))
62
65
  console.log(chalk.gray(line));
63
66
  }
64
- function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn) {
67
+ function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers) {
65
68
  console.log(chalk.bold('Agent CLIs'));
66
69
  // Show the fleet you actually run — agents that are ready in PATH, plus any
67
70
  // you MANAGE (have installed versions) whose binary isn't resolving (a real
@@ -182,6 +185,10 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn) {
182
185
  // environment (readable via /proc/<pid>/environ) — the RUSH-1968 class. Flag
183
186
  // them here so the master passphrase never silently lives in `~/.zshenv` again.
184
187
  renderRcHygieneAdvisory();
188
+ // Repos that are behind upstream — surfaced here instead of on stderr during
189
+ // every command. Markers are written by the background fetch worker and persist
190
+ // until the user runs `agents repo pull <alias>`.
191
+ renderRepoBehindAdvisory(repoBehindMarkers);
185
192
  }
186
193
  // ─── windows execution-policy advisory ─────────────────────────────────────────
187
194
  /**
@@ -217,6 +224,25 @@ function renderRcHygieneAdvisory() {
217
224
  console.log(chalk.gray(` ${line}`));
218
225
  }
219
226
  }
227
+ // ─── repo-behind advisory ─────────────────────────────────────────────────────
228
+ /**
229
+ * Render repo-behind notices from background fetch markers as a "Repo updates"
230
+ * section in `agents doctor`. Reads without consuming the markers so repeated
231
+ * invocations keep showing the notice until the user runs `agents repo pull`.
232
+ */
233
+ function renderRepoBehindAdvisory(markers) {
234
+ const behind = markers.filter((m) => m.behind > 0);
235
+ if (behind.length === 0)
236
+ return;
237
+ console.log();
238
+ console.log(chalk.bold('Repo updates'));
239
+ for (const m of behind) {
240
+ const label = m.alias === 'user' ? '~/.agents/' : m.alias;
241
+ const commits = `${m.behind} commit${m.behind === 1 ? '' : 's'}`;
242
+ console.log(` ${chalk.yellow('warn ')} ${label} is ${commits} behind ${m.branch}`);
243
+ console.log(chalk.gray(` run \`agents repo pull ${m.alias}\` to update`));
244
+ }
245
+ }
220
246
  function renderExecPolicyAdvisory() {
221
247
  // Only probe the policy on Windows — getEffectiveExecutionPolicy() spawns
222
248
  // powershell, which is a wasted (doomed) process on POSIX where the advisory
@@ -314,29 +340,74 @@ async function probeFleetTarget(target) {
314
340
  };
315
341
  }
316
342
  }
343
+ /**
344
+ * Fetch a device's harness inventory by running its top-level `agents doctor
345
+ * --json` (which carries the `fleet` field, RUSH-2027). Separate from
346
+ * {@link probeFleetTarget} — that forwards `teams doctor --json` for per-agent
347
+ * readiness, a different payload. Returns the parsed {@link FleetInventory} or
348
+ * null (unreachable, non-zero exit, unparseable, or an older CLI with no
349
+ * `fleet` field); a null is skipped by the comparator, never a false gap.
350
+ */
351
+ async function probeFleetInventory(target) {
352
+ const isWin = /^win/i.test((target.os ?? '').trim());
353
+ const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
354
+ const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
355
+ if (res.code !== 0)
356
+ return null;
357
+ try {
358
+ const parsed = JSON.parse(res.stdout);
359
+ return parsed.fleet ?? null;
360
+ }
361
+ catch {
362
+ return null;
363
+ }
364
+ }
317
365
  async function runDevicesDoctor(opts) {
318
366
  const singleName = opts.host || opts.device;
319
367
  const targets = await resolveFleetTargets(opts);
320
368
  const localName = machineId();
321
369
  const results = [];
322
- // Local machine first, directly.
370
+ // Local machine first, directly. Its inventory is collected in-process — no
371
+ // SSH round-trip to this box.
323
372
  if (!singleName) {
324
- results.push({ name: localName, online: true, agents: await collectTeamsDoctorData() });
373
+ results.push({
374
+ name: localName,
375
+ online: true,
376
+ agents: await collectTeamsDoctorData(),
377
+ inventory: collectLocalFleetInventory(opts.cwd ?? process.cwd()),
378
+ });
325
379
  }
326
- // Remote targets in parallel, through the shared fleet fan-out helper.
327
- const remoteResults = await fanOutDevices(targets, probeFleetTarget);
380
+ // Remote targets in parallel: agent readiness (teams doctor) and the harness
381
+ // inventory (top-level doctor --json) in one fan-out per concern. Both run
382
+ // through the shared fleet helper so an offline box degrades to a skipped row.
383
+ const [remoteResults, inventoryResults] = await Promise.all([
384
+ fanOutDevices(targets, probeFleetTarget),
385
+ fanOutDevices(targets, probeFleetInventory),
386
+ ]);
387
+ const inventoryByName = new Map();
388
+ for (const r of inventoryResults)
389
+ inventoryByName.set(r.name, r.status === 'ok' ? (r.value ?? null) : null);
328
390
  results.push(...remoteResults.map((r) => {
391
+ const inventory = inventoryByName.get(r.name) ?? undefined;
329
392
  if (r.status === 'ok' && r.value)
330
- return r.value;
393
+ return { ...r.value, inventory: inventory ?? undefined };
331
394
  return {
332
395
  name: r.name,
333
396
  online: false,
334
397
  error: r.error ?? String(r.reason ?? 'skipped'),
335
398
  agents: {},
399
+ inventory: inventory ?? undefined,
336
400
  };
337
401
  }));
402
+ // Cross-device divergence: compare every device's inventory against the local
403
+ // baseline and flag resources / agent versions / repo state present on one box
404
+ // but missing on another (RUSH-2027). A single-device filter has no baseline
405
+ // to compare against, so the section is only meaningful for the full fan-out.
406
+ const divergence = singleName
407
+ ? null
408
+ : compareFleetInventories(results.map((r) => ({ name: r.name, inventory: r.inventory ?? null })), localName);
338
409
  if (opts.json) {
339
- console.log(JSON.stringify({ devices: results }, null, 2));
410
+ console.log(JSON.stringify({ devices: results, fleet: divergence }, null, 2));
340
411
  return;
341
412
  }
342
413
  if (results.length === 0) {
@@ -360,6 +431,49 @@ async function runDevicesDoctor(opts) {
360
431
  }
361
432
  console.log();
362
433
  console.log(chalk.gray(' rdy* = installed and signed in · rdy = installed · no = not installed · err = probe failed · - = offline'));
434
+ if (divergence) {
435
+ console.log();
436
+ for (const line of renderFleetDivergence(divergence))
437
+ console.log(line);
438
+ }
439
+ }
440
+ /**
441
+ * Render the cross-device divergence section for `agents doctor --devices`
442
+ * (RUSH-2027): a clean all-clear when the fleet agrees, otherwise the specific
443
+ * gaps — a resource/version present here but missing on a box, or a diverged
444
+ * config repo — grouped by device, each a plain-language line. Devices that
445
+ * couldn't be compared (offline / older CLI) are named so the readout is honest.
446
+ */
447
+ export function renderFleetDivergence(report) {
448
+ const lines = [chalk.bold('Cross-device divergence')];
449
+ const skippedNote = report.skippedDevices.length > 0
450
+ ? chalk.gray(` (not compared: ${report.skippedDevices.join(', ')} — offline or no inventory)`)
451
+ : null;
452
+ if (!report.hasDivergence) {
453
+ lines.push(chalk.green(` Fleet is consistent — every compared device matches ${report.baseline}.`));
454
+ if (skippedNote)
455
+ lines.push(skippedNote);
456
+ return lines;
457
+ }
458
+ // Group findings by the LAGGING device so a box with several gaps reads as one
459
+ // block AND the remediation points at the right box. For a `*-missing-local`
460
+ // finding the resource is absent on the local baseline (present on `d.device`),
461
+ // so the box that's behind is the baseline — not the remote that has it.
462
+ const byDevice = new Map();
463
+ for (const d of report.divergences) {
464
+ const lagging = d.kind.endsWith('-missing-local') ? report.baseline : d.device;
465
+ (byDevice.get(lagging) ?? byDevice.set(lagging, []).get(lagging)).push(d);
466
+ }
467
+ for (const device of Array.from(byDevice.keys()).sort()) {
468
+ lines.push(` ${chalk.yellow('⚠')} ${chalk.bold(device)}`);
469
+ for (const d of byDevice.get(device)) {
470
+ lines.push(chalk.gray(` ${d.message}`));
471
+ }
472
+ }
473
+ if (skippedNote)
474
+ lines.push(skippedNote);
475
+ lines.push(chalk.gray(' Read-only — run `agents apply` or `agents repo pull` on a lagging box to reconcile.'));
476
+ return lines;
363
477
  }
364
478
  function parseTargetArg(arg) {
365
479
  const at = arg.indexOf('@');
@@ -647,7 +761,7 @@ export function registerDoctorCommand(program) {
647
761
  .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
648
762
  .option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
649
763
  .option('--release <agent>', 'Undo --adopt: restore the native launcher agents-cli previously adopted')
650
- .option('--devices', 'Check agent readiness on every registered device (alias --hosts)')
764
+ .option('--devices', 'Check agent readiness AND cross-device harness divergence (missing resources/versions, repo drift) on every registered device (alias --hosts)')
651
765
  .option('--hosts', 'Alias of --devices');
652
766
  setHelpSections(doctorCmd, {
653
767
  examples: `
@@ -671,6 +785,10 @@ export function registerDoctorCommand(program) {
671
785
 
672
786
  # Heal just one agent (all its installed versions)
673
787
  agents doctor claude --fix
788
+
789
+ # Fleet: agent readiness + cross-device divergence (missing plugins/skills,
790
+ # agent-version gaps, .agents/.system repo drift) vs this machine
791
+ agents doctor --devices
674
792
  `,
675
793
  });
676
794
  doctorCmd.action(async (target, opts) => {
@@ -758,6 +876,7 @@ export function registerDoctorCommand(program) {
758
876
  const syncRows = checkSyncStatus(cwd);
759
877
  const orphanRows = countOrphans();
760
878
  const hostClis = listCliStatus(cwd);
879
+ const repoBehindMarkers = readRepoBehindMarkers();
761
880
  // Advisory login state per installed agent (file-based getAccountInfo,
762
881
  // no home → the account-global/active credential). Best-effort: a probe
763
882
  // failure just leaves that agent's badge as "logged out".
@@ -782,6 +901,12 @@ export function registerDoctorCommand(program) {
782
901
  auth: summarizeHostAuth(readAuthHealthCache(), machineId()),
783
902
  sync: syncRows,
784
903
  orphans: orphanRows,
904
+ // This host's harness inventory — installed resources per kind,
905
+ // installed version ids per agent, and `.agents`/`.system` repo
906
+ // state — so `agents doctor --devices` can compare presence across
907
+ // the fleet and flag a resource/version present here but missing
908
+ // elsewhere (RUSH-2027). Read-only; no network.
909
+ fleet: collectLocalFleetInventory(cwd),
785
910
  hostClis: {
786
911
  statuses: hostClis.statuses.map((s) => ({
787
912
  name: s.manifest.name,
@@ -791,10 +916,19 @@ export function registerDoctorCommand(program) {
791
916
  })),
792
917
  errors: hostClis.errors,
793
918
  },
919
+ // Repos behind upstream — emitted here so menubar and other consumers
920
+ // can surface the notices without reading stderr from normal commands.
921
+ repos: repoBehindMarkers.map((m) => ({
922
+ alias: m.alias,
923
+ dir: m.dir,
924
+ behind: m.behind,
925
+ branch: m.branch,
926
+ fetchedAt: m.fetchedAt,
927
+ })),
794
928
  }, null, 2));
795
929
  return;
796
930
  }
797
- renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn);
931
+ renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn, repoBehindMarkers);
798
932
  // Point at the interactive reconcile when anything is out of sync — the
799
933
  // report shouldn't be a dead end. `agents status` runs the unified
800
934
  // home-reading engine and offers to sync (opt-in, never auto-fires here).
@@ -383,8 +383,8 @@ export function registerRunCommand(program) {
383
383
  .option('--budget <tokens>', 'Loop token hard-cap: stop once cumulative tokens reach this (stoppedBy: budget), enforced outside the agent. Loop only.')
384
384
  .option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
385
385
  .option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
386
- .option('--host <name>', 'Offload this run onto another machine over SSH instead of running locally — a device, a registered agent host, or user@host. See `agents devices` / `agents hosts`.')
387
- .option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
386
+ .option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, or user@host. Pass "auto" to pick from 14d usage affinity (most-used online device has highest probability). See `agents devices`.')
387
+ .option('--device <name>', 'Alias of --host. Pass "auto" for affinity-based device pick (same as --host auto).')
388
388
  .option('--remote-cwd <dir>', "Explicit host working directory for --host runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
389
389
  .option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
390
390
  .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
@@ -399,6 +399,8 @@ export function registerRunCommand(program) {
399
399
  // `--on` and `--computer` are hidden aliases of `--host` — same behavior.
400
400
  runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
401
401
  runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
402
+ // Deprecated one-release alias: `agents run … --smart` → treat as `--device auto`.
403
+ runCmd.addOption(new Option('--smart', 'Deprecated: use --device auto (affinity host pick).').hideHelp());
402
404
  // Internal: the `--host` dispatch forwards this so the REMOTE run prints its
403
405
  // resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
404
406
  // letting the launcher relate the remote-created session back to itself for
@@ -495,11 +497,14 @@ export function registerRunCommand(program) {
495
497
  // their existing meaning in every dispatch path below.
496
498
  const accountPicker = parseRunAccountPickerRequest(agentSpec);
497
499
  const accountPickerRequested = accountPicker.requested;
498
- const normalizedAgentSpec = accountPicker.normalizedAgentSpec;
500
+ let normalizedAgentSpec = accountPicker.normalizedAgentSpec;
499
501
  if (!accountPicker.valid) {
500
502
  console.error(chalk.red(`Invalid account picker target: ${agentSpec}. Use agents run <agent>@.`));
501
503
  process.exit(1);
502
504
  }
505
+ // Account-picker conflict check runs BEFORE device=auto may set balanced,
506
+ // so an implicit balanced preference never surfaces as a fake
507
+ // "cannot be combined with --balanced" when the user only typed trailing @.
503
508
  if (accountPickerRequested) {
504
509
  const conflicts = runAccountPickerConflicts(options);
505
510
  if (conflicts.length > 0) {
@@ -508,6 +513,27 @@ export function registerRunCommand(program) {
508
513
  process.exit(1);
509
514
  }
510
515
  }
516
+ // --device auto / --host auto (and deprecated --smart): affinity-pick host.
517
+ // Harness is always the agent the user typed — never auto-picked.
518
+ // Affinity failure degrades to local (does not kill the run).
519
+ {
520
+ const { applyDeviceAutoToOptions } = await import('../lib/smart-launch.js');
521
+ const result = applyDeviceAutoToOptions(options, {
522
+ accountPickerRequested,
523
+ });
524
+ if (!options.quiet && result.deprecationSmart) {
525
+ process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
526
+ }
527
+ if (!options.quiet && result.skipped) {
528
+ process.stderr.write(chalk.yellow(`[agents] device=auto skipped: ${result.skipped} (running local)\n`));
529
+ }
530
+ if (!options.quiet && result.banner) {
531
+ const { hostLabel, deviceHint, acctNote } = result.banner;
532
+ process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
533
+ (deviceHint ? ` (affinity ${deviceHint})` : '') +
534
+ ` · ${acctNote}\n`));
535
+ }
536
+ }
511
537
  // --lease: invent a disposable cloud box for this run (via crabbox), run
512
538
  // the agent there, then tear it down. --box: reuse a named warm crabbox
513
539
  // box, run the same bootstrap there, and leave the box running.
@@ -856,6 +856,10 @@ export function registerRepoCommands(program) {
856
856
  }
857
857
  else {
858
858
  spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
859
+ // A failed repo must fail the command. Without this, `agents fleet run
860
+ // "agents repo pull user"` reported 11 ok across a fleet that pulled
861
+ // nothing — the silence that hid RUSH-2056. Matches commands/sync.ts.
862
+ process.exitCode = 1;
859
863
  }
860
864
  }
861
865
  // RUSH-1980: a pull rewrites the routine YAML on disk, but the daemon's
@@ -911,6 +915,10 @@ export function registerRepoCommands(program) {
911
915
  }
912
916
  else {
913
917
  spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
918
+ // A failed repo must fail the command. Without this, `agents fleet run
919
+ // "agents repo push user"` reported ok across a fleet that pushed
920
+ // nothing — the silence that hid RUSH-2056. Matches commands/sync.ts.
921
+ process.exitCode = 1;
914
922
  }
915
923
  }
916
924
  });
@@ -297,8 +297,10 @@ export function registerRoutinesCommands(program) {
297
297
  - Detached/daemon fires use the pre-flight pick only (next tick re-selects).
298
298
  - Diagnostic lines log which account was picked, which were skipped, and
299
299
  each failover hop: look for "[agents] routine <name>:" in the run log.
300
- - Headless Claude auth: store CLAUDE_CODE_OAUTH_TOKEN in the 'claude'
301
- secrets bundle so the daemon can inject it into routine spawns.
300
+ - Claude auth: a routine authenticates through the pinned account's own
301
+ on-disk login (the same one 'agents run claude' uses on this device).
302
+ The daemon injects no token; if that account's login has expired the
303
+ run is skipped up front with a re-login hint.
302
304
  `,
303
305
  });
304
306
  routinesCmd
@@ -22,6 +22,7 @@ import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundl
22
22
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
23
23
  import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
24
24
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
25
+ import { GLOBAL_HARNESS } from '../lib/secrets/scope.js';
25
26
  import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentStatus, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
26
27
  import { saveSession, deleteBundleSessions, deleteAllSessions } from '../lib/secrets/session-store.js';
27
28
  import { getCliVersionFresh } from '../lib/version.js';
@@ -2057,7 +2058,7 @@ Examples:
2057
2058
  .description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
2058
2059
  .option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
2059
2060
  .option('--durable', 'Keep the unlock across sleep + reboot too (default: survives upgrade/restart but re-locks on sleep). Set secrets.agent.durable in agents.yaml to make this the default.')
2060
- .option('--for <agent>', 'Scope the unlock to one harness type (for example claude, codex, or kimi).')
2061
+ .option('--for <agent>', 'Narrow the unlock to ONE harness type (for example claude, codex, or kimi). Default: the grant is global — every harness and a plain shell can read it, so one Touch ID covers them all.')
2061
2062
  .option('--all', 'Unlock every configured bundle')
2062
2063
  .option('--host <target>', 'Unlock the bundle(s) on this remote machine over SSH instead of locally (file-backed bundles only — the remote\'s passphrase prompt surfaces on your terminal over a -tt session). Single-valued (NOT variadic) so it never swallows the bundle name: `unlock <name> --host <machine>`.')
2063
2064
  .action(async (names, opts) => {
@@ -2130,7 +2131,12 @@ Examples:
2130
2131
  // (single-instance start lock, #414) and best-effort — never blocks unlock.
2131
2132
  ensureDaemonStarted();
2132
2133
  let loaded = 0;
2133
- const harness = opts.for || process.env.AGENTS_AGENT_NAME || 'cli';
2134
+ // An unlock is a deliberate act, so it grants GLOBALLY unless `--for`
2135
+ // narrows it to one harness. It must NOT inherit the ambient
2136
+ // AGENTS_AGENT_NAME: that silently scoped a terminal unlock to whichever
2137
+ // agent happened to launch the shell, leaving the grant unreadable to
2138
+ // every other reader for its whole TTL.
2139
+ const harness = opts.for || GLOBAL_HARNESS;
2134
2140
  for (const name of targets) {
2135
2141
  try {
2136
2142
  // noAgent: read the real keychain (one Touch ID) rather than the
@@ -9,7 +9,8 @@
9
9
  * Built on {@link dynamicPicker}; every data source (discover, fleet, live index,
10
10
  * preview, resume dispatch) is reused from the existing sessions plumbing.
11
11
  */
12
- import type { SessionMeta } from '../lib/session/types.js';
12
+ import { type SessionMeta } from '../lib/session/types.js';
13
+ import type { ActiveSession } from '../lib/session/active.js';
13
14
  /**
14
15
  * The single canonical filter state. Every field has a flag equivalent, so the
15
16
  * same view is reachable interactively (hotkeys) or from the command line (flags).
@@ -59,11 +60,14 @@ export declare function activeBrowserSeed(opts: {
59
60
  agent?: string;
60
61
  host?: string[];
61
62
  since?: string;
63
+ all?: boolean;
62
64
  }): Partial<BrowserFilter>;
63
65
  /**
64
66
  * The initial filter for the bare interactive listing: current-repo subtree by
65
- * default (matches the static overview's cwd scoping), `--all` widens to every
66
- * directory, `--since` seeds the window.
67
+ * default (matches the static overview's cwd scoping). `--all` sets every
68
+ * non-status filter to its "all" value — every directory (project) AND all-time
69
+ * (window) — so one flag maxes the view; `--since` still overrides the window and
70
+ * `-a`/`--device` still narrow their axis.
67
71
  */
68
72
  export declare function bareBrowserSeed(opts: {
69
73
  teams?: boolean;
@@ -71,6 +75,44 @@ export declare function bareBrowserSeed(opts: {
71
75
  all?: boolean;
72
76
  since?: string;
73
77
  }): Partial<BrowserFilter>;
78
+ /**
79
+ * A live session's stable row key: its session id when the agent reported one,
80
+ * else a per-machine handle so an id-less live session (a just-booted harness, a
81
+ * cloud task) still gets exactly one row instead of being dropped. Cloud rows key
82
+ * on the task id because they have no pid — keying them all on the machine alone
83
+ * would collapse two cloud tasks into one row, the same silent-drop this whole
84
+ * change exists to remove.
85
+ */
86
+ export declare function liveRowKey(a: ActiveSession, self: string): string;
87
+ /** Index live sessions by {@link liveRowKey} — the join key between the live scan
88
+ * and the transcript pool. Id-carrying rows key on the session id, so a live row
89
+ * and its transcript row collapse to one. */
90
+ export declare function indexLiveRows(rows: ActiveSession[], self: string): Map<string, ActiveSession>;
91
+ /**
92
+ * Project a live session onto the picker's row shape, for a session the transcript
93
+ * pool doesn't carry — a peer's session, a transcript outside the current window,
94
+ * or an agent that has not written one yet. `filePath` is the live transcript path
95
+ * when the scan resolved one and empty otherwise, which is what
96
+ * {@link handlePickedSession} keys "there is nothing to open yet" off.
97
+ */
98
+ export declare function liveSessionToMeta(a: ActiveSession, self: string): SessionMeta;
99
+ /**
100
+ * Fold the live scan INTO the transcript pool. The running filter used to be a
101
+ * pure intersection (`pool ∩ live`), which meant a session had to already be in
102
+ * the pool to be shown as running — so every session on another machine, and
103
+ * every local one outside the pool's window, was invisible in the browser while
104
+ * `--active --json` listed it. Live sessions the pool lacks are appended as their
105
+ * own rows, then the whole set is grouped local-machine-first.
106
+ */
107
+ export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string, ActiveSession>, self: string): SessionMeta[];
108
+ /**
109
+ * Whether to render the host-program column. It belongs to the running view
110
+ * only, so this gates on the FILTER — not on the live index being populated.
111
+ * `liveCache` outlives a toggle of the `r` hotkey, so testing `live` alone would
112
+ * keep the column after running is turned back off, widening a plain transcript
113
+ * listing that has no live rows to explain it.
114
+ */
115
+ export declare function shouldShowHostColumn(f: BrowserFilter, live: Map<string, ActiveSession> | null, rows: SessionMeta[]): boolean;
74
116
  /**
75
117
  * Launch the interactive session browser. `initial` seeds the filter (e.g.
76
118
  * `{ running: true }` for `--active`). Resolves after the user resumes a session