@phnx-labs/agents-cli 1.21.0 → 1.21.2

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 (102) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +2 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.js +5 -2
  5. package/dist/commands/feed.js +63 -40
  6. package/dist/commands/hooks.js +9 -45
  7. package/dist/commands/menubar.js +24 -24
  8. package/dist/commands/message.js +23 -3
  9. package/dist/commands/monitors.js +8 -6
  10. package/dist/commands/perf.d.ts +13 -0
  11. package/dist/commands/perf.js +80 -23
  12. package/dist/commands/projects.d.ts +11 -0
  13. package/dist/commands/projects.js +153 -21
  14. package/dist/commands/routines.js +46 -1
  15. package/dist/commands/ssh.js +69 -0
  16. package/dist/commands/trends.d.ts +2 -0
  17. package/dist/commands/trends.js +158 -0
  18. package/dist/commands/usage.d.ts +4 -4
  19. package/dist/commands/view.d.ts +6 -0
  20. package/dist/commands/view.js +90 -45
  21. package/dist/index.js +14 -1
  22. package/dist/lib/activity.d.ts +6 -0
  23. package/dist/lib/activity.js +3 -0
  24. package/dist/lib/agents.js +2 -2
  25. package/dist/lib/analytics/dashboard.d.ts +11 -0
  26. package/dist/lib/analytics/dashboard.js +31 -0
  27. package/dist/lib/analytics/recipes.d.ts +32 -0
  28. package/dist/lib/analytics/recipes.js +316 -0
  29. package/dist/lib/analytics/usage-db.d.ts +84 -0
  30. package/dist/lib/analytics/usage-db.js +301 -0
  31. package/dist/lib/browser/service.js +18 -0
  32. package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
  33. package/dist/lib/channels/resolve.d.ts +18 -1
  34. package/dist/lib/channels/resolve.js +15 -5
  35. package/dist/lib/cli-resources.d.ts +20 -0
  36. package/dist/lib/cli-resources.js +48 -1
  37. package/dist/lib/daemon.js +51 -14
  38. package/dist/lib/devices/health-report.d.ts +5 -0
  39. package/dist/lib/devices/health-report.js +3 -0
  40. package/dist/lib/feed-broadcast.d.ts +93 -12
  41. package/dist/lib/feed-broadcast.js +235 -30
  42. package/dist/lib/feed-post.d.ts +16 -2
  43. package/dist/lib/feed-post.js +35 -7
  44. package/dist/lib/feed.js +1 -1
  45. package/dist/lib/fleet-cache.d.ts +37 -0
  46. package/dist/lib/fleet-cache.js +40 -0
  47. package/dist/lib/fleet-status.d.ts +53 -0
  48. package/dist/lib/fleet-status.js +120 -0
  49. package/dist/lib/friction-heuristics.d.ts +32 -0
  50. package/dist/lib/friction-heuristics.js +47 -0
  51. package/dist/lib/hooks/cache.js +28 -6
  52. package/dist/lib/hooks/profile.d.ts +8 -0
  53. package/dist/lib/hooks/profile.js +14 -4
  54. package/dist/lib/hooks.d.ts +16 -1
  55. package/dist/lib/hooks.js +133 -18
  56. package/dist/lib/linear-cache.d.ts +63 -0
  57. package/dist/lib/linear-cache.js +146 -0
  58. package/dist/lib/linear-project-counts.d.ts +35 -5
  59. package/dist/lib/linear-project-counts.js +61 -16
  60. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  61. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
  62. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  63. package/dist/lib/menubar/install-menubar.d.ts +7 -0
  64. package/dist/lib/menubar/install-menubar.js +36 -6
  65. package/dist/lib/monitors/config.d.ts +1 -1
  66. package/dist/lib/monitors/dispatch.d.ts +6 -2
  67. package/dist/lib/monitors/dispatch.js +10 -15
  68. package/dist/lib/notify.d.ts +42 -6
  69. package/dist/lib/notify.js +41 -32
  70. package/dist/lib/perf/db.d.ts +6 -1
  71. package/dist/lib/perf/db.js +35 -5
  72. package/dist/lib/perf/types.d.ts +10 -0
  73. package/dist/lib/project-doctor.d.ts +36 -0
  74. package/dist/lib/project-doctor.js +45 -0
  75. package/dist/lib/project-import.d.ts +11 -1
  76. package/dist/lib/project-import.js +17 -3
  77. package/dist/lib/project-status.d.ts +25 -5
  78. package/dist/lib/project-status.js +48 -6
  79. package/dist/lib/rotate.d.ts +27 -0
  80. package/dist/lib/rotate.js +44 -17
  81. package/dist/lib/routines.d.ts +16 -0
  82. package/dist/lib/routines.js +39 -0
  83. package/dist/lib/runner.js +34 -0
  84. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  85. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  86. package/dist/lib/secrets/usage-db.d.ts +3 -63
  87. package/dist/lib/secrets/usage-db.js +46 -186
  88. package/dist/lib/session/db.d.ts +2 -1
  89. package/dist/lib/session/db.js +14 -3
  90. package/dist/lib/session/discover.d.ts +3 -0
  91. package/dist/lib/session/discover.js +8 -0
  92. package/dist/lib/session/types.d.ts +1 -0
  93. package/dist/lib/startup/command-registry.d.ts +1 -0
  94. package/dist/lib/startup/command-registry.js +2 -0
  95. package/dist/lib/state.d.ts +31 -3
  96. package/dist/lib/state.js +53 -10
  97. package/dist/lib/types.d.ts +14 -4
  98. package/dist/lib/usage-refresh.d.ts +106 -0
  99. package/dist/lib/usage-refresh.js +238 -0
  100. package/dist/lib/usage.d.ts +152 -17
  101. package/dist/lib/usage.js +393 -79
  102. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,203 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.2
4
+
5
+ - **`agents trends` — resource and session analytics dashboard.** Baked recipes
6
+ (harness/model mix, tools per session, token ratio, secrets/browser hot lists)
7
+ read `sessions.db` plus a new value-free warehouse at
8
+ `~/.agents/.history/analytics/usage.db`. Secrets usage migrates once from
9
+ `secrets.db`; agent run and browser launch/close emit into the warehouse.
10
+ Quota stays on `agents usage`, latency on `agents perf`.
11
+ Source: `apps/cli/src/commands/trends.ts`, `apps/cli/src/lib/analytics/`.
12
+
13
+ - **The macOS menu bar app is now named AGI Menu in System Settings and
14
+ Accessibility prompts.** Privacy & Security previously showed the executable
15
+ name `MenubarHelper` because the bundle had no `CFBundleDisplayName`. The
16
+ bundle now ships `CFBundleName` / `CFBundleDisplayName` = `AGI Menu`, and
17
+ `agents menubar` status/enable/disable copy uses the same name. An install
18
+ that was left ad-hoc-signed by an older heal path is also replaced from the
19
+ Developer-ID source on the next `agents` run, so Accessibility stops
20
+ re-prompting for a new identity every upgrade. Source:
21
+ `apps/cli/menubar/scripts/build.sh`, `apps/cli/src/commands/menubar.ts`,
22
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
23
+
24
+ - **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`.
25
+
26
+ - **`feed.broadcast` gains an in-process `channel:` sink and an implicit owner
27
+ fallback (RUSH-2123).** A `feed.broadcast` sink can now declare `channel: <name>`
28
+ (plus `to:` for a non-owner destination) instead of `command: [argv...]` — it
29
+ delivers through the same channel-provider registry `agents send`/`agents notify`
30
+ use (`deliverEnvelope()`), no spawn. `channel: owner` is the address alias,
31
+ expanding to `notify.owner.{channel,to}`. When an operator has `notify.owner`
32
+ configured but never wrote a `feed.broadcast` block at all, an important-level
33
+ post (`--level important`, or any `--blocked` post) now falls back to that owner
34
+ address automatically instead of reaching nobody — previously a `feed post
35
+ --blocked` with `notify.owner` set and no `feed.broadcast` looked recorded but
36
+ delivered to no one. A routine milestone post still stays record-only even with
37
+ the fallback available, and an operator-declared `feed.broadcast` always wins
38
+ outright. `command:` argv sinks (the tracker/webhook escape hatch) are unchanged.
39
+ Source: `apps/cli/src/lib/feed-broadcast.ts`, `apps/cli/src/commands/feed.ts`.
40
+
41
+ - **`agents run` no longer stalls on a live usage fetch, and the daemon keeps the
42
+ quota cache warm instead (RUSH-2061).** The router's candidate collection
43
+ (`collectRunCandidates`) used to block on a live provider HTTP read whenever an
44
+ account's usage snapshot was older than 5 minutes — one round trip per account
45
+ added to cold-start. It now reads the usage cache **cache-only** (`readOnly`) and
46
+ never touches the network; an unconfirmable snapshot is simply routed around by
47
+ the existing freshness guard (`isUsageVerified`). A new daemon refresher
48
+ (`runUsageRefresh`) keeps that cache fresh in the background: it refreshes only
49
+ accounts signed in on THIS host (sole-writer, no cross-host coordination), on an
50
+ adaptive cadence from each account's session-window burn rate (90s when racing
51
+ toward the 5h cap, up to 15min when idle), capped at ~6 provider calls per
52
+ account per hour and skipped entirely while a provider is under a 429 backoff.
53
+ Source: `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/usage-refresh.ts`,
54
+ `apps/cli/src/lib/rotate.ts`, `apps/cli/src/lib/daemon.ts`.
55
+
56
+ - **Balanced routing now deprioritizes an account projected to cap soon, not just
57
+ one already maxed (RUSH-2061).** `deriveUsageHeadroom` projects minutes-to-limit
58
+ from the session-window burn rate; balanced weighting scales an account's
59
+ headroom weight down as that projection shortens (`capacityWeight`), so a launch
60
+ avoids an account racing toward its 5-hour cap instead of only skipping a
61
+ 100%-maxed one. Source: `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/rotate.ts`.
62
+
63
+ - **The daemon no longer SSH-probes the whole fleet every 3 minutes — fleet status
64
+ is publish-own / read-union now (RUSH-2061, RUSH-2114).** The daemon's fleet-cache
65
+ warm force-probed every registered device over ssh on every tick; with N daemons
66
+ each probing N devices that was N² remote resource probes across the fleet every
67
+ 3 minutes, and the source of the orphaned fleet-doctor probe pile-up. Each daemon
68
+ now probes only **itself** (no ssh) and publishes its own row — resource stats
69
+ **plus live-agent workload** (running-agent count and a per-context / per-agent
70
+ breakdown) — to a shared local mirror (`~/.agents/.cache/.fleet-status.json`).
71
+ Cross-host rows are unioned on demand by the reader: `agents devices status`
72
+ gathers peers cache-first, ssh-reading a stale/missing peer via
73
+ `agents devices status --local --json` through a bounded, kill-on-timeout
74
+ fan-out. `agents devices status` (and `--json`) now shows how many agents are
75
+ running on each box. Source: `apps/cli/src/lib/fleet-status.ts`,
76
+ `apps/cli/src/lib/fleet-cache.ts`, `apps/cli/src/lib/daemon.ts`,
77
+ `apps/cli/src/lib/devices/health-report.ts`, `apps/cli/src/commands/ssh.ts`.
78
+
79
+ - **`agents doctor --json` is no longer a ~136-second stall (RUSH-2136).** The
80
+ overview probed every host-CLI manifest with a blocking `spawnSync` (10s timeout
81
+ each) one after another, so a dozen-plus slow checks summed into minutes. The
82
+ checks now run concurrently (`listCliStatusAsync`), so total time is the slowest
83
+ single check, not their sum; the per-check 10s kill-on-timeout is preserved.
84
+ Source: `apps/cli/src/lib/cli-resources.ts`, `apps/cli/src/commands/doctor.ts`.
85
+
86
+ - **Metrics foundation: hook/command instrumentation + routine metrics.** Every
87
+ hook now instruments through a generated shim — `matcher:`-only hooks like
88
+ git-guard/rm-guard/git-require-clean-tree previously fired with zero perf
89
+ samples; `agents perf hooks` now reports them. `agents perf` gains
90
+ `--project <key>` (scope to one repo), a `P95` column alongside P50/P99, and
91
+ an `ERR/TIMEOUT` rate column. New `agents perf friction` surfaces sessions
92
+ stuck repeatedly hitting the same guard block instead of adapting. New
93
+ `agents routines stats [name]` reports run count/failed/missed/avg/p50/p95
94
+ duration per routine; `agents routines runs --json` now includes `duration`.
95
+ Routine session transcripts are now archived for gemini/antigravity/droid/
96
+ kimi/grok routines, not just claude/codex/cursor. Source:
97
+ `apps/cli/src/lib/hooks.ts`, `apps/cli/src/lib/perf/db.ts`,
98
+ `apps/cli/src/commands/perf.ts`, `apps/cli/src/lib/routines.ts`,
99
+ `apps/cli/src/lib/runner.ts`.
100
+
101
+ - **The Linear line on `agents projects status` is cached, and stops vanishing.** The card
102
+ paged every issue in a project on every invocation — up to 10 requests per project — against
103
+ a 2500/hour request budget that an agent running `status` in a loop exhausts. Answers are now
104
+ cached on disk for 10 minutes (`~/.agents/.cache/linear-projects/`, one file per project written
105
+ by atomic rename so concurrent agent sessions cannot clobber each other), so a repeated
106
+ `status` spends zero Linear requests. More importantly, a failed or rate-limited fetch now
107
+ serves the last good answer marked stale instead of dropping the line: a populated Linear row
108
+ silently disappearing on one 8s timeout was the observed defect, and it is the same rule
109
+ `mergeAuthHealthEntries` already keeps for account health. A 429 records its
110
+ `x-ratelimit-requests-reset` so later runs don't spend a request to be told there are none
111
+ left. Source: `apps/cli/src/lib/linear-cache.ts`.
112
+
113
+ - **The compact `projects status` card shows the milestone it calls `next`.** Milestones are
114
+ listed in date order, and Linear can flag a later-dated one as next — so slicing the front
115
+ of the list showed an earlier milestone while burying the actual next under `+N more`, which
116
+ is the one thing that row exists to say. The next milestone now leads, and identity is
117
+ matched on name plus target date rather than name alone (two milestones can share a name,
118
+ which put the `next` label on the wrong row). Source: `apps/cli/src/commands/projects.ts`.
119
+
120
+ - **`agents projects` stops reading the wrong GitHub repository.** Factory derives a
121
+ project's `owner/repo` from the checkout path's last two segments, so a repo cloned to
122
+ `~/src/github.com/<you>/agents-cli` whose origin is `phnx-labs/agents-cli` imported as
123
+ `<you>/agents-cli`. Both are real repositories, so nothing errored — the card's merged-PR
124
+ and release lines simply reported a stranger's repo (0 merges in 7 days instead of 100).
125
+ `import --from-factory` now reads the checkout's actual `origin` and only falls back to the
126
+ path guess when there is no remote to ask, and `status`/`show` print a warning with the fix
127
+ when a stored slug disagrees with the remote. Source: `apps/cli/src/lib/project-doctor.ts`.
128
+ - **`agents projects set <name>` changes one field without destroying the rest.** Previously
129
+ the only ways to correct a field were `$EDITOR` on raw YAML or `add --force`, which rebuilds
130
+ the definition from flags alone and silently drops `linear`, `contexts`, and `description`.
131
+ `set` loads, patches the named field, and writes back. Flags: `--repo`, `--root`, `--path`,
132
+ `--description`. Source: `apps/cli/src/commands/projects.ts`.
133
+ - **Merged-PR counts say when they are a lower bound.** The `gh` fetch caps at 100, and a busy
134
+ repo where all 100 land inside the window has more — the count now renders `100+` rather than
135
+ presenting the cap as a total, matching the existing Linear `2500+` contract. Source:
136
+ `apps/cli/src/lib/project-status.ts`.
137
+
138
+ - **`agents projects view <name>`** replaces `show` (kept as an alias) and now renders the
139
+ project's full plan: every declared Linear milestone with its date and progress, issue
140
+ counts, and a warning when no issues are assigned to any milestone — a milestone nothing is
141
+ filed against cannot report progress, and a row of silent `0%`s hid that. Sixteen other
142
+ command groups already use `view <name>`; `projects` was the only one that did not. Source:
143
+ `apps/cli/src/commands/projects.ts`.
144
+ - **The status headline counts live agents, not corpses.** It read `39 agents` on a project
145
+ where 19 had crashed. It now reads `19 live`, with a separate `dead` row breaking down what
146
+ finished or was lost — 19 crashed sessions is a thing to go fix, not throughput. `orphaned`
147
+ counts as **live**: `session/active.ts` defines it as "alive, but no client is attached", and
148
+ the repo's own dead rule is `closed` + `crashed` only. Source:
149
+ `apps/cli/src/lib/project-status.ts`.
150
+ - **`planPct` is gone from the card and from `--json`.** It summed each matched session's most
151
+ recent checklist snapshot, so one agent opening a fresh 40-item plan rendered the whole
152
+ project `0% plan`, and a project where nobody had written a checklist showed no figure at
153
+ all. A cross-session sum of ad-hoc checklists does not measure project progress. `live` and
154
+ `dead` counts replace it in `--json`.
155
+ - **The next milestone comes from Linear's own `status: "next"`** when Linear sets it, falling
156
+ back to earliest-dated-unfinished only when nothing is flagged — Linear's answer is the one
157
+ shown in its UI, ours is a guess.
158
+
159
+ - **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`.
160
+
161
+ - **`agents view` columns stay aligned across agents, and usage no longer piles up (view-ui-perf).**
162
+ The multi-agent overview padded every row to the widest usage string — an
163
+ Antigravity account with four model quotas forced ~194-column lines that
164
+ wrapped so `rate-limited` and last-active drifted under the version column.
165
+ Overview now caps compact meters to two windows (`+N` for the rest), always
166
+ emits fixed account/usage/status/lastActive columns (empty cells space-padded),
167
+ and measures padding with `stringWidth` so chalk + block bars don't skew
168
+ gutters. Usage fetches go through one unified core: 5-minute fresh cache
169
+ (was 2), concurrency-capped live reads (`USAGE_FETCH_CONCURRENCY=3`),
170
+ single-flight per identity, and a background SWR queue capped at 2 so delayed
171
+ HTTP responses cannot stack. Spinner stays up through account+usage load.
172
+ Source: `apps/cli/src/commands/view.ts`, `apps/cli/src/lib/usage.ts`,
173
+ `apps/cli/src/lib/agents.ts`.
174
+
175
+ ## 1.21.1
176
+
177
+ - **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`.
178
+
179
+ - **Hook `timeout` in agents.yaml now accepts duration strings, not just bare seconds (#1555).**
180
+ A hook can be written `timeout: 5s` / `timeout: 2m` / `timeout: 1h30m` instead of only
181
+ `timeout: 30` — self-documenting at the call site. A bare number still means seconds, so
182
+ every existing manifest keeps working. `parseHookManifest` normalizes the value to a
183
+ seconds number once, so all harness serializers keep consuming a number; an unparseable
184
+ timeout is dropped with a warning rather than silently coerced. Source:
185
+ `apps/cli/src/lib/hooks.ts` (`normalizeHookTimeoutSeconds`, `parseHookManifest`),
186
+ `apps/cli/docs/hooks.md`.
187
+
188
+ - **Owner notifications route through the one channel seam.** The feed urgent-block
189
+ dispatch and the monitor `notify` action now send through the registered channel
190
+ provider (`lookupTransport` → `ChannelProvider.send`) instead of shelling out to
191
+ `openclaw` directly. The recipient comes from `notify.owner` in agents.yaml — the
192
+ hardcoded owner chat id is gone, so changing `notify.owner` is honoured by every
193
+ path. A bare `--notify` on a monitor now targets `notify.owner`; `--notify <channel>`
194
+ overrides the owner channel. The monitor path also gains the provider's missing-binary
195
+ guard (a clean error instead of a raw ENOENT). A channel name that resolves to no
196
+ registered provider (a typo in `notify.owner.channel`, or `--notify <channel>`) fails
197
+ that one send with a clean error — it does not exit the monitor daemon or abort the
198
+ `agents feed --dispatch` loop. Source: `apps/cli/src/lib/notify.ts`,
199
+ `apps/cli/src/lib/monitors/dispatch.ts`, `apps/cli/src/lib/channels/resolve.ts`.
200
+
3
201
  ## 1.21.0
4
202
 
5
203
  - **A clone of your own DotAgents repo no longer hijacks project-layer rule resolution (RUSH-2037).**
package/README.md CHANGED
@@ -341,7 +341,7 @@ agents feed --flat # one row per agent (legacy)
341
341
  agents feed --host mac-mini # scope the view to one or more hosts
342
342
  agents feed --local # skip the SSH fan-out
343
343
  agents feed --json # blocks stamped with their outcome key
344
- agents feed post "halfway done" # agent status post (auto session identity)
344
+ agents feed post --title "Halfway done" "CI green, watching merge" # title + body
345
345
  ```
346
346
 
347
347
  Top-level questions and waiting notifications publish one atomic open-block record per session, including the mailbox id, host, runtime, and every answer option. The default view collapses agents under the **outcome** they serve (Linear ticket, PR, worktree slug, or Unassigned) so a 1,100-agent fleet reads as dozens of deliverables. Answered, resumed, and stopped blocks clear automatically; Task subagents are excluded. The rendered reply command uses the same mailbox id with `agents message`, so the decision routes back to the agent that asked it.
@@ -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
@@ -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';
@@ -234,51 +234,52 @@ export function registerFeedCommand(program) {
234
234
  feed
235
235
  .command('post')
236
236
  .description('Post a status update to the fleet activity stream (for agents)')
237
- .argument('<text...>', 'What just happened one short human line')
237
+ .argument('<text...>', 'Body: what just happened (after --title)')
238
+ .requiredOption('--title <title>', 'Short subject, ~4-5 words (phone first line)')
238
239
  .option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
239
240
  .option('--attach <path-or-url...>', 'Attach an artifact (local file or URL); repeatable')
240
241
  .option('--level <level>', 'How loudly to broadcast: milestone (default) or important. Configured sinks with minLevel: important only fire on the latter.', 'milestone')
241
- .option('--blocked', 'You are STUCK and need the user. Opens an answerable block and always broadcasts at important do not also pass --level.')
242
+ .option('--blocked', 'You are STUCK and need the user. Opens an answerable block and always broadcasts at important - do not also pass --level.')
242
243
  .option('--option <label...>', 'With --blocked: an answer the user can pick; repeatable')
243
244
  .option('--default <answer>', 'With --blocked: a safe default policy may apply if nobody answers in time')
244
245
  .option('--json', 'Emit the written event as JSON')
245
246
  .addHelpText('after', `
246
247
  Examples:
247
- # Inside an agents-cli run (session identity is already in the env):
248
- agents feed post "CHANGELOG pushed; watching CI and mac-mini E2E"
249
- agents feed post "cover render ready" --attach ./out/cover.png
250
- agents feed post "ready for review" --json
248
+ # Title (subject) + body. Phone broadcasts put title first, body after a
249
+ # blank line, then a "Sent from agent/session on host" footer.
250
+ agents feed post --title "CHANGELOG pushed" "Watching CI and mac-mini E2E"
251
+ agents feed post --title "Cover ready" "render at ./out/cover.png" --attach ./out/cover.png
252
+ agents feed post --title "Ready for review" "PR opened, waiting on prix-cloud" --json
251
253
 
252
- # Worth interrupting someone over reaches sinks gated on minLevel: important:
253
- agents feed post "release blocked: npm token expired" --level important
254
+ # Worth interrupting someone over - reaches sinks gated on minLevel: important:
255
+ agents feed post --title "npm token expired" "Cannot publish the release" --level important
254
256
 
255
- # You are STUCK and cannot proceed. Opens an answerable block that stays in
256
- # 'agents feed' until someone resolves it, and always reaches the owner —
257
- # do NOT also pass --level:
258
- agents feed post "force-push denied by git-guard on PR #1749" --blocked
259
- agents feed post "publish to npm or wait for review?" --blocked --option publish --option wait
260
- agents feed post "delete the stale preview env?" --blocked --default "leave it"
257
+ # Stuck: opens a needs-you block and always broadcasts at important:
258
+ agents feed post --title "Force-push denied" "git-guard blocked PR #1749" --blocked
259
+ agents feed post --title "Publish or wait?" "npm publish now or after review" --blocked --option publish --option wait
260
+ agents feed post --title "Delete preview env?" "stale preview still running" --blocked --default "leave it"
261
261
 
262
262
  # Exhaust self-serve FIRST. A block is for what you genuinely cannot do:
263
263
  # a credential only the user holds, a decision only they can make, an
264
264
  # approval only they can give. Not "should I do the obvious next step?".
265
265
 
266
266
  # Outside a run, pass the session explicitly:
267
- agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
267
+ agents feed post --title "Manual note" "context for the next agent" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
268
268
 
269
- Identity (session, agent, host, runtime, pid, launchId) is stamped automatically.
270
- Domain facts (tickets, PRs) are not CLI flags the ticket is joined from the
271
- session index at post time, so a broadcast sink can comment on it without the
272
- agent having to remember it.
269
+ Identity (session, agent, host, runtime, pid, launchId) is stamped automatically
270
+ and rides the phone footer of feed.broadcast {message}. Domain facts (tickets,
271
+ PRs) are not CLI flags - the ticket is joined from the session index at post
272
+ time. No em-dashes in title/body - they are scrubbed on the way out.
273
273
 
274
- Configure where a post is mirrored under feed.broadcast in agents.yaml see
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.
281
281
  const flags = {
282
+ title: opts?.title ?? cmd?.opts?.()?.title,
282
283
  session: opts?.session ?? cmd?.opts?.()?.session,
283
284
  attach: opts?.attach ?? cmd?.opts?.()?.attach,
284
285
  level: opts?.level ?? cmd?.opts?.()?.level,
@@ -298,8 +299,13 @@ docs/06-observability.md.
298
299
  if (!flags.blocked && (flags.option?.length || flags.default)) {
299
300
  throw new Error('--option/--default only apply with --blocked.');
300
301
  }
302
+ if (!flags.title?.trim()) {
303
+ throw new Error('Missing --title. Usage: agents feed post --title "Short subject" "body text"');
304
+ }
301
305
  const level = flags.blocked ? 'important' : parseFeedPostLevel(flags.level);
306
+ const meta = readMeta();
302
307
  const { event } = postFeedStatus({
308
+ title: flags.title,
303
309
  text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
304
310
  sessionId: flags.session,
305
311
  attach: flags.attach,
@@ -312,15 +318,23 @@ docs/06-observability.md.
312
318
  let outcomes;
313
319
  if (flags.blocked) {
314
320
  const block = buildDeclaredBlock(event, {
315
- text: event.detail ?? '',
321
+ // Prefer title as the front-loaded ask on the phone; body is detail.
322
+ text: event.title
323
+ ? (event.detail ? `${event.title}: ${event.detail}` : event.title)
324
+ : (event.detail ?? ''),
316
325
  options: flags.option,
317
326
  safeDefault: flags.default,
318
327
  });
319
328
  publishBlock(block);
320
- outcomes = broadcastBlock(block, { project: event.project, agent: event.agent });
329
+ outcomes = await broadcastBlock(block, {
330
+ project: event.project,
331
+ agent: event.agent,
332
+ title: event.title,
333
+ body: event.detail,
334
+ }, meta);
321
335
  }
322
336
  else {
323
- outcomes = broadcastPostedEvent(event, level);
337
+ outcomes = await broadcastPostedEvent(event, level, meta);
324
338
  }
325
339
  // Fail loud when a block reached nobody. This is computed BEFORE the
326
340
  // --json early return: a machine caller is exactly the one that reads the
@@ -549,18 +563,25 @@ docs/06-observability.md.
549
563
  }
550
564
  /**
551
565
  * Mirror a written post to the configured sinks (`feed.broadcast` in
552
- * agents.yaml). The ticket is JOINED from the session index rather than asked
553
- * for as a flag it is a domain fact about the session, and an agent that has
554
- * to remember a `--ticket` argument is an agent that will forget it. Returns the
555
- * per-sink outcomes; an empty array means nothing is configured, which is the
556
- * 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`.
557
577
  */
558
- function broadcastPostedEvent(event, level) {
559
- const config = readMeta().feed?.broadcast;
560
- 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)
561
581
  return [];
562
582
  const ticket = getSessionById(event.sessionId)?.ticketId;
563
583
  const planned = planFeedBroadcast(config, {
584
+ title: event.title,
564
585
  text: event.detail ?? '',
565
586
  level,
566
587
  ticket,
@@ -572,22 +593,24 @@ function broadcastPostedEvent(event, level) {
572
593
  .map((a) => a.href)
573
594
  .filter((href) => /^https?:\/\//i.test(href)),
574
595
  });
575
- return runFeedBroadcast(planned);
596
+ return runFeedBroadcast(planned, meta);
576
597
  }
577
598
  /**
578
- * 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).
579
602
  *
580
603
  * Blocks previously never broadcast at all: `broadcastPostedEvent` ran only for
581
604
  * `feed post`, while every `publishBlock` call wrote to the ledger and stopped
582
605
  * there — so a "needs you" record was durable and invisible at the same time.
583
606
  */
584
- function broadcastBlock(block, extras) {
585
- const config = readMeta().feed?.broadcast;
586
- 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)
587
610
  return [];
588
611
  const ticket = getSessionById(block.sessionId)?.ticketId;
589
612
  const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
590
- return runFeedBroadcast(planFeedBroadcast(config, ctx));
613
+ return runFeedBroadcast(planFeedBroadcast(config, ctx), meta);
591
614
  }
592
615
  /** One line per sink that ran. Silent when nothing is configured. */
593
616
  function reportBroadcast(outcomes) {
@@ -675,7 +698,7 @@ function renderUpdatesView(updates) {
675
698
  }));
676
699
  console.log();
677
700
  if (updates.length === 0) {
678
- console.log(chalk.gray(' No progress updates yet. Agents post them with `agents feed post "…"`.'));
701
+ console.log(chalk.gray(' No progress updates yet. Agents post them with `agents feed post --title "…" "…"`.'));
679
702
  return;
680
703
  }
681
704
  for (const ev of updates) {
@@ -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
  }