@phnx-labs/agents-cli 1.20.88 → 1.20.89

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 (55) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +9 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/commands.js +7 -7
  5. package/dist/commands/factory.js +26 -2
  6. package/dist/commands/funnel.js +16 -1
  7. package/dist/commands/menubar.js +117 -34
  8. package/dist/commands/routines.js +23 -1
  9. package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
  10. package/dist/commands/secrets-rotate-passphrase.js +96 -0
  11. package/dist/commands/secrets.js +2 -0
  12. package/dist/commands/sessions.d.ts +7 -1
  13. package/dist/commands/sessions.js +39 -12
  14. package/dist/commands/webhook.js +7 -2
  15. package/dist/lib/commands.js +9 -1
  16. package/dist/lib/daemon.d.ts +29 -0
  17. package/dist/lib/daemon.js +58 -4
  18. package/dist/lib/events.d.ts +1 -1
  19. package/dist/lib/factory/snapshot.d.ts +78 -0
  20. package/dist/lib/factory/snapshot.js +209 -0
  21. package/dist/lib/fs-atomic.d.ts +14 -1
  22. package/dist/lib/fs-atomic.js +35 -3
  23. package/dist/lib/funnel.d.ts +1 -0
  24. package/dist/lib/funnel.js +8 -0
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  26. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  27. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
  28. package/dist/lib/menubar/install-menubar.d.ts +53 -2
  29. package/dist/lib/menubar/install-menubar.js +183 -28
  30. package/dist/lib/platform/process.d.ts +2 -0
  31. package/dist/lib/platform/process.js +5 -3
  32. package/dist/lib/resources.d.ts +8 -0
  33. package/dist/lib/resources.js +34 -1
  34. package/dist/lib/routines-placement.d.ts +2 -1
  35. package/dist/lib/routines-placement.js +8 -4
  36. package/dist/lib/routines.d.ts +57 -1
  37. package/dist/lib/routines.js +74 -1
  38. package/dist/lib/runner.d.ts +2 -0
  39. package/dist/lib/runner.js +21 -8
  40. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  41. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  42. package/dist/lib/secrets/bundles.js +9 -34
  43. package/dist/lib/secrets/filestore.d.ts +152 -34
  44. package/dist/lib/secrets/filestore.js +676 -123
  45. package/dist/lib/session/remote-active.d.ts +4 -1
  46. package/dist/lib/session/remote-active.js +8 -2
  47. package/dist/lib/session/viewing-in.d.ts +31 -0
  48. package/dist/lib/session/viewing-in.js +47 -0
  49. package/dist/lib/state.d.ts +17 -0
  50. package/dist/lib/state.js +30 -2
  51. package/dist/lib/triggers/handlers.d.ts +95 -0
  52. package/dist/lib/triggers/handlers.js +384 -0
  53. package/dist/lib/triggers/webhook.d.ts +10 -2
  54. package/dist/lib/triggers/webhook.js +65 -11
  55. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,268 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.89
4
+
5
+ - **Webhook handler layer for one-off agent/workflow/command/routine triggers.**
6
+ Routines still fire from signed webhooks, but a new `~/.agents/webhooks/*.yml`
7
+ layer can also run one-off actions: `run.agent`, `run.workflow`, `run.command`,
8
+ or delegate to an existing `routine`. Handlers support the same source/event/
9
+ action/label/repo/branch filters as routine triggers, plus Linear
10
+ `stateTo`/`stateFrom` state-change filters. Prompts and commands can use
11
+ `{{issue.identifier}}`, `{{updatedFrom.state.name}}`, etc. The receiver emits
12
+ `webhook.received`, `webhook.authorized`, `webhook.rejected`, `webhook.matched`,
13
+ `webhook.fired`, `webhook.handler.start`, and `webhook.handler.end` events.
14
+ Source: `apps/cli/src/lib/triggers/handlers.ts`,
15
+ `apps/cli/src/lib/triggers/webhook.ts`, `apps/cli/src/lib/routines.ts`,
16
+ `apps/cli/src/commands/routines.ts`, `apps/cli/docs/03-routines.md`.
17
+
18
+ - **`agents routines add` gains `--state-to` and `--state-from` filters for Linear
19
+ triggers.** A Linear routine or handler can now fire only on a specific state
20
+ transition (for example `--state-to Plan`), instead of on every issue update.
21
+
22
+ - **Values substituted into `run.command` are shell-quoted.** A webhook context is
23
+ built from an external payload, and fields like `issue.title` or a GitHub
24
+ `pull_request` title are free text any outside contributor can set — pasted raw
25
+ into a shell command they would be a command-injection sink. Substituted values
26
+ are now single-quoted (POSIX `sh`), so a payload stays one inert argument while
27
+ the operator's own template keeps its pipes, redirects, and `&&`. On Windows,
28
+ where `exec` runs through `cmd.exe` and these quoting rules do not hold, a
29
+ `run.command` containing `{{…}}` is refused with a clear error rather than run.
30
+ `run.prompt` is unaffected — it never reaches a shell.
31
+ Source: `apps/cli/src/lib/routines.ts` (`substituteWebhookCommand`,
32
+ `assertShellSubstitutionSupported`), `apps/cli/src/lib/triggers/handlers.ts`.
33
+
34
+ - **The `Cmd-Shift-O` quick-dispatch bar now lists the repo's open Linear tickets,
35
+ and dispatches one on a click (RUSH-2098).** The panel only captured NEW work;
36
+ it now also shows what already exists. Switching the repo dropdown switches the
37
+ Linear project (the repo name is matched against `linear projects` reduced to
38
+ lowercase alphanumerics, so `agents-cli` finds "Agents CLI" with nothing to
39
+ configure; a worktree resolves to its parent repo, and a repo that matches no
40
+ project says so and lets you pick one, remembered per repo). Rows are ranked
41
+ urgent-first — Linear priority, then overdue, then in progress, then newest —
42
+ and typing filters them, so an existing ticket surfaces before Return files a
43
+ duplicate. Clicking a row (or `⌘1`–`⌘5`) dispatches that ticket to the selected
44
+ agents in the picked repo: **Run** claims it and implements it, **Plan** posts a
45
+ plan as a ticket comment. `⌘`-click opens it in Linear instead. The list renders
46
+ from a 90-second warm cache so the panel still appears instantly. Source:
47
+ `apps/cli/menubar/Sources/MenubarHelper/LinearTickets.swift`,
48
+ `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`,
49
+ `apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
50
+
51
+ - **Fixed: a menu-bar dispatch whose child printed more than ~64 KiB hung forever
52
+ and never notified.** The helper read a monitored child's stdout only from the
53
+ process-termination handler, so a child that filled the pipe buffer blocked on
54
+ write, never exited, and the completion callback never fired — two `linear`
55
+ processes were left wedged by a single ticket fetch. Both monitored paths (the
56
+ ticket agent and `linear create`) now drain stdout, and feed stdin, on a
57
+ background queue while the child runs. Source:
58
+ `apps/cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
59
+
60
+ - **The daemon warns when it was launched from an ephemeral root.** A daemon
61
+ started from a temp dir (`/tmp`, `/var/folders`, `/dev/shm`) or a git worktree
62
+ resolves its own job modules by dynamic `import()` rooted at the launch entry
63
+ (`getAgentsBinPath` → `process.argv[1]`). When that directory is later removed
64
+ — a `/tmp` cleanup, a review/verify checkout teardown, `git worktree remove` —
65
+ the long-lived daemon keeps ENOENT-ing on every routine's imports
66
+ (`auto-dispatch.ts`, `routines-placement.ts`, `devices/fleet.ts`), silently
67
+ wedging until restart. `anchorDaemonCwd` already rescues the cwd, but nothing
68
+ can re-root a deleted module tree. `runDaemon` now calls
69
+ `warnEphemeralDaemonRoot` at startup, so the risk is logged the moment the
70
+ daemon comes up — including a direct `agents __daemon-run` that never passes
71
+ through the launch-time `validateDaemonBinary` check. That launch-time check is
72
+ also broadened from git-worktree-only to any ephemeral root via the shared
73
+ `describeEphemeralDaemonRoot` predicate. The fix for a wedged daemon is
74
+ unchanged: run it from the globally installed binary
75
+ (`npm i -g @phnx-labs/agents-cli`) so its entry roots at a stable version home.
76
+ Source: `apps/cli/src/lib/daemon.ts`
77
+ (`describeEphemeralDaemonRoot`, `warnEphemeralDaemonRoot`, `validateDaemonBinary`).
78
+
79
+ - **A `README.md` / `AGENTS.md` sitting in a resource directory is no longer
80
+ installed as a resource.** `listResources` skipped only dotfiles, so every `.md`
81
+ beside the actual resources was materialized as one: `commands/README.md` — which
82
+ the system repo has shipped for months — installed a bogus `/README` slash command
83
+ into every agent home, and adding per-directory `AGENTS.md` docs would have added
84
+ `/AGENTS`, `/CLAUDE`, and `/GEMINI` alongside it. `README`, `AGENTS`, `CLAUDE`, and
85
+ `GEMINI` are now filtered from both `listResources` and `resolveResource` for every
86
+ kind **except `rules`**, where `AGENTS.md` *is* the resource (the composed ruleset
87
+ that syncs as each agent's memory file). The check tests `!entry.isDirectory()`
88
+ rather than `isFile()`, because a `Dirent` for a symlink reports
89
+ `isFile() === false` and `CLAUDE.md`/`GEMINI.md` are symlinks to `AGENTS.md` by
90
+ convention — a resource *directory* named `agents/` is still a real resource.
91
+ Verified against the real installed layers: 30 commands with `README` leaking
92
+ before, 29 with none after.
93
+ - **`agents commands list` and the command picker no longer offer a name that
94
+ cannot be opened.** `listCentralCommands` and `discoverCommands`
95
+ (`src/lib/commands.ts`) run their own `readdirSync` scans rather than going
96
+ through `listResources`, so they kept offering `README` while
97
+ `agents commands view README` answered "not found" — a listed-but-unopenable
98
+ name. Both now share the one exported `isDirectoryDoc` predicate, so every
99
+ enumerator agrees. Verified: 27 names with `README` before, 26 with none after.
100
+ - **`agents commands add/remove/view` no longer suggest `README` as the example
101
+ command name.** With `README` reserved as a directory doc, the six hardcoded
102
+ examples in the help text and non-interactive hints named a command that can never
103
+ exist. They now use `plan`, which actually ships.
104
+
105
+ - **File-backed secrets bundles no longer require `AGENTS_SECRETS_PASSPHRASE` on
106
+ macOS.** The encrypted file store now silently auto-provisions a stable
107
+ machine-local key (a 0600 file under `~/.agents/.secrets-key/`, kept outside the
108
+ encrypted store) on first use on **every** platform, macOS included — no prompt,
109
+ no Touch ID, nothing to set or remember. Previously a file-backed bundle on a Mac
110
+ hard-failed unless `AGENTS_SECRETS_PASSPHRASE` was exported, which blocked
111
+ headless reads (e.g. the `auth` bundle the usage/auth reader consults) and
112
+ frequently hung. Setting `AGENTS_SECRETS_PASSPHRASE` still works and takes
113
+ precedence — use it to hold the key off disk or to share one bundle's ciphertext
114
+ across boxes under a common key. Source: `apps/cli/src/lib/secrets/filestore.ts`,
115
+ `apps/cli/src/lib/secrets/bundles.ts`.
116
+
117
+ - **Menu-bar & daemon notifications now use the current agents-cli mark, not the
118
+ legacy logo.** A desktop notification from the menu-bar helper or the routines
119
+ daemon showed the old `assets/logo.png` gradient "A" — outdated, and blank in the
120
+ notification's left-hand app-icon slot. `MenubarHelper.app`'s `AppIcon.icns` is
121
+ now generated from the current brand mark (`assets/app-icon.svg` → `app-icon.png`:
122
+ the lime-tile lowercase `a` shared with the agi-cli web favicon and the menu-bar
123
+ glyph), which drives both the notification's right-hand `contentImage` and its
124
+ left-hand app icon. The installer also registers the bundle with LaunchServices
125
+ (`lsregister -f`) at its `~/Library/Application Support` path so the OS can resolve
126
+ that app icon. Source: `apps/cli/menubar/scripts/build.sh`,
127
+ `apps/cli/src/lib/menubar/install-menubar.ts`, `assets/app-icon.svg`.
128
+
129
+ - **The menu bar is a single instance, always.** Two copies of the helper could
130
+ run at once — launchd's `KeepAlive` service plus a LaunchServices/`open` launch
131
+ of the same `.app` — putting two agents marks in the menu bar, and the second
132
+ copy could hold `Cmd-Shift-V`/`Cmd-Shift-O` (`RegisterEventHotKey` is
133
+ first-come). The helper now takes an `flock` on
134
+ `~/.agents/.cache/state/menubar.lock` at launch and holds it for its lifetime;
135
+ a helper that cannot take the lock pops the **running** helper's menu open and
136
+ exits 0, since re-launching a menu-bar app means "show me the one I already
137
+ have". An `flock` rather than a pid file: the kernel releases it when the
138
+ holder dies, so a `SIGKILL`ed helper cannot leave a stale "already running"
139
+ that blocks every later launch. Source:
140
+ `apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift`,
141
+ `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`.
142
+
143
+ - **`agents menubar setup` configures the menu bar end-to-end.** One idempotent
144
+ command for a machine that is wrong — never configured, helper down, or showing
145
+ a duplicate icon. It ends every running helper, installs/refreshes the bundle,
146
+ checks its code signature, writes the launchd login item (`RunAtLoad` +
147
+ `KeepAlive`), clears a previous `agents menubar disable`, and verifies exactly
148
+ one helper came back up — reporting each as its own step and exiting nonzero if
149
+ it cannot reach that state. `--check` reports without changing; `--json` emits
150
+ the step list. Source: `apps/cli/src/commands/menubar.ts`,
151
+ `apps/cli/src/lib/menubar/install-menubar.ts`.
152
+
153
+ - **`agents menubar status` now shows a duplicate.** Live helper processes were
154
+ collapsed to a boolean `running`, so two copies of the *installed* bundle — the
155
+ duplicate a user actually sees — reported as healthy. `--json` now carries an
156
+ `instances` array (copies of the installed bundle) beside the existing
157
+ `foreignInstances`, and the text readout names every extra pid and points at
158
+ `agents menubar setup`. Source:
159
+ `apps/cli/src/lib/menubar/install-menubar.ts` (`classifyMenubarProcesses`).
160
+
161
+ - **Quick-dispatch ticket list: one-row filter + sort, and a scrollable list.**
162
+ The ticket controls sit on a single row of popups next to the Linear project
163
+ (project · filter · sort) — not a chip matrix or two-column block. Quick filter
164
+ options: All open, Todo, Doing, Backlog, P1 only, P2 only, Overdue. Quick sort
165
+ options: Urgent first, Newest, Oldest, Due date, Priority (flat list, no
166
+ status grouping). Filter and sort picks are remembered across summons. Ticket
167
+ rows scroll inside a fixed viewport so more than five matches stay reachable
168
+ without growing the panel. Source:
169
+ `apps/cli/menubar/Sources/MenubarHelper/LinearTickets.swift`,
170
+ `apps/cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
171
+
172
+ - **`release.sh` now takes a release lease, and refuses to bump past an
173
+ unpublished tag.** Releases run from whichever fleet box an agent happens to be
174
+ on, so two agents could enter the pipeline at once; the collision only surfaced
175
+ at the publish gate (`merged tree != built tree -- refusing to publish`), after
176
+ one run had already merged and tagged, leaving the version merged but unshipped.
177
+ A new `scripts/release-lease.sh` holds mutual exclusion on `origin` as an orphan
178
+ commit at `refs/release-lock/held` — a second claimant's push can never be a
179
+ fast-forward, so git's rejection *is* the failed lock acquisition. The lease is
180
+ claimed before the first mutation and dropped by the existing cleanup trap on
181
+ every exit path. Because a healthy release routinely outlives any sane
182
+ expiry — the CI matrix alone has run 57 minutes and release 1.20.77 took 186
183
+ minutes — the lease is **renewed** by a background renewer for the whole run,
184
+ and the squash-merge, the tag, and the publish each **verify** ownership first,
185
+ failing closed if it can no longer be proven. A lease that stops being renewed
186
+ is reclaimable after 30 minutes, and reclaiming names the dead holder instead
187
+ of silently overwriting it. Separately,
188
+ `release.sh` now refuses to cut a new version while an older `v*` tag exists
189
+ that npm never received, and points at the re-run that finishes it — bumping
190
+ past an unpublished tag is what turned a one-version gap into npm 1.20.78 vs
191
+ main 1.20.81. Source: `apps/cli/scripts/release-lease.sh`,
192
+ `apps/cli/scripts/release.sh`.
193
+
194
+ - **`agents funnel down` disables a public Funnel port from the same wrapper used
195
+ to enable ingress.** Webhook ingress now has a complete local receiver runbook:
196
+ keep GitHub/Linear signing keys in `agents secrets`, bind the receiver to
197
+ `127.0.0.1`, expose it with `agents funnel up`, rotate one source secret at a
198
+ time, and turn the public port off with `agents funnel down` before stopping or
199
+ moving the receiver. Source: `apps/cli/src/commands/funnel.ts`,
200
+ `apps/cli/src/lib/funnel.ts`, `apps/cli/docs/03-routines.md`.
201
+
202
+ - **New `agents secrets rotate-passphrase` re-keys the encrypted file store under
203
+ a new master passphrase, atomically (RUSH-1975).** Until now there was no
204
+ supported way to rotate the file-store passphrase — `rekey` only renames macOS
205
+ keychain service names and `rotate <bundle> <key>` replaces a single secret
206
+ value, so a leaked passphrase (RUSH-1968) could only be remediated by a
207
+ hand-rolled non-atomic script or an export-to-plaintext round-trip (the exact
208
+ exposure being fixed). The new command decrypts every `<item>.enc` under the
209
+ current key, re-encrypts under a freshly generated one, and swaps both the
210
+ ciphertext and the 0600 key file by directory rename after verifying every item
211
+ round-trips. A crash at any point self-heals on the next *rotate* run to a single
212
+ readable store — content-aware recovery probes which key actually decrypts the
213
+ live store (not merely which files are present) and classifies the WHOLE store:
214
+ it completes the rotation forward or rolls back only when one key opens every
215
+ item, and if a later `secrets set` contaminated a crashed rotation into a MIXED
216
+ store (items under two keys at once, or a store dir recreated by an interstitial
217
+ write after the crash left it absent, so its backup holds items the live dir does
218
+ not) it refuses with an actionable error and preserves every recovery artifact
219
+ rather than sweeping the only copy of a key or the backed-up ciphertext — so a
220
+ crash anywhere in the swap can never orphan the store, even when a write landed in
221
+ between. The rotation and every store write run under
222
+ one cross-process lock, so a `secrets set` or a second rotation can never
223
+ interleave with a swap in the first place. No plaintext secret or passphrase is
224
+ ever written to disk, argv, or a log. Items
225
+ that don't decrypt under the current key (orphan caches, stale test artifacts)
226
+ are carried through verbatim, never re-keyed. Dry-run by default (`--commit` to
227
+ apply). A dry run never re-keys, but it *does* heal an interrupted rotation —
228
+ that is how a crashed store becomes readable again without re-keying it — and it
229
+ says so instead of claiming nothing was written. Refuses while the secrets-agent
230
+ holds live unlocks or while
231
+ `AGENTS_SECRETS_PASSPHRASE` is exported in the environment, unless `--force`.
232
+ Headless-safe and Linux-first. Source: `apps/cli/src/lib/secrets/filestore.ts`,
233
+ `apps/cli/src/commands/secrets-rotate-passphrase.ts`.
234
+
235
+ - **`agents sessions --active --json` now reports who is watching each session.**
236
+ The `viewingIn` field carries the same string the table prints — `codium tab 3`,
237
+ `ghostty tab 2`, or `detached` for a live tmux pane with **no client attached**
238
+ (its terminal was closed or crashed). It is `null` both for a session that isn't
239
+ tmux-hosted and for one whose pane the locator could not resolve — `detached` is
240
+ claimed only when the pane was actually located, so absence of evidence is never
241
+ reported as evidence of absence. Previously the JSON path returned
242
+ before the locator pass ran, so the field never appeared and a machine consumer
243
+ could not tell a session someone is looking at from an orphaned one — which is
244
+ exactly what the Factory extension's `Agents: Resume` picker ranks by. The JSON
245
+ path resolves tmux clients only — no osascript — so scriptable output keeps the
246
+ cheapness the old ordering was protecting; a Ghostty-attached client resolves as
247
+ `ghostty` without its tab number. Peers running an older CLI that still emits the
248
+ `{app, tab}` object are normalized at the fan-out boundary, so a mixed-version
249
+ fleet sweep stays correct. Source: `apps/cli/src/lib/session/viewing-in.ts`
250
+ (`viewingInLabel`, `parseViewingIn`), `apps/cli/src/commands/sessions.ts`
251
+ (`serializeActiveSessionsForJson`, `enrichTmuxLocators`),
252
+ `apps/cli/src/lib/session/remote-active.ts`.
253
+
254
+ - **Webhook handlers gain `run.env` and `host` placement.** A handler can now
255
+ inject environment variables into the process it spawns (`run.env`), and choose
256
+ where that run executes (`host`). `host` takes a device name (`yosemite-s0`), or
257
+ `fleet` to pick any eligible online worker, or `fleet/<platform>` /
258
+ `<platform>/fleet` (also a bare `linux` / `macos` / `windows`) to restrict that
259
+ pick to one platform. A fleet expression that matches no eligible device fails
260
+ loudly rather than silently falling back to the local machine, so `fleet/linux`
261
+ can never land on a macOS box. Omitting `host` runs locally, as before.
262
+ Source: `apps/cli/src/lib/triggers/handlers.ts` (`resolveHandlerHost`),
263
+ `apps/cli/src/lib/routines-placement.ts` (`pickFleetDevice` platform filter),
264
+ `apps/cli/src/lib/routines.ts` (`JobConfig.env`), `apps/cli/src/lib/runner.ts`.
265
+
3
266
  ## 1.20.88
4
267
 
5
268
  - **`agents doctor` redesigned into a prioritized, fleet-aware, per-version
package/README.md CHANGED
@@ -1002,16 +1002,24 @@ Other useful commands: `agents doctor` checks CLI availability and resource sync
1002
1002
  On macOS, `agents-cli` puts a status item in your menu bar -- a live glance at what your agents are doing, plus a Spotlight-style bar for filing work without breaking focus.
1003
1003
 
1004
1004
  ```bash
1005
- agents menubar enable # install + launch at login
1005
+ agents menubar setup # configure end-to-end: one instance, started at login
1006
1006
  agents menubar status # is it installed and running?
1007
1007
  ```
1008
1008
 
1009
+ There is only ever **one** agents mark: the helper takes a lock at launch, so a
1010
+ second copy surfaces the running one's menu and exits instead of adding a
1011
+ duplicate icon. `agents menubar setup` is the recovery command when a machine is
1012
+ already wrong -- it ends any duplicate, installs the bundle, wires the login
1013
+ item, and verifies exactly one helper came back up.
1014
+
1009
1015
  The dropdown surfaces a **NEEDS YOU** queue (agents waiting on a question, a plan review, or a permission prompt), the running roster, and a routines summary -- the same live state as `agents sessions --active`, one click away.
1010
1016
 
1011
1017
  ### Quick-issue bar (⌘⇧O)
1012
1018
 
1013
1019
  Press `Cmd-Shift-O` anywhere for a thin capture surface: type a one-line note, `Cmd-V` to paste, and attach one or more recent screenshots (double-click a thumbnail to preview it in full). Submit, and a headless agent picks the right project from your recent sessions, investigates, and files the Linear ticket itself -- you never leave what you were doing.
1014
1020
 
1021
+ The bar also lists the **open Linear tickets of the repo you picked**, urgent first. Switching the repo switches the Linear project; typing filters the list, so an existing ticket shows up before you file a duplicate; and clicking a row (or `⌘1`-`⌘5`) dispatches that ticket to the selected agents -- **Run** implements it, **Plan** posts a plan as a ticket comment.
1022
+
1015
1023
  <p align="center">
1016
1024
  <img src="assets/menubar-quickissue.svg" alt="The Cmd-Shift-O quick-issue bar: a one-line note with attached screenshot thumbnails that a headless agent turns into a filed Linear ticket" width="100%" />
1017
1025
  </p>
package/dist/bin/agents CHANGED
Binary file
@@ -35,7 +35,7 @@ Examples:
35
35
  agents commands add
36
36
 
37
37
  # Install specific commands by name
38
- agents commands add --names README,debug --agents codex@0.116.0
38
+ agents commands add --names plan,debug --agents codex@0.116.0
39
39
 
40
40
  When to use:
41
41
  - Project setup: 'agents commands add gh:team/commands' to sync everyone's workflow
@@ -90,7 +90,7 @@ Examples:
90
90
  agents commands add
91
91
 
92
92
  # Install specific commands to a single version
93
- agents commands add --names README,debug --agents codex@0.116.0
93
+ agents commands add --names plan,debug --agents codex@0.116.0
94
94
 
95
95
  # Pull commands from GitHub and sync to all installed agents
96
96
  agents commands add gh:user/repo --agents claude,codex,cursor
@@ -125,7 +125,7 @@ Examples:
125
125
  else {
126
126
  if (!isInteractiveTerminal()) {
127
127
  requireInteractiveSelection('Selecting commands from ~/.agents/commands/', [
128
- 'agents commands add --names README,debug --agents codex',
128
+ 'agents commands add --names plan,debug --agents codex',
129
129
  'agents commands add gh:user/repo --agents codex',
130
130
  ]);
131
131
  }
@@ -272,7 +272,7 @@ Examples:
272
272
  .addHelpText('after', `
273
273
  Examples:
274
274
  # Remove a command by name
275
- agents commands remove README
275
+ agents commands remove plan
276
276
 
277
277
  # Interactive: pick commands to remove
278
278
  agents commands remove
@@ -305,7 +305,7 @@ Examples:
305
305
  }
306
306
  if (!isInteractiveTerminal()) {
307
307
  requireInteractiveSelection('Selecting commands to remove', [
308
- 'agents commands remove README',
308
+ 'agents commands remove plan',
309
309
  ]);
310
310
  }
311
311
  try {
@@ -422,7 +422,7 @@ Examples:
422
422
  .addHelpText('after', `
423
423
  Examples:
424
424
  # View a specific command
425
- agents commands view README
425
+ agents commands view plan
426
426
 
427
427
  # Interactive picker
428
428
  agents commands view
@@ -437,7 +437,7 @@ Examples:
437
437
  }
438
438
  if (!isInteractiveTerminal()) {
439
439
  requireInteractiveSelection('Selecting a command to view', [
440
- 'agents commands view README',
440
+ 'agents commands view plan',
441
441
  ]);
442
442
  }
443
443
  try {
@@ -6,6 +6,7 @@ import { homedir } from 'os';
6
6
  import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
7
7
  import { insertTask } from '../lib/cloud/store.js';
8
8
  import { emit } from '../lib/events.js';
9
+ import { buildFactorySnapshot } from '../lib/factory/snapshot.js';
9
10
  function requireFactoryUrl() {
10
11
  const url = process.env.FACTORY_FLOOR_URL;
11
12
  if (!url) {
@@ -51,14 +52,28 @@ export function registerFactoryCommands(program) {
51
52
  Examples:
52
53
  agents factory submit PROJ-123
53
54
  agents factory submit https://linear.app/example/issue/PROJ-123
55
+ agents factory snapshot --json
54
56
  `);
55
- factory.hook('preAction', () => {
56
- if (enabled)
57
+ factory.hook('preAction', (_thisCommand, actionCommand) => {
58
+ // Foreman must be able to read its tick input before any beta-gated action.
59
+ if (enabled || actionCommand.name() === 'snapshot')
57
60
  return;
58
61
  console.error(chalk.red('agents factory is in beta.'));
59
62
  console.error(chalk.gray(betaEnableHint('factory')));
60
63
  process.exit(1);
61
64
  });
65
+ factory
66
+ .command('snapshot')
67
+ .description('Read the complete Software Factory state without dispatching or changing it.')
68
+ .option('--json', 'Output the stable machine-readable snapshot')
69
+ .action(async (opts) => {
70
+ const snapshot = await buildFactorySnapshot();
71
+ if (opts.json) {
72
+ console.log(JSON.stringify(snapshot, null, 2));
73
+ return;
74
+ }
75
+ renderSnapshot(snapshot);
76
+ });
62
77
  factory
63
78
  .command('submit <linear-ref>')
64
79
  .description('Submit a Linear issue (PROJ-123 or URL) to the Software Factory.')
@@ -93,3 +108,12 @@ Examples:
93
108
  console.log(` tail output agents cloud logs ${result.cloud_execution_id}`);
94
109
  });
95
110
  }
111
+ function renderSnapshot(snapshot) {
112
+ console.log(chalk.bold(`Factory snapshot ${snapshot.generatedAt}`));
113
+ console.log(` sessions ${snapshot.sessions.length}`);
114
+ console.log(` open PRs ${snapshot.prs.length}`);
115
+ console.log(` devices ${snapshot.devices.length}`);
116
+ for (const [project, queue] of Object.entries(snapshot.queues)) {
117
+ console.log(` ${project.padEnd(12)} todo ${queue.todo} · in progress ${queue.inProgress} · blocked ${queue.blocked}`);
118
+ }
119
+ }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
2
+ import { buildFunnelDownCommand, buildFunnelStatusCommand, buildFunnelUpCommand, parseFunnelPort } from '../lib/funnel.js';
3
3
  import { resolveHost } from '../lib/hosts/registry.js';
4
4
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
5
5
  import { sshTargetFor } from '../lib/hosts/types.js';
@@ -59,4 +59,19 @@ export function registerFunnelCommand(program) {
59
59
  process.exit(1);
60
60
  }
61
61
  });
62
+ funnel
63
+ .command('down <host>')
64
+ .description('Disable Tailscale Funnel exposure for a public HTTPS port.')
65
+ .option('--port <n>', 'Public Funnel port: 443, 8443, or 10000', '443')
66
+ .action(async (host, opts) => {
67
+ try {
68
+ const publicPort = parseFunnelPort(opts.port ?? '443');
69
+ await runOnHost(host, buildFunnelDownCommand(publicPort));
70
+ console.log(chalk.green(`Funnel disabled on ${host}: public :${publicPort}`));
71
+ }
72
+ catch (err) {
73
+ console.error(chalk.red(err.message));
74
+ process.exit(1);
75
+ }
76
+ });
62
77
  }
@@ -7,7 +7,8 @@
7
7
  * user; these commands are the manual override.
8
8
  */
9
9
  import chalk from 'chalk';
10
- import { enableMenubarService, disableMenubarService, getMenubarStatus, } from '../lib/menubar/install-menubar.js';
10
+ import { setHelpSections } from '../lib/help.js';
11
+ import { enableMenubarService, disableMenubarService, getMenubarStatus, runMenubarSetup, } from '../lib/menubar/install-menubar.js';
11
12
  function notMac() {
12
13
  if (process.platform !== 'darwin') {
13
14
  console.log(chalk.yellow('The menu bar helper is macOS only.'));
@@ -15,10 +16,122 @@ function notMac() {
15
16
  }
16
17
  return false;
17
18
  }
19
+ /** Shared status readout — `status`, bare `menubar`, and `setup --check` all end here. */
20
+ function printStatus(s, opts = {}) {
21
+ const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
22
+ console.log(chalk.bold('Menu bar helper\n'));
23
+ console.log(` running ${yn(s.running)}`);
24
+ console.log(` service installed ${yn(s.serviceInstalled)}`);
25
+ if (opts.brief) {
26
+ console.log(chalk.gray('\n setup | enable | disable | status'));
27
+ return;
28
+ }
29
+ console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
30
+ console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
31
+ console.log(` current version ${chalk.gray(s.currentVersion)}`);
32
+ console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
33
+ console.log(` disabled by user ${yn(s.disabledByUser)}`);
34
+ // Two copies of the INSTALLED bundle is the duplicate the user sees as two
35
+ // agents marks in the menu bar. It used to read as a healthy `running: yes`.
36
+ if (s.instances.length > 1) {
37
+ console.log(chalk.yellow(`\n ${s.instances.length} copies of the installed helper are running — that is the duplicate menu-bar icon:`));
38
+ for (const p of s.instances)
39
+ console.log(chalk.gray(` ${p.pid} ${p.executable}`));
40
+ console.log(chalk.gray(' Fix it with `agents menubar setup`.'));
41
+ }
42
+ if (s.foreignInstances.length > 0) {
43
+ // RegisterEventHotKey is first-come, so the helper that registered the
44
+ // chord first owns Cmd-Shift-V/O. A process list cannot say which that
45
+ // was — only that a rival exists — so report the conflict, not a winner.
46
+ // The loser has no other symptom: its chords simply never fire.
47
+ const n = s.foreignInstances.length;
48
+ console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
49
+ for (const p of s.foreignInstances)
50
+ console.log(chalk.gray(` ${p.pid} ${p.executable}`));
51
+ console.log(chalk.gray(' End them with `agents menubar setup`.'));
52
+ }
53
+ if (s.stale) {
54
+ console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar setup` now.'));
55
+ }
56
+ else if (!s.serviceInstalled && !s.disabledByUser) {
57
+ console.log(chalk.gray('\n Set it up with `agents menubar setup`.'));
58
+ }
59
+ }
60
+ function printSetupResult(r) {
61
+ console.log(chalk.bold('Menu bar setup\n'));
62
+ for (const step of r.steps) {
63
+ const mark = step.outcome === 'failed' ? chalk.red('✗')
64
+ : step.outcome === 'changed' ? chalk.green('+') : chalk.green('✓');
65
+ console.log(` ${mark} ${step.name.padEnd(15)} ${chalk.gray(step.detail)}`);
66
+ }
67
+ console.log();
68
+ if (r.configured) {
69
+ console.log(chalk.green('Menu bar configured.') + chalk.gray(' One agents mark, started at login.'));
70
+ }
71
+ else {
72
+ console.log(chalk.red('Menu bar not fully configured.') + chalk.gray(' See the failed step above.'));
73
+ }
74
+ }
18
75
  export function registerMenubarCommands(program) {
19
76
  const menubar = program
20
77
  .command('menubar')
21
78
  .description('Manage the macOS menu-bar helper (running sessions, agents awaiting input, routines)');
79
+ // `setup` is the one command that gets a machine to the intended state:
80
+ // exactly one status item, started at login. `enable` stays the narrow
81
+ // install+start; setup adds duplicate cleanup and verifies the end state.
82
+ const setup = menubar
83
+ .command('setup')
84
+ .description('Configure the menu bar end-to-end: one instance, started at login')
85
+ .option('--check', 'Report the current state, change nothing')
86
+ .option('--json', 'Emit machine-readable JSON')
87
+ .action((options) => {
88
+ if (options.check) {
89
+ const s = getMenubarStatus();
90
+ if (options.json) {
91
+ process.stdout.write(JSON.stringify(s) + '\n');
92
+ return;
93
+ }
94
+ if (notMac())
95
+ return;
96
+ printStatus(s);
97
+ return;
98
+ }
99
+ if (!options.json && notMac())
100
+ return;
101
+ const r = runMenubarSetup();
102
+ if (options.json) {
103
+ process.stdout.write(JSON.stringify(r) + '\n');
104
+ }
105
+ else {
106
+ printSetupResult(r);
107
+ }
108
+ if (!r.configured)
109
+ process.exitCode = 1;
110
+ });
111
+ setHelpSections(setup, {
112
+ examples: `
113
+ # Configure the menu bar end-to-end (idempotent — safe to re-run)
114
+ agents menubar setup
115
+
116
+ # Two agents marks in the menu bar? This ends the duplicate.
117
+ agents menubar setup
118
+
119
+ # See the current state without changing anything
120
+ agents menubar setup --check
121
+ `,
122
+ notes: `
123
+ Configures, in order: every running helper ended, the helper bundle at
124
+ ~/Library/Application Support/agents-cli, its code signature, the launchd
125
+ login item (com.phnx-labs.agents-menubar — RunAtLoad + KeepAlive), then
126
+ verifies exactly one helper came back up.
127
+
128
+ Every running helper is ended and launchd restarts one, so the survivor is
129
+ always the login-managed copy. Exits nonzero if it cannot reach that state.
130
+
131
+ Setup clears a previous \`agents menubar disable\`. To turn the menu bar off
132
+ again, run \`agents menubar disable\`.
133
+ `,
134
+ });
22
135
  menubar
23
136
  .command('enable')
24
137
  .description('Install and start the menu-bar helper (launches at login)')
@@ -40,7 +153,7 @@ export function registerMenubarCommands(program) {
40
153
  if (notMac())
41
154
  return;
42
155
  disableMenubarService();
43
- console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar enable`.'));
156
+ console.log(chalk.green('Menu bar helper disabled.') + chalk.gray(' Re-enable any time with `agents menubar setup`.'));
44
157
  });
45
158
  menubar
46
159
  .command('status')
@@ -56,33 +169,7 @@ export function registerMenubarCommands(program) {
56
169
  console.log(chalk.yellow('The menu bar helper is macOS only.'));
57
170
  return;
58
171
  }
59
- const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
60
- console.log(chalk.bold('Menu bar helper\n'));
61
- console.log(` running ${yn(s.running)}`);
62
- console.log(` service installed ${yn(s.serviceInstalled)}`);
63
- console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
64
- console.log(` installed version ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
65
- console.log(` current version ${chalk.gray(s.currentVersion)}`);
66
- console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
67
- console.log(` disabled by user ${yn(s.disabledByUser)}`);
68
- if (s.foreignInstances.length > 0) {
69
- // RegisterEventHotKey is first-come, so the helper that registered the
70
- // chord first owns Cmd-Shift-V/O. A process list cannot say which that
71
- // was — only that a rival exists — so report the conflict, not a
72
- // winner. The loser
73
- // has no other symptom: its chords simply never fire.
74
- const n = s.foreignInstances.length;
75
- console.log(chalk.yellow(`\n ${n} other helper process${n === 1 ? '' : 'es'} running — ${n === 1 ? 'it' : 'they'} may hold Cmd-Shift-V/O instead of the installed one:`));
76
- for (const p of s.foreignInstances)
77
- console.log(chalk.gray(` ${p.pid} ${p.executable}`));
78
- console.log(chalk.gray(' End it, then `agents menubar enable` to restart the installed helper.'));
79
- }
80
- if (s.stale) {
81
- console.log(chalk.yellow('\n Installed helper is stale — runs on next `agents` startup, or `agents menubar enable` now.'));
82
- }
83
- else if (!s.serviceInstalled && !s.disabledByUser) {
84
- console.log(chalk.gray('\n Enable it with `agents menubar enable`.'));
85
- }
172
+ printStatus(s);
86
173
  });
87
174
  // Bare `agents menubar` -> status.
88
175
  menubar.action(() => {
@@ -91,10 +178,6 @@ export function registerMenubarCommands(program) {
91
178
  console.log(chalk.yellow('The menu bar helper is macOS only.'));
92
179
  return;
93
180
  }
94
- const yn = (b) => (b ? chalk.green('yes') : chalk.gray('no'));
95
- console.log(chalk.bold('Menu bar helper\n'));
96
- console.log(` running ${yn(s.running)}`);
97
- console.log(` service installed ${yn(s.serviceInstalled)}`);
98
- console.log(chalk.gray('\n enable | disable | status'));
181
+ printStatus(s, { brief: true });
99
182
  });
100
183
  }