@phnx-labs/agents-cli 1.22.25 → 1.22.26

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 (82) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +17 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/browser.js +35 -0
  7. package/dist/commands/doctor.js +8 -0
  8. package/dist/commands/insights.d.ts +25 -19
  9. package/dist/commands/insights.js +107 -33
  10. package/dist/commands/reconnect.d.ts +46 -0
  11. package/dist/commands/reconnect.js +109 -0
  12. package/dist/commands/routines.js +2 -2
  13. package/dist/commands/secrets.d.ts +2 -8
  14. package/dist/commands/secrets.js +29 -105
  15. package/dist/commands/sessions.js +4 -0
  16. package/dist/commands/setup-secrets.d.ts +1 -0
  17. package/dist/commands/setup-secrets.js +1 -1
  18. package/dist/commands/setup.d.ts +26 -3
  19. package/dist/commands/setup.js +105 -46
  20. package/dist/commands/teams.d.ts +6 -0
  21. package/dist/commands/teams.js +43 -0
  22. package/dist/commands/trends.d.ts +8 -0
  23. package/dist/commands/trends.js +10 -156
  24. package/dist/index.js +1 -1
  25. package/dist/lib/agents.d.ts +11 -0
  26. package/dist/lib/agents.js +29 -2
  27. package/dist/lib/analytics/dashboard.d.ts +10 -6
  28. package/dist/lib/analytics/dashboard.js +6 -4
  29. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  30. package/dist/lib/analytics/mix-commands.js +229 -0
  31. package/dist/lib/analytics/recipes.d.ts +19 -14
  32. package/dist/lib/analytics/recipes.js +4 -2
  33. package/dist/lib/browser/ipc.d.ts +26 -0
  34. package/dist/lib/browser/ipc.js +139 -24
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +1 -1
  37. package/dist/lib/browser/stream.d.ts +14 -0
  38. package/dist/lib/browser/stream.js +71 -0
  39. package/dist/lib/channels/owner-sink.d.ts +27 -0
  40. package/dist/lib/channels/owner-sink.js +93 -0
  41. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  42. package/dist/lib/devices/doctor-findings.js +33 -1
  43. package/dist/lib/fleet/apply.d.ts +59 -3
  44. package/dist/lib/fleet/apply.js +183 -6
  45. package/dist/lib/fleet/types.d.ts +21 -2
  46. package/dist/lib/hooks/cache.js +15 -0
  47. package/dist/lib/hosts/passthrough.d.ts +23 -0
  48. package/dist/lib/hosts/passthrough.js +45 -0
  49. package/dist/lib/hosts/ready.d.ts +2 -0
  50. package/dist/lib/hosts/ready.js +10 -1
  51. package/dist/lib/hosts/reconnect.d.ts +14 -12
  52. package/dist/lib/hosts/reconnect.js +41 -40
  53. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  54. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  55. package/dist/lib/routines.js +14 -2
  56. package/dist/lib/runner.d.ts +0 -3
  57. package/dist/lib/runner.js +1 -14
  58. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  59. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  60. package/dist/lib/secrets/push.d.ts +94 -0
  61. package/dist/lib/secrets/push.js +145 -0
  62. package/dist/lib/secrets/reaper.d.ts +15 -1
  63. package/dist/lib/secrets/reaper.js +30 -3
  64. package/dist/lib/session/db.d.ts +21 -3
  65. package/dist/lib/session/db.js +221 -13
  66. package/dist/lib/session/discover.d.ts +1 -0
  67. package/dist/lib/session/discover.js +115 -19
  68. package/dist/lib/session/insights.d.ts +18 -0
  69. package/dist/lib/session/insights.js +143 -1
  70. package/dist/lib/session/tool-index.js +133 -22
  71. package/dist/lib/session/tool-store.d.ts +26 -2
  72. package/dist/lib/session/tool-store.js +36 -17
  73. package/dist/lib/ssh-exec.js +8 -2
  74. package/dist/lib/startup/command-registry.d.ts +1 -0
  75. package/dist/lib/startup/command-registry.js +4 -0
  76. package/dist/lib/teams/agents.d.ts +13 -0
  77. package/dist/lib/teams/agents.js +75 -7
  78. package/dist/lib/teams/placement-probe.d.ts +21 -0
  79. package/dist/lib/teams/placement-probe.js +135 -0
  80. package/dist/lib/teams/scheduler.d.ts +74 -1
  81. package/dist/lib/teams/scheduler.js +187 -10
  82. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,178 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.26
4
+
5
+ - Make bare `agents setup` a re-runnable onboarding hub with live capability status and direct access to browser, computer, secrets, fleet, share, watchdog, and preference wizards.
6
+
7
+ - **`agents apply --provision-secrets` pushes the manifest's declared secrets
8
+ bundles to each device, instead of only printing a reminder (RUSH-1968).** This
9
+ gap is a direct cause of the ticket: an operator who needed secrets on a worker
10
+ box had no supported path — `apply` said "recreate manually" and nothing else —
11
+ so they hand-exported the file store's master key across the fleet. The
12
+ provisioning primitive now exists, and `apply` runs it as a fifth reconcile
13
+ phase, last, because it is the most sensitive mutation `apply` performs.
14
+
15
+ It is **off by default** and is a **flag, not a manifest field**: `agents.yaml`
16
+ is shared, so a file-level default would mean someone else's `apply -y` silently
17
+ ships credential values. Three gates, and every refusal still prints a
18
+ `needs-secret` reminder so a skipped device is never silent — the flag must be
19
+ set, the device must be reachable, and its host key must be **pinned** (the same
20
+ bar `agents exec --copy-creds` sets, EXEC-34).
21
+
22
+ **Backend follows the platform: `file` on Linux, `keychain` on macOS/Windows.**
23
+ That is the load-bearing default — a headless Linux box has no keychain and its
24
+ file store auto-provisions its OWN machine-local key, so each device gets an
25
+ unshared at-rest key and **no passphrase is forwarded**. That is the direct
26
+ alternative to the fleet-wide shared secret this ticket is about.
27
+
28
+ With provisioning on, `apply` runs one extra `agents secrets list --json` per
29
+ device (metadata only — names and timestamps, never values) and skips a bundle
30
+ the device already has; without that, every run re-resolves the bundle locally
31
+ and a resolve can prompt for Touch ID, so a converged fleet would nag on every
32
+ apply. It compares presence, not content — `--force` re-pushes regardless. The
33
+ `--plan` matrix gains a `secrets` column, shown only when the manifest declares
34
+ bundles, and names the flag when the capability is available but off. Source:
35
+ `apps/cli/src/lib/secrets/push.ts` (extracted from the `export --host` action so
36
+ a lib no longer needs a command module), `apps/cli/src/lib/fleet/apply.ts`,
37
+ `apps/cli/src/commands/apply.ts`.
38
+
39
+ - **`agents teams` auto-scheduling is health-, harness-, and load-aware, and fails loud when no pool device can run the agent (RUSH-2002).** Placing an unpinned teammate onto a `--devices` pool used to be a pure roster count that could land it on an unreachable box, an overloaded one, or one where its agent isn't installed. `teams start` now probes the pool once (reachability + load from the same snapshot `agents devices` shows, plus whether the teammate's agent is installed there) and: excludes unreachable / overloaded (`loaded` headroom) / `agents.max-concurrent`-capped / not-installed devices, then ranks the survivors by agent installed + signed in, then lower load, then fewer running teammates. If no pool device can run a pending teammate's agent, `teams start` fails loud — `No device in the team pool can run claude@2.1.112. Run 'agents devices ping' to see which devices have the agent installed + signed in.` — instead of stranding the teammate or silently falling back to a local run; `--force` downgrades it to a warning. A probe that could not reach the pool does not trigger the failure (no false positives). The pick stays pure and fully unit-tested; `teams add` is unchanged (no probe on the add path). Source: `apps/cli/src/lib/teams/scheduler.ts`, `apps/cli/src/lib/teams/placement-probe.ts`, `apps/cli/src/lib/teams/agents.ts`, `apps/cli/src/commands/teams.ts`.
40
+
41
+ - **`agents reconnect [session-id]` re-enters a dropped remote agent terminal, and the auto-reconnect no longer dead-ends on a dead pane (RUSH-2085).** When the network dropped during `agents run --device <box>` and the peer's tmux pane was gone by the time the link came back, the reattach ran `agents sessions focus <id> --local --attach-only`, which hard-failed with `No live session matching …` and dropped the user at a bare shell with the id scrolled off screen. Two fixes: the auto-reconnect reattach now runs `agents sessions focus <id> --local` (no `--attach-only`), so a surviving pane is joined and a dead one RESUMES in place instead of dead-ending; and a new `agents reconnect` (also `agents sessions reconnect`) is the manual companion for after the auto-loop gave up or a VS Code terminal tab closed — attach the live pane if it survived, else resume the session. With no id it targets the most recent session started from the current directory (the terminal that most likely just dropped), not the full fleet picker. The exhausted / remote-exit notices now print the exact `agents reconnect <id>` command instead of a raw id and a shell prompt. Source: `apps/cli/src/lib/hosts/reconnect.ts`, `apps/cli/src/commands/reconnect.ts`.
42
+
43
+ - **`agents routines add` now rejects an agent the local daemon can't fire, at add time (RUSH-2102).** `--agent opencode` (or any real, installable agent outside the daemon's `AGENT_COMMANDS` table — currently `claude`, `codex`, `gemini`, `cursor`, `kimi`, `droid`, `muse`) used to pass `validateJob` because it only checked the agent against the full agent registry, not the daemon-runnable subset — the routine was written to disk and only failed once the scheduler fired it (`Unsupported agent for daemon jobs: opencode`). `validateJob` now rejects it immediately for the default local placement, with an error naming the supported agents. Routines placed with `hostStrategy: host`/`fleet`/`cloud` are unaffected — those dispatch through `agents run`/a cloud provider, not this table, so a wider agent set is legitimately supported there. Source: `apps/cli/src/lib/routines.ts`, `apps/cli/src/lib/agents.ts`.
44
+
45
+ - **OpenClaw's capability table no longer claims `hooks: true` with zero hooks
46
+ ever installed (RUSH-2122).** `registerHooksToSettings` has no `openclaw`
47
+ branch and silently returned `{ registered: [], errors: [] }` for it, so
48
+ `agents sync openclaw` reported success while installing nothing, and
49
+ `agents doctor` treated the agent as hooks-capable with no way to detect the
50
+ gap. OpenClaw only exposes a fixed set of internal, named hooks (e.g.
51
+ `boot-md`, which runs `BOOT.md` on gateway restart) — there is no general
52
+ event->shell-command registration surface an agents-cli `hooks.yaml`
53
+ manifest could target — so `capabilities.hooks` and `supportsHooks` now read
54
+ `false`, matching what the CLI can actually do. A new completeness test
55
+ (`hooks-capability-completeness.test.ts`) pins every `hooks: true` agent to a
56
+ real branch in `registerHooksToSettings` so a capability flip can never ship
57
+ again without a registrar behind it. Source: `apps/cli/src/lib/agents.ts`,
58
+ `apps/cli/src/lib/hooks-capability-completeness.test.ts`.
59
+
60
+ - **`agents browser stream` keeps one Node process and browser-daemon IPC socket warm across repeated actions (RUSH-2149).** The newline-delimited JSON interface sends every request through the existing browser daemon and its cached CDP connection, so screenshot/click loops no longer need a fresh `agents` process or IPC connection per action. `--task` (or `AGENTS_BROWSER_TASK`) supplies the default task, a `start` response becomes the default for later lines, and malformed input returns an error response without ending the stream. Fleet-remote `start` requests enforce the same device-local `browser remote-control` consent gate as the ordinary command. Source: `apps/cli/src/lib/browser/ipc.ts`, `apps/cli/src/lib/browser/stream.ts`, `apps/cli/src/commands/browser.ts`.
61
+
62
+ - **`agents sessions backfill tools` reads a growing transcript incrementally, and
63
+ the search index compacts itself (RUSH-2208).** Incremental discovery already
64
+ appended tool calls for a Claude/Codex session that grew; the backfill did not.
65
+ Every `ensureToolIndex` pass re-read the transcript from byte 0 and deleted the
66
+ session's stored evidence to rewrite it, so a session backfilled N times cost N
67
+ full parses of an ever-larger file — measured on a 4.5 MiB, 4000-call transcript
68
+ that grew by 20 calls: 344 ms and 4020 calls re-parsed, now 12 ms and 20 calls.
69
+ Schema v36 adds a resume point to `tool_scan_ledger` (`parsed_offset`, the byte
70
+ just past the last complete record consumed, and `parser_state`, the collector
71
+ snapshot at that offset), so a transcript that only grew is read from where the
72
+ last pass stopped and merged into what is already stored; the batch byte budget
73
+ now counts the bytes a pass actually reads rather than the file's size, so one
74
+ bounded batch covers far more growing sessions. A different extractor version, a
75
+ mismatched ledger path, a file shorter than what was parsed, or an unreadable
76
+ snapshot still re-reads the whole file. Two related fixes ride along:
77
+ `tool_call_text` rows are addressed by the `rowid` of the call they describe
78
+ instead of the UNINDEXED `call_key`, which made every delete a full scan of the
79
+ FTS index, and the scan path now runs a bounded, threshold-gated FTS `'merge'`
80
+ after each batch of writes, so index health no longer depends on someone running
81
+ `agents sessions optimize` by hand. Source: `apps/cli/src/lib/session/tool-index.ts`,
82
+ `apps/cli/src/lib/session/tool-store.ts`, `apps/cli/src/lib/session/db.ts`.
83
+
84
+ - **OpenCode session scans re-index only the sessions that changed (RUSH-2210).** OpenCode
85
+ keeps every session in one shared `opencode.db`, and the scanner stamped each session
86
+ with that whole file's mtime/size. Any write to any session therefore invalidated every
87
+ indexed session, so a single new turn re-emitted up to 1000 sessions — and the indexer
88
+ re-opened `opencode.db` once per re-emitted session to re-parse a transcript that had
89
+ not moved. Each session is now stamped with its own newest write time (across its
90
+ `session` row, its messages, and its parts) and the byte length of its message + part
91
+ payloads, so an unchanged session is skipped; the file-level stat stays only as the
92
+ cheap "nothing changed at all" short-circuit, and a scan opens `opencode.db` once
93
+ instead of once per session. The stamp deliberately does not rely on
94
+ `session.time_updated` alone, which real databases leave hours behind the session's
95
+ newest part. A side effect: `sessions.file_size` for an OpenCode row is now that
96
+ session's payload size instead of the whole database's size, so the tool-backfill byte
97
+ budget and its 16 MiB in-memory parser cap finally reflect the real cost of parsing
98
+ that one session. Source: `apps/cli/src/lib/session/discover.ts`.
99
+
100
+ - **Session query hot path is indexable again (RUSH-2211).** The default `agents
101
+ sessions` listing sort (`ORDER BY IFNULL(last_activity, timestamp) DESC`)
102
+ wrapped the sort column in `IFNULL()`, which defeats `idx_sessions_last_activity`
103
+ and forces a full table sort on every list/resume query; a new migration
104
+ backfills `last_activity` so it's unconditionally `NOT NULL` and the sort now
105
+ runs on the bare column (`EXPLAIN QUERY PLAN` confirms `USING INDEX
106
+ idx_sessions_last_activity`). The post-query existence check now batches
107
+ `fs.existsSync` per directory instead of one stat syscall per row — real
108
+ transcript trees put many sessions in one project directory, so this collapses
109
+ thousands of stats into a handful of `readdirSync` calls with the same result.
110
+ Interactive label search (`ftsSearch`) no longer runs a leading-wildcard
111
+ `LOWER(label) LIKE '%q%'` scan of the whole `sessions` table on every keystroke;
112
+ it now queries the already-indexed FTS5 `label` column. Source:
113
+ `apps/cli/src/lib/session/db.ts`.
114
+
115
+ - **The standalone `browser` binary now routes `--host`/`--device` (RUSH-2214).** `browser start --host <box>` dispatches to the remote over SSH, exactly like `agents browser start --host <box>` already did — previously the standalone bin dropped the flag with `unknown option '--host'` because it never entered the top-level router. A self-named or absent host still runs locally. Source: `apps/cli/src/browser.ts`, `apps/cli/src/lib/hosts/passthrough.ts`.
116
+
117
+ - **`agents run --device` no longer reports a host as unreachable when the SSH probe times out (RUSH-2249).** The ready probe (`readyProbe` in `hosts/ready.ts`) now disables SSH multiplexing so a stale control socket cannot hang the local client, and it checks `r.timedOut` before parsing stdout — a slow login shell (nvm/sdkman init, cold node startup) producing an empty stdout was silently treated as "not reachable". A timeout now surfaces a distinct, actionable error that names the cause and suggests `agents ssh <host> agents view` to confirm manually, rather than the misleading "not reachable over SSH" message. Source: `apps/cli/src/lib/hosts/ready.ts`.
118
+
119
+ - **`agents teams start` nudges the operator toward feed milestones (RUSH-2250).**
120
+ After launching teammates, `teams start` now prints a one-line tip — teammates are
121
+ briefed to post IMPORTANT milestones to the feed (watch them with
122
+ `agents feed timeline`), and team progress is watched with `agents teams status
123
+ <team>`. Print-only in both the single-wave and `--watch` paths (suppressed under
124
+ `--json`); no engine behavior changes. Pairs with the `.agents-system` guidance
125
+ that instructs teammates to post those milestones. Source:
126
+ `apps/cli/src/commands/teams.ts`.
127
+
128
+ - **`secrets list` no longer skips every biometry-ACL'd item, so `hold`/`always`-policy
129
+ bundles are readable again (RUSH-2251).** A regression first shipped in v1.22.10 added
130
+ `kSecUseAuthenticationUI: kSecUseAuthenticationUISkip` to the keychain helper's `list`
131
+ data-protection pass. `UISkip` makes `SecItemCopyMatching` silently omit every item
132
+ protected by a biometry access control — which is exactly the value items `set` writes —
133
+ so enumeration returned only the no-ACL metadata and `never`-policy items. Every consumer
134
+ that builds its keychain read set from that enumeration (`secrets exec`/`get`/`unlock`/
135
+ `view --reveal`/`export`, `agents run --secrets`, `ssh`, `browser`, `share`) then reported
136
+ the real secrets as `stored item '…' not found`, and `unlock` could not even warm the
137
+ broker to work around it. The DP pass is now attributes-only with **no** `kSecUseAuthenticationUI`
138
+ key: `kSecReturnAttributes` without `kSecReturnData` never evaluates the ACL, so it
139
+ neither prompts for Touch ID nor filters the ACL'd items out — restoring the design the
140
+ code comment already described. The RUSH-2233 timeout bound on that pass is unchanged.
141
+ Source: `apps/cli/src/lib/secrets/keychain-helper.swift`.
142
+
143
+ - **Hook-cache background refresh recovers from an orphaned single-flight lock (RUSH-2259).**
144
+ The stale-while-revalidate lock (`<cache>.bg.lck`) was only released by the
145
+ background refresh's `EXIT` trap, so a hard kill (SIGKILL, OOM, reboot) that
146
+ skipped the trap orphaned the dir and every future refresh's `mkdir` failed —
147
+ permanently stalling background refresh while stale cache was served forever.
148
+ The shim now reclaims a lock older than a 5-minute TTL before acquiring, so a
149
+ dead lock self-heals on the next fire. Source: `apps/cli/src/lib/hooks/cache.ts`.
150
+
151
+ - **`agents doctor` now fails loud when this box cannot reach the owner-delivery lane (RUSH-2262).** The feed/notify owner lane (`agents notify`, `agents feed post --level important` / `--blocked`) delivers over the rush-backed owner channel (iMessage), which only works from a context that has `rush` on PATH and can read its keychain-bound session — so a headless Linux fleet box (no rush) or a non-GUI SSH session on a mac (login keychain locked) silently could not escalate a blocked agent, surfacing only as an after-the-fact `owner failed: …` line. `agents doctor` had no signal for it. A new critical finding, `owner-sink-unreachable`, probes the same transport from the same context doctor runs in (`which rush` + `rush whoami`, never `~/.rush/user.yaml`, since the token is a keychain item) and reports `owner → unreachable: rush CLI not on this box's PATH` / `rush has no usable session here` with the fix. It fires only when owner delivery is configured for the fleet, so an un-opted-in box is never flagged; `agents notify --dry-run` is not this check (it short-circuits before the `which rush` preflight and reports success even where rush is absent). Source: `apps/cli/src/lib/channels/owner-sink.ts`, `apps/cli/src/lib/devices/doctor-findings.ts`, `apps/cli/src/commands/doctor.ts`.
152
+
153
+ - **`agents insights` owns counter mix; `agents trends` is a deprecated alias.** The
154
+ former top-level `trends` tree (harness/model mix, tools-per-session, token ratios,
155
+ secrets/browser recipes, raw usage query) now lives under `agents insights mix` and
156
+ `agents insights <recipe>` / `query` / `recipes`. Bare `agents insights` remains the
157
+ behavioural report (transcript content, account split). `agents trends` still works
158
+ but prints one deprecation line and runs the same mix tree — no second implementation.
159
+ **Why:** two peer "analytics" verbs (`insights` + `trends`) taught agents and humans
160
+ to guess; one verb, two engines (content vs counters). Latency stays on `agents perf`;
161
+ quota on `agents usage`; skill/slash popularity on `agents sessions stats`. Source:
162
+ `apps/cli/src/lib/analytics/mix-commands.ts`, `commands/insights.ts`,
163
+ `commands/trends.ts`, `docs/06-observability.md`.
164
+
165
+ - **The keychain reaper no longer kills the auto-lock-on-sleep watcher (RUSH-2232 follow-up).**
166
+ The reaper (shipped in 1.22.23) classified a process as a reap target purely by the
167
+ helper binary path, which also matches the broker's deliberately long-lived
168
+ `watch-lock` watcher — a healthy child of the live daemon that wipes the in-memory
169
+ secret store on sleep. Its class-(b) rule ("helper child of a live parent, older than
170
+ 90s") therefore killed the watcher on its second sweep (~10 min after the daemon
171
+ started hosting the broker), silently disabling auto-lock-on-sleep. Reap-eligibility
172
+ now matches the full command line and excludes the `watch-lock` verb, so only the
173
+ short-lived keychain reads/writes a wedged `coreauthd` can hang are ever reaped.
174
+ Source: `apps/cli/src/lib/secrets/reaper.ts` (`isReapableHelperCommand`).
175
+
3
176
  ## 1.22.25
4
177
 
5
178
  ---
@@ -168,6 +341,16 @@ Add Cursor Cloud Agents as a native cloud provider so `agents run cursor --cloud
168
341
 
169
342
  - **`agents run --lease` now shares one warm pool across repositories by default (RUSH-2225).** Repo sandbox/CI `profile:` labels no longer split lease reuse into one idle box per repo; a dedicated lease pool is explicit with `.crabbox.yaml` `leaseProfile:`. An empty pool keeps its newly warmed box for later callers. Concurrent runs attach with crabbox `--reclaim` and launch with separate working trees, agent homes, and credential files, so callers share compute without clobbering run state. Switching repos re-syncs the checkout, trading cache latency for lower idle-compute cost. Source: `apps/cli/src/lib/crabbox/config.ts`, `apps/cli/src/lib/crabbox/lease.ts`, `apps/cli/src/commands/exec.ts`.
170
343
 
344
+ - **`agents sessions insights` turns multi-harness session history into an action list
345
+ (RUSH-2280).** The existing `agents insights` command is now also nested under the
346
+ sessions noun, accepts repeatable `--agent` filters, and reports deterministic offline
347
+ friction/thrash, owner corrections, automatable repeats, harness split, and ranked
348
+ rule/skill/automation/product actions with evidence counts and shortened sample session
349
+ ids. `/sessions-insights` is a thin agent entry over the same CLI implementation;
350
+ `--narrative` remains opt-in and receives aggregate data only. Source:
351
+ `apps/cli/src/commands/insights.ts`, `apps/cli/src/lib/session/insights.ts`,
352
+ `.agents/commands/sessions-insights.md`.
353
+
171
354
  - **`agents sessions focus` recovers dead panes and shares the sessions browser's selectors (GH-2108).** A retained tmux `remain-on-exit` pane is probed through `#{pane_dead}` immediately before attach, so dead or missing panes no longer open a `Pane is dead` screen. `focus` accepts session ids, topic/path searches, `agent@version` selectors (including per-device `latest`/`oldest`), device, project/time, team/routine, skill/plugin, favorites, and the complete live-state union. Focus, resume, attach, and `run --resume` now use one recovery decision on the origin device: a healthy exact origin performs native resume; otherwise balanced selection chooses a healthy version of the same harness and sends `/continue <id>` to read the indexed transcript, including transcripts retained under version trash. Host-dispatched rows persist the dispatch host as their origin, and `attach` routes its detach-record cleanup there before resuming. No usable same-harness version fails with the device, origin version, and account-health reason. Source: `apps/cli/src/commands/focus.ts`, `apps/cli/src/commands/sessions-browser.ts`, `apps/cli/src/lib/session/recovery.ts`.
172
355
 
173
356
  - **`agents secrets setup` no longer tells you to set `AGENTS_SECRETS_PASSPHRASE`, and
package/README.md CHANGED
@@ -45,11 +45,12 @@ https://agents-cli.sh/demo.mp4
45
45
 
46
46
  ```bash
47
47
  npm install -g @phnx-labs/agents-cli # or: curl -fsSL agi-cli.sh | sh
48
- agents setup # first-time setup -- config + pick your agents
48
+ agents setup # first-time setup, or re-open the capability hub
49
+ agents setup status # readiness for browser, computer, fleet, and more
49
50
  agents run claude "explain this repo" # run any agent on your existing subscription
50
51
  ```
51
52
 
52
- `agents setup` is interactive and idempotent -- safe to re-run on a new machine. The `agi-cli.sh` one-liner installs this same canonical `@phnx-labs/agents-cli` package. Prefer bun? `bun install -g @phnx-labs/agents-cli` works too.
53
+ `agents setup` is interactive and idempotent -- safe to re-run on any machine. Once core setup exists, it opens a status-aware menu for browser, computer, secrets, fleet, share, watchdog, and device preferences; each choice delegates to the same wizard available under `agents setup <capability>`. In CI or another non-TTY, bare setup prints the checklist without prompting. The `agi-cli.sh` one-liner installs this same canonical `@phnx-labs/agents-cli` package. Prefer bun? `bun install -g @phnx-labs/agents-cli` works too.
53
54
 
54
55
  Already installed? `agents upgrade` updates agents-cli itself to the latest version (`agents upgrade 1.2.3` for a specific version or dist-tag, `-y` to skip the confirm prompt). The command is `upgrade` on every platform -- there is no `agents update` (on macOS, `agents helper update` is a different command that reinstalls the keychain helper, not agents-cli).
55
56
 
@@ -295,8 +296,16 @@ agents sessions backfill tools --fleet
295
296
  agents sessions stats
296
297
  agents sessions stats --zero # only the never-invoked (dead weight)
297
298
  agents sessions backfill resources # fold historical sessions into the usage index
299
+
300
+ # Friction, owner corrections, repeated recipes, and ranked actions across harnesses
301
+ agents sessions insights --since 30d
302
+ agents sessions insights --agent claude --agent codex --json
303
+ # Top-level alias
304
+ agents insights --since 7d
298
305
  ```
299
306
 
307
+ `sessions insights` is deterministic and offline by default. It caches per-session facets, compares harnesses, and emits an actions table with evidence counts plus shortened sample session ids. `--narrative` is opt-in and receives aggregates only, never raw transcripts. The installed `/sessions-insights` slash command invokes the same CLI source of truth.
308
+
300
309
  Interactive picker when you're in a terminal. Structured output (`--json`, `--markdown`, filtered by role or turn count) when piped.
301
310
 
302
311
  Backed by a SQLite + FTS5 index at `~/.agents/.history/sessions/sessions.db` with incremental scanning -- warm reads in ~100ms. Tool-call evidence is redacted and bounded before it is cached; repeated `--query` clauses must match distinct calls in one session. Tool queries read SQLite only: `agents sessions backfill tools` performs the one-time historical parse, while normal incremental scans index new and changed sessions. The index stores ordered static Bash program sites, so `--count` reports occurrences, containing tool calls, and distinct sessions without reparsing. `--fleet` executes one origin partition per device, so synced mirrors cannot duplicate compact evidence or counts returned over SSH; transcript bodies stay on their origin machine. This uses relational SQLite rows and literal FTS5 only, with no embeddings, vector database, or model calls. External tools can consume `--json` output as a programmatic observability layer; see [docs/05-sessions.md](apps/cli/docs/05-sessions.md) for the schemas and [docs/06-observability.md](apps/cli/docs/06-observability.md) for the consumption patterns.
@@ -820,6 +829,12 @@ agents browser done # Close task's tabs when finished
820
829
 
821
830
  # Need to address a different task in the same shell? Override per call:
822
831
  agents browser screenshot --task other-flow
832
+
833
+ # Repeated observe/action loops: one Node process and daemon socket stay warm.
834
+ printf '%s\n' \
835
+ '{"action":"screenshot","path":"/tmp/page.jpg"}' \
836
+ '{"action":"click","atX":320,"atY":540}' \
837
+ | agents browser stream --task "$AGENTS_BROWSER_TASK"
823
838
  ```
824
839
 
825
840
  ### Why this works where Playwright fails
package/dist/bin/agents CHANGED
Binary file
package/dist/browser.js CHANGED
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { registerBrowserSubcommands } from './commands/browser.js';
4
- const program = new Command();
5
- program.name('browser').description('Browser automation via CDP');
6
- registerBrowserSubcommands(program);
7
- program.parse();
4
+ import { maybeRunStandaloneOnHost } from './lib/hosts/passthrough.js';
5
+ async function main() {
6
+ // `browser … --host <box>` routes to a remote over SSH, exactly like
7
+ // `agents browser … --host <box>` does through index.ts. Standalone-only:
8
+ // this binary never enters index.ts, so without this the flag was dropped.
9
+ if (await maybeRunStandaloneOnHost('browser')) {
10
+ process.exit(process.exitCode ?? 0);
11
+ }
12
+ const program = new Command();
13
+ program.name('browser').description('Browser automation via CDP');
14
+ registerBrowserSubcommands(program);
15
+ program.parse();
16
+ }
17
+ void main();
@@ -15,6 +15,7 @@ import chalk from 'chalk';
15
15
  import { setHelpSections } from '../lib/help.js';
16
16
  import { machineId } from '../lib/session/sync/config.js';
17
17
  import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
18
+ import { isHostPinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
18
19
  import { ensureDevicesRegistered } from '../lib/devices/sync.js';
19
20
  import { readFleetFile, resolveDesired } from '../lib/fleet/manifest.js';
20
21
  import { snapshotAuth, materializeAuth, parseAuthBundle, KEYCHAIN_BOUND_ON_MAC, isCredentialSafeToPropagate } from '../lib/fleet/auth-sync.js';
@@ -82,7 +83,10 @@ function renderPlan(plan) {
82
83
  }
83
84
  return chalk.cyan('↑ ' + acts.map((a) => a.agent ?? a.kind.replace('-cli', '')).join(','));
84
85
  };
85
- const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}login`;
86
+ // Only show the secrets column when the manifest declares any — an all-`-`
87
+ // column on every fleet that uses no bundles is noise.
88
+ const anySecrets = rows.some((r) => r.actions.some((a) => a.kind === 'push-secret' || a.kind === 'needs-secret'));
89
+ const header = ` ${'device'.padEnd(nameWidth)} ${'agents-cli'.padEnd(12)}${'agents'.padEnd(20)}${'config'.padEnd(10)}${anySecrets ? 'login'.padEnd(18) + 'secrets' : 'login'}`;
86
90
  console.log(chalk.gray(header));
87
91
  for (const row of rows) {
88
92
  const cli = row.probe.reachable
@@ -100,10 +104,33 @@ function renderPlan(plan) {
100
104
  ? (row.actions.some((a) => a.kind === 'sync-config') ? chalk.cyan('↑ sync') : chalk.green('ok'))
101
105
  : chalk.gray('-');
102
106
  const loginCell = cell(row, ['push-login', 'needs-login'], `${row.desired.agents.length}/${row.desired.agents.length}`);
103
- console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginCell}`);
107
+ const secretsCell = (() => {
108
+ if (!anySecrets)
109
+ return '';
110
+ if (!row.probe.reachable)
111
+ return chalk.gray('- offline');
112
+ const push = row.actions.filter((a) => a.kind === 'push-secret').length;
113
+ const blocked = row.actions.filter((a) => a.kind === 'needs-secret').length;
114
+ if (push === 0 && blocked === 0)
115
+ return chalk.green('ok');
116
+ if (push > 0 && blocked === 0)
117
+ return chalk.cyan(`↑ push ${push}`);
118
+ if (push === 0)
119
+ return chalk.yellow(`blocked ${blocked}`);
120
+ return chalk.yellow(`↑ push ${push} · blocked ${blocked}`);
121
+ })();
122
+ const loginPart = anySecrets ? stripPad(loginCell, 18) + secretsCell : loginCell;
123
+ console.log(` ${row.device.padEnd(nameWidth)} ${stripPad(cli, 12)}${stripPad(agentsCell, 20)}${stripPad(configCell, 10)}${loginPart}`);
104
124
  }
105
125
  console.log();
106
126
  console.log(chalk.gray(` ${plan.actions.length} action(s) across ${rows.filter((r) => r.probe.reachable).length} reachable device(s)`));
127
+ // The capability is opt-in, so when it is OFF and the manifest declares
128
+ // bundles, say that it exists. Otherwise an operator reads "manual recreate"
129
+ // and concludes there is no supported path — which is exactly the conclusion
130
+ // that led to a master key being hand-exported across the fleet (RUSH-1968).
131
+ if (anySecrets && !rows.some((r) => r.actions.some((a) => a.kind === 'push-secret'))) {
132
+ console.log(chalk.gray(' secrets: not pushed. `--provision-secrets` pushes declared bundles to devices whose host key is pinned.'));
133
+ }
107
134
  // Distinguish *why* a login can't be propagated: macOS keychain-bound,
108
135
  // single-use rotating refresh token (never copied), or the source simply not
109
136
  // being signed in to that agent (no portable file).
@@ -130,14 +157,15 @@ function renderPlan(plan) {
130
157
  if (noToken.length > 0) {
131
158
  console.log(chalk.yellow(` manual login needed (no portable token on source): ${noToken.join(', ')}`));
132
159
  }
133
- // Secrets bundles are declared once for the fleet; surface the distinct set to
134
- // recreate on any device missing them (values are keychain-local, never pushed).
160
+ // Secrets bundles are declared once for the fleet; surface the distinct set the
161
+ // gate did NOT push, so a refusal is never silent. "never pushed" used to be
162
+ // literally true here and no longer is — `--provision-secrets` pushes them.
135
163
  const bundles = [...new Set(rows.flatMap((r) => r.secretsNeeded))];
136
164
  if (bundles.length > 0) {
137
165
  const shown = bundles.slice(0, 12);
138
166
  const more = bundles.length - shown.length;
139
167
  const list = shown.join(', ') + (more > 0 ? `, +${more} more` : '');
140
- console.log(chalk.yellow(` ${bundles.length} secrets bundle(s) to recreate where missing (keychain-local, never pushed): ${list}`));
168
+ console.log(chalk.yellow(` ${bundles.length} secrets bundle(s) not pushed — recreate where missing: ${list}`));
141
169
  }
142
170
  }
143
171
  /** padEnd on the visible width, ignoring chalk color codes. Exported for tests. */
@@ -218,11 +246,24 @@ async function runApply(opts) {
218
246
  // Probe every target device in parallel.
219
247
  const nameToProfile = new Map(desired.map((d) => [d.device, registry[d.device]]));
220
248
  const withVersions = rosterNeedsVersions(desired);
249
+ // One extra `secrets list --json` per device, and only when it can change the
250
+ // plan: the manifest declares bundles AND provisioning is on. Same cost
251
+ // discipline as `withVersions` — a fleet that uses no bundles never pays it.
252
+ const withSecrets = opts.provisionSecrets === true && (manifest.secrets?.bundles?.length ?? 0) > 0;
221
253
  console.log(chalk.gray(`Probing ${desired.length} device(s)…`));
222
- const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions }));
254
+ const probeList = await pool(desired, 6, async (d) => probeDevice(nameToProfile.get(d.device), { withVersions, withSecrets }));
223
255
  const probes = new Map(probeList.map((p) => [p.device, p]));
224
256
  const targetCliVersion = localCliVersion();
225
- let plan = diffFleet(desired, probes, { targetCliVersion, sourceAuth, secretsBundles: manifest.secrets?.bundles });
257
+ let plan = diffFleet(desired, probes, {
258
+ targetCliVersion,
259
+ sourceAuth,
260
+ secretsBundles: manifest.secrets?.bundles,
261
+ provisionSecrets: opts.provisionSecrets === true,
262
+ forceSecrets: opts.force === true,
263
+ // Same bar as `exec --copy-creds` (EXEC-34): credential values only ever go
264
+ // to a host whose key we already pinned.
265
+ isHostPinned: (device) => isHostPinned(device, managedKnownHostsPath()),
266
+ });
226
267
  // --only filter.
227
268
  if (opts.only) {
228
269
  const keep = new Set();
@@ -242,7 +283,8 @@ async function runApply(opts) {
242
283
  return;
243
284
  // `needs-login`/`needs-secret` are surfaced manual reminders, not executable
244
285
  // mutations — exclude them so an otherwise-converged fleet still says "nothing
245
- // to do" instead of looping forever on un-actionable surfacing.
286
+ // to do" instead of looping forever on un-actionable surfacing. `push-secret`
287
+ // IS executable and deliberately stays counted.
246
288
  if (plan.actions.filter((a) => a.kind !== 'needs-login' && a.kind !== 'needs-secret').length === 0) {
247
289
  console.log(chalk.green('\nNothing to do — fleet already matches the profile.'));
248
290
  return;
@@ -301,6 +343,8 @@ export function configureApplyCommand(cmd) {
301
343
  .option('--agent <specs...>', 'Override the roster for targeted device(s): install these specs instead of the manifest\'s. Use `claude@all` to replicate every version installed on this machine.')
302
344
  .option('--only <dims>', 'Limit to dimensions: comma list of agents,config,login')
303
345
  .option('--no-login', 'Do not propagate logins')
346
+ .option('--provision-secrets', "Push the manifest's declared secrets bundles to each device (OFF by default; moves credential values over SSH, and only to a device whose host key is already pinned)")
347
+ .option('--force', 'With --provision-secrets: re-push a bundle the device already has')
304
348
  .addOption(new Option('--recv-auth', 'internal: receive an auth bundle on stdin').hideHelp())
305
349
  .action(async (opts) => {
306
350
  try {
@@ -20,6 +20,7 @@ import { isInteractiveTerminal } from './utils.js';
20
20
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
21
21
  import { buildHar } from '../lib/browser/har.js';
22
22
  import { getCliVersion } from '../lib/version.js';
23
+ import { runBrowserIPCStream } from '../lib/browser/stream.js';
23
24
  /**
24
25
  * Resolve which browser task a command targets. Order:
25
26
  * 1. `--task <name>` flag (explicit per-command override)
@@ -48,6 +49,7 @@ const TASK_OPTION_DESC = 'Task name (defaults to $AGENTS_BROWSER_TASK)';
48
49
  // trailing "Other commands" section automatically.
49
50
  const BROWSER_HELP_GROUPS = [
50
51
  { title: 'Session lifecycle', names: ['start', 'done', 'status'] },
52
+ { title: 'Fast action loop', names: ['stream'] },
51
53
  {
52
54
  title: 'Drive the page',
53
55
  names: ['navigate', 'tabs', 'screenshot', 'evaluate', 'click', 'type', 'press', 'wait'],
@@ -79,6 +81,9 @@ export function registerBrowserCommand(program) {
79
81
  agents browser navigate https://example.com
80
82
  agents browser screenshot
81
83
 
84
+ # Keep one process and daemon socket warm for repeated actions
85
+ agents browser stream --task "$AGENTS_BROWSER_TASK"
86
+
82
87
  # Drive another machine's browser (needs its consent — see remote-control)
83
88
  agents browser start --host zion
84
89
 
@@ -654,6 +659,36 @@ function registerTaskCommands(browser) {
654
659
  ? 'Other fleet machines can now drive this browser via `browser --host <this-device>`.'
655
660
  : 'Cross-machine `browser --host` drives to this machine are refused.');
656
661
  });
662
+ const stream = browser
663
+ .command('stream')
664
+ .description('Keep one process and daemon IPC socket open; read NDJSON requests from stdin and write NDJSON responses')
665
+ .option(TASK_OPTION_FLAG, 'Default task for requests that omit `task` (defaults to $AGENTS_BROWSER_TASK)')
666
+ .action(async (opts) => {
667
+ await runBrowserIPCStream({
668
+ input: process.stdin,
669
+ output: process.stdout,
670
+ task: opts.task ?? process.env.AGENTS_BROWSER_TASK,
671
+ actor: resolveActor().id,
672
+ launchId: process.env.AGENT_LAUNCH_ID,
673
+ });
674
+ });
675
+ setHelpSections(stream, {
676
+ examples: `
677
+ # Batch two warm actions through one process and one daemon connection
678
+ printf '%s\\n' \\
679
+ '{"action":"screenshot","path":"/tmp/page.jpg"}' \\
680
+ '{"action":"click","atX":320,"atY":540}' \\
681
+ | agents browser stream --task "$AGENTS_BROWSER_TASK"
682
+
683
+ # Keep the command open and send one JSON object per line from a long-lived shell
684
+ agents browser stream --task "$AGENTS_BROWSER_TASK"
685
+ `,
686
+ notes: `
687
+ stdout is protocol-only: one compact JSON response for each non-empty input line.
688
+ Malformed JSON returns an error response without closing the stream.
689
+ The first start response becomes the default task for later lines in the same stream.
690
+ `,
691
+ });
657
692
  browser
658
693
  .command('start')
659
694
  .description('Start a browser task. Pass --profile <name>; omit to use your configured default (`agents browser profiles set-default`), else auto-pick an installed Chromium-family browser.')
@@ -26,6 +26,8 @@ import { checkVersionHookWiring, inspectDuplicateVersionHooks, registerHooksToSe
26
26
  import { isVersionIsolated } from '../lib/versions.js';
27
27
  import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
28
28
  import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
29
+ import { readMeta } from '../lib/state.js';
30
+ import { probeOwnerSink } from '../lib/channels/owner-sink.js';
29
31
  import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
30
32
  import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
31
33
  import { setHelpSections } from '../lib/help.js';
@@ -405,6 +407,9 @@ async function runDevicesDoctor(opts) {
405
407
  isolatedVersions: localReports
406
408
  .filter((rep) => isVersionIsolated(rep.agent, rep.version))
407
409
  .map((rep) => `${rep.agent}@${rep.version}`),
410
+ // Can the owner-delivery lane escalate a block from THIS box? Local only —
411
+ // remote boxes self-report it in their own `agents doctor --json`.
412
+ ownerSink: await probeOwnerSink(readMeta()),
408
413
  }));
409
414
  accounts[localName] = r.inventory?.signIn ?? {};
410
415
  continue;
@@ -1486,6 +1491,9 @@ export function registerDoctorCommand(program) {
1486
1491
  ? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
1487
1492
  : undefined,
1488
1493
  isolatedVersions,
1494
+ // Can the owner-delivery lane (feed/notify) escalate a block from this
1495
+ // box? A factory that cannot escalate is not healthy (RUSH-2262).
1496
+ ownerSink: await probeOwnerSink(readMeta()),
1489
1497
  });
1490
1498
  if (opts.json) {
1491
1499
  const overviewPayload = {
@@ -1,23 +1,26 @@
1
1
  /**
2
- * Insights command — how you actually work, split by the account that did the work.
3
- *
4
- * The behavioural sibling of the existing rollups, and deliberately not a duplicate of
5
- * any of them:
6
- *
7
- * agents cost what you spent ($ and duration)
8
- * agents output what shipped (burn vs PRs and commits)
9
- * agents usage live quota headroom (rate-limit windows, right now)
10
- * agents trends aggregate distributions (harness mix, tools-per-session, token ratios)
11
- * agents sessions browse individual work (search, resume, render)
12
- * agents insights HOW you work (tools, friction, rhythm, per account)
13
- *
14
- * The closest neighbour is `agents trends`, and the boundary is the data path: trends
15
- * reads counters `tool_scan_ledger` call counts and the analytics warehouse — to
16
- * produce distributions ("how many tool calls per session, by harness"). This reads
17
- * transcript CONTENT through `parseSession` to produce behaviour ("which tools, which
18
- * languages, where it went wrong, when you were working"), and splits all of it by
19
- * account, a dimension trends does not have. They overlap in spirit on tool and model
20
- * mix; they do not read the same store or answer the same question.
2
+ * Insights command — one observe verb for "how work looks".
3
+ *
4
+ * Two data paths under one name (do not re-split into peer top-level commands):
5
+ *
6
+ * agents insights HOW you work (transcript content: tools, friction,
7
+ * rhythm, edits) split by Claude account by default
8
+ * agents insights mix COUNTERS (sessions index + usage.db recipes:
9
+ * harness/model mix, token ratios, secrets, browser)
10
+ * agents insights <recipe> One baked mix recipe (harness-mix, tools-per-session, )
11
+ * agents insights query Raw usage.db rows
12
+ *
13
+ * Sibling observe verbs (stay separate — different questions):
14
+ *
15
+ * agents cost what you spent ($ and duration)
16
+ * agents output what shipped (burn vs PRs and commits)
17
+ * agents usage live quota headroom
18
+ * agents perf latency (hooks, CLI commands, agent.run) not popularity
19
+ * agents sessions stats which skills/slash-commands were explicitly invoked
20
+ *
21
+ * Why mix lives here (not a second top-level `trends`): two abstract "analytics"
22
+ * nouns taught agents and humans to guess. One verb, two engines — cheap SQL mix
23
+ * vs transcript facets. Latency stays on `perf` so it is never confused with mix.
21
24
  *
22
25
  * Modelled on Claude Code's `/insights`, with the difference that motivated it: that
23
26
  * command reads one account's directory, while `balanced` rotation sprays sessions
@@ -27,6 +30,9 @@
27
30
  * The deterministic report makes zero network calls. `--narrative` is opt-in and adds
28
31
  * the coaching prose by piping the AGGREGATE (never raw transcripts) through a headless
29
32
  * `claude -p`.
33
+ *
34
+ * `agents trends` is a thin deprecated alias of the mix tree only (see commands/trends.ts).
30
35
  */
31
36
  import type { Command } from 'commander';
32
37
  export declare function registerInsightsCommand(program: Command): void;
38
+ export declare function registerSessionsInsightsCommand(sessions: Command): void;