@phnx-labs/agents-cli 1.20.76 → 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.
- package/CHANGELOG.md +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,269 @@
|
|
|
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
|
+
|
|
105
|
+
## 1.20.77
|
|
106
|
+
|
|
107
|
+
- **Interactive `agents run --host` now tracks the real session for every agent,
|
|
108
|
+
not just Claude.** Codex, Kimi, Grok, and Gemini coin their own session id and
|
|
109
|
+
reject a caller-supplied one, so an interactive host run of any of them showed a
|
|
110
|
+
stale/absent id locally — `agents sessions` couldn't surface it and a dropped
|
|
111
|
+
link couldn't auto-reconnect it (RUSH-2033 fixed only the Claude `--session-id`
|
|
112
|
+
path). The launcher now forwards one correlation key it controls
|
|
113
|
+
(`AGENT_LAUNCH_ID`); the remote `agents run` adopts that key
|
|
114
|
+
(`resolveLaunchId`), so its SessionStart hook records the agent's real session id
|
|
115
|
+
under it. After the stream the launcher does one ssh read of the remote hook
|
|
116
|
+
record, resolves the real id by launch id (`resolveRemoteSessionId` /
|
|
117
|
+
`pickRemoteSessionId`), registers it in the local session index, and reconnects
|
|
118
|
+
against it on a dropped link. Claude still forces its own id up front and is
|
|
119
|
+
unchanged. Source: `apps/cli/src/lib/hosts/remote-session-id.ts`,
|
|
120
|
+
`resolveLaunchId` in `apps/cli/src/lib/exec.ts`, and the interactive `--host`
|
|
121
|
+
branch in `apps/cli/src/commands/exec.ts`. (RUSH-2034)
|
|
122
|
+
|
|
123
|
+
- **Project routines can opt into daemon firing with source tracking, sync, and
|
|
124
|
+
host placement (RUSH-2035).** Project YAML under `<project>/.agents/routines/*.yml`
|
|
125
|
+
stays inspection-only until `agents routines enable-project` (with interactive /
|
|
126
|
+
`--yes` approval) records the project on `meta.routines.projects` and materialises
|
|
127
|
+
copies into `~/.agents/routines/` with a `source:` block (`projectPath`, git
|
|
128
|
+
`repo`/`branch`/`commit`). `agents routines sync` (and daemon start/SIGHUP reload)
|
|
129
|
+
refreshes those copies when project YAML changes; `disable-project` / `projects`
|
|
130
|
+
manage the allowlist. New `hostStrategy: local|host|fleet|cloud` (CLI `--placement`)
|
|
131
|
+
chooses where the job body runs: local, a named `--run-on` host, one online fleet
|
|
132
|
+
device per fire (no cross-device double-fire — off-box strategies auto-pin
|
|
133
|
+
`devices`), or the agent's native cloud provider. `--host` remains the remote-
|
|
134
|
+
management passthrough. List/JSON surfaces source repo/branch and strategy. Source:
|
|
135
|
+
`apps/cli/src/lib/routines.ts`, `routines-project.ts`, `routines-placement.ts`,
|
|
136
|
+
`runner.ts`, `daemon.ts`, `commands/routines.ts`, `docs/03-routines.md`.
|
|
137
|
+
|
|
138
|
+
- **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`.
|
|
139
|
+
|
|
140
|
+
- **Codex approval blocks now notify you.** A headless or terminal Codex agent
|
|
141
|
+
blocked on an approval prompt used to stall silently — the feed/notification path
|
|
142
|
+
only fired for Claude. Codex emits `PermissionRequest` (not Claude's
|
|
143
|
+
`Notification`), which the `feed-publish` hook now handles: it publishes an
|
|
144
|
+
approval-class block with a high cost-of-delay and a `deny` safe-default, so the
|
|
145
|
+
blocked agent surfaces on `agents feed` and `agents feed --dispatch` pages the
|
|
146
|
+
phone as urgent. A Codex approval card clears once the approved tool runs, via a
|
|
147
|
+
matcher-less `PostToolUse` clear hook registered **for Codex only** — so Claude's
|
|
148
|
+
card lifetime (its `permission_prompt`/`idle_prompt`/`elicitation_dialog`
|
|
149
|
+
notification blocks persist until `Stop`/`SessionEnd`) and per-tool overhead are
|
|
150
|
+
exactly as before. The other feed hooks are now registered for Codex too, not
|
|
151
|
+
Claude only. The Factory extension bridges the same waiting state to an
|
|
152
|
+
edge-triggered VS Code notification with a "Focus terminal" action. Claude's path
|
|
153
|
+
is unchanged. Source: `FEED_PUBLISH_HOOK_SCRIPT` / `ensureFeedPublishHook` in
|
|
154
|
+
`apps/cli/src/lib/feed.ts`, `apps/factory/src/core/waitingNotifier.ts`.
|
|
155
|
+
(RUSH-2039)
|
|
156
|
+
|
|
157
|
+
- **`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`).
|
|
158
|
+
|
|
159
|
+
- **`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`.
|
|
160
|
+
|
|
161
|
+
- **Checklist completions emit a feed event (RUSH-2046).** When an agent marks a
|
|
162
|
+
task-checklist item done, the `11-activity-log.py` hook now appends a
|
|
163
|
+
`task.completed` milestone to the session activity log (and `checklist.created`
|
|
164
|
+
the first time a checklist appears), so completions show in `agents feed` and the
|
|
165
|
+
unified `agents events` stream with the item subject and running `N/M`. Detection
|
|
166
|
+
folds the transcript across harnesses — Claude `TaskUpdate`/`TodoWrite`, Grok
|
|
167
|
+
`todo_write`, Codex `update_plan` — so a completion is recognized regardless of
|
|
168
|
+
which agent produced it. Source: `apps/cli/src/lib/activity.ts` (incl. the embedded
|
|
169
|
+
hook), `apps/cli/src/lib/events.ts`, `apps/cli/src/commands/feed.ts`.
|
|
170
|
+
|
|
171
|
+
- **Actor provenance now survives the SSH hop.** A run dispatched to another host
|
|
172
|
+
(`agents run --host`, a remote `agents teams` supervisor, or any `--host`
|
|
173
|
+
passthrough) used to drop the resolved actor at the SSH boundary, so the remote
|
|
174
|
+
re-resolved it from the *originating* box's `SSH_CONNECTION` and mis-credited the
|
|
175
|
+
work to the shared machine or `UNRESOLVED@<host>`. The dispatch layer now forwards
|
|
176
|
+
`AGENTS_ACTOR*` / `GIT_*` across the wire (POSIX `export` and Windows `$env:`
|
|
177
|
+
alike), so the remote inherits the origin identity instead of re-resolving. A
|
|
178
|
+
caller-supplied env value still wins on collision (mirrors `buildExecEnv`).
|
|
179
|
+
Source: `withActorEnv` in `apps/cli/src/lib/hosts/dispatch.ts`, wired into
|
|
180
|
+
`launchDetached` / `runInteractiveOnHost` and the `--host` passthrough. (RUSH-2028)
|
|
181
|
+
|
|
182
|
+
- **A Linux-driven release now auto-discovers its macOS sign host instead of
|
|
183
|
+
hardcoding `mac-mini`.** `scripts/remote-sign-mac.sh` previously defaulted
|
|
184
|
+
`SIGN_HOST` to `mac-mini`, so a release from a Linux box failed outright whenever
|
|
185
|
+
that one appliance was offline — the recurring reason a release stalled and a
|
|
186
|
+
human had to finish it by hand. With `SIGN_HOST` unset the script now reads
|
|
187
|
+
`agents devices list --json`, keeps the reachable/online macOS devices, and picks
|
|
188
|
+
the first that answers `ssh` in preference order `mac-mini` → `zion` → any other
|
|
189
|
+
online Mac. `mac-mini` stays first because it signs headlessly (no Touch ID);
|
|
190
|
+
`zion` (the interactive Mac) is the fallback. An explicit `SIGN_HOST=<host>` still
|
|
191
|
+
pins one and skips discovery, and when no reachable Mac qualifies the script fails
|
|
192
|
+
with the ordered list it tried rather than hanging on a dead host. Source:
|
|
193
|
+
`apps/cli/scripts/remote-sign-mac.sh`.
|
|
194
|
+
|
|
195
|
+
- **An agent launch never raises a Touch ID sheet.** On macOS, starting an agent
|
|
196
|
+
terminal or firing a routine could pop several biometric prompts in a row, because
|
|
197
|
+
each keychain read runs in its own helper process and the biometric assertion never
|
|
198
|
+
reuses across processes. Two causes: `interactiveUnlock` defaulted to true whenever
|
|
199
|
+
an agent name was present, which let an agent-initiated read fall through the
|
|
200
|
+
`agentOnly` guard; and `isHeadlessSecretsContext` recognized the `headless` and
|
|
201
|
+
`teams` runtimes but not `terminal`, which is what an interactive run sets. Agent
|
|
202
|
+
launches now resolve broker-only and a locked bundle fails fast naming
|
|
203
|
+
`agents secrets unlock <bundle>`; `AGENTS_SECRETS_NO_PROMPT=1` is no longer needed
|
|
204
|
+
as a workaround. `agents secrets get/export/exec` typed in a **plain shell** still
|
|
205
|
+
prompts — it carries no `AGENTS_RUNTIME`, so the guard does not apply. Run beneath
|
|
206
|
+
an agent it refuses, because there the agent is the caller. This narrows the
|
|
207
|
+
agent-triggered approval added in RUSH-2032, which is unreleased.
|
|
208
|
+
|
|
209
|
+
- **`release.sh` now borrows the npm token from a primary device when the local box
|
|
210
|
+
has none, so a Linux-driven release stops asking a human to approve a token.**
|
|
211
|
+
Token resolution was env → local `npmjs.com` bundle → *die*. On a fleet box whose
|
|
212
|
+
own keychain holds no npm token, that dead end pushed agents to hand-move a
|
|
213
|
+
credential between machines (and correctly get gated on it). A third step now
|
|
214
|
+
resolves the bundle **ephemerally from a primary device over SSH** —
|
|
215
|
+
`agents secrets exec npmjs.com --host <host>`, which resolves on the remote and
|
|
216
|
+
injects into the run only, never storing the token locally. It tries `SECRET_HOST`
|
|
217
|
+
first, then `zion`, then `mac-mini`, and fails with the list it tried if none
|
|
218
|
+
answer. Combined with the sign-host auto-discovery, a Linux box can now cut a full
|
|
219
|
+
release end-to-end given a reachable Mac for signing and any reachable device that
|
|
220
|
+
holds the npm token. Source: `apps/cli/scripts/release.sh`.
|
|
221
|
+
|
|
222
|
+
- **Branded, actionable daemon notifications on the routine lifecycle (RUSH-2030).**
|
|
223
|
+
Daemon desktop notifications (overdue routines, config heal, the no-credential
|
|
224
|
+
warning) now route through the `MenubarHelper.app` companion instead of raw
|
|
225
|
+
AppleScript, so they carry the agents-cli mark rather than the generic Script
|
|
226
|
+
Editor icon; they degrade to `osascript`/`notify-send` only when the helper is
|
|
227
|
+
not installed. The daemon also notifies when a routine **starts** and
|
|
228
|
+
**finishes** (success/failure, with the report's first line or the error reason
|
|
229
|
+
folded in), suppressing command-housekeeping start/success pings to avoid spam.
|
|
230
|
+
Clicking a finish notification opens the run report/log; start/overdue open the
|
|
231
|
+
runs folder. Source: `apps/cli/src/lib/menubar/notify-desktop.ts`,
|
|
232
|
+
`apps/cli/src/lib/routine-notify.ts`, `apps/cli/src/lib/daemon.ts`,
|
|
233
|
+
`apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
|
|
234
|
+
|
|
235
|
+
- **`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`.
|
|
236
|
+
|
|
237
|
+
- **`agents sessions <id>` with a short/partial id resolves by id only — no more
|
|
238
|
+
"Multiple sessions match" from fuzzy content.** A complete UUID already resolved
|
|
239
|
+
by id, but a bare hex short-id like `d3470b57` was not caught by
|
|
240
|
+
`isCompleteSessionId`, so it fell through to the ranked content search and
|
|
241
|
+
surfaced every transcript that merely MENTIONED the string (a resume prompt
|
|
242
|
+
echoes the parent id into the body of many later sessions) — a real view id
|
|
243
|
+
returned a list of unrelated sessions. Any id-shaped query — complete id OR hex
|
|
244
|
+
short-id/prefix (`looksLikeSessionId`) — now resolves through the index by id in
|
|
245
|
+
both `resolveSessionQuery` and the `renderOneSession` content-widen gate, and
|
|
246
|
+
reports "no session found" when nothing matches instead of content-searching.
|
|
247
|
+
Free-text phrases keep the ranked search path. Source:
|
|
248
|
+
`apps/cli/src/commands/sessions.ts`.
|
|
249
|
+
|
|
250
|
+
- **`agents sessions --active` attributes the initiating device for SSH-launched
|
|
251
|
+
sessions.** A session started by ssh'ing into a box (common for tmux-hosted
|
|
252
|
+
runs) used to render as `local` with no origin, because the tmux discovery path
|
|
253
|
+
stamped a `transport:'local'` placeholder that made provenance enrichment skip
|
|
254
|
+
it. Enrichment now probes the pane process's env and upgrades the row to `ssh`
|
|
255
|
+
with the real origin, then resolves the SSH client IP against the device
|
|
256
|
+
registry into `provenance.origin` (`{ device, user? }`). Both the flat listing
|
|
257
|
+
and the interactive browser read `ssh←<device>` (e.g. `ssh←zion`); an
|
|
258
|
+
unregistered IP stays bare `ssh`. Answers "which box launched this session"
|
|
259
|
+
without scraping `ps`/`who`/`tailscale`. Source:
|
|
260
|
+
`apps/cli/src/lib/session/active.ts`, `apps/cli/src/lib/session/provenance.ts`,
|
|
261
|
+
`apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions-browser.ts`.
|
|
262
|
+
|
|
263
|
+
- **`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`.
|
|
264
|
+
|
|
265
|
+
- **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`.
|
|
266
|
+
|
|
3
267
|
## 1.20.76
|
|
4
268
|
|
|
5
269
|
- **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/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;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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({
|
|
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
|
|
327
|
-
|
|
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).
|