@phnx-labs/agents-cli 1.20.89 → 1.20.91

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 (114) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/README.md +6 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +7 -1
  5. package/dist/commands/feed.js +77 -4
  6. package/dist/commands/harness.d.ts +27 -0
  7. package/dist/commands/harness.js +120 -13
  8. package/dist/commands/hooks.js +22 -6
  9. package/dist/commands/perf.d.ts +14 -0
  10. package/dist/commands/perf.js +221 -0
  11. package/dist/commands/profiles.d.ts +3 -0
  12. package/dist/commands/profiles.js +1 -1
  13. package/dist/commands/routines.d.ts +19 -0
  14. package/dist/commands/routines.js +58 -30
  15. package/dist/commands/secrets.d.ts +52 -4
  16. package/dist/commands/secrets.js +234 -37
  17. package/dist/commands/send.d.ts +5 -1
  18. package/dist/commands/send.js +1 -1
  19. package/dist/commands/sessions-browser.d.ts +4 -0
  20. package/dist/commands/sessions-browser.js +51 -9
  21. package/dist/commands/sessions-favorite.d.ts +20 -0
  22. package/dist/commands/sessions-favorite.js +120 -0
  23. package/dist/commands/sessions-picker.js +70 -1
  24. package/dist/commands/sessions.d.ts +103 -20
  25. package/dist/commands/sessions.js +356 -62
  26. package/dist/commands/setup-secrets.d.ts +7 -0
  27. package/dist/commands/setup-secrets.js +12 -9
  28. package/dist/commands/versions.js +12 -4
  29. package/dist/commands/view.d.ts +14 -1
  30. package/dist/commands/view.js +103 -128
  31. package/dist/index.js +18 -3
  32. package/dist/lib/activity.d.ts +11 -1
  33. package/dist/lib/activity.js +1 -0
  34. package/dist/lib/agents.d.ts +4 -2
  35. package/dist/lib/agents.js +21 -6
  36. package/dist/lib/catchup.d.ts +105 -0
  37. package/dist/lib/catchup.js +160 -0
  38. package/dist/lib/channels/providers/desktop.d.ts +49 -0
  39. package/dist/lib/channels/providers/desktop.js +132 -0
  40. package/dist/lib/channels/providers/index.js +2 -0
  41. package/dist/lib/daemon.js +74 -13
  42. package/dist/lib/events.d.ts +12 -0
  43. package/dist/lib/events.js +122 -9
  44. package/dist/lib/exec.js +10 -0
  45. package/dist/lib/feed-broadcast.d.ts +47 -0
  46. package/dist/lib/feed-broadcast.js +65 -1
  47. package/dist/lib/feed-post.d.ts +10 -0
  48. package/dist/lib/feed-post.js +1 -1
  49. package/dist/lib/feed.d.ts +47 -1
  50. package/dist/lib/feed.js +38 -0
  51. package/dist/lib/hooks/cache.d.ts +2 -0
  52. package/dist/lib/hooks/cache.js +24 -4
  53. package/dist/lib/hosts/dispatch.js +19 -1
  54. package/dist/lib/hq/floor.js +12 -0
  55. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  56. package/dist/lib/overdue.d.ts +14 -0
  57. package/dist/lib/overdue.js +37 -1
  58. package/dist/lib/perf/db.d.ts +25 -0
  59. package/dist/lib/perf/db.js +290 -0
  60. package/dist/lib/perf/spool.d.ts +18 -0
  61. package/dist/lib/perf/spool.js +79 -0
  62. package/dist/lib/perf/types.d.ts +45 -0
  63. package/dist/lib/perf/types.js +2 -0
  64. package/dist/lib/picker.d.ts +27 -2
  65. package/dist/lib/picker.js +71 -7
  66. package/dist/lib/profiles.d.ts +48 -0
  67. package/dist/lib/profiles.js +67 -0
  68. package/dist/lib/rotate.d.ts +24 -2
  69. package/dist/lib/rotate.js +63 -6
  70. package/dist/lib/routines-project.js +6 -0
  71. package/dist/lib/routines.d.ts +30 -1
  72. package/dist/lib/routines.js +11 -0
  73. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  74. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  75. package/dist/lib/secrets/list-filter.d.ts +94 -0
  76. package/dist/lib/secrets/list-filter.js +245 -0
  77. package/dist/lib/session/active.d.ts +109 -3
  78. package/dist/lib/session/active.js +269 -13
  79. package/dist/lib/session/db.d.ts +14 -0
  80. package/dist/lib/session/db.js +35 -0
  81. package/dist/lib/session/digest.d.ts +7 -0
  82. package/dist/lib/session/digest.js +29 -1
  83. package/dist/lib/session/discover.d.ts +1 -2
  84. package/dist/lib/session/discover.js +7 -24
  85. package/dist/lib/session/favorites.d.ts +39 -0
  86. package/dist/lib/session/favorites.js +101 -0
  87. package/dist/lib/session/highlights.d.ts +82 -0
  88. package/dist/lib/session/highlights.js +251 -0
  89. package/dist/lib/session/host-link.d.ts +68 -0
  90. package/dist/lib/session/host-link.js +64 -0
  91. package/dist/lib/session/parse.js +23 -1
  92. package/dist/lib/session/presence.d.ts +85 -0
  93. package/dist/lib/session/presence.js +150 -0
  94. package/dist/lib/session/relative-time.d.ts +14 -0
  95. package/dist/lib/session/relative-time.js +36 -0
  96. package/dist/lib/session/remote-list.d.ts +10 -0
  97. package/dist/lib/session/remote-list.js +47 -9
  98. package/dist/lib/session/render.d.ts +7 -0
  99. package/dist/lib/session/render.js +87 -17
  100. package/dist/lib/session/types.d.ts +4 -1
  101. package/dist/lib/startup/command-registry.d.ts +1 -0
  102. package/dist/lib/startup/command-registry.js +2 -0
  103. package/dist/lib/state.d.ts +9 -0
  104. package/dist/lib/state.js +11 -0
  105. package/dist/lib/tmux/binary.d.ts +7 -0
  106. package/dist/lib/tmux/binary.js +11 -1
  107. package/dist/lib/types.d.ts +4 -3
  108. package/dist/lib/usage-backoff.d.ts +29 -0
  109. package/dist/lib/usage-backoff.js +165 -0
  110. package/dist/lib/usage.d.ts +112 -5
  111. package/dist/lib/usage.js +464 -46
  112. package/dist/lib/watchdog/runner.d.ts +13 -0
  113. package/dist/lib/watchdog/runner.js +16 -1
  114. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,366 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.91
4
+
5
+ - **An agent can now say it is stuck: `agents feed post --blocked` (RUSH-2110).** The
6
+ feed carried benign progress but had no way to signal "I cannot proceed", so agents
7
+ hand-rolled it into the status text (`NEEDS MUQSIT: …`) and it reached nobody. A
8
+ blocked post writes `status.blocked` to the shared activity stream *and* opens an
9
+ answerable block in the ledger, so the ask stays open until someone resolves it
10
+ instead of scrolling away. It is a flag on the existing verb, not a new command —
11
+ one thing for an agent to learn, and one stream where most posts are benign and
12
+ some need a human. Blocked is a state, not a volume: it always broadcasts at
13
+ `important`, so passing `--level` too is a usage error rather than a silent
14
+ override. Pair it with `--option` for an answerable choice or `--default` for a
15
+ safe fallback policy may apply. Source: `apps/cli/src/commands/feed.ts`,
16
+ `apps/cli/src/lib/feed.ts`.
17
+ - **Feed blocks are actually delivered.** `publishBlock` wrote every "needs you"
18
+ record to the ledger and stopped there — `broadcastPostedEvent` ran only for
19
+ `feed post`, so a block was durable and invisible at the same time. Blocks now
20
+ reach the configured `feed.broadcast` sinks, carrying the ask and the literal
21
+ `agents focus <id>` command that unblocks it, and a block that reaches nobody
22
+ exits non-zero instead of looking like a success. Source:
23
+ `apps/cli/src/lib/feed-broadcast.ts`.
24
+ - **New `desktop` channel provider.** `agents send --channel desktop` (and
25
+ `notify.owner.channel: desktop`) posts a native notification through the branded
26
+ menu-bar helper. It is the only channel with no external dependency — no network,
27
+ no login, no vendor CLI — so it still reaches you at your Mac when a messaging
28
+ gateway is down. It reports real deliverability rather than always succeeding:
29
+ on Linux it probes for `notify-send` instead of trusting the platform name.
30
+ Source: `apps/cli/src/lib/channels/providers/desktop.ts`.
31
+
32
+ - **`agents perf` — disposable SQLite latency warehouse.** Indexed p50/p99
33
+ rollups for hooks, CLI commands, and `agent.run` timings without scanning the
34
+ audit JSONL. Warehouse lives at `~/.agents/.cache/perf/perf.db` (safe to wipe);
35
+ identity columns reuse sessions/events string shapes (`session_id`, `agent`,
36
+ `machine`, …) for soft cross-reference — no foreign keys. Hook shims spool
37
+ into the same DB; `agents hooks profile` reads it first. Source:
38
+ `apps/cli/src/lib/perf/db.ts`, `apps/cli/src/commands/perf.ts`.
39
+
40
+ - **A routine that misses its fire now runs late instead of being silently lost.** Fires
41
+ are in-process croner timers, and croner only ever schedules forward from "now" — so a
42
+ daemon that was down, asleep, or wedged when a routine came due dropped that fire
43
+ outright, and `loadAll()` rebuilt every timer looking only at the future. Detection
44
+ existed but ran **once, at daemon startup**, and only logged a warning plus a
45
+ notification; catching up was a manual `agents routines catchup`. Observed cost: zion's
46
+ daemon was down from 02:03Z to 08:23Z while the laptop slept, `weekly-fleet-retro` was
47
+ armed for exactly 04:00Z, never ran, and the restart logged `2 routine(s) overdue` and
48
+ did nothing. The daemon now re-scans every 5 minutes as well as at startup and runs each
49
+ missed routine via the same detached path `catchup` already used. Source:
50
+ `apps/cli/src/lib/catchup.ts`, `apps/cli/src/lib/daemon.ts`.
51
+ - **New `catchup:` routine field, and `agents routines add --no-catchup`.** Defaults to
52
+ true — a routine you scheduled is one you expect to have run. Set `catchup: false` for a
53
+ routine whose worth expires with its slot (a 9am brief is useless at 3pm); the miss is
54
+ still recorded, it just is not re-run. `agents routines list --json` reports the
55
+ effective value as `catchup`.
56
+ - **New `missed` run status.** A missed fire previously left no trace anywhere — no run
57
+ record, no log line in the routine's history — so `agents routines list` kept showing the
58
+ previous run's `completed` as though it were current, sometimes for weeks. A miss is now
59
+ written as a real run stamped at the moment the fire was due, so `agents routines runs
60
+ <name>` shows the gap, and the listing renders it distinctly from `failed` (a miss is an
61
+ infrastructure problem, not a task failure). That record is also what makes catch-up
62
+ idempotent: it advances the overdue comparison, so the same missed fire is never
63
+ reconsidered across ticks or a daemon restart storm, and its directory is created with a
64
+ non-recursive `mkdir` — an atomic claim, so if the daemon's timer and a manual
65
+ `agents routines catchup` overlap, only one of them runs the routine. Source: `apps/cli/src/lib/routines.ts` (`RunMeta`),
66
+ `apps/cli/src/commands/routines.ts`.
67
+ - **A routine is never caught up for a fire that predates it.** `detectOverdueJobs` walks back
68
+ a week for the most recent expected occurrence, and a routine with no runs is overdue by
69
+ definition — so before this, `agents routines add` on any daily or weekly schedule whose slot
70
+ had already passed made the routine instantly "overdue". That was cosmetic while catch-up was
71
+ a manual command; with the daemon now catching up automatically it would have run every newly
72
+ created routine once, within five minutes of creating it. Routines gain a `createdAt` stamp
73
+ (written once, like `actor`), and overdue detection floors the expected fire at it — falling
74
+ back to the routine file's mtime for routines written before the field existed. Observed on
75
+ the live fleet: `agents-cli-updates`, created Aug 1 and never run, was flagged overdue for a
76
+ Jul 27 fire. Source: `apps/cli/src/lib/overdue.ts` (`routineEffectiveStart`),
77
+ `apps/cli/src/lib/routines.ts` (`writeJob`).
78
+
79
+ - **`agents secrets list` can be filtered.** It had no filtering at all —
80
+ `--host` picks a machine and `--json` picks a format, but nothing selected over
81
+ the bundles themselves, so "which of these read with no Touch ID?", "which
82
+ still store a raw value inline?", "what have I not touched in three months?"
83
+ meant piping the table through `grep` or went unanswered. There is now an axis
84
+ per question: a `[query]` positional over name and description, `--policy`,
85
+ `--backend`, `--type`, `--kind`, `--held`/`--not-held`, `--expired`,
86
+ `--expiring [days]`, `--unused <duration>`, plus `--sort` and `-n/--limit`.
87
+ Every axis narrows independently, so they compose. Following the `agents
88
+ sessions` house style, an unknown value is a loud error naming the valid set
89
+ rather than an empty list, filters apply before `--json` so the payload is the
90
+ exact twin of the table, and they are forwarded over `--host` so a remote list
91
+ narrows the same way. `--held`/`--not-held` read live broker state and so
92
+ refuse to run off macOS instead of reporting every bundle as unheld. An empty
93
+ result names the filters that emptied it and the total it started from. Source:
94
+ `apps/cli/src/lib/secrets/list-filter.ts`, `apps/cli/src/commands/secrets.ts`.
95
+
96
+ - **The EXPIRING column no longer hides keys that have already expired.**
97
+ `countExpiringSoon` counted only keys due in the next 30 days — the guard is
98
+ `d >= 0` — so a bundle whose token died last month rendered `-`, identical to
99
+ one with no expiry at all. The only places a lapsed key surfaced were
100
+ `agents secrets view` and a hard abort at inject time, i.e. after it had already
101
+ broken a run. The column now counts lapsed and upcoming together and turns red
102
+ once anything has lapsed, and `secrets list --json` gains an `expired` count
103
+ alongside the existing `expiringSoon`. Source: `apps/cli/src/commands/secrets.ts`.
104
+
105
+ - **`agents secrets list` now states the hold window instead of the bare word
106
+ `hold`.** The `hold` tier is a duration — prompt once, then stay silent for
107
+ `secrets.agent.holdMs` (7 days by default) — but the POLICY column printed only
108
+ the tier name, so a reader could not tell it meant a window, let alone which
109
+ one; finding out meant running `agents secrets status`. The column now reads
110
+ `hold 7d`, and `hold 7d · held 6d` while the broker is actually caching the
111
+ bundle. It follows the configured window, so a 24-hour hold reads `hold 1d`.
112
+ `always` and `never` are unchanged — neither has a window, and annotating one
113
+ would repeat the mistake the `daily` rename fixed. Two adjacent bugs go with
114
+ it: `agents secrets view` printed "7d by default" as a string literal and so
115
+ misstated the window for anyone who had configured `holdMs`, and a stale broker
116
+ entry past its expiry rendered as `hold · held expired` because the column
117
+ tested the entry for presence rather than liveness. `secrets list --json` and
118
+ `secrets view --json` gain an additive `holdMs` field (null on `always`/`never`)
119
+ so a machine caller gets the window too. Source:
120
+ `apps/cli/src/commands/secrets.ts`.
121
+
122
+ - **Richer session previews: skills, hooks, links, artifacts, repos, todo status.** The `agents sessions` quick preview and full summary now show the skills a session invoked (with counts), the hooks that fired (Claude transcripts, with repeat counts and failures), a clickable Links section (Linear/Jira/GitHub/GitLab URLs harvested from the conversation), the documents the session produced (`.agents/artifacts|plans|reports` and other `*.md`/`*.html` creations), the repos it worked in (via `.git` walk-up), and an error tally in the picker. The full summary's Plan section now marks checklist items `[x]`/`[>]`/`[ ]` and renders the checklist alongside the ExitPlanMode text instead of hiding it. Changes/Dirs lines collapse `.agents/worktrees/<slug>` prefixes to `⧉ <slug>/…`, are width-capped, and no longer list shell junk (`2>&1`, `$VAR` paths), `node_modules`, or agents-cli internal archives. Source: `apps/cli/src/lib/session/highlights.ts`, `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/lib/session/render.ts`, `apps/cli/src/commands/sessions-picker.ts`.
123
+
124
+ ## 1.20.90
125
+
126
+ - **`agents sessions --active` now shows one row per agent, not one per directory.**
127
+ A live tmux agent pane whose durable identity records were missing (the common case
128
+ once meta/pid-registry entries age out) was dropped, then re-surfaced by the ps-scan
129
+ under the newest transcript in its cwd — so many distinct sessions collapsed onto one
130
+ stranger's id with an inflated `×N` badge, and `agents sessions focus <id>` could not
131
+ find them. The scanner now recovers the session id straight from the `ag-<agent>-<shortid>`
132
+ tmux pane name (resolved to the full UUID via the short-id index in one batched query),
133
+ and refuses to borrow a co-located sibling's transcript when no id is known — so every
134
+ live session surfaces as its own row and is focus-able again. Also adds a `runTmux`
135
+ timeout so a wedged tmux server can't hang the scan. Source: `apps/cli/src/lib/session/active.ts`.
136
+
137
+ - **`agents view` now shows live usage bars for Antigravity.** The `agy` account
138
+ row renders one bar per model quota bucket (`3.1P: ███░░ 42% (1d)` style),
139
+ sourced from the same Google Code Assist `:retrieveUserQuota` endpoint `agy`
140
+ itself talks to. Auth reuses the stored `agy` OAuth credential (macOS Keychain
141
+ item `gemini`/`antigravity`, Linux Secret Service, or the
142
+ `~/.gemini/antigravity-cli/antigravity-oauth-token` file fallback), refreshing
143
+ the access token in memory when expired — safe from a read path because
144
+ Google's refresh tokens are non-rotating, and never written back to the
145
+ keychain. Each per-model bucket also flows into the throttle badge, run
146
+ rotation eligibility, and `agents view --json` (whose usage windows now carry
147
+ a `label` so same-keyed per-model bars are distinguishable). Source:
148
+ `apps/cli/src/lib/usage.ts`, `apps/cli/src/lib/agents.ts`,
149
+ `apps/cli/src/commands/view.ts`.
150
+
151
+ - **A custom harness is now its own agent type in `agents view`.** A harness created
152
+ with `agents harness add` (or `agents profiles add`) used to render as an indented
153
+ `profile` row under whichever host CLI executes it. It now gets its own block beside
154
+ Claude and Codex — a bold name header, then one row carrying the pinned model, the
155
+ account/auth state, and `via <host> <version>` naming the native harness underneath.
156
+ That matches how it is already launched: `agents run <name>` treats a custom harness
157
+ exactly like a native agent id. A harness whose host CLI has no install is flagged
158
+ `(host <id> not installed)` rather than listed as runnable, and the separate
159
+ "Profile-only Agents" section is gone — those harnesses now render in the main list
160
+ like every other one. Source: `apps/cli/src/commands/view.ts`.
161
+ - **`agents view <harness>` describes a custom harness** — host, model, provider, auth,
162
+ fork lineage, YAML path — instead of failing with "unknown agent";
163
+ `agents view <harness> --json` emits its summary. Source:
164
+ `apps/cli/src/commands/harness.ts` (`renderHarnessDetail`).
165
+ - **New `agents harness fork <source> <name>`.** One verb over both starting points:
166
+ fork a native harness (`agents harness fork opencode deepseek --model
167
+ deepseek/deepseek-v4-flash-0731 --auth-provider openrouter`) or copy a custom one you
168
+ already tuned and change only what you name (`agents harness fork deepseek
169
+ deepseek-chat --model deepseek/deepseek-chat-v3`). Forking a custom harness is a full
170
+ copy — env, endpoint, auth binding, `fallback_model`, host version pin — so the two
171
+ diverge and deleting the source never affects the fork; forking a native harness
172
+ requires `--model` because there is no model to inherit. Flags: `--model`,
173
+ `--base-url`, `--auth-provider`, `--version`, `--label`, `--description`,
174
+ `--key-stdin`, `--force`. Source: `apps/cli/src/lib/profiles.ts` (`forkProfile`).
175
+ - **Profile YAML gains optional `label:` and `forkedFrom:`.** `label` sets the name
176
+ `agents view` prints for the harness (defaults to the file name); `forkedFrom` records
177
+ the parent as display-only lineage. Existing profiles keep working untouched. Source:
178
+ `apps/cli/src/lib/profiles.ts`.
179
+ - **Breaking (`--json`):** in `agents view <agent> --json`, the per-agent `profiles` key
180
+ is now `harnesses`, and each entry carries new `label`, `hostVersion`, `description`,
181
+ and `forkedFrom` fields alongside the existing ones. Source:
182
+ `apps/cli/src/commands/view.ts` (`ViewJsonAgent`).
183
+
184
+ - **Menu bar ACTIVE: project accordion + session detail submenu.** Projects are
185
+ collapsed by default as a status strip (`▶ agents-cli ●8 ◐1 zion`); click
186
+ `▶`/`▼` to fold agents open under the project (idle-row caps removed — collapse
187
+ is the wall protection). Focusing an agent opens a side submenu with linkable
188
+ detail (work title URL, cwd, Linear ticket, GitHub PR, duration, copy session
189
+ id) from the warm `sessions --active` cache. Accordion reopen rebuilds from
190
+ cache only (no teams walk / no CLI schedule). Local/remote uses the same host
191
+ normalize as CLI `machineId()` so local rows are not mislabeled remote. Source:
192
+ `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`,
193
+ `LocalState.swift`, `Models.swift`.
194
+
195
+ - **An offloaded editor tab no longer displays another session's id.** A Factory
196
+ tab launched with `agents run --host <device>` has no local agent process, but
197
+ the extension still resolved its "live" session id by reading the SessionStart
198
+ hook's `~/.agents/.cache/state/sessions/<pid>.json` for the local pid tree —
199
+ the pid of the ssh client. Those files are keyed by pid alone and are only
200
+ pruned when the pid is dead, so once the OS recycled a pid the tab adopted
201
+ whatever session had last held it: one remote tab showed the id and version of
202
+ an unrelated synthetic run from 20 days earlier while `/status` inside it
203
+ reported the truth. An offloaded tab now takes its identity from the device
204
+ instead of local disk, and a local tab rejects any state record whose
205
+ SessionStart timestamp predates the tab itself.
206
+ - **`AGENT_TERMINAL_ID` now rides the SSH hop.** `agents run --host` forwarded
207
+ actor provenance but not the launching tab's terminal id, so the remote pid
208
+ registry recorded no terminal — leaving `agents sessions --active --host
209
+ <device>` unable to answer "which session is this tab running?" once the agent
210
+ moved on (a `/clear`, or an exit and rerun in the same tab).
211
+ - **`agents sessions --active --json` now carries `terminalId`.** The pid registry
212
+ has always recorded it; the emitted row dropped it, so no consumer could join a
213
+ live session back to the editor tab that launched it.
214
+
215
+ - **Balanced routing no longer launches into an account it only *thinks* has
216
+ headroom.** Account usage is cached per machine under stale-while-revalidate:
217
+ a snapshot up to 24h old was served instantly, and the background refresh that
218
+ should have corrected it lands after the pick is already made. On a box whose
219
+ refresh is failing that state is permanent — measured on `yosemite-s1`, every
220
+ Claude snapshot sat 26 hours to 2.7 days old, so balanced read
221
+ `muqsit@getrush.ai` as 48% used and launched into it while the account was at
222
+ its weekly cap; the session answered "You've hit your weekly limit" on its
223
+ first turn. Routing now caps how stale a snapshot may be when it is about to
224
+ decide (5 minutes), blocking on one bounded, parallel live read past that — and
225
+ no read at all inside the existing 2-minute fresh window, which back-to-back
226
+ launches hit. Display paths (`agents view`) keep the full 24h window and stay
227
+ off the network.
228
+ - **A pick made on unconfirmed data says so.** When no account on the machine
229
+ could be refreshed, routing still launches — a broken refresh must not make a
230
+ box unusable — but the banner now reads `… (2 of 5 healthy, usage unverified —
231
+ no account could be refreshed)` instead of presenting a guess as a fact. An
232
+ account with a verified snapshot always wins over one with a stale snapshot,
233
+ even when the stale number looks emptier. This applies to `--strategy
234
+ available` as well as `balanced` — both route on the same cache, and
235
+ `available`'s headroom sort was inverted by a stale number in exactly the same
236
+ way. An explicit version preference is an instruction, not a ranking signal, so
237
+ it still wins.
238
+ - **The mid-run failover chain is unchanged.** Declining to *pick* an account on
239
+ unconfirmed data and declining to *fail over to* it after the primary already
240
+ hit a 429 are different risks — by then the alternative is not launching at
241
+ all. Every eligible account stays in the failover chain; only the initial pick
242
+ prefers verified ones.
243
+
244
+ - **`agents routines list` no longer reports another device's routine as failed.** Run
245
+ records are written into the runs dir of whichever machine fired the routine and carry
246
+ no device attribution, but the listing resolved Last Status from any local record and
247
+ rendered it even on rows for routines pinned elsewhere. A routine re-pinned to another
248
+ device therefore kept reporting the old machine's leftover records forever — on zion,
249
+ `security-sweep`, `review-open-prs` and `hetzner-lease-gc` all read `failed` from late
250
+ July while `yosemite-s0`/`s1`, the devices that actually fire them, had completed them
251
+ that morning. The macOS menu bar reads this JSON, so it painted a column of red `exit 1`
252
+ rows for routines that were green. Last Status is now scoped to the device that owns the
253
+ run: a routine this device does not fire shows `-`, and `--json` returns `null` for
254
+ `lastStatus`, `exitCode`, `failureReason`, `lastRunStartedAt` and `lastRunCompletedAt`
255
+ (`runsHere: false` already says why). A routine pinned to several devices renders one row
256
+ per device but carries a status only on its **This machine** row. Read a peer's status
257
+ with `agents routines list --device <name>`; the local history is untouched and still
258
+ readable via `agents routines runs <name>`. Source: `apps/cli/src/commands/routines.ts`
259
+ (`localLatestRun`, `groupRoutineJobsByDevice`), `apps/cli/docs/03-routines.md`.
260
+
261
+ - **`agents watchdog` now tracks per-session presence (RUSH-2007 Layer C).** Each
262
+ tick reconciles a per-session presence record — `{location, device, transport,
263
+ lastSeen, status}` at `~/.agents/.cache/state/watchdog/presence.json` — from the
264
+ tick's active scan, deriving `connected` / `disconnected` by diffing consecutive
265
+ ticks. A session that was tracked but is now absent (its SSH link dropped or the
266
+ peer went unreachable) flips to `disconnected`, and the flip is surfaced in
267
+ `agents watchdog --json` under `presence.transitions` — an interactive drop as a
268
+ `reconnect-nudge` candidate, a headless remote as `keep-alive`. Folded into the
269
+ existing tick (no revived daemon, no extra SSH fan-out); additive and does not
270
+ change the tick's nudge decisions. Source:
271
+ `apps/cli/src/lib/session/presence.ts`, `apps/cli/src/lib/watchdog/runner.ts`.
272
+
273
+ - **`agents setup secrets --policy hold` no longer fails, and `agents secrets
274
+ status` stops naming the retired `daily` policy.** The 1.20.79 `daily` → `hold`
275
+ rename swept the help, docs, and the `secrets list` POLICY column, but two
276
+ surfaces were never migrated. The worse one was functional: the onboarding
277
+ wizard carried its own copy of the policy vocabulary, so
278
+ `agents setup secrets --policy hold` — the canonical name every other secrets
279
+ command prints — exited with `Invalid --policy 'hold'. Use daily, always, or
280
+ never.`, and its interactive prompt still offered `daily` as the default
281
+ choice. It now shares `parsePolicyOpt` with `agents secrets policy`, so the two
282
+ commands can't disagree about what a policy is called; `daily`/`session` stay
283
+ accepted as aliases and the wizard's default is unchanged (the hold tier). The
284
+ second was cosmetic: `agents secrets status` printed "a daily bundle prompts
285
+ once…" and "the next read of each daily bundle…" — the one command a user runs
286
+ to answer *why did it prompt again*, naming a policy its sibling commands no
287
+ longer emit. Both lines now say `hold` and are pure values pinned by tests, so
288
+ the vocabulary can't drift again. Source:
289
+ `apps/cli/src/commands/setup-secrets.ts`, `apps/cli/src/commands/secrets.ts`.
290
+
291
+ - **Favorite sessions from the browser.** `*` stars the highlighted session in
292
+ `agents sessions` and `f` filters the list to the starred ones; outside a TTY,
293
+ `agents sessions favorite <id>` (`--remove` / `--list` / `--json`) and
294
+ `agents sessions --favorites` do the same. Stars live in
295
+ `~/.agents/.history/favorites.json` keyed by session id, so they survive a reindex
296
+ of the session cache. They are per-machine — session sync carries transcripts, not
297
+ this file. Source:
298
+ `apps/cli/src/lib/session/favorites.ts`, `apps/cli/src/commands/sessions-favorite.ts`.
299
+ - **Detect sessions that lost their host — two new statuses, `crashed` and `orphaned`.**
300
+ A session whose editor window or connection went down hard used to just VANISH from
301
+ `agents sessions --active` (its dead-pid registry entry was filtered out), and one
302
+ still running in tmux with nobody attached reported a plain `idle`. Both now say so:
303
+ `✗ crashed` when the host window stopped republishing and the agent died with it,
304
+ `◍ orphan` when the agent is alive with zero clients attached. Derived from tmux's
305
+ `#{session_attached}` and the IDE window's registry heartbeat — never from a
306
+ deliberate `agents sessions detach`, and never over a session that is still working.
307
+ Source: `apps/cli/src/lib/session/host-link.ts`, `apps/cli/src/lib/session/active.ts`.
308
+ - **`agents sessions --active --favorites` now actually filters.** The flag was wired
309
+ into the interactive browser only, so every path that skips it — `--json`,
310
+ `--waiting`, a pipe, a multi-host scope, an SSH-fanout peer — silently returned the
311
+ whole fleet. Source: `apps/cli/src/commands/sessions.ts`.
312
+ - **`agents sessions --active --waiting` no longer counts a dead session.** `activity`
313
+ is not rewritten when a session dies, so one that crashed mid-question reported "needs
314
+ your input" forever — what it needs is a relaunch. Source:
315
+ `apps/cli/src/commands/sessions.ts`.
316
+
317
+ - **Resolve historical sessions safely across the fleet (#1757).** `agents sessions --resolve <full-id|prefix|keywords> --json` uses a versioned safe peer protocol, returns only resolver metadata, reports every full-ID candidate on ambiguity, treats synced copies as one match, and exits 2 without deciding when a peer fails, returns malformed output, or runs an older CLI. Source: `apps/cli/src/commands/sessions.ts`.
318
+
319
+ - **A rate-limited usage endpoint is now backed off instead of hammered.** The
320
+ daemon warms auth-health every 3 minutes and probes *every installed version
321
+ home* in one parallel batch, so a machine with five Claude accounts sent five
322
+ concurrent requests to `api.anthropic.com/api/oauth/usage` every three minutes
323
+ — roughly 100/hour — before the usage refresh added its own. Nothing read
324
+ `Retry-After`. Measured on `yosemite-s1`: the endpoint answered
325
+ `429 rate_limit_error` with `retry-after: 2678` (about 45 minutes) for every
326
+ account while the credentials themselves read healthy, and the next tick fired
327
+ three minutes later, deep inside the penalty window, re-arming it. The box
328
+ never recovered, every usage read failed, and its cache froze — the
329
+ permanently-stale state balanced routing was already having to defend against.
330
+ - **A 429 now records its deadline and every read honours it.** Usage fetches and
331
+ health probes for that provider short-circuit until the window passes — no
332
+ request, no renewed penalty — and report
333
+ `Claude rate-limited this machine — not retrying for 45 minutes.` The state is
334
+ on disk, because the callers are separate processes: the long-lived daemon and
335
+ every one-shot `agents view` / `agents run` — one empty file per penalty under
336
+ `~/.agents/.cache/usage-backoff/`, named `<agent>.<deadline>`, so two
337
+ processes recording the same provider at once cannot displace each other and a
338
+ read takes the furthest deadline. A server delay is capped at an hour, and a
339
+ missing or unparseable `Retry-After` still backs off.
340
+
341
+ - **A usage read that fails now says so, instead of returning a silent null.**
342
+ Four branches in every networked usage fetch — Claude, Kimi, Droid and
343
+ Cursor — returned `{ snapshot: null, error: null }`: no readable credential, a
344
+ locally-expired one, a rejected request, and a request that threw (timeout,
345
+ DNS/TLS, an unparseable payload). The caller could not tell any of them apart
346
+ from a healthy read, so it fell
347
+ back to whatever the stale-while-revalidate cache held and drew those bars as
348
+ fact. Measured on `yosemite-s1`: every Claude account's stored access token had
349
+ expired (one of them eleven days earlier), so no read could succeed, and
350
+ `agents view claude --refresh` printed a full, healthy-looking table twice
351
+ while writing nothing to the cache. A usage read never refreshes a token
352
+ (RUSH-1822), so an expired credential does not heal on its own — the account
353
+ stays unreadable until that agent actually runs. A rate-limited endpoint (429)
354
+ now reads differently from a rejected credential (401), because re-authing
355
+ fixes one and not the other.
356
+ - **`agents view` marks bars the live read could not confirm.** A row whose
357
+ snapshot came from the cache after a failed live read renders the reading plus
358
+ `unverified`, rather than looking identical to a confirmed one. The number
359
+ still shows — it is the last thing we saw — but it no longer reads as current.
360
+ - **`agents view --refresh` reports what it could not refresh.** It now lists
361
+ each account it failed to reach and why, instead of rendering a table that
362
+ looks fully refreshed regardless.
363
+
3
364
  ## 1.20.89
4
365
 
5
366
  - **Webhook handler layer for one-off agent/workflow/command/routine triggers.**
package/README.md CHANGED
@@ -265,6 +265,8 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
265
265
  |---|---|---|
266
266
  | `s` | search text | `--query` / positional |
267
267
  | `r` | running only | `--active` |
268
+ | `f` | favorites only | `--favorites` |
269
+ | `*` | star / unstar the highlighted session | `agents sessions favorite <id>` |
268
270
  | `c` | team sessions | `--teams` |
269
271
  | `a` | agent (cycles) | `-a` |
270
272
  | `d` | device (cycles) | `--device` |
@@ -274,6 +276,10 @@ On a terminal, `agents sessions --active` (and a bare `agents sessions`) open th
274
276
  | `⏎` | resume / attach | `resume` / `focus` |
275
277
  | `y` | copy the equivalent command | `--print-cmd` |
276
278
 
279
+ **Star the sessions you keep coming back to.** `*` marks the highlighted row (a `★` shows in the listing), `f` narrows to the starred ones, and `agents sessions favorite <id>` / `--favorites` do the same outside a TTY. Stars live in `~/.agents/.history/favorites.json` keyed by session id, so they survive a reindex of the session cache. They're per-machine — session sync carries transcripts, not this file.
280
+
281
+ **A session that lost its host says so.** When an editor window or an SSH connection goes down hard, the agent it owned used to simply disappear from `--active`; when an agent outlived its window in tmux, it reported a plain `idle`. Both now carry their own status: `✗ crashed` (the host went down and took the agent with it) and `◍ orphan` (still alive, but no client is attached — nothing is showing it). Read from tmux's attached-client count and the editor window's registry heartbeat, so a deliberate `agents sessions detach` is never mistaken for one, and a session that is still *working* headlessly is left alone.
282
+
277
283
  Filters **stack** (they AND together), the active set shows in the header, and the highlighted row **previews below by default** (`tab` hides it) — prompt, activity, last response, plus a links line where the worked-on ticket and the PR the session opened are **clickable** (OSC 8 hyperlinks: the ticket jumps to Linear, the `PR#` to GitHub, in terminals that support them). The Linear workspace is resolved from `LINEAR_WORKSPACE` or the linear-cli config, so tickets stay plain text when it's unknown. Because every hotkey has a flag, the view you build by hand is a real command: press `y` (or run `--print-cmd`) to get the exact `ag sessions …` line — explore interactively, hand the line to an agent. Piped output, `--json`, or `--no-interactive` keep the plain listing for scripts. Peek without opening the pager with `agents sessions <id> --preview`.
278
284
 
279
285
  | before — preview hidden | after — preview open + clickable links |
package/dist/bin/agents CHANGED
Binary file
@@ -59,7 +59,13 @@ function formatRotationBanner(result, verb = 'balanced') {
59
59
  const { picked, healthy, excluded } = result;
60
60
  const label = picked.email ? `${picked.email} · ${picked.agent}@${picked.version}` : `${picked.agent}@${picked.version}`;
61
61
  const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
62
- return `[agents] ${verb} picked ${label} (${ratio})`;
62
+ // Say it when the pick was a guess. A machine whose usage refresh is failing
63
+ // reports old percentages with total confidence, so a silent banner reads
64
+ // identical whether the router knew the account had headroom or merely hoped
65
+ // so — and the operator only finds out when the agent answers "you've hit
66
+ // your weekly limit".
67
+ const caveat = result.usageUnverified ? ', usage unverified — no account could be refreshed' : '';
68
+ return `[agents] ${verb} picked ${label} (${ratio}${caveat})`;
63
69
  }
64
70
  /**
65
71
  * Whether `cwd` is inside a git work tree.
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
- import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
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, } from '../lib/feed-broadcast.js';
5
+ import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, 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';
@@ -238,6 +238,9 @@ export function registerFeedCommand(program) {
238
238
  .option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
239
239
  .option('--attach <path-or-url...>', 'Attach an artifact (local file or URL); repeatable')
240
240
  .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('--option <label...>', 'With --blocked: an answer the user can pick; repeatable')
243
+ .option('--default <answer>', 'With --blocked: a safe default policy may apply if nobody answers in time')
241
244
  .option('--json', 'Emit the written event as JSON')
242
245
  .addHelpText('after', `
243
246
  Examples:
@@ -249,6 +252,17 @@ Examples:
249
252
  # Worth interrupting someone over — reaches sinks gated on minLevel: important:
250
253
  agents feed post "release blocked: npm token expired" --level important
251
254
 
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"
261
+
262
+ # Exhaust self-serve FIRST. A block is for what you genuinely cannot do:
263
+ # a credential only the user holds, a decision only they can make, an
264
+ # approval only they can give. Not "should I do the obvious next step?".
265
+
252
266
  # Outside a run, pass the session explicitly:
253
267
  agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
254
268
 
@@ -268,22 +282,66 @@ docs/06-observability.md.
268
282
  session: opts?.session ?? cmd?.opts?.()?.session,
269
283
  attach: opts?.attach ?? cmd?.opts?.()?.attach,
270
284
  level: opts?.level ?? cmd?.opts?.()?.level,
285
+ blocked: Boolean(opts?.blocked ?? cmd?.opts?.()?.blocked),
286
+ option: opts?.option ?? cmd?.opts?.()?.option,
287
+ default: opts?.default ?? cmd?.opts?.()?.default,
271
288
  json: Boolean(opts?.json ?? cmd?.opts?.()?.json ?? cmd?.parent?.opts?.()?.json),
272
289
  };
273
290
  try {
274
- const level = parseFeedPostLevel(flags.level);
291
+ // Blocked is a state, not a volume: it always broadcasts at `important`,
292
+ // so an agent has exactly one thing to say. Passing both is a usage
293
+ // error rather than a silent override -- an agent that thinks it chose
294
+ // the level should not be quietly ignored.
295
+ if (flags.blocked && flags.level && flags.level !== 'milestone') {
296
+ throw new Error('--blocked already broadcasts at important; drop --level.');
297
+ }
298
+ if (!flags.blocked && (flags.option?.length || flags.default)) {
299
+ throw new Error('--option/--default only apply with --blocked.');
300
+ }
301
+ const level = flags.blocked ? 'important' : parseFeedPostLevel(flags.level);
275
302
  const { event } = postFeedStatus({
276
303
  text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
277
304
  sessionId: flags.session,
278
305
  attach: flags.attach,
306
+ blocked: flags.blocked,
279
307
  });
280
- const outcomes = broadcastPostedEvent(event, level);
308
+ // A blocked post lands in BOTH stores: the event in the shared activity
309
+ // stream (what happened) and an OpenBlock in the ledger (what is still
310
+ // open). The ledger is what makes it answerable and clearable -- without
311
+ // it the ask would scroll away like any other update.
312
+ let outcomes;
313
+ if (flags.blocked) {
314
+ const block = buildDeclaredBlock(event, {
315
+ text: event.detail ?? '',
316
+ options: flags.option,
317
+ safeDefault: flags.default,
318
+ });
319
+ publishBlock(block);
320
+ outcomes = broadcastBlock(block, { project: event.project, agent: event.agent });
321
+ }
322
+ else {
323
+ outcomes = broadcastPostedEvent(event, level);
324
+ }
325
+ // Fail loud when a block reached nobody. This is computed BEFORE the
326
+ // --json early return: a machine caller is exactly the one that reads the
327
+ // exit code, so returning 0 there while a human gets 1 would make the
328
+ // undelivered block invisible to the caller most likely to act on it —
329
+ // reintroducing, behind a flag, the silent failure this exists to remove.
330
+ // One sink failing among several stays a warning: the channels are
331
+ // redundant by design.
332
+ const undelivered = blockDeliveryFailure(flags.blocked, outcomes);
333
+ if (undelivered)
334
+ process.exitCode = 1;
281
335
  if (flags.json) {
282
336
  console.log(JSON.stringify(outcomes.length ? { ...event, broadcast: outcomes } : event, null, 2));
337
+ if (undelivered)
338
+ console.error(chalk.red(undelivered));
283
339
  return;
284
340
  }
285
341
  console.log(formatProgressUpdate(event));
286
342
  reportBroadcast(outcomes);
343
+ if (undelivered)
344
+ console.error(chalk.red(undelivered));
287
345
  }
288
346
  catch (err) {
289
347
  console.error(chalk.red(err.message));
@@ -516,6 +574,21 @@ function broadcastPostedEvent(event, level) {
516
574
  });
517
575
  return runFeedBroadcast(planned);
518
576
  }
577
+ /**
578
+ * Mirror a declared block to the same sinks a post reaches.
579
+ *
580
+ * Blocks previously never broadcast at all: `broadcastPostedEvent` ran only for
581
+ * `feed post`, while every `publishBlock` call wrote to the ledger and stopped
582
+ * there — so a "needs you" record was durable and invisible at the same time.
583
+ */
584
+ function broadcastBlock(block, extras) {
585
+ const config = readMeta().feed?.broadcast;
586
+ if (!config || Object.keys(config).length === 0)
587
+ return [];
588
+ const ticket = getSessionById(block.sessionId)?.ticketId;
589
+ const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
590
+ return runFeedBroadcast(planFeedBroadcast(config, ctx));
591
+ }
519
592
  /** One line per sink that ran. Silent when nothing is configured. */
520
593
  function reportBroadcast(outcomes) {
521
594
  for (const o of outcomes) {
@@ -11,4 +11,31 @@
11
11
  * native harness registry. The `agents profiles` tree stays unchanged.
12
12
  */
13
13
  import type { Command } from 'commander';
14
+ import { type Profile } from '../lib/profiles.js';
15
+ /**
16
+ * Print one custom harness. Shared by `agents harness view <name>` and by
17
+ * `agents view <name>` — a custom harness resolves as an agent type there, so
18
+ * both entry points must describe it identically.
19
+ */
20
+ export declare function renderHarnessDetail(name: string): void;
21
+ /** Options accepted by `agents harness fork`. */
22
+ export interface ForkOptions {
23
+ model?: string;
24
+ baseUrl?: string;
25
+ authProvider?: string;
26
+ version?: string;
27
+ label?: string;
28
+ description?: string;
29
+ keyStdin?: boolean;
30
+ force?: boolean;
31
+ }
32
+ /**
33
+ * Build the new harness for `agents harness fork <source> <name>`.
34
+ *
35
+ * Two sources, one verb: an existing custom harness is copied and overridden;
36
+ * a native agent id is turned into a harness pinned to `--model` on that host.
37
+ * Forking a native harness therefore requires `--model` — there is nothing to
38
+ * copy a model from.
39
+ */
40
+ export declare function buildFork(source: string, name: string, opts: ForkOptions): Profile;
14
41
  export declare function registerHarnessCommands(program: Command): void;