@phnx-labs/agents-cli 1.21.1 → 1.21.3

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 (111) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +1 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +34 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/doctor.js +5 -2
  8. package/dist/commands/feed.js +28 -19
  9. package/dist/commands/hooks.js +9 -45
  10. package/dist/commands/menubar.js +24 -24
  11. package/dist/commands/message.js +23 -3
  12. package/dist/commands/perf.d.ts +29 -0
  13. package/dist/commands/perf.js +90 -23
  14. package/dist/commands/projects.d.ts +21 -0
  15. package/dist/commands/projects.js +169 -23
  16. package/dist/commands/routines.js +46 -1
  17. package/dist/commands/sessions-picker.js +17 -2
  18. package/dist/commands/sessions.d.ts +5 -0
  19. package/dist/commands/sessions.js +14 -0
  20. package/dist/commands/ssh.js +69 -0
  21. package/dist/commands/trends.d.ts +2 -0
  22. package/dist/commands/trends.js +158 -0
  23. package/dist/commands/usage.d.ts +4 -4
  24. package/dist/commands/view.d.ts +6 -0
  25. package/dist/commands/view.js +90 -45
  26. package/dist/index.js +22 -1
  27. package/dist/lib/agents.js +2 -2
  28. package/dist/lib/analytics/dashboard.d.ts +11 -0
  29. package/dist/lib/analytics/dashboard.js +31 -0
  30. package/dist/lib/analytics/recipes.d.ts +32 -0
  31. package/dist/lib/analytics/recipes.js +316 -0
  32. package/dist/lib/analytics/usage-db.d.ts +84 -0
  33. package/dist/lib/analytics/usage-db.js +301 -0
  34. package/dist/lib/browser/service.js +31 -0
  35. package/dist/lib/cli-resources.d.ts +20 -0
  36. package/dist/lib/cli-resources.js +48 -1
  37. package/dist/lib/computer/dispatch.d.ts +3 -1
  38. package/dist/lib/computer/dispatch.js +10 -2
  39. package/dist/lib/daemon.js +51 -14
  40. package/dist/lib/devices/health-report.d.ts +5 -0
  41. package/dist/lib/devices/health-report.js +3 -0
  42. package/dist/lib/event-stream.d.ts +2 -0
  43. package/dist/lib/event-stream.js +3 -0
  44. package/dist/lib/events.d.ts +3 -1
  45. package/dist/lib/events.js +4 -2
  46. package/dist/lib/feed-broadcast.d.ts +52 -7
  47. package/dist/lib/feed-broadcast.js +125 -18
  48. package/dist/lib/fleet-cache.d.ts +37 -0
  49. package/dist/lib/fleet-cache.js +40 -0
  50. package/dist/lib/fleet-status.d.ts +53 -0
  51. package/dist/lib/fleet-status.js +120 -0
  52. package/dist/lib/friction-heuristics.d.ts +32 -0
  53. package/dist/lib/friction-heuristics.js +47 -0
  54. package/dist/lib/git.d.ts +14 -0
  55. package/dist/lib/git.js +36 -0
  56. package/dist/lib/hooks/cache.js +28 -6
  57. package/dist/lib/hooks/profile.d.ts +8 -0
  58. package/dist/lib/hooks/profile.js +15 -18
  59. package/dist/lib/hooks.js +72 -17
  60. package/dist/lib/linear-cache.d.ts +63 -0
  61. package/dist/lib/linear-cache.js +146 -0
  62. package/dist/lib/linear-project-counts.d.ts +35 -5
  63. package/dist/lib/linear-project-counts.js +61 -16
  64. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  65. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
  66. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  67. package/dist/lib/menubar/install-menubar.d.ts +7 -0
  68. package/dist/lib/menubar/install-menubar.js +36 -6
  69. package/dist/lib/percentile.d.ts +12 -0
  70. package/dist/lib/percentile.js +24 -0
  71. package/dist/lib/perf/db.d.ts +7 -3
  72. package/dist/lib/perf/db.js +37 -19
  73. package/dist/lib/perf/types.d.ts +10 -0
  74. package/dist/lib/plugins.js +12 -1
  75. package/dist/lib/project-doctor.d.ts +36 -0
  76. package/dist/lib/project-doctor.js +45 -0
  77. package/dist/lib/project-import.d.ts +11 -1
  78. package/dist/lib/project-import.js +17 -3
  79. package/dist/lib/project-status.d.ts +25 -5
  80. package/dist/lib/project-status.js +48 -6
  81. package/dist/lib/resources.d.ts +16 -0
  82. package/dist/lib/resources.js +25 -14
  83. package/dist/lib/rotate.d.ts +27 -0
  84. package/dist/lib/rotate.js +44 -17
  85. package/dist/lib/routines.d.ts +16 -0
  86. package/dist/lib/routines.js +26 -0
  87. package/dist/lib/runner.js +41 -0
  88. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  89. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  90. package/dist/lib/secrets/usage-db.d.ts +3 -63
  91. package/dist/lib/secrets/usage-db.js +46 -186
  92. package/dist/lib/session/db.d.ts +17 -1
  93. package/dist/lib/session/db.js +240 -6
  94. package/dist/lib/session/discover.d.ts +31 -0
  95. package/dist/lib/session/discover.js +50 -1
  96. package/dist/lib/session/highlights.d.ts +24 -4
  97. package/dist/lib/session/highlights.js +52 -7
  98. package/dist/lib/session/parse.js +19 -3
  99. package/dist/lib/session/prompt.d.ts +19 -0
  100. package/dist/lib/session/prompt.js +43 -0
  101. package/dist/lib/session/types.d.ts +43 -0
  102. package/dist/lib/startup/command-registry.d.ts +1 -0
  103. package/dist/lib/startup/command-registry.js +2 -0
  104. package/dist/lib/state.d.ts +31 -3
  105. package/dist/lib/state.js +53 -10
  106. package/dist/lib/types.d.ts +22 -4
  107. package/dist/lib/usage-refresh.d.ts +106 -0
  108. package/dist/lib/usage-refresh.js +238 -0
  109. package/dist/lib/usage.d.ts +152 -17
  110. package/dist/lib/usage.js +393 -79
  111. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,211 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.3
4
+
5
+ - **`agents projects import --from-factory` stops printing raw git errors.** Reading each
6
+ checkout's real remote is done per registry row, and a checkout with no `origin` makes git
7
+ write `error: No such remote 'origin'` straight to the terminal — its own stderr, which the
8
+ surrounding try/catch never sees. Importing 12 rows printed two of them between the progress
9
+ lines. The probe now discards git's stderr; an absent remote is an expected answer, not
10
+ something to report. Source: `apps/cli/src/commands/projects.ts`.
11
+
12
+ - **Sessions now track browser/computer tool use and skill/plugin/slash-command
13
+ usage, queryable with `agents sessions --skill <name>` / `--plugin <name>`.**
14
+ `browser.navigate`, `browser.screenshot`, and a new `computer.action` event
15
+ fire on every `agents browser`/`agents computer` action, carrying session
16
+ identity for free. The sessions index persists `usedBrowser`/`usedComputer`
17
+ (from a scoped events-log read, not a transcript re-scan) and a new
18
+ `session_resource_usage` table records every skill and slash-command
19
+ invocation with its owning plugin, source repo, and git commit — resolved
20
+ against `resolveResource()`/`discoverPlugins()` at scan time. The sessions
21
+ picker preview surfaces both as `browser`/`computer` and `Skills:` tags.
22
+ Source: `apps/cli/src/lib/browser/service.ts`, `apps/cli/src/commands/computer-actions.ts`,
23
+ `apps/cli/src/lib/session/db.ts`, `apps/cli/src/lib/session/highlights.ts`,
24
+ `apps/cli/src/lib/session/discover.ts`, `apps/cli/src/commands/sessions.ts`.
25
+ - **`ResolvedResource` and `DiscoveredPlugin` carry provenance: `repoRoot` and a
26
+ lazily-resolved `snapshotSha`.** Every resource/plugin resolution can now
27
+ answer "which DotAgents repo, which commit" without an extra lookup; the git
28
+ shell-out is memoized per repo root and only runs when a caller actually
29
+ reads `snapshotSha`. Source: `apps/cli/src/lib/resources.ts`,
30
+ `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/git.ts`.
31
+ - **`SessionEvent.slashCommand` captures a typed or model-invoked slash command**
32
+ (both the `<command-name>` wrapper and the `SlashCommand` tool call), and
33
+ `agents sessions`'s perf sample for `command.end` now carries the session id
34
+ and agent instead of being anonymous. Source: `apps/cli/src/lib/session/prompt.ts`,
35
+ `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/index.ts`.
36
+
37
+ ## 1.21.2
38
+
39
+ - **`agents trends` — resource and session analytics dashboard.** Baked recipes
40
+ (harness/model mix, tools per session, token ratio, secrets/browser hot lists)
41
+ read `sessions.db` plus a new value-free warehouse at
42
+ `~/.agents/.history/analytics/usage.db`. Secrets usage migrates once from
43
+ `secrets.db`; agent run and browser launch/close emit into the warehouse.
44
+ Quota stays on `agents usage`, latency on `agents perf`.
45
+ Source: `apps/cli/src/commands/trends.ts`, `apps/cli/src/lib/analytics/`.
46
+
47
+ - **The macOS menu bar app is now named AGI Menu in System Settings and
48
+ Accessibility prompts.** Privacy & Security previously showed the executable
49
+ name `MenubarHelper` because the bundle had no `CFBundleDisplayName`. The
50
+ bundle now ships `CFBundleName` / `CFBundleDisplayName` = `AGI Menu`, and
51
+ `agents menubar` status/enable/disable copy uses the same name. An install
52
+ that was left ad-hoc-signed by an older heal path is also replaced from the
53
+ Developer-ID source on the next `agents` run, so Accessibility stops
54
+ re-prompting for a new identity every upgrade. Source:
55
+ `apps/cli/menubar/scripts/build.sh`, `apps/cli/src/commands/menubar.ts`,
56
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
57
+
58
+ - **Cursor usage bars now show Auto/API/Total, and Cursor sessions carry live todo progress.** `agents view` reads Cursor's dashboard `get-current-period-usage` first for the Auto + Composer (`A`) / API (`API`) / Total (`T`) percent breakdown, falls back to `usage-summary` for accounts without a usable `planUsage`, and only drops to the legacy monthly request bar (`M`) for request-capped free/legacy plans. `agents sessions` also now folds a Cursor session's `TodoWrite` calls into `SessionMeta.todos`, so the checklist progress shown for Claude/Codex/Kimi sessions renders for Cursor too. Source: `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/session/discover.ts`.
59
+
60
+ - **`feed.broadcast` gains an in-process `channel:` sink and an implicit owner
61
+ fallback (RUSH-2123).** A `feed.broadcast` sink can now declare `channel: <name>`
62
+ (plus `to:` for a non-owner destination) instead of `command: [argv...]` — it
63
+ delivers through the same channel-provider registry `agents send`/`agents notify`
64
+ use (`deliverEnvelope()`), no spawn. `channel: owner` is the address alias,
65
+ expanding to `notify.owner.{channel,to}`. When an operator has `notify.owner`
66
+ configured but never wrote a `feed.broadcast` block at all, an important-level
67
+ post (`--level important`, or any `--blocked` post) now falls back to that owner
68
+ address automatically instead of reaching nobody — previously a `feed post
69
+ --blocked` with `notify.owner` set and no `feed.broadcast` looked recorded but
70
+ delivered to no one. A routine milestone post still stays record-only even with
71
+ the fallback available, and an operator-declared `feed.broadcast` always wins
72
+ outright. `command:` argv sinks (the tracker/webhook escape hatch) are unchanged.
73
+ Source: `apps/cli/src/lib/feed-broadcast.ts`, `apps/cli/src/commands/feed.ts`.
74
+
75
+ - **`agents run` no longer stalls on a live usage fetch, and the daemon keeps the
76
+ quota cache warm instead (RUSH-2061).** The router's candidate collection
77
+ (`collectRunCandidates`) used to block on a live provider HTTP read whenever an
78
+ account's usage snapshot was older than 5 minutes — one round trip per account
79
+ added to cold-start. It now reads the usage cache **cache-only** (`readOnly`) and
80
+ never touches the network; an unconfirmable snapshot is simply routed around by
81
+ the existing freshness guard (`isUsageVerified`). A new daemon refresher
82
+ (`runUsageRefresh`) keeps that cache fresh in the background: it refreshes only
83
+ accounts signed in on THIS host (sole-writer, no cross-host coordination), on an
84
+ adaptive cadence from each account's session-window burn rate (90s when racing
85
+ toward the 5h cap, up to 15min when idle), capped at ~6 provider calls per
86
+ account per hour and skipped entirely while a provider is under a 429 backoff.
87
+ Source: `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/usage-refresh.ts`,
88
+ `apps/cli/src/lib/rotate.ts`, `apps/cli/src/lib/daemon.ts`.
89
+
90
+ - **Balanced routing now deprioritizes an account projected to cap soon, not just
91
+ one already maxed (RUSH-2061).** `deriveUsageHeadroom` projects minutes-to-limit
92
+ from the session-window burn rate; balanced weighting scales an account's
93
+ headroom weight down as that projection shortens (`capacityWeight`), so a launch
94
+ avoids an account racing toward its 5-hour cap instead of only skipping a
95
+ 100%-maxed one. Source: `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/rotate.ts`.
96
+
97
+ - **The daemon no longer SSH-probes the whole fleet every 3 minutes — fleet status
98
+ is publish-own / read-union now (RUSH-2061, RUSH-2114).** The daemon's fleet-cache
99
+ warm force-probed every registered device over ssh on every tick; with N daemons
100
+ each probing N devices that was N² remote resource probes across the fleet every
101
+ 3 minutes, and the source of the orphaned fleet-doctor probe pile-up. Each daemon
102
+ now probes only **itself** (no ssh) and publishes its own row — resource stats
103
+ **plus live-agent workload** (running-agent count and a per-context / per-agent
104
+ breakdown) — to a shared local mirror (`~/.agents/.cache/.fleet-status.json`).
105
+ Cross-host rows are unioned on demand by the reader: `agents devices status`
106
+ gathers peers cache-first, ssh-reading a stale/missing peer via
107
+ `agents devices status --local --json` through a bounded, kill-on-timeout
108
+ fan-out. `agents devices status` (and `--json`) now shows how many agents are
109
+ running on each box. Source: `apps/cli/src/lib/fleet-status.ts`,
110
+ `apps/cli/src/lib/fleet-cache.ts`, `apps/cli/src/lib/daemon.ts`,
111
+ `apps/cli/src/lib/devices/health-report.ts`, `apps/cli/src/commands/ssh.ts`.
112
+
113
+ - **`agents doctor --json` is no longer a ~136-second stall (RUSH-2136).** The
114
+ overview probed every host-CLI manifest with a blocking `spawnSync` (10s timeout
115
+ each) one after another, so a dozen-plus slow checks summed into minutes. The
116
+ checks now run concurrently (`listCliStatusAsync`), so total time is the slowest
117
+ single check, not their sum; the per-check 10s kill-on-timeout is preserved.
118
+ Source: `apps/cli/src/lib/cli-resources.ts`, `apps/cli/src/commands/doctor.ts`.
119
+
120
+ - **Metrics foundation: hook/command instrumentation + routine metrics.** Every
121
+ hook now instruments through a generated shim — `matcher:`-only hooks like
122
+ git-guard/rm-guard/git-require-clean-tree previously fired with zero perf
123
+ samples; `agents perf hooks` now reports them. `agents perf` gains
124
+ `--project <key>` (scope to one repo), a `P95` column alongside P50/P99, and
125
+ an `ERR/TIMEOUT` rate column. New `agents perf friction` surfaces sessions
126
+ stuck repeatedly hitting the same guard block instead of adapting. New
127
+ `agents routines stats [name]` reports run count/failed/missed/avg/p50/p95
128
+ duration per routine; `agents routines runs --json` now includes `duration`.
129
+ Routine session transcripts are now archived for gemini/antigravity/droid/
130
+ kimi/grok routines, not just claude/codex/cursor. Source:
131
+ `apps/cli/src/lib/hooks.ts`, `apps/cli/src/lib/perf/db.ts`,
132
+ `apps/cli/src/commands/perf.ts`, `apps/cli/src/lib/routines.ts`,
133
+ `apps/cli/src/lib/runner.ts`.
134
+
135
+ - **The Linear line on `agents projects status` is cached, and stops vanishing.** The card
136
+ paged every issue in a project on every invocation — up to 10 requests per project — against
137
+ a 2500/hour request budget that an agent running `status` in a loop exhausts. Answers are now
138
+ cached on disk for 10 minutes (`~/.agents/.cache/linear-projects/`, one file per project written
139
+ by atomic rename so concurrent agent sessions cannot clobber each other), so a repeated
140
+ `status` spends zero Linear requests. More importantly, a failed or rate-limited fetch now
141
+ serves the last good answer marked stale instead of dropping the line: a populated Linear row
142
+ silently disappearing on one 8s timeout was the observed defect, and it is the same rule
143
+ `mergeAuthHealthEntries` already keeps for account health. A 429 records its
144
+ `x-ratelimit-requests-reset` so later runs don't spend a request to be told there are none
145
+ left. Source: `apps/cli/src/lib/linear-cache.ts`.
146
+
147
+ - **The compact `projects status` card shows the milestone it calls `next`.** Milestones are
148
+ listed in date order, and Linear can flag a later-dated one as next — so slicing the front
149
+ of the list showed an earlier milestone while burying the actual next under `+N more`, which
150
+ is the one thing that row exists to say. The next milestone now leads, and identity is
151
+ matched on name plus target date rather than name alone (two milestones can share a name,
152
+ which put the `next` label on the wrong row). Source: `apps/cli/src/commands/projects.ts`.
153
+
154
+ - **`agents projects` stops reading the wrong GitHub repository.** Factory derives a
155
+ project's `owner/repo` from the checkout path's last two segments, so a repo cloned to
156
+ `~/src/github.com/<you>/agents-cli` whose origin is `phnx-labs/agents-cli` imported as
157
+ `<you>/agents-cli`. Both are real repositories, so nothing errored — the card's merged-PR
158
+ and release lines simply reported a stranger's repo (0 merges in 7 days instead of 100).
159
+ `import --from-factory` now reads the checkout's actual `origin` and only falls back to the
160
+ path guess when there is no remote to ask, and `status`/`show` print a warning with the fix
161
+ when a stored slug disagrees with the remote. Source: `apps/cli/src/lib/project-doctor.ts`.
162
+ - **`agents projects set <name>` changes one field without destroying the rest.** Previously
163
+ the only ways to correct a field were `$EDITOR` on raw YAML or `add --force`, which rebuilds
164
+ the definition from flags alone and silently drops `linear`, `contexts`, and `description`.
165
+ `set` loads, patches the named field, and writes back. Flags: `--repo`, `--root`, `--path`,
166
+ `--description`. Source: `apps/cli/src/commands/projects.ts`.
167
+ - **Merged-PR counts say when they are a lower bound.** The `gh` fetch caps at 100, and a busy
168
+ repo where all 100 land inside the window has more — the count now renders `100+` rather than
169
+ presenting the cap as a total, matching the existing Linear `2500+` contract. Source:
170
+ `apps/cli/src/lib/project-status.ts`.
171
+
172
+ - **`agents projects view <name>`** replaces `show` (kept as an alias) and now renders the
173
+ project's full plan: every declared Linear milestone with its date and progress, issue
174
+ counts, and a warning when no issues are assigned to any milestone — a milestone nothing is
175
+ filed against cannot report progress, and a row of silent `0%`s hid that. Sixteen other
176
+ command groups already use `view <name>`; `projects` was the only one that did not. Source:
177
+ `apps/cli/src/commands/projects.ts`.
178
+ - **The status headline counts live agents, not corpses.** It read `39 agents` on a project
179
+ where 19 had crashed. It now reads `19 live`, with a separate `dead` row breaking down what
180
+ finished or was lost — 19 crashed sessions is a thing to go fix, not throughput. `orphaned`
181
+ counts as **live**: `session/active.ts` defines it as "alive, but no client is attached", and
182
+ the repo's own dead rule is `closed` + `crashed` only. Source:
183
+ `apps/cli/src/lib/project-status.ts`.
184
+ - **`planPct` is gone from the card and from `--json`.** It summed each matched session's most
185
+ recent checklist snapshot, so one agent opening a fresh 40-item plan rendered the whole
186
+ project `0% plan`, and a project where nobody had written a checklist showed no figure at
187
+ all. A cross-session sum of ad-hoc checklists does not measure project progress. `live` and
188
+ `dead` counts replace it in `--json`.
189
+ - **The next milestone comes from Linear's own `status: "next"`** when Linear sets it, falling
190
+ back to earliest-dated-unfinished only when nothing is flagged — Linear's answer is the one
191
+ shown in its UI, ours is a guess.
192
+
193
+ - **A regression guard for the distributed `--active --local` / `--host` session-query paths, wired into CI (#1866).** RUSH-2118 fixed a `--local` query dialing remote-host teammates over real ssh, but nothing bench-guarded the fix's latency, and the `--host` cross-fleet fan-out had no bench at all. `bench/sessions-active-perf.ts` times `AgentManager(..., localOnly=true).listAll()` against N synthetic remote-host teammates (asserting zero ssh calls and sub-500ms latency, with a positive-control run proving the ssh-PATH shim actually intercepts) and the `gatherActiveSessions({ hosts })` fan-out against N synthetic peers (asserting it stays parallel, not sequential). Wired into `.github/workflows/bench.yml` as the one gating step in that workflow — every other bench step stays `continue-on-error`. Documented with measured baselines in `apps/cli/docs/05-sessions.md#benchmarks`. Source: `apps/cli/bench/sessions-active-perf.ts`, `.github/workflows/bench.yml`.
194
+
195
+ - **`agents view` columns stay aligned across agents, and usage no longer piles up (view-ui-perf).**
196
+ The multi-agent overview padded every row to the widest usage string — an
197
+ Antigravity account with four model quotas forced ~194-column lines that
198
+ wrapped so `rate-limited` and last-active drifted under the version column.
199
+ Overview now caps compact meters to two windows (`+N` for the rest), always
200
+ emits fixed account/usage/status/lastActive columns (empty cells space-padded),
201
+ and measures padding with `stringWidth` so chalk + block bars don't skew
202
+ gutters. Usage fetches go through one unified core: 5-minute fresh cache
203
+ (was 2), concurrency-capped live reads (`USAGE_FETCH_CONCURRENCY=3`),
204
+ single-flight per identity, and a background SWR queue capped at 2 so delayed
205
+ HTTP responses cannot stack. Spinner stays up through account+usage load.
206
+ Source: `apps/cli/src/commands/view.ts`, `apps/cli/src/lib/usage.ts`,
207
+ `apps/cli/src/lib/agents.ts`.
208
+
3
209
  ## 1.21.1
4
210
 
5
211
  - **Feed posts require a title + body; phone `{message}` ends with a Sent-from footer.** `agents feed post --title "Short subject" "body text"` — title is the phone first line (~4–5 words), body follows after a blank line, then `Sent from <agent>/<session-chunk> on <host>` (like "Sent from my iPhone"). Em/en dashes in title/body are scrubbed to ASCII ` - `. Source: `apps/cli/src/lib/feed-broadcast.ts`, `feed-post.ts`, `commands/feed.ts`.
package/README.md CHANGED
@@ -875,6 +875,7 @@ agents routines add daily-digest \
875
875
  agents routines list # All jobs + next run times
876
876
  agents routines run daily-digest # Test it now, ignore the schedule
877
877
  agents routines logs daily-digest # Last execution — status + report (add --full for raw stdout)
878
+ agents routines stats # Run count, failed, missed, avg/p50/p95 duration — per job or all
878
879
 
879
880
  # Routines sync to every device; restrict to an allowlist with --devices
880
881
  agents routines add nightly-drain --schedule "0 3 * * *" --agent claude \
package/dist/bin/agents CHANGED
Binary file
@@ -82,4 +82,8 @@ export declare function shouldRaise(opts: {
82
82
  }): boolean;
83
83
  export declare function appPathIsElectron(appPath: string | null, exists?: (p: string) => boolean): boolean;
84
84
  export declare function electronWebviewTip(appLabel: string): string;
85
+ export declare function emitComputerAction(verb: string, targetPid: number | undefined, opts: {
86
+ bundle?: string;
87
+ host?: string;
88
+ }, extra?: Record<string, unknown>): void;
85
89
  export declare function registerActionCommands(program: Command): void;
@@ -11,6 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { openComputerClient, describeTransport, resolvePolicyPath, } from '../lib/computer-rpc.js';
13
13
  import { COMPUTER_INPUT_GATED_VERBS, formatComputerPermissionGrantHint, } from '../lib/permissions.js';
14
+ import { emit as emitEvent } from '../lib/events.js';
14
15
  function isComputerInputVerb(verb) {
15
16
  return typeof verb === 'string' && COMPUTER_INPUT_GATED_VERBS.includes(verb);
16
17
  }
@@ -350,6 +351,22 @@ function emit(result, json, human) {
350
351
  console.log(human());
351
352
  }
352
353
  }
354
+ // Record one `computer.action` per verb invocation, one call site per command
355
+ // below. Session/agent/machine identity is stamped for free by emitEvent's
356
+ // provenance floor (events.ts resolveProvenance) — this only carries the
357
+ // action-specific facts: which verb, against which target pid/bundle/host.
358
+ // NOTE: the field is `targetPid`, never `pid` — `pid` is a reserved envelope
359
+ // key (the emitting process's OWN pid, events.ts RESERVED_META_KEYS) that
360
+ // sanitizePayload() silently strips from the payload before it can collide.
361
+ export function emitComputerAction(verb, targetPid, opts, extra = {}) {
362
+ emitEvent('computer.action', {
363
+ command: verb,
364
+ targetPid,
365
+ bundle: opts.bundle,
366
+ host: opts.host,
367
+ ...extra,
368
+ });
369
+ }
353
370
  // Add the shared --pid/--bundle/--host target options to a verb. `--host` routes
354
371
  // the verb at a remote Windows device: the `computer` preAction hook hydrates
355
372
  // COMPUTER_HELPER_TCP from the tunnel `start --host` recorded, so withClient's
@@ -376,6 +393,7 @@ export function registerActionCommands(program) {
376
393
  await withClient(async (client) => {
377
394
  const res = unwrap(await client.call('list_apps'));
378
395
  const list = res.apps || [];
396
+ emitComputerAction('apps', undefined, opts);
379
397
  emit(res, Boolean(opts.json), () => list.length === 0
380
398
  ? '(no allow-listed apps running)'
381
399
  : list
@@ -422,6 +440,7 @@ export function registerActionCommands(program) {
422
440
  if (opts.background)
423
441
  params.background = true;
424
442
  const res = unwrap(await client.call('click', params));
443
+ emitComputerAction('click', pid, opts, { id: opts.id, count: opts.count });
425
444
  emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
426
445
  });
427
446
  });
@@ -440,6 +459,7 @@ export function registerActionCommands(program) {
440
459
  }
441
460
  await applyFocusPolicy(client, pid, opts);
442
461
  const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
462
+ emitComputerAction('right-click', pid, opts, { id: opts.id });
443
463
  emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
444
464
  });
445
465
  });
@@ -466,6 +486,8 @@ export function registerActionCommands(program) {
466
486
  if (opts.allowSecureField)
467
487
  params.allow_secure_field = true;
468
488
  const res = unwrap(await client.call('type', params));
489
+ // textLength, never the text itself — users type passwords/secrets into fields.
490
+ emitComputerAction('type', pid, opts, { id: opts.id, textLength: opts.text.length, committed: Boolean(res.committed) });
469
491
  emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
470
492
  });
471
493
  });
@@ -493,6 +515,7 @@ export function registerActionCommands(program) {
493
515
  params.char_delay_ms = charDelay;
494
516
  const res = unwrap(await client.call('type_text', params));
495
517
  warnIfNotFrontmost(res);
518
+ emitComputerAction('type-text', pid, opts, { textLength: opts.text.length, committed: Boolean(opts.commit) });
496
519
  emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
497
520
  });
498
521
  });
@@ -513,6 +536,7 @@ export function registerActionCommands(program) {
513
536
  params.require_frontmost = true;
514
537
  const res = unwrap(await client.call('key', params));
515
538
  warnIfNotFrontmost(res);
539
+ emitComputerAction('key', pid, opts, { keys: opts.keys });
516
540
  emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
517
541
  });
518
542
  });
@@ -548,6 +572,7 @@ export function registerActionCommands(program) {
548
572
  if (opts.background)
549
573
  params.background = true;
550
574
  const res = unwrap(await client.call('drag', params));
575
+ emitComputerAction('drag', pid, opts, { from: opts.from, to: opts.to });
551
576
  emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
552
577
  });
553
578
  });
@@ -574,6 +599,7 @@ export function registerActionCommands(program) {
574
599
  if (opts.dx != null)
575
600
  params.dx = opts.dx;
576
601
  const res = unwrap(await client.call('scroll', params));
602
+ emitComputerAction('scroll', pid, opts, { id: opts.id, dx: opts.dx, dy: opts.dy });
577
603
  emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
578
604
  });
579
605
  });
@@ -587,6 +613,7 @@ export function registerActionCommands(program) {
587
613
  await withClient(async (client) => {
588
614
  const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
589
615
  const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
616
+ emitComputerAction('ax-action', pid, opts, { id: opts.id, action: opts.action });
590
617
  emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
591
618
  });
592
619
  });
@@ -599,6 +626,7 @@ export function registerActionCommands(program) {
599
626
  await withClient(async (client) => {
600
627
  const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
601
628
  const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
629
+ emitComputerAction('focus', pid, opts, { id: opts.id });
602
630
  emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
603
631
  });
604
632
  });
@@ -615,6 +643,7 @@ export function registerActionCommands(program) {
615
643
  await withClient(async (client) => {
616
644
  const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
617
645
  const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
646
+ emitComputerAction('raise', pid, opts, { windowId: opts.windowId, title: opts.title });
618
647
  emit(res, Boolean(opts.json), () => {
619
648
  const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
620
649
  return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
@@ -644,6 +673,9 @@ export function registerActionCommands(program) {
644
673
  if (params.duration_ms == null)
645
674
  params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
646
675
  const res = unwrap(await client.call('wait', params));
676
+ emitComputerAction('wait', params.pid, opts, {
677
+ until: opts.until, durationMs: opts.duration, satisfied: Boolean(res.satisfied),
678
+ });
647
679
  emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
648
680
  });
649
681
  });
@@ -662,6 +694,7 @@ export function registerActionCommands(program) {
662
694
  if (opts.maxChars != null)
663
695
  params.max_chars = opts.maxChars;
664
696
  const res = unwrap(await client.call('get_text', params));
697
+ emitComputerAction('get-text', pid, opts, { id: opts.id });
665
698
  emit(res, Boolean(opts.json), () => String(res.text ?? ''));
666
699
  });
667
700
  });
@@ -688,6 +721,7 @@ export function registerActionCommands(program) {
688
721
  if (opts.name)
689
722
  params.name = opts.name;
690
723
  const res = unwrap(await client.call('launch_app', params));
724
+ emitComputerAction('launch', res.pid, opts, { path: opts.path, name: opts.name });
691
725
  emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
692
726
  });
693
727
  });
@@ -7,7 +7,7 @@ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketP
7
7
  import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
8
8
  import { sshExec } from '../lib/ssh-exec.js';
9
9
  import { encodePowershell } from '../lib/hosts/remote-cmd.js';
10
- import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
10
+ import { registerActionCommands, withClient, unwrap, pickTarget, emitComputerAction } from './computer-actions.js';
11
11
  import { runComputerLoop } from '../lib/computer/loop.js';
12
12
  import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
13
13
  import { makeClaudeResponder, resolveApiKey, DEFAULT_CLAUDE_MODEL, DEFAULT_CLAUDE_BASE_URL } from '../lib/computer/model.js';
@@ -301,7 +301,7 @@ function registerRunCommand(program) {
301
301
  maxTokens: opts.maxTokens,
302
302
  });
303
303
  await withClient(async (client) => {
304
- const dispatch = makeVerbDispatcher(client);
304
+ const dispatch = makeVerbDispatcher(client, { host: opts.host });
305
305
  const targetInput = opts.bundle ? { bundle: opts.bundle } : {};
306
306
  const result = await runComputerLoop({
307
307
  task: opts.task,
@@ -375,6 +375,7 @@ function registerScreenshotCommand(program) {
375
375
  // --list: enumerate windows, no image.
376
376
  if (opts.list) {
377
377
  const res = unwrap(await client.call('screenshot', { pid, list: true }));
378
+ emitComputerAction('screenshot', pid, opts, { list: true });
378
379
  const windows = res.windows || [];
379
380
  if (opts.json) {
380
381
  console.log(JSON.stringify(res, null, 2));
@@ -402,6 +403,7 @@ function registerScreenshotCommand(program) {
402
403
  console.error('helper returned no image_data');
403
404
  process.exit(1);
404
405
  }
406
+ emitComputerAction('screenshot', pid, opts, { display: opts.display, windowId: opts.windowId });
405
407
  const buf = Buffer.from(b64, 'base64');
406
408
  // Sniff the real format and correct the extension so the filename never
407
409
  // lies about its bytes (macOS -> JPEG, Windows helper -> PNG).
@@ -25,7 +25,7 @@ import { isVersionIsolated } from '../lib/versions.js';
25
25
  import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
26
26
  import { readAuthHealthCache, summarizeHostAuth } from '../lib/auth-health.js';
27
27
  import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
28
- import { listCliStatus } from '../lib/cli-resources.js';
28
+ import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
29
29
  import { setHelpSections } from '../lib/help.js';
30
30
  import { heal, healChangedAnything } from '../lib/heal.js';
31
31
  import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
@@ -1291,7 +1291,10 @@ export function registerDoctorCommand(program) {
1291
1291
  const clis = checkAllClis();
1292
1292
  const syncRows = checkSyncStatus(cwd);
1293
1293
  const orphanRows = countOrphans();
1294
- const hostClis = listCliStatus(cwd);
1294
+ // Parallel host-CLI probe (RUSH-2136): the serial spawnSync version ran a
1295
+ // dozen+ blocking 10s-timeout checks one after another, which measured
1296
+ // ~136s on an idle box and stalled the menu-bar helper's poll.
1297
+ const hostClis = await listCliStatusAsync(cwd);
1295
1298
  const repoBehindMarkers = readRepoBehindMarkers();
1296
1299
  // The local inventory now carries per-version sign-in (RUSH-2069), so it
1297
1300
  // is the single source for both the accounts line and the logged-out
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed.js';
3
3
  import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/activity.js';
4
4
  import { postFeedStatus } from '../lib/feed-post.js';
5
- import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
5
+ import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
6
6
  import { getSessionById } from '../lib/session/db.js';
7
7
  import { readMeta } from '../lib/state.js';
8
8
  import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
@@ -274,7 +274,7 @@ time. No em-dashes in title/body - they are scrubbed on the way out.
274
274
  Configure where a post is mirrored under feed.broadcast in agents.yaml - see
275
275
  docs/06-observability.md.
276
276
  `)
277
- .action((textParts, opts, cmd) => {
277
+ .action(async (textParts, opts, cmd) => {
278
278
  // Parent `feed` also declares `--json` (for the list view). Commander
279
279
  // binds the flag on the parent, so a `feed post … --json` lands on
280
280
  // parent.opts().json — not the child. Read both.
@@ -303,6 +303,7 @@ docs/06-observability.md.
303
303
  throw new Error('Missing --title. Usage: agents feed post --title "Short subject" "body text"');
304
304
  }
305
305
  const level = flags.blocked ? 'important' : parseFeedPostLevel(flags.level);
306
+ const meta = readMeta();
306
307
  const { event } = postFeedStatus({
307
308
  title: flags.title,
308
309
  text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
@@ -325,15 +326,15 @@ docs/06-observability.md.
325
326
  safeDefault: flags.default,
326
327
  });
327
328
  publishBlock(block);
328
- outcomes = broadcastBlock(block, {
329
+ outcomes = await broadcastBlock(block, {
329
330
  project: event.project,
330
331
  agent: event.agent,
331
332
  title: event.title,
332
333
  body: event.detail,
333
- });
334
+ }, meta);
334
335
  }
335
336
  else {
336
- outcomes = broadcastPostedEvent(event, level);
337
+ outcomes = await broadcastPostedEvent(event, level, meta);
337
338
  }
338
339
  // Fail loud when a block reached nobody. This is computed BEFORE the
339
340
  // --json early return: a machine caller is exactly the one that reads the
@@ -562,15 +563,21 @@ docs/06-observability.md.
562
563
  }
563
564
  /**
564
565
  * Mirror a written post to the configured sinks (`feed.broadcast` in
565
- * agents.yaml). The ticket is JOINED from the session index rather than asked
566
- * for as a flag it is a domain fact about the session, and an agent that has
567
- * to remember a `--ticket` argument is an agent that will forget it. Returns the
568
- * per-sink outcomes; an empty array means nothing is configured, which is the
569
- * default and is not a failure.
566
+ * agents.yaml, or the implicit `notify.owner` fallback for an important post
567
+ * see {@link effectiveBroadcastConfig}). The ticket is JOINED from the session
568
+ * index rather than asked for as a flag it is a domain fact about the
569
+ * session, and an agent that has to remember a `--ticket` argument is an agent
570
+ * that will forget it. Returns the per-sink outcomes; an empty array means
571
+ * nothing is configured and no fallback applies, which is not a failure for a
572
+ * routine post (see `blockDeliveryFailure` for the `--blocked` case).
573
+ *
574
+ * `meta` is threaded in rather than read here so the fallback/config decision
575
+ * and the delivery are pinned to one config snapshot, and so this is testable
576
+ * against a real in-memory `Meta` without touching `~/.agents/agents.yaml`.
570
577
  */
571
- function broadcastPostedEvent(event, level) {
572
- const config = readMeta().feed?.broadcast;
573
- if (!config || Object.keys(config).length === 0)
578
+ async function broadcastPostedEvent(event, level, meta) {
579
+ const config = effectiveBroadcastConfig(meta.feed?.broadcast, level, meta);
580
+ if (!config)
574
581
  return [];
575
582
  const ticket = getSessionById(event.sessionId)?.ticketId;
576
583
  const planned = planFeedBroadcast(config, {
@@ -586,22 +593,24 @@ function broadcastPostedEvent(event, level) {
586
593
  .map((a) => a.href)
587
594
  .filter((href) => /^https?:\/\//i.test(href)),
588
595
  });
589
- return runFeedBroadcast(planned);
596
+ return runFeedBroadcast(planned, meta);
590
597
  }
591
598
  /**
592
- * Mirror a declared block to the same sinks a post reaches.
599
+ * Mirror a declared block to the same sinks a post reaches (plus the implicit
600
+ * `notify.owner` fallback — a block is always `important`, so it always
601
+ * qualifies).
593
602
  *
594
603
  * Blocks previously never broadcast at all: `broadcastPostedEvent` ran only for
595
604
  * `feed post`, while every `publishBlock` call wrote to the ledger and stopped
596
605
  * there — so a "needs you" record was durable and invisible at the same time.
597
606
  */
598
- function broadcastBlock(block, extras) {
599
- const config = readMeta().feed?.broadcast;
600
- if (!config || Object.keys(config).length === 0)
607
+ async function broadcastBlock(block, extras, meta) {
608
+ const config = effectiveBroadcastConfig(meta.feed?.broadcast, 'important', meta);
609
+ if (!config)
601
610
  return [];
602
611
  const ticket = getSessionById(block.sessionId)?.ticketId;
603
612
  const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
604
- return runFeedBroadcast(planFeedBroadcast(config, ctx));
613
+ return runFeedBroadcast(planFeedBroadcast(config, ctx), meta);
605
614
  }
606
615
  /** One line per sink that ran. Silent when nothing is configured. */
607
616
  function reportBroadcast(outcomes) {
@@ -617,61 +617,25 @@ Examples:
617
617
  A hook whose p99 exceeds --warn-ms gets flagged in the cache column. Add
618
618
  'cache: 5m' or 'cache: 5m-bg' to its hooks.yaml entry to fix it.
619
619
  `)
620
+ .option('--project <key>', 'Scope to one project (see agents perf --help)')
620
621
  .action(async (options) => {
621
- const { aggregateHookProfile, loadHookFireEvents, formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS } = await import('../lib/hooks/profile.js');
622
- const { aggregateSamples } = await import('../lib/perf/db.js');
622
+ const { DEFAULT_SLOW_HOOK_WARN_MS } = await import('../lib/hooks/profile.js');
623
+ // Same rollup as `agents perf hooks` — this command predates the `perf`
624
+ // surface and is kept as a documented alias; delegate instead of
625
+ // duplicating the SQLite-vs-legacy-JSONL fallback and table rendering.
626
+ const { loadHookProfile, renderHookTable } = await import('./perf.js');
623
627
  const days = Math.max(1, parseInt(options.days, 10) || 7);
624
628
  const warnMs = Math.max(0, parseInt(options.warnMs, 10) || DEFAULT_SLOW_HOOK_WARN_MS);
625
- // Prefer the indexed warehouse; fall back to legacy JSONL for pre-warehouse shims.
626
- const fromDb = aggregateSamples({ days, kinds: ['hook.fire'] }).map((r) => ({
627
- hook: r.label,
628
- n: r.n,
629
- p50Ms: r.p50Ms,
630
- p99Ms: r.p99Ms,
631
- meanMs: r.meanMs,
632
- maxMs: r.maxMs,
633
- cacheHitPct: r.cacheHitPct ?? 0,
634
- cacheStalePct: r.cacheStalePct ?? 0,
635
- cacheMissPct: r.cacheMissPct ?? 0,
636
- errorCount: r.errorCount ?? 0,
637
- }));
638
- const rows = fromDb.length > 0 ? fromDb : aggregateHookProfile(loadHookFireEvents(days));
629
+ const rows = loadHookProfile(days, options.project);
639
630
  if (options.json) {
640
631
  console.log(JSON.stringify(rows, null, 2));
641
632
  return;
642
633
  }
643
634
  if (rows.length === 0) {
644
635
  console.log(chalk.gray(`No hook.fire samples in the last ${days} day${days === 1 ? '' : 's'}.`));
645
- console.log(chalk.gray('Add \'cache: 5m\' to a hook (or matches:) so a shim instruments it, then resync.'));
636
+ console.log(chalk.gray('Every hook now instruments via a generated shim (cache:, matches:, or a bare matcher:) resync hooks if this is unexpectedly empty.'));
646
637
  return;
647
638
  }
648
- const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache: 30 };
649
- const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
650
- const header = [
651
- pad('HOOK', widths.hook),
652
- pad('N', widths.n),
653
- pad('P50', widths.p50),
654
- pad('P99', widths.p99),
655
- pad('MEAN', widths.mean),
656
- pad('MAX', widths.max),
657
- pad('CACHE', widths.cache),
658
- ].join(' ');
659
- console.log(chalk.bold(header));
660
- console.log(chalk.gray('─'.repeat(header.length)));
661
- for (const r of rows) {
662
- const slow = r.p99Ms > warnMs;
663
- const cacheCol = formatCacheColumn(r);
664
- const warning = slow && r.cacheHitPct + r.cacheStalePct === 0 ? ' ← add cache: 5m' : '';
665
- const line = [
666
- pad(r.hook, widths.hook),
667
- pad(String(r.n), widths.n),
668
- pad(formatMs(r.p50Ms), widths.p50),
669
- pad(formatMs(r.p99Ms), widths.p99),
670
- pad(formatMs(r.meanMs), widths.mean),
671
- pad(formatMs(r.maxMs), widths.max),
672
- pad(cacheCol, widths.cache),
673
- ].join(' ') + warning;
674
- console.log(slow ? chalk.yellow(line) : line);
675
- }
639
+ renderHookTable(rows, warnMs);
676
640
  });
677
641
  }