@phnx-labs/agents-cli 1.20.93 → 1.21.1

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 (128) hide show
  1. package/CHANGELOG.md +290 -0
  2. package/README.md +4 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/beta.js +3 -5
  5. package/dist/commands/doctor.js +23 -0
  6. package/dist/commands/exec.js +13 -0
  7. package/dist/commands/feed.js +37 -23
  8. package/dist/commands/mailboxes.js +39 -1
  9. package/dist/commands/message.js +12 -1
  10. package/dist/commands/monitors.js +8 -6
  11. package/dist/commands/packages.js +14 -5
  12. package/dist/commands/projects.d.ts +28 -0
  13. package/dist/commands/projects.js +173 -47
  14. package/dist/commands/routines.js +27 -1
  15. package/dist/commands/rules.js +6 -0
  16. package/dist/commands/sessions.d.ts +17 -0
  17. package/dist/commands/sessions.js +16 -8
  18. package/dist/commands/setup-browser.js +5 -1
  19. package/dist/commands/setup-fleet.js +5 -0
  20. package/dist/commands/setup-preferences.d.ts +53 -0
  21. package/dist/commands/setup-preferences.js +142 -0
  22. package/dist/commands/setup.js +8 -9
  23. package/dist/commands/ssh.js +220 -10
  24. package/dist/commands/sync.d.ts +2 -2
  25. package/dist/commands/sync.js +4 -9
  26. package/dist/commands/watchdog.js +26 -3
  27. package/dist/index.js +22 -6
  28. package/dist/lib/activity.d.ts +13 -0
  29. package/dist/lib/activity.js +22 -2
  30. package/dist/lib/auto-dispatch.d.ts +6 -1
  31. package/dist/lib/auto-dispatch.js +7 -2
  32. package/dist/lib/beta.d.ts +1 -1
  33. package/dist/lib/beta.js +1 -1
  34. package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
  35. package/dist/lib/channels/resolve.d.ts +18 -1
  36. package/dist/lib/channels/resolve.js +15 -5
  37. package/dist/lib/daemon.d.ts +13 -0
  38. package/dist/lib/daemon.js +80 -60
  39. package/dist/lib/device-config.d.ts +82 -0
  40. package/dist/lib/device-config.js +296 -0
  41. package/dist/lib/event-provenance.d.ts +19 -0
  42. package/dist/lib/event-provenance.js +48 -0
  43. package/dist/lib/events.d.ts +2 -2
  44. package/dist/lib/events.js +4 -53
  45. package/dist/lib/feed-broadcast.d.ts +41 -5
  46. package/dist/lib/feed-broadcast.js +110 -12
  47. package/dist/lib/feed-post.d.ts +16 -2
  48. package/dist/lib/feed-post.js +35 -7
  49. package/dist/lib/feed.d.ts +1 -1
  50. package/dist/lib/feed.js +3 -1
  51. package/dist/lib/git.d.ts +13 -2
  52. package/dist/lib/git.js +38 -6
  53. package/dist/lib/hooks.d.ts +16 -1
  54. package/dist/lib/hooks.js +61 -1
  55. package/dist/lib/linear-project-counts.d.ts +72 -5
  56. package/dist/lib/linear-project-counts.js +90 -6
  57. package/dist/lib/linear-projects.d.ts +12 -0
  58. package/dist/lib/linear-projects.js +30 -0
  59. package/dist/lib/mailbox-gc.js +30 -7
  60. package/dist/lib/mailbox.d.ts +14 -1
  61. package/dist/lib/mailbox.js +35 -3
  62. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  63. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  64. package/dist/lib/menubar/install-menubar.d.ts +5 -21
  65. package/dist/lib/menubar/install-menubar.js +35 -44
  66. package/dist/lib/migrate.js +1 -1
  67. package/dist/lib/models.js +21 -11
  68. package/dist/lib/monitors/config.d.ts +1 -1
  69. package/dist/lib/monitors/dispatch.d.ts +6 -2
  70. package/dist/lib/monitors/dispatch.js +10 -15
  71. package/dist/lib/notify.d.ts +42 -6
  72. package/dist/lib/notify.js +41 -32
  73. package/dist/lib/overdue.d.ts +1 -1
  74. package/dist/lib/overdue.js +54 -19
  75. package/dist/lib/project-import.d.ts +96 -0
  76. package/dist/lib/project-import.js +171 -0
  77. package/dist/lib/remote-agents-json.d.ts +14 -1
  78. package/dist/lib/remote-agents-json.js +21 -3
  79. package/dist/lib/routines.d.ts +53 -0
  80. package/dist/lib/routines.js +105 -4
  81. package/dist/lib/rules/run-sync.d.ts +18 -0
  82. package/dist/lib/rules/run-sync.js +92 -0
  83. package/dist/lib/scheduler.js +12 -2
  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/index.d.ts +4 -5
  87. package/dist/lib/secrets/index.js +4 -5
  88. package/dist/lib/session/active.d.ts +16 -2
  89. package/dist/lib/session/active.js +8 -4
  90. package/dist/lib/session/bundle.js +5 -1
  91. package/dist/lib/session/remote-list.d.ts +1 -15
  92. package/dist/lib/session/remote-list.js +22 -111
  93. package/dist/lib/session/sync/config.d.ts +13 -5
  94. package/dist/lib/session/sync/config.js +21 -13
  95. package/dist/lib/startup/command-registry.d.ts +0 -2
  96. package/dist/lib/startup/command-registry.js +1 -5
  97. package/dist/lib/state.d.ts +7 -2
  98. package/dist/lib/state.js +85 -10
  99. package/dist/lib/sync-umbrella.d.ts +11 -23
  100. package/dist/lib/sync-umbrella.js +14 -45
  101. package/dist/lib/teams/agents.d.ts +35 -3
  102. package/dist/lib/teams/agents.js +52 -8
  103. package/dist/lib/teams/scheduler.d.ts +29 -2
  104. package/dist/lib/teams/scheduler.js +60 -16
  105. package/dist/lib/types.d.ts +23 -1
  106. package/package.json +1 -2
  107. package/dist/commands/drive.d.ts +0 -10
  108. package/dist/commands/drive.js +0 -183
  109. package/dist/commands/hq.d.ts +0 -2
  110. package/dist/commands/hq.js +0 -58
  111. package/dist/commands/sessions-sync.d.ts +0 -17
  112. package/dist/commands/sessions-sync.js +0 -135
  113. package/dist/commands/sync-provision.d.ts +0 -23
  114. package/dist/commands/sync-provision.js +0 -107
  115. package/dist/lib/drive-sync.d.ts +0 -45
  116. package/dist/lib/drive-sync.js +0 -238
  117. package/dist/lib/hq/floor.d.ts +0 -87
  118. package/dist/lib/hq/floor.js +0 -243
  119. package/dist/lib/session/sync/crdt.d.ts +0 -44
  120. package/dist/lib/session/sync/crdt.js +0 -119
  121. package/dist/lib/session/sync/manifest.d.ts +0 -62
  122. package/dist/lib/session/sync/manifest.js +0 -100
  123. package/dist/lib/session/sync/provision.d.ts +0 -49
  124. package/dist/lib/session/sync/provision.js +0 -91
  125. package/dist/lib/session/sync/r2.d.ts +0 -32
  126. package/dist/lib/session/sync/r2.js +0 -121
  127. package/dist/lib/session/sync/sync.d.ts +0 -106
  128. package/dist/lib/session/sync/sync.js +0 -374
package/CHANGELOG.md CHANGED
@@ -1,5 +1,295 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.1
4
+
5
+ - **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`.
6
+
7
+ - **Hook `timeout` in agents.yaml now accepts duration strings, not just bare seconds (#1555).**
8
+ A hook can be written `timeout: 5s` / `timeout: 2m` / `timeout: 1h30m` instead of only
9
+ `timeout: 30` — self-documenting at the call site. A bare number still means seconds, so
10
+ every existing manifest keeps working. `parseHookManifest` normalizes the value to a
11
+ seconds number once, so all harness serializers keep consuming a number; an unparseable
12
+ timeout is dropped with a warning rather than silently coerced. Source:
13
+ `apps/cli/src/lib/hooks.ts` (`normalizeHookTimeoutSeconds`, `parseHookManifest`),
14
+ `apps/cli/docs/hooks.md`.
15
+
16
+ - **Owner notifications route through the one channel seam.** The feed urgent-block
17
+ dispatch and the monitor `notify` action now send through the registered channel
18
+ provider (`lookupTransport` → `ChannelProvider.send`) instead of shelling out to
19
+ `openclaw` directly. The recipient comes from `notify.owner` in agents.yaml — the
20
+ hardcoded owner chat id is gone, so changing `notify.owner` is honoured by every
21
+ path. A bare `--notify` on a monitor now targets `notify.owner`; `--notify <channel>`
22
+ overrides the owner channel. The monitor path also gains the provider's missing-binary
23
+ guard (a clean error instead of a raw ENOENT). A channel name that resolves to no
24
+ registered provider (a typo in `notify.owner.channel`, or `--notify <channel>`) fails
25
+ that one send with a clean error — it does not exit the monitor daemon or abort the
26
+ `agents feed --dispatch` loop. Source: `apps/cli/src/lib/notify.ts`,
27
+ `apps/cli/src/lib/monitors/dispatch.ts`, `apps/cli/src/lib/channels/resolve.ts`.
28
+
29
+ ## 1.21.0
30
+
31
+ - **A clone of your own DotAgents repo no longer hijacks project-layer rule resolution (RUSH-2037).**
32
+ Cloning `~/.agents` to the canonical `~/src/github.com/<you>/.agents` path (to edit
33
+ rules in an editor) made that checkout eligible as a *project* layer whenever you
34
+ worked from its parent directory. Because project outranks user, a stale clone's
35
+ `rules/subrules/*` then silently shadowed the live user rules by filename, and the
36
+ compile planted an out-of-date `AGENTS.md` in an ancestor dir that every session
37
+ beneath it ingested. Project-layer discovery now identifies a DotAgents repo by
38
+ **repo identity** (git origin), not path: a `.agents/` that is itself a git checkout
39
+ whose origin matches the user's or system's DotAgents repo is skipped, so the live
40
+ user layer wins. Legitimate project `.agents/` layers (a plain subdirectory of a
41
+ project, or a git repo with an unrelated origin) are unaffected.
42
+ Source: `apps/cli/src/lib/state.ts`.
43
+
44
+ - **`agents sessions --local` no longer dials remote-host teammates over ssh,
45
+ in the default listing or `--active` (RUSH-2118).** `--local` is supposed to
46
+ mean this-machine-only, but the underlying `AgentManager` poll still fired a
47
+ real ssh round-trip for every teammate dispatched via
48
+ `agents teams add --device` — even a teammate that had already finished.
49
+ On a box with 30 completed remote-host teammates that measured out to 180
50
+ real `ssh` execve calls (6 per teammate: two ssh calls in
51
+ `syncRemoteMirror`, run three times per poll) and a ~4.3s
52
+ `--active --local` call. A `--local` query now reads a remote-host
53
+ teammate's last-persisted `meta.json` state instead of dialing it, and a
54
+ teammate that has already reached a terminal status (completed/failed/
55
+ stopped) is never re-dialed by ANY `--active` query, local or not — its
56
+ final log bytes and exit code were already captured on the poll that
57
+ resolved it. The same gate now covers every `--local` surface: the bare
58
+ default listing's live-glyph enrichment (`maybeLiveIndex`) and `--preview`
59
+ (`renderSessionPreview`, freely combinable with `--local`) both called the
60
+ local-only `getActiveSessions()` with no `localOnly` threaded through,
61
+ despite the `--local` help text already promising this-machine-only for
62
+ all of them. Source: `apps/cli/src/lib/teams/agents.ts`
63
+ (`syncRemoteMirror`, `readNewEvents`, `updateStatusFromProcess`,
64
+ `AgentManager`), `apps/cli/src/lib/session/active.ts` (`listTeamsActive`,
65
+ `getActiveSessions`), `apps/cli/src/commands/sessions.ts`
66
+ (`gatherActiveSessions`, `maybeLiveIndex`, `renderSessionPreview`).
67
+
68
+ - **A rules preset now applies at `agents run` time, not only after `agents
69
+ rules switch` (RUSH-2128).** `setActiveRulesPreset` used to take effect only on the next
70
+ explicit `agents rules switch`/`agents add`/`agents use` — a preset change
71
+ made any other way left the harness launching against a stale rules file
72
+ until someone remembered to re-sync. `agents run` now re-applies the active
73
+ preset for the resolved agent+version immediately before dispatch, every
74
+ time, with a skip-fast sentinel so an unchanged preset costs no recompose or
75
+ rewrite. Version-scoped only; per-model preset scoping is a follow-up.
76
+ Source: `apps/cli/src/lib/rules/run-sync.ts`, `apps/cli/src/commands/exec.ts`.
77
+
78
+ - **Activity events now carry the same actor and session lineage as operational events.**
79
+ The TypeScript activity writer and the embedded PostToolUse hook stamp actor kind,
80
+ launch id, and parent session id from the shared execution provenance floor, so
81
+ `agents events` no longer invents an agent name as the activity record's OS user.
82
+ Source: `apps/cli/src/lib/event-provenance.ts`, `apps/cli/src/lib/activity.ts`.
83
+
84
+ - **`agents view` no longer re-scans every installed Claude binary on each run.**
85
+ When a Claude model extractor produced zero models (a broken regex, or a
86
+ mid-install CLI), the result was never cached — so `getModelCatalog` re-ran a
87
+ full `readFileSync` scan of the 230-270MB Claude binary for every affected
88
+ installed version, on every invocation (~1.85s each). With 4 affected
89
+ versions installed, that was ~7.5s added to every `agents view`. A 0-model
90
+ extraction is now cached too, stamped with when it was attempted, and served
91
+ for 24 hours before self-healing by retrying; an upgrade/reinstall (a new
92
+ source mtime) still re-extracts immediately, as before. Measured on a real
93
+ install with 7 Claude versions (4 of them hitting the broken extractor): the
94
+ cold first-call cost (~12.5s, unavoidable) drops to ~1-2ms on every
95
+ subsequent call. Source: `apps/cli/src/lib/models.ts`.
96
+
97
+ - **Per-device and fleet-wide config keys now have a home: the `config:` block in
98
+ the two-tier agents.yaml store.** Three new subcommands under `agents devices`
99
+ (no new top-level noun): `agents devices set-interactive <name>` records the one
100
+ device agents show YOU artifacts on (browser opens, dashboards) as
101
+ `config.interactiveHost` in the central, synced agents.yaml — skills no longer
102
+ guess "the online macOS box", and the host is marked `★ interactive` in
103
+ `agents devices list`. `agents devices configure <name> --max-agents N
104
+ --scheduler on|off` and `agents devices note <name> "…"` (repeat
105
+ to append, `--clear` to empty) write device-scope keys under `config:` in
106
+ `~/.agents/devices/<name>/agents.yaml` — targetable for any device from any box
107
+ (the devices/ tree syncs; each machine reads only its own). The default browser
108
+ profile joins the same registry as `browser.profile`, routed to the existing
109
+ device-local `defaultBrowserProfile` field (no duplicate key, resolution order
110
+ unchanged). Unset keys always mean today's behavior; everything is scriptable
111
+ with `--json`, and `devices list --json` now carries each row's `config` and an
112
+ `interactive` flag. agents.yaml files the CLI writes now carry a
113
+ `yaml-language-server` hint pointing at the new
114
+ `apps/cli/schema/agents-yaml.schema.json`.
115
+
116
+ The keys are live inputs, not just stored values. `--scheduler off` stops the
117
+ routines scheduler from starting on that device — `routines add` skips the
118
+ auto-start with the reason, a manual `routines start` refuses, and the daemon
119
+ re-evaluates the gate on every SIGHUP reload (boot it again with
120
+ `agents devices configure <host> --scheduler on` + any reload, no daemon
121
+ restart). `--max-agents` feeds host ranking: Factory auto-launch excludes a
122
+ device at its cap (counting device-wide running agents) and names the cap when
123
+ a pool is exhausted; teams placement excludes it from the least-loaded
124
+ auto-pick (counting the team's own roster, local teammates included) and an
125
+ all-capped pool fails loud instead of over-filling a machine. Setup asks
126
+ instead of guessing: bare `agents setup` ends with a skippable preferences
127
+ step (which machine you sit at → interactive host; which browser agents drive
128
+ here → device default), `agents setup fleet` offers the interactive host after
129
+ a sync, and the `agents setup browser` picker highlights the auto-detect
130
+ winner. Source: `apps/cli/src/lib/device-config.ts`,
131
+ `apps/cli/src/lib/state.ts`, `apps/cli/src/lib/daemon.ts`,
132
+ `apps/cli/src/lib/teams/scheduler.ts`, `apps/cli/src/commands/ssh.ts`,
133
+ `apps/cli/src/commands/setup-preferences.ts`,
134
+ `apps/factory/src/core/launchHost.ts`,
135
+ `apps/cli/schema/agents-yaml.schema.json`.
136
+
137
+ - **Mailbox messages now expire and dead boxes are reaped automatically.** Messages
138
+ enqueued without an explicit TTL used to sit in the spool forever, so pending mail
139
+ would outlive the session that needed it. They now get a 24-hour default TTL
140
+ (`AGENTS_MAILBOX_TTL` overrides the default; `agents message … --ttl 2h` sets it
141
+ per message). When a message expires, a live-but-idle box archives it with a
142
+ `dropped: expired` receipt. The watchdog tick also runs a liveness sweep using the
143
+ same live-session set as `agents sessions --active`, archiving pending mail in dead
144
+ boxes as `dropped: dead` and pruning stale consumed entries. Dropped messages tied
145
+ to a feed block surface a failure receipt (`status: dropped` / `expired`) so the
146
+ sender sees the bounce instead of silence. Run the sweep manually with
147
+ `agents mailboxes gc` (`--json` supported). Source:
148
+ `apps/cli/src/lib/mailbox.ts`, `apps/cli/src/lib/mailbox-gc.ts`,
149
+ `apps/cli/src/commands/message.ts`, `apps/cli/src/commands/mailboxes.ts`,
150
+ `apps/cli/src/commands/watchdog.ts`, `apps/cli/src/lib/feed.ts`.
151
+
152
+ - **The menu-bar helper can no longer leak CLI processes until the machine is unusable.**
153
+ Its poll shelled `agents doctor --json` through an unbounded `Process` +
154
+ `readDataToEndOfFile()`. Two properties composed badly: the call had no deadline
155
+ (`doctor --json` measures **136s on an idle box**, against a 60s poll interval), and a
156
+ helper that died mid-call left the child reparented to launchd (PPID 1) with nothing
157
+ to reap it — along with the `node -e` version probes that child had forked. Both fire
158
+ together, because the helper crashes under exactly the conditions that make the CLI
159
+ slow: `NSApplication.shared` segfaults inside `SLSNewConnection` when WindowServer is
160
+ too starved to hand out a connection, launchd's `KeepAlive` restarts it, and the
161
+ restart spawns a new doctor while the old one keeps burning a core. Observed on a real
162
+ machine: 38 orphaned doctors + 92 orphaned probes, ~13 of 18 cores consumed, load
163
+ average 490, keystrokes visibly lagging.
164
+ The crash itself cannot be prevented from inside the app — it is AppKit dereferencing
165
+ a null connection before any of our code runs — so a crash no longer costs anything
166
+ permanent: every child carries a deadline (30s; 180s for `doctor --json`, above its
167
+ real measured cost); it is spawned as its own process-group leader so a timeout kills
168
+ the whole subtree rather than just the CLI; and each live child is recorded on disk so
169
+ the *next* launch reaps whatever a crash abandoned (no exit handler runs on SIGSEGV).
170
+ The doctor refresh also drops from every 60s to every 15 minutes, and the launchd job
171
+ gains `ThrottleInterval` 30 so a startup crash-loop cannot respawn every 10s.
172
+ A poll that blows its deadline now shows a stale menu instead of taking the machine
173
+ down with it. Source: `apps/cli/menubar/Sources/MenubarHelper/ChildProcess.swift`,
174
+ `AgentsCLI.swift`, `StatusItemController.swift`, `main.swift`,
175
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
176
+
177
+ - **The macOS menu-bar helper is now notarized, ending the "app is damaged"
178
+ dialog and the per-run `no valid code signature; skipping launch` spam
179
+ (RUSH-2134).** The helper shipped Developer-ID signed but *not* notarized, so
180
+ Gatekeeper on macOS 26+ rejected it as damaged and the install path tried to
181
+ heal it by re-signing ad-hoc on every `agents` invocation — which can never
182
+ satisfy Gatekeeper, so the dialog and the noise persisted. The release now
183
+ notarizes + staples the helper (`menubar/scripts/build.sh`, mandatory for any
184
+ Developer-ID build, run under the release's `agents secrets exec apple.com`
185
+ context), the `prepack` gate refuses to pack an un-notarized bundle
186
+ (`scripts/verify-menubar-helper.sh` now requires a stapled ticket), and the
187
+ runtime ad-hoc re-sign band-aid is deleted — a notarized + stapled bundle
188
+ survives npm's tarball round-trip untouched, so the helper launches with no
189
+ per-machine healing. The launch guards now verify Gatekeeper acceptance (not
190
+ just `codesign --verify`) and fail loud pointing at an upgrade rather than
191
+ re-signing over it. Source: `apps/cli/menubar/scripts/build.sh`,
192
+ `apps/cli/scripts/verify-menubar-helper.sh`, `apps/cli/scripts/release.sh`,
193
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
194
+
195
+ - **`agents projects import` gains Linear as a source, and gates the Factory guess.**
196
+ `import --from-linear` turns the workspace's Linear projects into definitions via the
197
+ `linear` CLI, binding a local checkout only on an exact name match so it never
198
+ silently points a project at the wrong repo. `--from-factory` now imports only
199
+ `high`-confidence rows by default (`--min-confidence low|medium|high`, `--all` to
200
+ take everything), and prints why each row was skipped — the auto-detected registry
201
+ used to absorb every stale clone it found. Source: `apps/cli/src/lib/project-import.ts`.
202
+ - **`agents projects list` columns line up again.** Widths are computed from the rows
203
+ being printed instead of a fixed 32-character path pad that every home-relative root
204
+ ran straight through. Source: `apps/cli/src/commands/projects.ts`.
205
+
206
+ - **`agents projects status` shows the next Linear milestone.** A new `next` line names
207
+ the project's earliest unfinished milestone with its progress and a human due date
208
+ (`Beta cut · 3/8 · due in 6 days`, `overdue by 3 days`, `due Aug 21`) — a percentage
209
+ says how far along a project is, the milestone says what it is due to hit next. The
210
+ milestone list comes from the project rather than from issue assignments, so a
211
+ milestone with nothing filed under it yet still shows; it rides along on the first
212
+ page of the existing issue fetch, costing no extra request. Source:
213
+ `apps/cli/src/lib/linear-project-counts.ts`.
214
+
215
+ - **`agents publish --branch <b>` now pushes the index to `<b>`, not just the printed URL (#1061).**
216
+ The flag rewrote the printed `raw.githubusercontent.com/.../<b>/skills-index.json`
217
+ URL, but the commit still landed on the checked-out branch — so `--branch dev` from a
218
+ `main` checkout published the index to `main` while advertising a `dev` URL that didn't
219
+ resolve. `commitAndPush` now takes an optional target branch and pushes
220
+ `<current>:<target>`, reporting back the branch the index actually landed on so the URL
221
+ references it. Omitting `--branch` still publishes to the repo's current branch.
222
+ Source: `apps/cli/src/lib/git.ts` (`commitAndPush`, `pushOrigin`, `getCurrentBranch`),
223
+ `apps/cli/src/commands/packages.ts`.
224
+
225
+ - **Remove the unused `agents hq` command.** `agents hq floor --json` was a
226
+ machine-readable bridge for an interactive Agents HQ floor UI that was never
227
+ built — `apps/factory` has zero references to it and it had no other consumer.
228
+ Typing `agents hq` now prints a clear removal notice and exits non-zero instead
229
+ of silently disappearing. Source: `apps/cli/src/index.ts`,
230
+ `apps/cli/src/lib/startup/command-registry.ts` (removed
231
+ `apps/cli/src/commands/hq.ts`, `apps/cli/src/lib/hq/`).
232
+
233
+ - **Removed `agents drive` and the R2/CRDT background session-sync beta.** Both
234
+ predate `agents sessions export`/`import`, which now cover the same ground
235
+ without a daemon: `agents drive` (rsync-based session/config mirroring) and
236
+ the opt-in `session-sync` beta (`agents sessions sync`, the daemon's ~90s R2
237
+ push/pull loop, `agents sync --sessions`) are gone. If you had `session-sync`
238
+ or `drive` enabled, re-enable is no longer possible — use `--host` for live
239
+ cross-machine reads or `agents sessions export --encrypt` /
240
+ `agents sessions import` for portable, encrypted transcript bundles instead.
241
+ The R2 network client and CRDT merge machinery are removed entirely with the
242
+ rest of the background sync. Export/import's own encrypted-bundle path
243
+ survives unchanged: it never talked to R2 over the network — it only reuses
244
+ the `r2.backups` bundle's shared `R2_SYNC_ENC_KEY` for local AES-256-GCM
245
+ encryption, falling back to a printed ephemeral key when that bundle isn't
246
+ configured. Source: `apps/cli/src/commands/drive.ts`,
247
+ `apps/cli/src/commands/sessions-sync.ts`, `apps/cli/src/lib/session/sync/crdt.ts`,
248
+ `apps/cli/src/lib/session/sync/sync.ts`, `apps/cli/src/lib/session/sync/r2.ts`,
249
+ `apps/cli/src/lib/daemon.ts`.
250
+
251
+ - **A routine that fires less often than weekly can now be caught up at all.** Overdue
252
+ detection walked a fixed one-week window for the most recent expected fire, so any cron whose
253
+ gap exceeds that returned nothing and the routine was skipped entirely — never flagged
254
+ overdue on any device, never caught up, no `missed` record, silently. Monthly, semi-monthly,
255
+ quarterly and annual routines were all in that class. Measured on a real schedule
256
+ (`0 9 1,13,25 * *`, 12-day gaps): on **10 of every 28 days** the routine could not be
257
+ evaluated. The lookback now widens (week → month → quarter → year) only when the narrower
258
+ window finds nothing, so a dense schedule never walks more than a week of occurrences.
259
+ - **Catch-up no longer resurrects a retired routine.** `detectOverdueJobs` never checked
260
+ `endAt`, and the scheduler only auto-disables lazily inside a live cron tick — so a routine
261
+ whose `endAt` elapsed while the daemon was down was still enabled on disk, rescheduled on
262
+ restart, and executed by the catch-up pass.
263
+ - **One-shot detection matches the scheduler's.** Overdue used the raw `runOnce` flag while the
264
+ scheduler uses `isOneShotRoutine`, so a one-shot-*like* schedule (a fixed minute/hour/day/
265
+ month) that never carried the flag could be replayed by catch-up.
266
+ - **The creation floor now covers built-in routines.** `routineEffectiveStart` resolved a
267
+ routine's file through a user-layer-only lookup, but `listJobs` reads the system layer too —
268
+ so a built-in shipped in the system repo had neither a `createdAt` stamp nor a resolvable
269
+ path, the floor was skipped, and it read as instantly overdue on first daemon start. Added
270
+ `resolveJobFilePath`, which resolves across every layer the loader reads.
271
+ - **A `createdAt` in the future is clamped to now.** Left unclamped (clock skew, a hand-edited
272
+ year) it sits after every possible expected fire, so the routine could never be flagged
273
+ overdue until wall-clock time caught up.
274
+
275
+ - **A routine now runs on exactly one device, instead of once per device listed.** `devices:`
276
+ was an allowlist where *every* listed device fired independently, so a routine pinned to two
277
+ boxes ran twice on every schedule — two full agent sessions doing identical work and burning
278
+ double the agent quota. On one live fleet seven routines were in that state: `security-sweep`
279
+ ran at 15:30:02 on one box and 15:30:03 on the other, both completing. Ownership is now a pure
280
+ function of the config (the first device in normalized sort order), so every daemon reaches the
281
+ same answer with no lease, no cross-device coordination, and no split brain when the fleet
282
+ partitions. Omitting `devices` still means fleet-wide, which is what `watchdog` and
283
+ `check-updates` want.
284
+ - **`agents routines add --devices a,b` and `devices --set a,b` are now rejected.** A routine
285
+ belongs to one machine; the error names the fix. Routines already on disk with a multi-device
286
+ pin keep running — on their owner only — rather than being dropped.
287
+ - **`agents doctor` lists any routine still carrying a multi-device pin**, with the devices it
288
+ names, the one that now fires, and the command to make it explicit. Also in `doctor --json` as
289
+ `ambiguousDevicePins`. The remediation deliberately offers the candidates rather than
290
+ prescribing the owner: the lowest-sorted name can be a registry alias that matches no live
291
+ machine, and cementing that would keep the routine dead.
292
+
3
293
  ## 1.20.93
4
294
 
5
295
  - **`agents send` is a real delivery envelope; `notify` is just `--to owner` (RUSH-2123).** Flag-first form: `--to`, `--text`, `--channel`, `--attach`, `--url`. `--to owner` expands from `notify.owner` in agents.yaml. Positional text still works. Help names the three planes (deliver / record / control) so send is not confused with `feed post`, `activity`, or `message`/`sessions inject`. Source: `apps/cli/src/commands/send.ts`, `apps/cli/src/lib/channels/send.ts`.
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.
@@ -476,6 +476,9 @@ agents devices list # fleet + headroom: load, mem, idle/busy
476
476
  agents devices list --live # force a live probe of every device (alias of --refresh)
477
477
  agents devices list --full # add per-device cores and free/total RAM
478
478
  agents devices list --no-stats # instant: names/addresses only, skip the probe
479
+ agents devices set-interactive zion # the device agents show YOU artifacts on (★ in the list)
480
+ agents devices configure mac-mini --max-agents 4 --scheduler off # per-device config (syncs via devices/<name>/agents.yaml)
481
+ agents devices note mac-mini "runs the releases — don't reboot" # operator notes, repeat to append
479
482
  agents ssh mac-mini # hardened SSH: fails fast if offline,
480
483
  # PowerShell on Windows, password-from-Keychain,
481
484
  # auto-syncs your terminfo (Ghostty/kitty/…) so
package/dist/bin/agents CHANGED
Binary file
@@ -1,9 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { ALL_BETA_FEATURES, getBetaConfigLocation, getEnabledBetaFeatures, setBetaEnabled, } from '../lib/beta.js';
3
3
  const BETA_DESCRIPTIONS = {
4
- drive: 'Google Drive integration for reading and writing files',
5
4
  factory: 'Cloud-based agent dispatch via Rush Factory',
6
- 'session-sync': 'Cross-machine session transcript sync via R2 (daemon push/pull)',
7
5
  projects: 'Named multi-repo projects with a progress rollup (agents projects)',
8
6
  };
9
7
  function parseFeatures(values) {
@@ -19,12 +17,12 @@ function parseFeatures(values) {
19
17
  export function registerBetaCommands(program) {
20
18
  const beta = program
21
19
  .command('beta')
22
- .description('Enable or disable preview features like drive and factory.')
20
+ .description('Enable or disable preview features like factory.')
23
21
  .addHelpText('after', `
24
22
  Examples:
25
23
  agents beta list
26
- agents beta enable drive factory
27
- agents beta disable drive
24
+ agents beta enable factory
25
+ agents beta disable factory
28
26
  `);
29
27
  beta
30
28
  .command('list')
@@ -13,6 +13,7 @@ import { resolveHost } from '../lib/hosts/registry.js';
13
13
  import { sshExecAsync } from '../lib/ssh-exec.js';
14
14
  import { sshTargetFor } from '../lib/hosts/types.js';
15
15
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
16
+ import { findAmbiguousDevicePins } from '../lib/routines.js';
16
17
  import chalk from 'chalk';
17
18
  import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
18
19
  import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, getAccountInfo } from '../lib/agents.js';
@@ -1298,6 +1299,10 @@ export function registerDoctorCommand(program) {
1298
1299
  const inventory = await collectLocalFleetInventory(cwd);
1299
1300
  const localName = machineId();
1300
1301
  const duplicateHooks = inspectDuplicateVersionHooks(cwd);
1302
+ // A routine belongs to one device. A multi-device pin used to fire it
1303
+ // once per listed device — duplicate agent runs, duplicate spend — so
1304
+ // surface any that are still on disk with the exact fix.
1305
+ const ambiguousPins = findAmbiguousDevicePins(cwd);
1301
1306
  // Legacy account-global sign-in map, kept for `--json` back-compat
1302
1307
  // (ssh.ts RemoteDoctorJson / menubar read `signIn`). File-based, no home.
1303
1308
  const signIn = {};
@@ -1361,6 +1366,9 @@ export function registerDoctorCommand(program) {
1361
1366
  // reading `sync`/`orphans`/`repos` are unaffected.
1362
1367
  health: computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, duplicateHooks),
1363
1368
  duplicateHooks,
1369
+ // Routines whose `devices` names more than one machine — each used to
1370
+ // fire once per device. `owner` is the one that fires now.
1371
+ ambiguousDevicePins: ambiguousPins,
1364
1372
  // Prioritized RUSH-2069 findings (critical/warning, per-version, with
1365
1373
  // remediation). Additive alongside the legacy fields above.
1366
1374
  findings,
@@ -1404,6 +1412,21 @@ export function registerDoctorCommand(program) {
1404
1412
  if (syncRows.some((r) => r.status !== 'fresh' || (r.unwiredHooks ?? 0) > 0) || repoBehindMarkers.some((m) => m.behind > 0)) {
1405
1413
  console.log(chalk.gray('\nRun `agents status` to review and sync what has drifted.'));
1406
1414
  }
1415
+ // A routine runs on exactly one device. Each of these named several and
1416
+ // used to fire once per device — duplicate agent runs on every schedule.
1417
+ if (ambiguousPins.length > 0) {
1418
+ console.log();
1419
+ console.log(chalk.yellow(`${ambiguousPins.length} routine(s) pin more than one device — a routine runs on exactly one:`));
1420
+ for (const pin of ambiguousPins) {
1421
+ console.log(` ${chalk.cyan(pin.name)} ${chalk.gray(`[${pin.devices.join(', ')}]`)} ` +
1422
+ `${chalk.gray('→ fires only on')} ${pin.owner}`);
1423
+ // Deliberately not prescribing `--set ${pin.owner}`: ownership is the
1424
+ // lowest-sorted name, which can be a registry alias that matches no
1425
+ // live machine (`worker` here), and cementing that keeps the routine
1426
+ // dead. Name the candidates and let the operator pick the real box.
1427
+ console.log(chalk.gray(` fix: agents routines devices ${pin.name} --set <${pin.devices.join('|')}>`));
1428
+ }
1429
+ }
1407
1430
  return;
1408
1431
  }
1409
1432
  const parsed = parseTargetArg(target);
@@ -23,6 +23,7 @@ import * as os from 'os';
23
23
  import { spawnSync } from 'child_process';
24
24
  import { randomUUID } from 'crypto';
25
25
  import { isSessionTrackedAgent } from '../lib/session/types.js';
26
+ import { applyActiveRulesPresetAtRun } from '../lib/rules/run-sync.js';
26
27
  /** Distinguish a terminal account-picker marker from an explicit @version pin. */
27
28
  export function parseRunAccountPickerRequest(agentSpec) {
28
29
  const requested = agentSpec.endsWith('@');
@@ -1978,6 +1979,18 @@ export function registerRunCommand(program) {
1978
1979
  }
1979
1980
  }
1980
1981
  const defaultVersion = version ?? resolveVersion(agent, cwd);
1982
+ // Re-apply the active rules preset before every launch (issue: preset
1983
+ // changes via `setActiveRulesPreset` only took effect after an explicit
1984
+ // `agents rules switch` / `agents sync`). Version-scoped, skip-fast when
1985
+ // nothing changed — see lib/rules/run-sync.ts. Placed here (immediately
1986
+ // after the resolved version is known, before ACP/loop/fallback branch
1987
+ // off) so every downstream dispatch path for this agent+version sees a
1988
+ // fresh rules file, not just the plain execAgent path. `defaultVersion`
1989
+ // is null when nothing is installed yet — execAgent handles that error
1990
+ // path itself; there's no version home to sync into.
1991
+ if (defaultVersion) {
1992
+ applyActiveRulesPresetAtRun(agent, defaultVersion, getVersionHomePath(agent, defaultVersion));
1993
+ }
1981
1994
  // Login preflight (advisory, warn + continue). On a local INTERACTIVE
1982
1995
  // launch, probe whether this agent's account has a credential and print a
1983
1996
  // one-line warning if it looks logged out — so you find out BEFORE the TUI
@@ -234,44 +234,44 @@ 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
277
  .action((textParts, opts, cmd) => {
@@ -279,6 +279,7 @@ docs/06-observability.md.
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,12 @@ 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);
302
306
  const { event } = postFeedStatus({
307
+ title: flags.title,
303
308
  text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
304
309
  sessionId: flags.session,
305
310
  attach: flags.attach,
@@ -312,12 +317,20 @@ docs/06-observability.md.
312
317
  let outcomes;
313
318
  if (flags.blocked) {
314
319
  const block = buildDeclaredBlock(event, {
315
- text: event.detail ?? '',
320
+ // Prefer title as the front-loaded ask on the phone; body is detail.
321
+ text: event.title
322
+ ? (event.detail ? `${event.title}: ${event.detail}` : event.title)
323
+ : (event.detail ?? ''),
316
324
  options: flags.option,
317
325
  safeDefault: flags.default,
318
326
  });
319
327
  publishBlock(block);
320
- outcomes = broadcastBlock(block, { project: event.project, agent: event.agent });
328
+ outcomes = broadcastBlock(block, {
329
+ project: event.project,
330
+ agent: event.agent,
331
+ title: event.title,
332
+ body: event.detail,
333
+ });
321
334
  }
322
335
  else {
323
336
  outcomes = broadcastPostedEvent(event, level);
@@ -561,6 +574,7 @@ function broadcastPostedEvent(event, level) {
561
574
  return [];
562
575
  const ticket = getSessionById(event.sessionId)?.ticketId;
563
576
  const planned = planFeedBroadcast(config, {
577
+ title: event.title,
564
578
  text: event.detail ?? '',
565
579
  level,
566
580
  ticket,
@@ -675,7 +689,7 @@ function renderUpdatesView(updates) {
675
689
  }));
676
690
  console.log();
677
691
  if (updates.length === 0) {
678
- console.log(chalk.gray(' No progress updates yet. Agents post them with `agents feed post "…"`.'));
692
+ console.log(chalk.gray(' No progress updates yet. Agents post them with `agents feed post --title "…" "…"`.'));
679
693
  return;
680
694
  }
681
695
  for (const ev of updates) {
@@ -2,7 +2,9 @@ import * as os from 'os';
2
2
  import * as path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { die, humanDuration, relTime, truncate, visibleWidth } from '../lib/format.js';
5
+ import { setHelpSections } from '../lib/help.js';
5
6
  import { listBoxes, readBox, mailboxDir, isValidMailboxId, watchMessages, } from '../lib/mailbox.js';
7
+ import { gcMailbox } from '../lib/mailbox-gc.js';
6
8
  import { GLYPH, masthead, sparkline, aggregate, hourlyCounts, graphEdges, } from '../lib/comms-render.js';
7
9
  import { getMailboxRootDir } from '../lib/state.js';
8
10
  import { getActiveSessions } from '../lib/session/active.js';
@@ -262,6 +264,21 @@ function renderBetween(boxes, a, b, json) {
262
264
  console.log(` ${truncate(m.text.replace(/\s+/g, ' ').trim(), 100)}`);
263
265
  }
264
266
  }
267
+ async function runGcCommand(opts) {
268
+ const sessions = await getActiveSessions();
269
+ const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
270
+ const result = gcMailbox(activeBoxIds);
271
+ if (opts.json) {
272
+ console.log(JSON.stringify(result, null, 2));
273
+ return;
274
+ }
275
+ if (result.messagesDroppedDead === 0 && result.messagesDroppedExpired === 0) {
276
+ console.log(chalk.dim(`gc: ${result.boxesScanned} boxes scanned, no pending messages dropped.`));
277
+ return;
278
+ }
279
+ console.log(chalk.yellow(`gc: ${result.messagesDroppedDead} dead-box messages, ${result.messagesDroppedExpired} expired messages dropped ` +
280
+ `(${result.boxesScanned} boxes scanned, ${result.deadBoxes} dead boxes).`));
281
+ }
265
282
  function renderGraph(boxes, filters, json) {
266
283
  const nonEmpty = boxes.filter((b) => b.messages.length > 0);
267
284
  const msgs = aggregate(nonEmpty).filter((m) => matchesFilters(m, m.toLabel, m.box, filters));
@@ -313,7 +330,7 @@ function newestMessage(box) {
313
330
  return box.messages.reduce((a, b) => (a.ts >= b.ts ? a : b));
314
331
  }
315
332
  export function registerMailboxesCommand(program) {
316
- program
333
+ const cmd = program
317
334
  .command('mailboxes')
318
335
  .alias('mailbox')
319
336
  .argument('[id]', 'A mailbox id (session UUID / teams agentId) to inspect in full')
@@ -387,4 +404,25 @@ export function registerMailboxesCommand(program) {
387
404
  }
388
405
  renderOverview(boxes, limit, filters);
389
406
  });
407
+ const gcCmd = cmd
408
+ .command('gc')
409
+ .description('Run a liveness sweep: archive pending messages in dead boxes and prune stale consumed mail.')
410
+ .option('--json', 'Emit the GC result as JSON')
411
+ .action(async (_opts, command) => runGcCommand({ json: command.optsWithGlobals().json === true }));
412
+ setHelpSections(gcCmd, {
413
+ examples: `
414
+ # One-shot sweep using the live session set as the liveness source
415
+ agents mailboxes gc
416
+
417
+ # Machine-readable summary (for scripts / monitors)
418
+ agents mailboxes gc --json
419
+ `,
420
+ notes: `
421
+ A box is considered dead when no live session (the same source
422
+ \`agents sessions --active\` uses) owns it. Pending messages in dead boxes
423
+ are archived as \`dropped: dead\`; expired messages in live boxes are
424
+ archived as \`dropped: expired\'. Both surfaces a failure receipt back to
425
+ the feed store when the message carried a blockId.
426
+ `,
427
+ });
390
428
  }