@phnx-labs/agents-cli 1.20.93 → 1.21.0

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 (112) hide show
  1. package/CHANGELOG.md +264 -0
  2. package/README.md +3 -0
  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/mailboxes.js +39 -1
  8. package/dist/commands/message.js +12 -1
  9. package/dist/commands/packages.js +14 -5
  10. package/dist/commands/projects.d.ts +28 -0
  11. package/dist/commands/projects.js +173 -47
  12. package/dist/commands/routines.js +27 -1
  13. package/dist/commands/rules.js +6 -0
  14. package/dist/commands/sessions.d.ts +17 -0
  15. package/dist/commands/sessions.js +16 -8
  16. package/dist/commands/setup-browser.js +5 -1
  17. package/dist/commands/setup-fleet.js +5 -0
  18. package/dist/commands/setup-preferences.d.ts +53 -0
  19. package/dist/commands/setup-preferences.js +142 -0
  20. package/dist/commands/setup.js +8 -9
  21. package/dist/commands/ssh.js +220 -10
  22. package/dist/commands/sync.d.ts +2 -2
  23. package/dist/commands/sync.js +4 -9
  24. package/dist/commands/watchdog.js +26 -3
  25. package/dist/index.js +22 -6
  26. package/dist/lib/activity.d.ts +7 -0
  27. package/dist/lib/activity.js +19 -2
  28. package/dist/lib/auto-dispatch.d.ts +6 -1
  29. package/dist/lib/auto-dispatch.js +7 -2
  30. package/dist/lib/beta.d.ts +1 -1
  31. package/dist/lib/beta.js +1 -1
  32. package/dist/lib/daemon.d.ts +13 -0
  33. package/dist/lib/daemon.js +80 -60
  34. package/dist/lib/device-config.d.ts +82 -0
  35. package/dist/lib/device-config.js +296 -0
  36. package/dist/lib/event-provenance.d.ts +19 -0
  37. package/dist/lib/event-provenance.js +48 -0
  38. package/dist/lib/events.d.ts +2 -2
  39. package/dist/lib/events.js +4 -53
  40. package/dist/lib/feed.d.ts +1 -1
  41. package/dist/lib/feed.js +2 -0
  42. package/dist/lib/git.d.ts +13 -2
  43. package/dist/lib/git.js +38 -6
  44. package/dist/lib/linear-project-counts.d.ts +72 -5
  45. package/dist/lib/linear-project-counts.js +90 -6
  46. package/dist/lib/linear-projects.d.ts +12 -0
  47. package/dist/lib/linear-projects.js +30 -0
  48. package/dist/lib/mailbox-gc.js +30 -7
  49. package/dist/lib/mailbox.d.ts +14 -1
  50. package/dist/lib/mailbox.js +35 -3
  51. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  52. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  53. package/dist/lib/menubar/install-menubar.d.ts +5 -21
  54. package/dist/lib/menubar/install-menubar.js +35 -44
  55. package/dist/lib/migrate.js +1 -1
  56. package/dist/lib/models.js +21 -11
  57. package/dist/lib/overdue.d.ts +1 -1
  58. package/dist/lib/overdue.js +54 -19
  59. package/dist/lib/project-import.d.ts +96 -0
  60. package/dist/lib/project-import.js +171 -0
  61. package/dist/lib/remote-agents-json.d.ts +14 -1
  62. package/dist/lib/remote-agents-json.js +21 -3
  63. package/dist/lib/routines.d.ts +53 -0
  64. package/dist/lib/routines.js +105 -4
  65. package/dist/lib/rules/run-sync.d.ts +18 -0
  66. package/dist/lib/rules/run-sync.js +92 -0
  67. package/dist/lib/scheduler.js +12 -2
  68. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  69. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  70. package/dist/lib/secrets/index.d.ts +4 -5
  71. package/dist/lib/secrets/index.js +4 -5
  72. package/dist/lib/session/active.d.ts +16 -2
  73. package/dist/lib/session/active.js +8 -4
  74. package/dist/lib/session/bundle.js +5 -1
  75. package/dist/lib/session/remote-list.d.ts +1 -15
  76. package/dist/lib/session/remote-list.js +22 -111
  77. package/dist/lib/session/sync/config.d.ts +13 -5
  78. package/dist/lib/session/sync/config.js +21 -13
  79. package/dist/lib/startup/command-registry.d.ts +0 -2
  80. package/dist/lib/startup/command-registry.js +1 -5
  81. package/dist/lib/state.d.ts +7 -2
  82. package/dist/lib/state.js +85 -10
  83. package/dist/lib/sync-umbrella.d.ts +11 -23
  84. package/dist/lib/sync-umbrella.js +14 -45
  85. package/dist/lib/teams/agents.d.ts +35 -3
  86. package/dist/lib/teams/agents.js +52 -8
  87. package/dist/lib/teams/scheduler.d.ts +29 -2
  88. package/dist/lib/teams/scheduler.js +60 -16
  89. package/dist/lib/types.d.ts +17 -1
  90. package/package.json +1 -2
  91. package/dist/commands/drive.d.ts +0 -10
  92. package/dist/commands/drive.js +0 -183
  93. package/dist/commands/hq.d.ts +0 -2
  94. package/dist/commands/hq.js +0 -58
  95. package/dist/commands/sessions-sync.d.ts +0 -17
  96. package/dist/commands/sessions-sync.js +0 -135
  97. package/dist/commands/sync-provision.d.ts +0 -23
  98. package/dist/commands/sync-provision.js +0 -107
  99. package/dist/lib/drive-sync.d.ts +0 -45
  100. package/dist/lib/drive-sync.js +0 -238
  101. package/dist/lib/hq/floor.d.ts +0 -87
  102. package/dist/lib/hq/floor.js +0 -243
  103. package/dist/lib/session/sync/crdt.d.ts +0 -44
  104. package/dist/lib/session/sync/crdt.js +0 -119
  105. package/dist/lib/session/sync/manifest.d.ts +0 -62
  106. package/dist/lib/session/sync/manifest.js +0 -100
  107. package/dist/lib/session/sync/provision.d.ts +0 -49
  108. package/dist/lib/session/sync/provision.js +0 -91
  109. package/dist/lib/session/sync/r2.d.ts +0 -32
  110. package/dist/lib/session/sync/r2.js +0 -121
  111. package/dist/lib/session/sync/sync.d.ts +0 -106
  112. package/dist/lib/session/sync/sync.js +0 -374
package/CHANGELOG.md CHANGED
@@ -1,5 +1,269 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.0
4
+
5
+ - **A clone of your own DotAgents repo no longer hijacks project-layer rule resolution (RUSH-2037).**
6
+ Cloning `~/.agents` to the canonical `~/src/github.com/<you>/.agents` path (to edit
7
+ rules in an editor) made that checkout eligible as a *project* layer whenever you
8
+ worked from its parent directory. Because project outranks user, a stale clone's
9
+ `rules/subrules/*` then silently shadowed the live user rules by filename, and the
10
+ compile planted an out-of-date `AGENTS.md` in an ancestor dir that every session
11
+ beneath it ingested. Project-layer discovery now identifies a DotAgents repo by
12
+ **repo identity** (git origin), not path: a `.agents/` that is itself a git checkout
13
+ whose origin matches the user's or system's DotAgents repo is skipped, so the live
14
+ user layer wins. Legitimate project `.agents/` layers (a plain subdirectory of a
15
+ project, or a git repo with an unrelated origin) are unaffected.
16
+ Source: `apps/cli/src/lib/state.ts`.
17
+
18
+ - **`agents sessions --local` no longer dials remote-host teammates over ssh,
19
+ in the default listing or `--active` (RUSH-2118).** `--local` is supposed to
20
+ mean this-machine-only, but the underlying `AgentManager` poll still fired a
21
+ real ssh round-trip for every teammate dispatched via
22
+ `agents teams add --device` — even a teammate that had already finished.
23
+ On a box with 30 completed remote-host teammates that measured out to 180
24
+ real `ssh` execve calls (6 per teammate: two ssh calls in
25
+ `syncRemoteMirror`, run three times per poll) and a ~4.3s
26
+ `--active --local` call. A `--local` query now reads a remote-host
27
+ teammate's last-persisted `meta.json` state instead of dialing it, and a
28
+ teammate that has already reached a terminal status (completed/failed/
29
+ stopped) is never re-dialed by ANY `--active` query, local or not — its
30
+ final log bytes and exit code were already captured on the poll that
31
+ resolved it. The same gate now covers every `--local` surface: the bare
32
+ default listing's live-glyph enrichment (`maybeLiveIndex`) and `--preview`
33
+ (`renderSessionPreview`, freely combinable with `--local`) both called the
34
+ local-only `getActiveSessions()` with no `localOnly` threaded through,
35
+ despite the `--local` help text already promising this-machine-only for
36
+ all of them. Source: `apps/cli/src/lib/teams/agents.ts`
37
+ (`syncRemoteMirror`, `readNewEvents`, `updateStatusFromProcess`,
38
+ `AgentManager`), `apps/cli/src/lib/session/active.ts` (`listTeamsActive`,
39
+ `getActiveSessions`), `apps/cli/src/commands/sessions.ts`
40
+ (`gatherActiveSessions`, `maybeLiveIndex`, `renderSessionPreview`).
41
+
42
+ - **A rules preset now applies at `agents run` time, not only after `agents
43
+ rules switch` (RUSH-2128).** `setActiveRulesPreset` used to take effect only on the next
44
+ explicit `agents rules switch`/`agents add`/`agents use` — a preset change
45
+ made any other way left the harness launching against a stale rules file
46
+ until someone remembered to re-sync. `agents run` now re-applies the active
47
+ preset for the resolved agent+version immediately before dispatch, every
48
+ time, with a skip-fast sentinel so an unchanged preset costs no recompose or
49
+ rewrite. Version-scoped only; per-model preset scoping is a follow-up.
50
+ Source: `apps/cli/src/lib/rules/run-sync.ts`, `apps/cli/src/commands/exec.ts`.
51
+
52
+ - **Activity events now carry the same actor and session lineage as operational events.**
53
+ The TypeScript activity writer and the embedded PostToolUse hook stamp actor kind,
54
+ launch id, and parent session id from the shared execution provenance floor, so
55
+ `agents events` no longer invents an agent name as the activity record's OS user.
56
+ Source: `apps/cli/src/lib/event-provenance.ts`, `apps/cli/src/lib/activity.ts`.
57
+
58
+ - **`agents view` no longer re-scans every installed Claude binary on each run.**
59
+ When a Claude model extractor produced zero models (a broken regex, or a
60
+ mid-install CLI), the result was never cached — so `getModelCatalog` re-ran a
61
+ full `readFileSync` scan of the 230-270MB Claude binary for every affected
62
+ installed version, on every invocation (~1.85s each). With 4 affected
63
+ versions installed, that was ~7.5s added to every `agents view`. A 0-model
64
+ extraction is now cached too, stamped with when it was attempted, and served
65
+ for 24 hours before self-healing by retrying; an upgrade/reinstall (a new
66
+ source mtime) still re-extracts immediately, as before. Measured on a real
67
+ install with 7 Claude versions (4 of them hitting the broken extractor): the
68
+ cold first-call cost (~12.5s, unavoidable) drops to ~1-2ms on every
69
+ subsequent call. Source: `apps/cli/src/lib/models.ts`.
70
+
71
+ - **Per-device and fleet-wide config keys now have a home: the `config:` block in
72
+ the two-tier agents.yaml store.** Three new subcommands under `agents devices`
73
+ (no new top-level noun): `agents devices set-interactive <name>` records the one
74
+ device agents show YOU artifacts on (browser opens, dashboards) as
75
+ `config.interactiveHost` in the central, synced agents.yaml — skills no longer
76
+ guess "the online macOS box", and the host is marked `★ interactive` in
77
+ `agents devices list`. `agents devices configure <name> --max-agents N
78
+ --scheduler on|off` and `agents devices note <name> "…"` (repeat
79
+ to append, `--clear` to empty) write device-scope keys under `config:` in
80
+ `~/.agents/devices/<name>/agents.yaml` — targetable for any device from any box
81
+ (the devices/ tree syncs; each machine reads only its own). The default browser
82
+ profile joins the same registry as `browser.profile`, routed to the existing
83
+ device-local `defaultBrowserProfile` field (no duplicate key, resolution order
84
+ unchanged). Unset keys always mean today's behavior; everything is scriptable
85
+ with `--json`, and `devices list --json` now carries each row's `config` and an
86
+ `interactive` flag. agents.yaml files the CLI writes now carry a
87
+ `yaml-language-server` hint pointing at the new
88
+ `apps/cli/schema/agents-yaml.schema.json`.
89
+
90
+ The keys are live inputs, not just stored values. `--scheduler off` stops the
91
+ routines scheduler from starting on that device — `routines add` skips the
92
+ auto-start with the reason, a manual `routines start` refuses, and the daemon
93
+ re-evaluates the gate on every SIGHUP reload (boot it again with
94
+ `agents devices configure <host> --scheduler on` + any reload, no daemon
95
+ restart). `--max-agents` feeds host ranking: Factory auto-launch excludes a
96
+ device at its cap (counting device-wide running agents) and names the cap when
97
+ a pool is exhausted; teams placement excludes it from the least-loaded
98
+ auto-pick (counting the team's own roster, local teammates included) and an
99
+ all-capped pool fails loud instead of over-filling a machine. Setup asks
100
+ instead of guessing: bare `agents setup` ends with a skippable preferences
101
+ step (which machine you sit at → interactive host; which browser agents drive
102
+ here → device default), `agents setup fleet` offers the interactive host after
103
+ a sync, and the `agents setup browser` picker highlights the auto-detect
104
+ winner. Source: `apps/cli/src/lib/device-config.ts`,
105
+ `apps/cli/src/lib/state.ts`, `apps/cli/src/lib/daemon.ts`,
106
+ `apps/cli/src/lib/teams/scheduler.ts`, `apps/cli/src/commands/ssh.ts`,
107
+ `apps/cli/src/commands/setup-preferences.ts`,
108
+ `apps/factory/src/core/launchHost.ts`,
109
+ `apps/cli/schema/agents-yaml.schema.json`.
110
+
111
+ - **Mailbox messages now expire and dead boxes are reaped automatically.** Messages
112
+ enqueued without an explicit TTL used to sit in the spool forever, so pending mail
113
+ would outlive the session that needed it. They now get a 24-hour default TTL
114
+ (`AGENTS_MAILBOX_TTL` overrides the default; `agents message … --ttl 2h` sets it
115
+ per message). When a message expires, a live-but-idle box archives it with a
116
+ `dropped: expired` receipt. The watchdog tick also runs a liveness sweep using the
117
+ same live-session set as `agents sessions --active`, archiving pending mail in dead
118
+ boxes as `dropped: dead` and pruning stale consumed entries. Dropped messages tied
119
+ to a feed block surface a failure receipt (`status: dropped` / `expired`) so the
120
+ sender sees the bounce instead of silence. Run the sweep manually with
121
+ `agents mailboxes gc` (`--json` supported). Source:
122
+ `apps/cli/src/lib/mailbox.ts`, `apps/cli/src/lib/mailbox-gc.ts`,
123
+ `apps/cli/src/commands/message.ts`, `apps/cli/src/commands/mailboxes.ts`,
124
+ `apps/cli/src/commands/watchdog.ts`, `apps/cli/src/lib/feed.ts`.
125
+
126
+ - **The menu-bar helper can no longer leak CLI processes until the machine is unusable.**
127
+ Its poll shelled `agents doctor --json` through an unbounded `Process` +
128
+ `readDataToEndOfFile()`. Two properties composed badly: the call had no deadline
129
+ (`doctor --json` measures **136s on an idle box**, against a 60s poll interval), and a
130
+ helper that died mid-call left the child reparented to launchd (PPID 1) with nothing
131
+ to reap it — along with the `node -e` version probes that child had forked. Both fire
132
+ together, because the helper crashes under exactly the conditions that make the CLI
133
+ slow: `NSApplication.shared` segfaults inside `SLSNewConnection` when WindowServer is
134
+ too starved to hand out a connection, launchd's `KeepAlive` restarts it, and the
135
+ restart spawns a new doctor while the old one keeps burning a core. Observed on a real
136
+ machine: 38 orphaned doctors + 92 orphaned probes, ~13 of 18 cores consumed, load
137
+ average 490, keystrokes visibly lagging.
138
+ The crash itself cannot be prevented from inside the app — it is AppKit dereferencing
139
+ a null connection before any of our code runs — so a crash no longer costs anything
140
+ permanent: every child carries a deadline (30s; 180s for `doctor --json`, above its
141
+ real measured cost); it is spawned as its own process-group leader so a timeout kills
142
+ the whole subtree rather than just the CLI; and each live child is recorded on disk so
143
+ the *next* launch reaps whatever a crash abandoned (no exit handler runs on SIGSEGV).
144
+ The doctor refresh also drops from every 60s to every 15 minutes, and the launchd job
145
+ gains `ThrottleInterval` 30 so a startup crash-loop cannot respawn every 10s.
146
+ A poll that blows its deadline now shows a stale menu instead of taking the machine
147
+ down with it. Source: `apps/cli/menubar/Sources/MenubarHelper/ChildProcess.swift`,
148
+ `AgentsCLI.swift`, `StatusItemController.swift`, `main.swift`,
149
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
150
+
151
+ - **The macOS menu-bar helper is now notarized, ending the "app is damaged"
152
+ dialog and the per-run `no valid code signature; skipping launch` spam
153
+ (RUSH-2134).** The helper shipped Developer-ID signed but *not* notarized, so
154
+ Gatekeeper on macOS 26+ rejected it as damaged and the install path tried to
155
+ heal it by re-signing ad-hoc on every `agents` invocation — which can never
156
+ satisfy Gatekeeper, so the dialog and the noise persisted. The release now
157
+ notarizes + staples the helper (`menubar/scripts/build.sh`, mandatory for any
158
+ Developer-ID build, run under the release's `agents secrets exec apple.com`
159
+ context), the `prepack` gate refuses to pack an un-notarized bundle
160
+ (`scripts/verify-menubar-helper.sh` now requires a stapled ticket), and the
161
+ runtime ad-hoc re-sign band-aid is deleted — a notarized + stapled bundle
162
+ survives npm's tarball round-trip untouched, so the helper launches with no
163
+ per-machine healing. The launch guards now verify Gatekeeper acceptance (not
164
+ just `codesign --verify`) and fail loud pointing at an upgrade rather than
165
+ re-signing over it. Source: `apps/cli/menubar/scripts/build.sh`,
166
+ `apps/cli/scripts/verify-menubar-helper.sh`, `apps/cli/scripts/release.sh`,
167
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
168
+
169
+ - **`agents projects import` gains Linear as a source, and gates the Factory guess.**
170
+ `import --from-linear` turns the workspace's Linear projects into definitions via the
171
+ `linear` CLI, binding a local checkout only on an exact name match so it never
172
+ silently points a project at the wrong repo. `--from-factory` now imports only
173
+ `high`-confidence rows by default (`--min-confidence low|medium|high`, `--all` to
174
+ take everything), and prints why each row was skipped — the auto-detected registry
175
+ used to absorb every stale clone it found. Source: `apps/cli/src/lib/project-import.ts`.
176
+ - **`agents projects list` columns line up again.** Widths are computed from the rows
177
+ being printed instead of a fixed 32-character path pad that every home-relative root
178
+ ran straight through. Source: `apps/cli/src/commands/projects.ts`.
179
+
180
+ - **`agents projects status` shows the next Linear milestone.** A new `next` line names
181
+ the project's earliest unfinished milestone with its progress and a human due date
182
+ (`Beta cut · 3/8 · due in 6 days`, `overdue by 3 days`, `due Aug 21`) — a percentage
183
+ says how far along a project is, the milestone says what it is due to hit next. The
184
+ milestone list comes from the project rather than from issue assignments, so a
185
+ milestone with nothing filed under it yet still shows; it rides along on the first
186
+ page of the existing issue fetch, costing no extra request. Source:
187
+ `apps/cli/src/lib/linear-project-counts.ts`.
188
+
189
+ - **`agents publish --branch <b>` now pushes the index to `<b>`, not just the printed URL (#1061).**
190
+ The flag rewrote the printed `raw.githubusercontent.com/.../<b>/skills-index.json`
191
+ URL, but the commit still landed on the checked-out branch — so `--branch dev` from a
192
+ `main` checkout published the index to `main` while advertising a `dev` URL that didn't
193
+ resolve. `commitAndPush` now takes an optional target branch and pushes
194
+ `<current>:<target>`, reporting back the branch the index actually landed on so the URL
195
+ references it. Omitting `--branch` still publishes to the repo's current branch.
196
+ Source: `apps/cli/src/lib/git.ts` (`commitAndPush`, `pushOrigin`, `getCurrentBranch`),
197
+ `apps/cli/src/commands/packages.ts`.
198
+
199
+ - **Remove the unused `agents hq` command.** `agents hq floor --json` was a
200
+ machine-readable bridge for an interactive Agents HQ floor UI that was never
201
+ built — `apps/factory` has zero references to it and it had no other consumer.
202
+ Typing `agents hq` now prints a clear removal notice and exits non-zero instead
203
+ of silently disappearing. Source: `apps/cli/src/index.ts`,
204
+ `apps/cli/src/lib/startup/command-registry.ts` (removed
205
+ `apps/cli/src/commands/hq.ts`, `apps/cli/src/lib/hq/`).
206
+
207
+ - **Removed `agents drive` and the R2/CRDT background session-sync beta.** Both
208
+ predate `agents sessions export`/`import`, which now cover the same ground
209
+ without a daemon: `agents drive` (rsync-based session/config mirroring) and
210
+ the opt-in `session-sync` beta (`agents sessions sync`, the daemon's ~90s R2
211
+ push/pull loop, `agents sync --sessions`) are gone. If you had `session-sync`
212
+ or `drive` enabled, re-enable is no longer possible — use `--host` for live
213
+ cross-machine reads or `agents sessions export --encrypt` /
214
+ `agents sessions import` for portable, encrypted transcript bundles instead.
215
+ The R2 network client and CRDT merge machinery are removed entirely with the
216
+ rest of the background sync. Export/import's own encrypted-bundle path
217
+ survives unchanged: it never talked to R2 over the network — it only reuses
218
+ the `r2.backups` bundle's shared `R2_SYNC_ENC_KEY` for local AES-256-GCM
219
+ encryption, falling back to a printed ephemeral key when that bundle isn't
220
+ configured. Source: `apps/cli/src/commands/drive.ts`,
221
+ `apps/cli/src/commands/sessions-sync.ts`, `apps/cli/src/lib/session/sync/crdt.ts`,
222
+ `apps/cli/src/lib/session/sync/sync.ts`, `apps/cli/src/lib/session/sync/r2.ts`,
223
+ `apps/cli/src/lib/daemon.ts`.
224
+
225
+ - **A routine that fires less often than weekly can now be caught up at all.** Overdue
226
+ detection walked a fixed one-week window for the most recent expected fire, so any cron whose
227
+ gap exceeds that returned nothing and the routine was skipped entirely — never flagged
228
+ overdue on any device, never caught up, no `missed` record, silently. Monthly, semi-monthly,
229
+ quarterly and annual routines were all in that class. Measured on a real schedule
230
+ (`0 9 1,13,25 * *`, 12-day gaps): on **10 of every 28 days** the routine could not be
231
+ evaluated. The lookback now widens (week → month → quarter → year) only when the narrower
232
+ window finds nothing, so a dense schedule never walks more than a week of occurrences.
233
+ - **Catch-up no longer resurrects a retired routine.** `detectOverdueJobs` never checked
234
+ `endAt`, and the scheduler only auto-disables lazily inside a live cron tick — so a routine
235
+ whose `endAt` elapsed while the daemon was down was still enabled on disk, rescheduled on
236
+ restart, and executed by the catch-up pass.
237
+ - **One-shot detection matches the scheduler's.** Overdue used the raw `runOnce` flag while the
238
+ scheduler uses `isOneShotRoutine`, so a one-shot-*like* schedule (a fixed minute/hour/day/
239
+ month) that never carried the flag could be replayed by catch-up.
240
+ - **The creation floor now covers built-in routines.** `routineEffectiveStart` resolved a
241
+ routine's file through a user-layer-only lookup, but `listJobs` reads the system layer too —
242
+ so a built-in shipped in the system repo had neither a `createdAt` stamp nor a resolvable
243
+ path, the floor was skipped, and it read as instantly overdue on first daemon start. Added
244
+ `resolveJobFilePath`, which resolves across every layer the loader reads.
245
+ - **A `createdAt` in the future is clamped to now.** Left unclamped (clock skew, a hand-edited
246
+ year) it sits after every possible expected fire, so the routine could never be flagged
247
+ overdue until wall-clock time caught up.
248
+
249
+ - **A routine now runs on exactly one device, instead of once per device listed.** `devices:`
250
+ was an allowlist where *every* listed device fired independently, so a routine pinned to two
251
+ boxes ran twice on every schedule — two full agent sessions doing identical work and burning
252
+ double the agent quota. On one live fleet seven routines were in that state: `security-sweep`
253
+ ran at 15:30:02 on one box and 15:30:03 on the other, both completing. Ownership is now a pure
254
+ function of the config (the first device in normalized sort order), so every daemon reaches the
255
+ same answer with no lease, no cross-device coordination, and no split brain when the fleet
256
+ partitions. Omitting `devices` still means fleet-wide, which is what `watchdog` and
257
+ `check-updates` want.
258
+ - **`agents routines add --devices a,b` and `devices --set a,b` are now rejected.** A routine
259
+ belongs to one machine; the error names the fix. Routines already on disk with a multi-device
260
+ pin keep running — on their owner only — rather than being dropped.
261
+ - **`agents doctor` lists any routine still carrying a multi-device pin**, with the devices it
262
+ names, the one that now fires, and the command to make it explicit. Also in `doctor --json` as
263
+ `ambiguousDevicePins`. The remediation deliberately offers the candidates rather than
264
+ prescribing the owner: the lowest-sorted name can be a registry alias that matches no live
265
+ machine, and cementing that would keep the routine dead.
266
+
3
267
  ## 1.20.93
4
268
 
5
269
  - **`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
@@ -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
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { spawn } from 'child_process';
3
3
  import { die } from '../lib/format.js';
4
+ import { parseDuration } from '../lib/hooks/cache.js';
4
5
  import { getActiveSessions } from '../lib/session/active.js';
5
6
  import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
6
7
  import { resolveProvider } from '../lib/cloud/registry.js';
@@ -53,6 +54,7 @@ async function deliverViaMailbox(mailboxId, text, block, opts) {
53
54
  text,
54
55
  from: opts.from,
55
56
  blockId: block?.blockId,
57
+ ttlSeconds: opts.ttlSeconds,
56
58
  });
57
59
  if (block) {
58
60
  recordMessageReceipt(block.blockId, {
@@ -116,10 +118,19 @@ export function registerMessageCommand(program) {
116
118
  .option('--from <who>', 'Label recorded as the sender of this message')
117
119
  .option('--as <operator>', 'Verified operator id answering a high-consequence block')
118
120
  .option('--surface <surface>', 'Surface that is sending this answer (feed, terminal, etc.)', 'cli')
121
+ .option('--ttl <dur>', 'Delivery TTL if the message is not consumed (e.g. 30m, 1h, 24h); 0 disables expiry')
119
122
  .action(async (target, text, opts) => {
120
123
  if (!target.trim()) {
121
124
  die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
122
125
  }
126
+ let ttlSeconds;
127
+ if (opts.ttl !== undefined) {
128
+ const parsed = parseDuration(opts.ttl);
129
+ if (parsed == null) {
130
+ die(`Invalid --ttl ${JSON.stringify(opts.ttl)}: expected a duration like 30m, 1h, 24h, or 0.`);
131
+ }
132
+ ttlSeconds = parsed;
133
+ }
123
134
  const sessions = await getActiveSessions();
124
135
  const res = resolveMessageTarget(target, sessions, (id) => getTaskById(id) != null);
125
136
  switch (res.kind) {
@@ -152,7 +163,7 @@ export function registerMessageCommand(program) {
152
163
  // First-answer-wins for any path that closes an open block.
153
164
  claimBlockAnswer(block, opts);
154
165
  if (route.kind === 'mailbox') {
155
- await deliverViaMailbox(res.id, text, block, opts);
166
+ await deliverViaMailbox(res.id, text, block, { from: opts.from, ttlSeconds });
156
167
  return;
157
168
  }
158
169
  if (route.kind === 'tmux' || route.kind === 'iterm' || route.kind === 'pty') {
@@ -14,7 +14,7 @@ import { AGENTS, getAllCliStates, agentLabel, } from '../lib/agents.js';
14
14
  import { capableAgents } from '../lib/capabilities.js';
15
15
  import { DEFAULT_REGISTRIES } from '../lib/types.js';
16
16
  import { getRegistries, setRegistry, removeRegistry, search as searchRegistries, resolvePackage, validatedNpmSpec, validatedPyPISpec, buildSkillIndex, verifySkillIntegrity, parseOwnerRepoFromRemote, } from '../lib/registry.js';
17
- import { cloneRepo, commitAndPush, getRemoteUrl, isGitRepo } from '../lib/git.js';
17
+ import { cloneRepo, commitAndPush, getCurrentBranch, getRemoteUrl, isGitRepo } from '../lib/git.js';
18
18
  import { discoverCommands, resolveCommandSource, installCommand, installCommandCentrally, } from '../lib/commands.js';
19
19
  import { discoverSkillsFromRepo, installSkill, installSkillCentrally, } from '../lib/skills.js';
20
20
  import { discoverHooksFromRepo, installHooks, installHooksCentrally, } from '../lib/hooks.js';
@@ -335,7 +335,7 @@ When to use:
335
335
  .description('Generate a skills-index.json for a git repo and push it, making its skills discoverable via agents search/install')
336
336
  .option('--repo <alias>', 'Publish an extra repo added via `agents repo add` (default: your ~/.agents repo)')
337
337
  .option('--name <name>', 'Registry name to suggest in the output (default: the repo name)')
338
- .option('--branch <branch>', 'Branch the raw URL should reference', 'main')
338
+ .option('--branch <branch>', 'Branch to push the index to and reference in the raw URL (default: the repo\'s current branch)')
339
339
  .option('--dry-run', 'Write the index and print the URL without committing or pushing')
340
340
  .addHelpText('after', `
341
341
  Publish walks a repo's skills/ directory, records a sha256 of every SKILL.md,
@@ -398,20 +398,29 @@ the sha256 in the index and abort on mismatch.
398
398
  for (const s of index.skills) {
399
399
  console.log(` ${chalk.cyan(s.name)} ${chalk.gray(`sha256:${s.sha256?.slice(0, 12)}…`)}`);
400
400
  }
401
+ // The branch the URL references must be the branch the index actually
402
+ // lands on. In a real push that's whatever commitAndPush reports back
403
+ // (the requested --branch, else the checked-out branch); in a dry run we
404
+ // resolve it the same way without pushing.
405
+ let urlBranch;
401
406
  if (options.dryRun) {
407
+ urlBranch = options.branch || (await getCurrentBranch(repoDir));
402
408
  console.log(chalk.gray(`\nDry run — wrote ${indexPath} but did not commit or push.`));
403
409
  }
404
410
  else {
405
411
  const pushSpinner = ora('Committing and pushing skills-index.json...').start();
406
- const result = await commitAndPush(repoDir, 'chore: update skills-index.json (agents publish)');
412
+ const result = await commitAndPush(repoDir, 'chore: update skills-index.json (agents publish)', options.branch);
407
413
  if (!result.success) {
408
414
  pushSpinner.fail(`Push failed: ${result.error}`);
409
415
  console.log(chalk.gray('The index was written locally — commit and push it manually to publish.'));
410
416
  process.exit(1);
411
417
  }
412
- pushSpinner.succeed('Pushed skills-index.json');
418
+ // commitAndPush always reports the pushed branch on success; the `??`
419
+ // only satisfies the optional return type, it is not a behavior path.
420
+ urlBranch = result.branch ?? (await getCurrentBranch(repoDir));
421
+ pushSpinner.succeed(`Pushed skills-index.json to ${urlBranch}`);
413
422
  }
414
- const rawUrl = `https://raw.githubusercontent.com/${repoSlug}/${options.branch}/skills-index.json`;
423
+ const rawUrl = `https://raw.githubusercontent.com/${repoSlug}/${urlBranch}/skills-index.json`;
415
424
  const registryName = options.name || repoSlug.split('/')[1] || 'my-skills';
416
425
  console.log(chalk.bold('\nPublished. Share these with anyone who wants your skills:\n'));
417
426
  console.log(chalk.gray(' Index URL:'));
@@ -9,6 +9,7 @@
9
9
  * lifecycle state, plan completion, open PRs, and tickets in flight.
10
10
  */
11
11
  import type { Command } from 'commander';
12
+ import { type LinearMilestone } from '../lib/linear-project-counts.js';
12
13
  /** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
13
14
  export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
14
15
  /**
@@ -19,4 +20,31 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
19
20
  * string when everything answered.
20
21
  */
21
22
  export declare function formatFleetSkippedNote(skipped: string[]): string;
23
+ /** One `projects list` row, pre-render. */
24
+ export interface ProjectListRow {
25
+ name: string;
26
+ path: string;
27
+ repo: string;
28
+ }
29
+ /**
30
+ * Column widths for `projects list`, sized to the rows actually being printed.
31
+ * Fixed padding was the bug: home-relative roots run past 50 characters, so a
32
+ * hardcoded 32 pushed the repo column off its gridline on every long path.
33
+ * Paths longer than {@link LIST_PATH_MAX} truncate rather than widen the table.
34
+ */
35
+ export declare function computeProjectListWidths(rows: ProjectListRow[]): {
36
+ name: number;
37
+ path: number;
38
+ repo: number;
39
+ };
40
+ /**
41
+ * A milestone's target date as a person would say it — "due tomorrow",
42
+ * "overdue by 3 days", "due Aug 21" — never a raw `2026-08-21` or a duration in
43
+ * hours. Linear stores a calendar date with no timezone, so both sides are
44
+ * compared at LOCAL midnight; parsing `YYYY-MM-DD` with `new Date(str)` would
45
+ * read it as UTC and shift the answer by a day for anyone west of Greenwich.
46
+ */
47
+ export declare function formatMilestoneDue(targetDate: string, nowMs: number): string | undefined;
48
+ /** The `next` card line: what this project is due to hit, and how far along it is. */
49
+ export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
22
50
  export declare function registerProjectsCommands(program: Command): void;