@phnx-labs/agents-cli 1.22.51 → 1.22.53

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 (117) hide show
  1. package/CHANGELOG.md +238 -0
  2. package/README.md +1 -1
  3. package/dist/commands/accounts.js +1 -1
  4. package/dist/commands/attach.js +7 -0
  5. package/dist/commands/browser.js +118 -56
  6. package/dist/commands/daemon.d.ts +2 -0
  7. package/dist/commands/daemon.js +8 -4
  8. package/dist/commands/detach.js +1 -1
  9. package/dist/commands/exec.js +16 -9
  10. package/dist/commands/fleet-capture.js +7 -0
  11. package/dist/commands/focus.d.ts +1 -10
  12. package/dist/commands/focus.js +16 -79
  13. package/dist/commands/go.d.ts +26 -0
  14. package/dist/commands/go.js +65 -6
  15. package/dist/commands/monitors.js +1 -1
  16. package/dist/commands/repo.js +31 -3
  17. package/dist/commands/sessions-inject.js +8 -3
  18. package/dist/commands/sessions-picker.js +2 -1
  19. package/dist/commands/sessions-resume.d.ts +1 -0
  20. package/dist/commands/sessions-resume.js +13 -2
  21. package/dist/commands/sessions-stop.js +1 -1
  22. package/dist/commands/sessions.d.ts +23 -13
  23. package/dist/commands/sessions.js +69 -39
  24. package/dist/commands/setup-browser.d.ts +5 -2
  25. package/dist/commands/setup-browser.js +14 -29
  26. package/dist/commands/setup-preferences.d.ts +22 -3
  27. package/dist/commands/setup-preferences.js +25 -8
  28. package/dist/commands/share.js +12 -8
  29. package/dist/commands/ssh.js +35 -12
  30. package/dist/commands/status.js +5 -0
  31. package/dist/commands/sync.js +102 -2
  32. package/dist/commands/tmux.d.ts +8 -1
  33. package/dist/commands/tmux.js +167 -17
  34. package/dist/lib/account-registry.d.ts +15 -5
  35. package/dist/lib/account-registry.js +150 -50
  36. package/dist/lib/answer-router.js +2 -1
  37. package/dist/lib/browser/ipc.d.ts +44 -0
  38. package/dist/lib/browser/ipc.js +120 -8
  39. package/dist/lib/browser/profiles.d.ts +57 -17
  40. package/dist/lib/browser/profiles.js +77 -53
  41. package/dist/lib/browser/registry.d.ts +44 -14
  42. package/dist/lib/browser/registry.js +141 -45
  43. package/dist/lib/browser/runtime-state.d.ts +4 -2
  44. package/dist/lib/browser/runtime-state.js +4 -2
  45. package/dist/lib/browser/service.js +4 -3
  46. package/dist/lib/channels/owner-forward.d.ts +88 -0
  47. package/dist/lib/channels/owner-forward.js +116 -0
  48. package/dist/lib/channels/owner-sink.js +7 -0
  49. package/dist/lib/daemon/runner.js +10 -2
  50. package/dist/lib/device-config.js +3 -2
  51. package/dist/lib/devices/config-migration.js +147 -1
  52. package/dist/lib/devices/device-docs.d.ts +35 -0
  53. package/dist/lib/devices/device-docs.js +163 -0
  54. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  55. package/dist/lib/devices/discovery-policy.js +31 -21
  56. package/dist/lib/devices/registry.d.ts +11 -5
  57. package/dist/lib/devices/registry.js +46 -18
  58. package/dist/lib/exec.d.ts +66 -28
  59. package/dist/lib/exec.js +71 -26
  60. package/dist/lib/feed/feed.d.ts +10 -2
  61. package/dist/lib/feed/feed.js +12 -1
  62. package/dist/lib/feed-broadcast.js +15 -1
  63. package/dist/lib/git.d.ts +93 -0
  64. package/dist/lib/git.js +232 -0
  65. package/dist/lib/hosts/dispatch.d.ts +4 -3
  66. package/dist/lib/hosts/dispatch.js +12 -8
  67. package/dist/lib/hosts/providers/local.d.ts +9 -3
  68. package/dist/lib/hosts/providers/local.js +23 -12
  69. package/dist/lib/hosts/reconnect.d.ts +7 -4
  70. package/dist/lib/hosts/reconnect.js +29 -25
  71. package/dist/lib/hosts/registry.js +4 -1
  72. package/dist/lib/hosts/remote-os.js +3 -1
  73. package/dist/lib/monitors/remote.d.ts +18 -1
  74. package/dist/lib/monitors/remote.js +15 -2
  75. package/dist/lib/notify.d.ts +7 -0
  76. package/dist/lib/notify.js +15 -1
  77. package/dist/lib/session/active.d.ts +10 -1
  78. package/dist/lib/session/active.js +7 -1
  79. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  80. package/dist/lib/session/actor-sidecar.js +2 -0
  81. package/dist/lib/session/db.d.ts +1 -1
  82. package/dist/lib/session/db.js +39 -3
  83. package/dist/lib/session/discover.js +7 -12
  84. package/dist/lib/session/live-metadata.js +1 -0
  85. package/dist/lib/session/local-tmux-attach.d.ts +69 -0
  86. package/dist/lib/session/local-tmux-attach.js +164 -0
  87. package/dist/lib/session/pid-registry.d.ts +7 -0
  88. package/dist/lib/session/prompt.d.ts +15 -0
  89. package/dist/lib/session/prompt.js +21 -0
  90. package/dist/lib/session/remote-active.d.ts +8 -0
  91. package/dist/lib/session/remote-active.js +1 -0
  92. package/dist/lib/session/types.d.ts +17 -0
  93. package/dist/lib/session/types.js +10 -0
  94. package/dist/lib/share/publish.d.ts +8 -11
  95. package/dist/lib/share/publish.js +16 -20
  96. package/dist/lib/share/worker-template.js +104 -12
  97. package/dist/lib/state.d.ts +8 -0
  98. package/dist/lib/state.js +143 -11
  99. package/dist/lib/sync-status.d.ts +17 -0
  100. package/dist/lib/sync-status.js +21 -2
  101. package/dist/lib/terminal/resolve.d.ts +7 -0
  102. package/dist/lib/terminal/resolve.js +41 -2
  103. package/dist/lib/tmux/index.d.ts +1 -1
  104. package/dist/lib/tmux/index.js +1 -1
  105. package/dist/lib/tmux/session.d.ts +10 -0
  106. package/dist/lib/tmux/session.js +29 -0
  107. package/dist/lib/traces/insights.d.ts +67 -0
  108. package/dist/lib/traces/insights.js +178 -0
  109. package/dist/lib/traces/phenotype.d.ts +67 -0
  110. package/dist/lib/traces/phenotype.js +437 -0
  111. package/dist/lib/traces/segments.d.ts +133 -0
  112. package/dist/lib/traces/segments.js +301 -0
  113. package/dist/lib/traces/sync.d.ts +33 -0
  114. package/dist/lib/traces/sync.js +11 -2
  115. package/dist/lib/types.d.ts +47 -1
  116. package/dist/lib/watchdog/runner.js +18 -4
  117. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,243 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.53
4
+
5
+ - **Custom-harness runs launched via a profile now show their real harness name in agents sessions instead of 'claude' (PHNX-2935).**
6
+
7
+ - **Watchdog un-addressable stalls now include the same recovery command as `agents focus` (PHNX-3070).**
8
+ Rotate, needs-human declared-block, and nudge-refuse paths append `addressabilityRecoveryHint` so an un-addressable session prints `agents sessions resume <id>` / tmux wrap instead of a reason-only skip. Source: `cli/src/lib/watchdog/runner.ts`.
9
+
10
+ - **Un-addressable live sessions now surface a clear recovery hint at the point of use** — `agents focus`, `sessions inject`, and the session picker report when a bare interactive session has no addressable terminal rail (not tmux/iTerm/an IDE terminal/a pty sidecar) instead of failing silently, and the hint now names the session's real id in `agents sessions resume <id>` rather than a placeholder (PHNX-3070).
11
+
12
+ - **Vitest can no longer launch the developer's browser (PHNX-3072).** `tests/setup.ts`
13
+ sandboxed `HOME` but not desktop-opener binaries, so a test that reached a real
14
+ `spawn('open' | 'xdg-open')` opened the URL on the developer's machine — green on
15
+ Linux CI, where `xdg-open` is absent, and disruptive locally. The harness now
16
+ prepends stub `open` / `xdg-open` / `gnome-open` binaries to `PATH` so a spawn
17
+ cannot reach the real handler; an unauthorized spawn fails the file. Source:
18
+ `cli/tests/opener-sandbox.ts`.
19
+
20
+ - **Feed attention stamps `sourceCursor` at block-write time (PHNX-3073).**
21
+ `coveredByResolution` conservatively suppresses a candidate with no comparable
22
+ cursor when a tombstone exists, which is correct for a stale lifecycle re-read
23
+ but buried a genuinely new open block whenever `session.lastActivityMs` was
24
+ unresolvable (cloud / remote / index-lag) because neither writer stamped a
25
+ cursor. `buildDeclaredBlock` and the feed-publish hook now stamp
26
+ `sourceCursor.lastActivityMs` from the write instant, so a fresh generation
27
+ compares strictly newer than any prior tombstone. Source:
28
+ `cli/src/lib/feed/feed.ts`.
29
+
30
+ - **Cursor auto-titles go through the same skill-scaffolding cleaner as Claude (PHNX-3123).**
31
+ `readCursorMeta` took `chatMeta.title` verbatim, so a Cursor session whose
32
+ server-generated title echoed the injected `Base directory for this skill: …`
33
+ line kept that path as `SessionMeta.label` — the field that wins on every
34
+ surface for the session's whole life. Claude's `ai-title` was already collapsed
35
+ to `/<skill>` at label composition (PR #2995); that collapse now lives in one
36
+ shared helper (`cleanGeneratedSessionLabel`) and Cursor's title takes the same
37
+ path. An ordinary Cursor title, and one that merely names a `skills/…` path,
38
+ is unchanged. A Claude `/rename` (`custom-title`) is still never rewritten.
39
+ Codex / OpenCode / Kimi / Droid put auto-titles on `topic`, not `label`, so
40
+ they are out of this change. **Not retroactive:** already-indexed labels stay
41
+ until the transcript next changes (RUSH-3122). Source:
42
+ `cli/src/lib/session/prompt.ts`, `cli/src/lib/session/discover.ts`.
43
+
44
+ - **Traces insight engine — cross-session failure clustering + wasted time (PHNX-3141).** `agents traces sync`'s `index.json` now carries `failurePatterns[]` (top-K, ranked by wasted time, not raw occurrence count — a rare 8-hour retry loop still outranks a frequent 2-minute one), `wastedMsTotal`, and `latency` (time-to-first-tool p50/p90/p99/max). Failed `tool_calls` are grouped by `(tool, cause, normalized error)` so near-identical failures (a GitHub rate-limit message with a different countdown each time) fold into one pattern instead of a wall of near-duplicates. Per-session `SessionDetail` also gains `surfacedToolFailures` — every failed step, listed regardless of the run's overall outcome, so a session that ultimately succeeded still shows the tool failures it hit along the way. Source: `cli/src/lib/traces/insights.ts`, `cli/src/lib/traces/sync.ts`.
45
+
46
+ - **Traces insight group-by dimensions (PHNX-3141).** `cli/src/lib/traces/segments.ts` derives the console bar axes — agent (model × harness), task type, failure timing — and the time-to-first-tool latency insight (`firstToolMs` p50/p90/p99/max). Pure functions; the sync integrator wires them into the shard. Source: `cli/src/lib/traces/segments.ts`.
47
+
48
+ - **Native-account labels live on the central `accounts.native` rows in agents.yaml (already fleet-synced via `agents repo push/pull`). `remove`/`rename`/`label` operate on every row matching `(agent, identityKey)`, so a post-merge duplicate cannot silently survive (PHNX-3206).**
49
+
50
+ - **The top-level `agents.yaml` header heals itself instead of freezing (PHNX-3315).**
51
+ `serializeCentral` parses the committed file to preserve its hand-written body
52
+ comments, but that also preserved the leading metadata header verbatim — so a
53
+ top-level `agents.yaml` written before the `agi-cli` rename kept its stale
54
+ 3-line header (repo `agents-cli`, no `$schema:` line) forever while every
55
+ freshly-written device doc got the current header. The header is now rewritten
56
+ to the current `META_HEADER` on the next central write, textually (the `yaml`
57
+ library folds the leading comment onto the first key when that key already
58
+ carries a comment, so `doc.commentBefore` is unreliable), leaving body comments
59
+ and every config key intact, and byte-stable once healed so it never re-churns.
60
+ Source: `cli/src/lib/state.ts`.
61
+ - **The legacy central `browser:` tombstone drains itself on `agents sync` (PHNX-3315).**
62
+ New browser profiles write the per-device doc, but profiles created before the
63
+ device-scoped store lingered in the shared top-level `agents.yaml` and churned
64
+ every fleet pull until someone ran `agents browser profiles claim` by hand.
65
+ `agents sync` now auto-evicts a lingering central profile into this device's
66
+ doc and clears it from central — but ONLY for remote (`ssh://`) profiles. An
67
+ `ssh://` endpoint names a specific host, so the profile is fungible by design:
68
+ any box resolves it to the same browser, so a concurrent cross-machine
69
+ double-claim is harmless. A local/`cdp://` profile has no per-machine ownership
70
+ signal ("that browser is installed here" is not "I hold this profile's
71
+ credentialed session"), so auto-claiming it would let two boxes with the same
72
+ common browser each claim the same tombstone and flip a credentialed profile
73
+ from identity-bearing to fungible fleet-wide — the exact logged-out-browser
74
+ failure this module exists to prevent. Local/cdp tombstones are left central for
75
+ an explicit `agents browser profiles claim`. The claim is selected and committed
76
+ inside a single meta-lock acquisition, and non-throwing (a config conflict or
77
+ unhostable profile is left central rather than wedging the sync). `profileRegistry` is left as the single source of truth: a
78
+ claimed profile lives in the device doc alone, never double-counted across two
79
+ stores. Source: `cli/src/lib/browser/registry.ts`, `cli/src/commands/sync.ts`,
80
+ `cli/src/lib/state.ts`.
81
+
82
+ - **`fleet.discovery` / `fleet.ignored` are device-scoped with a union-on-read view (PHNX-3315).**
83
+ Every box used to rewrite one shared `fleet.discovery`/`fleet.ignored` map in the
84
+ top-level `agents.yaml` (each dismissal stamped `ignoredOn:<host>`), so two boxes
85
+ dismissing different peers produced guaranteed git line-conflicts on the next
86
+ pull. Each box now records ONLY its own discovery decisions and dismissals in its
87
+ tracked device doc (`devices/<host>/agents.yaml` `fleet:` block), and the
88
+ effective fleet view is recomputed as a deterministic, order-independent UNION
89
+ across every device doc at read time — `ignored` beats `approved` for a
90
+ conflicting name, and the newest `ignoredAt` wins a shared dismissal — so every
91
+ box computes the identical view and no box ever rewrites another's file. A
92
+ fold-then-delete migration moves the existing central maps into the owning box's
93
+ device doc before the central keys are dropped (the device-doc write lands before
94
+ the central strip, so a crash re-folds idempotently). `agents devices
95
+ register/ignore/unignore/ignored`, discovery reconcile, the sync pending-diff, and
96
+ `agents fleet capture` all read the union. Source:
97
+ `cli/src/lib/devices/{discovery-policy,registry,device-docs,config-migration}.ts`,
98
+ `cli/src/commands/{ssh,fleet-capture}.ts`, `cli/src/lib/state.ts`.
99
+ - **Agent-host registrations are device-scoped (`deviceHosts`) with a merged read (PHNX-3315).**
100
+ Locally-discovered SSH hosts and inline registrations landed in one shared
101
+ `hosts:` map in the top-level `agents.yaml`, so every box that enrolled a host
102
+ rewrote the fleet-shared file. Registrations now write this box's device doc
103
+ `hosts:` block; the effective host directory is the cross-box union (newest
104
+ `addedAt` wins a name collision), and `remove` only edits this box's own doc. A
105
+ fold-then-delete migration relocates the existing central `hosts:` map into this
106
+ box's device doc and drops the central key. Source:
107
+ `cli/src/lib/hosts/{providers/local,registry,remote-os}.ts`,
108
+ `cli/src/lib/devices/{device-docs,config-migration}.ts`, `cli/src/lib/state.ts`.
109
+ - **`scope:'device'` native accounts move off the shared file into the device doc (PHNX-3315).**
110
+ `addNativeAccount` accepted `scope:'device'` but always wrote the central
111
+ `accounts.native` map, so a per-box login (and its email / `identityKey` PII)
112
+ landed in the git-tracked, fleet-synced `agents.yaml`. A native login is
113
+ machine-local, so its home now follows its scope: a `scope:'device'` identity
114
+ (and any binding that targets it) writes this box's device doc `accounts:` block,
115
+ keeping its identity PII off the shared file; genuinely fleet-shared account
116
+ `defaults` and `scope:'version'` identities stay central. Every account reader
117
+ (`listNativeAccounts`, `findUnifiedAccount`, binding resolution, rename/remove)
118
+ merges the fleet-shared central store with this box's device slice. A
119
+ fold-then-delete migration relocates existing `scope:'device'` natives and their
120
+ bindings out of central (forward-only; no history rewrite). Source:
121
+ `cli/src/lib/account-registry.ts`, `cli/src/lib/devices/config-migration.ts`,
122
+ `cli/src/lib/state.ts`.
123
+
124
+ - **`tmux.enabled` now gates remote `--device` runs too — no more forced tmux wrap (PHNX-3316).**
125
+ `resolveTmuxWrap` (`cli/src/lib/exec.ts`) no longer lets `remoteDispatch`
126
+ override the device's `tmux.enabled`: off (the default) means a followed
127
+ `agents run --device <host>` spawns the agent directly, with no tmux session
128
+ on the peer. Durability for a bare remote run comes from the existing
129
+ auto-reconnect in `cli/src/lib/hosts/reconnect.ts`, whose target
130
+ (`agents sessions focus <id> --local`) already resumes the harness session in
131
+ place when no pane exists — a dropped link costs the in-flight turn, not the
132
+ conversation. One case still wraps regardless: a followed remote run whose
133
+ launcher has no TTY (CI, scripts, another agent driving the CLI), where the
134
+ detached pane is the run's only interface.
135
+ `undurable` is now reached only when a wrap was actually wanted (opted-in
136
+ device, or the no-TTY case) but the peer has no tmux; its refusal message
137
+ names the `agents ssh <host> 'agents devices config <host> tmux.enabled off'`
138
+ alternative (the key is machine-local, so it must be set on the peer)
139
+ alongside `--raw`.
140
+ RUSH-3125's forced wrap conflated durability with an ergonomics preference
141
+ and wrapped boxes whose operator had explicitly left tmux off.
142
+ `hosts/reconnect.ts`'s header premise, the `hosts/dispatch.ts`
143
+ REMOTE_INTERACTIVE_ENV comments, and the `tmux.enabled` help text
144
+ (`device-config.ts`) are updated to match; wrap-matrix tests cover the new
145
+ gate.
146
+
147
+ - **Share attribution bar: full-width chrome, not a floating box.** The bar was injected `position:sticky` inside `<body>`, so on a page whose body is a narrow centered column (`max-width` + `margin:auto`) it inherited that width and rendered as a floating black rectangle. It's now `position:fixed`, full viewport width, with an injected `html{padding-top}` pushing the page content down beneath it — clean top chrome on any page layout. `flex-wrap:nowrap` + left-side ellipsis keep it one line so the push height stays correct. Source: `cli/src/lib/share/worker-template.ts`.
148
+
149
+ ## 1.22.52
150
+
151
+ - **`agents artifacts share --visibility me|org` — identity-gated share pages (PHNX-3260).**
152
+ Extends the P1 `public|unlisted` tiers (RUSH-3135) with two Phoenix-gated GET
153
+ visibilities. `me` is visible only to the signed-in owner (the viewer's Phoenix
154
+ `userId` must equal the stamped `owner`); `org` is visible to any signed-in
155
+ Phoenix user whose **verified email domain** matches the owner's company domain
156
+ (`org_domain`, stamped from the owner's verified email on PUT). A mismatch on
157
+ either returns **404** — the same body as a missing object — so a wrong viewer
158
+ cannot even learn the page exists. Both are hidden from the public gallery and
159
+ `share list`, like `unlisted`. Publishing `org` from a public inbox
160
+ (gmail/googlemail/outlook/hotmail/live/icloud/me) is refused with **400**, and
161
+ `me`/`org` PUT without a Phoenix identity is a loud 400 (a BYO `WRITE_TOKEN`
162
+ alone cannot publish them). Browser GET of an unauthenticated `me`/`org` URL
163
+ **302**s to `${PHOENIX_ID_BASE}/login?return=<url>` (the same Google OAuth as
164
+ `/device`, no new IdP); the returned one-time `phoenix_ticket` is redeemed once
165
+ via `POST /api/v1/auth/ticket` and exchanged for an HttpOnly, HMAC-signed
166
+ `__Host-phoenix_share` cookie on the share host. An existing CLI
167
+ `Authorization: Bearer` still works with no redirect. `me`/`org` responses send
168
+ `Cache-Control: private, no-store` and `X-Robots-Tag: noindex`;
169
+ `public`/`unlisted` GET is unchanged (anonymous 200). Because the Worker template
170
+ changed, **already-provisioned BYO endpoints need `agents artifacts share update`**
171
+ to serve the new gate. Source: `cli/src/lib/share/worker-template.ts`,
172
+ `cli/src/lib/share/{backend,publish}.ts`, `cli/src/commands/share.ts`.
173
+
174
+ - **Harden the browser daemon against the socket-timeout wedge (PHNX-3289).** Four
175
+ fixes to the browser/daemon reliability surface. `agents daemon status` no longer
176
+ errors on macOS with `ps: etimes: keyword not found` — the process-uptime probe now
177
+ shells the portable `ps -o etime=` and parses it with the same helper the keychain
178
+ reaper uses. `waitForSocket` raises its ceiling from a flat 6s to 15s and re-probes
179
+ across an IPC-server restart (requiring two stable accepts), so a browser
180
+ start/navigate that lands in a restart window no longer intermittently throws
181
+ `Timeout waiting for browser daemon socket`. New `agents browser stop --daemon` stops
182
+ the browser daemon and clears a stale/wedged `browser.sock`, failing loud if a live
183
+ server still holds it, so the next `start` comes up clean. And `agents browser start
184
+ --device <host>` now resolves the browser/profile on the TARGET device instead of the
185
+ local one — a bare start from a browserless box forwards to `--device` instead of
186
+ failing with a misleading `No supported browser found`. Source:
187
+ `cli/src/commands/daemon.ts`, `cli/src/lib/browser/ipc.ts`, `cli/src/commands/browser.ts`.
188
+
189
+ - **Managed share: a BYO-published page no longer locks the rightful Phoenix owner out of their own handle (PHNX-3291).** The share Worker's handle-ownership check (`assertHandleOwner`) scanned every page under a namespace and 409'd `handle taken` when any page's stamped `owner` differed from the writer's Phoenix `userId`. But a BYO `WRITE_TOKEN` publish stamps `owner = SHARE_NAMESPACE` (the namespace string, e.g. `octocat`), not a userId — so once a namespace held any BYO page, its legitimate Phoenix handle owner got a 409 on every subsequent publish, and could neither publish nor change a page's `--visibility` (which blocked the me/org tiers from PHNX-3260 for that user entirely). The `__handles/<handle>` claim object is now authoritative when it exists: the recorded userId may write and anyone else is refused, regardless of a stray page's owner stamp. The page-owner scan remains as the pre-claim fallback but ignores BYO namespace stamps (`owner === handle`), so a first Phoenix publish can still claim a handle previously used only by BYO pages. Source: `cli/src/lib/share/worker-template.ts`. Requires `agents artifacts share update` on already-provisioned endpoints (the Worker template hash changed).
190
+
191
+ - **`sessions resume`/`sessions attach`/`sessions focus` no longer SSH-probe the fleet before attaching a live local tmux pane (PHNX-3292).** First unique match now wins: a selector naming a live pane on THIS box — the full `ag-<agent>-<8hex>` alias `agents tmux ls` prints, or its bare 8-hex suffix when it names exactly one live local pane — attaches with zero SSH, for bare `sessions resume <alias>` and deprecated `sessions attach <alias>` too, not just `--attach-only`. Previously `collectSessionCandidates` ran first — two fleet sweeps (transcript pool + live roster) that stall on offline devices (~2 minutes measured on yosemite-s0, two copies of `unreachable or no agents CLI — skipped`) — and bare `resume` of a live alias didn't even reach the tmux-attach path at all. A miss still races the reachable fleet in parallel: `isDefinitiveMatch`/`selectorAllowsEarlyExit` (RUSH-2203) now cover a tmux alias and an exact 8-hex short id, not just a full UUID, so the first peer to answer with that pane aborts the rest of the sweep instead of waiting out every offline box's timeout. Two LIVE panes (local or fleet-answered) sharing the same 8-hex suffix fail closed with both names printed, rather than guessing; a dead/retained pane is never attached. `--device` skips the local gate entirely and scopes the race to the named box(es). Source: `cli/src/lib/session/local-tmux-attach.ts` (new, `attachLocalLiveSelector`), `cli/src/commands/{sessions,sessions-resume,attach,focus}.ts`.
192
+
193
+ - **`agents tmux attach` tears the session down when the agent exits (PHNX-3293).**
194
+ Attaching to a wrapped pane and exiting the agent (`/exit`, Ctrl-c) used to
195
+ print `[detached]` and leave the tmux session in `tmux ls` — the v6 pane-died
196
+ hook only `detach-client`s when a client is attached, and the attach verbs
197
+ never killed the `remain-on-exit` husk. `tmux attach`, `sessions focus` /
198
+ `resume --attach-only`, and `go`'s local-tmux attach now destroy the session
199
+ when every pane is dead and still keep it on Ctrl-b d. A piped (no-TTY)
200
+ local `agents run --interactive` no longer wraps in tmux, so session-tracker
201
+ tests cannot leak live "trust this folder" panes. `agents tmux kill` with no
202
+ name opens a picker whose preview is the pane's last screen (so a leaked
203
+ first-run dialog is distinguishable from real work); `tmux ls` prints that
204
+ snippet on each row. Source:
205
+ `cli/src/lib/tmux/session.ts`, `cli/src/commands/tmux.ts`,
206
+ `cli/src/commands/focus.ts`, `cli/src/lib/exec.ts`.
207
+
208
+ - **`agents browser start` no longer silently mints a logged-out `auto-chrome` (PHNX-3296).** With no configured default and no existing launchable profile, start now errors and points at `agents setup` / `agents browser use <name>` / the `browser.device` fleet hub instead of popping a signed-out Chrome. A pre-existing `auto-chrome` or legacy `default` still resolves. `agents setup`'s browser pick replaces the misleading "auto-detect on first use" opt-out with an explicit "None — this box uses the fleet hub" choice. Source: `cli/src/lib/browser/profiles.ts`, `cli/src/commands/setup-preferences.ts`, `cli/src/commands/browser.ts`.
209
+
210
+ - **`sessions detach` / `sessions stop` of a unique live local session no longer wait for fleet SSH timeouts (PHNX-3298).** A live pane already visible locally is the hit: a full UUID or a unique 8-hex prefix skips `gatherRemoteActive` entirely, so a sleeping peer cannot print `unreachable or no agents CLI` or stall SIGTERM. Two local matches fail closed without waiting for unanswered boxes. A genuine miss still races the fleet and aborts remaining SSH on the first unique reachable live row; browse / `focus` with no id stays all-settle. Source: `cli/src/commands/go.ts`, `cli/src/commands/detach.ts`, `cli/src/commands/sessions-stop.ts`.
211
+
212
+ - **`agents monitors add` refuses immediately when a reachable peer already has the same watcher (PHNX-3299).** The fleet duplicate guard used to collect every peer before reporting a clash, so a single slow box forced the full 12-second timeout even when another peer had already answered with the same fingerprint. The fan-out now aborts remaining SSH captures the moment a peer reports a matching monitor, while the no-match path still waits for the whole fleet so uniqueness can be proved. Source: `cli/src/commands/monitors.ts`, `cli/src/lib/monitors/remote.ts`.
213
+
214
+ - **`agents repo sync user` self-heals a non-git or partial `~/.agents` by adopting it in place — no re-clone, no data loss (PHNX-3301).** When the user-layer checkout lost (or never had) its `.git`, sync hard-failed with `Not a git repo: ~/.agents` and the only fix was a destructive re-clone that wiped runtime state (`.cache` / `.history` / `scratch` / device config). It now git-backs the existing directory in place with plumbing only (`init` / `remote` / `fetch` / `update-ref` / `read-tree` / `checkout-index` / `restore`, so it never trips the fleet git-guard): materializes only the MISSING tracked files, restores a stale-stub top-level `agents.yaml` from origin (backing up the previous copy to `.history/agents.yaml.pre-adopt.bak` first), preserves gitignored runtime state, and surfaces (never clobbers) real local edits. The remote URL is resolved from an existing `origin`, `AGENTS_USER_REPO_URL`, or a device-local record left by a prior healthy sync — never hardcoded. `agents sync status` now flags a non-git `~/.agents` as a distinct drift state instead of burying it as "N missing". Source: `cli/src/lib/git.ts`, `cli/src/commands/sync.ts`, `cli/src/commands/repo.ts`, `cli/src/lib/sync-status.ts`, `cli/src/commands/status.ts`.
215
+
216
+ - **Owner notifications forward to a capable fleet peer when this box can't deliver (PHNX-3303).**
217
+ The owner's delivery provider for the rush-backed channels (imessage / telegram /
218
+ slack / discord via the `rush` CLI) is macOS-only, so `agents feed post --level
219
+ important` / `--blocked`, `agents notify`, and monitor `notify` actions run from a
220
+ headless Linux worker used to record the post but fail to reach the owner with
221
+ `owner failed: … rush CLI not found on PATH`. They now hand the delivery over SSH to
222
+ a reachable macOS peer that DOES have the provider — the same reroute
223
+ `agents message` already uses — instead of stranding the important post. It is
224
+ best-effort: it never throws or blocks the post, only the macOS-only rush family
225
+ triggers a forward (a Linux-capable transport like openclaw-telegram stays local),
226
+ and when no capable peer is reachable the existing clean local error stands. A box
227
+ that received a forward never forwards onward (`AGENTS_OWNER_NO_FORWARD` guard).
228
+ Source: `cli/src/lib/channels/owner-forward.ts`, `cli/src/lib/notify.ts`,
229
+ `cli/src/lib/feed-broadcast.ts`.
230
+
231
+ - **`agents artifacts share <file>` now derives a stable slug automatically (PHNX-3310).**
232
+ The default comes from the HTML `<title>` or Markdown frontmatter `title:`, then
233
+ the filename, so repeat publishes of a long-running artifact update the same URL;
234
+ `--slug` remains an optional explicit override. Source:
235
+ `cli/src/lib/share/publish.ts`, `cli/src/commands/share.ts`.
236
+
237
+ - **The release attestation now shards its test suite across the fleet instead of pinning one box.** `release-attestation-produce.sh` — the full-suite run that mints a release's exact-tree attestation — ran the ~13k-test suite on a single auto-picked box at `--maxWorkers=2` (~880s measured, ~14 min per attestation). It now fans the suite across the fleet via `test.sh --shard N` by default, resolving N from the eligible workers `agents devices pick` reports (capped) and falling back to a single box only when fewer than two are eligible. The suite is throughput-bound, so this is ~1/N the wall time (~269s on one box → ~31s on nine). Each shard still runs vitest at `--maxWorkers=2 --retry=2`, so the RUSH-3015 per-box flake mitigation is unchanged — sharding adds machines, not per-box concurrency. New `--test-shard <n>` / `--test-devices a,b,c` overrides mirror `test.sh`. Source: `cli/scripts/release-attestation-produce.sh`.
238
+
239
+ - **Shared pages now show who shared them and — the point — a visibility cue (PHNX-3260 follow-up).** The share Worker injects a slim attribution bar at the top of every served HTML page: `Shared by <handle>`, `Made with <agent>`, the date, and a colour-coded **visibility chip** — 🔒 `Only you` (me), 🏢 `Anyone at <org>` (org), 🌐 `Public`, 🔗 `Unlisted` — so a viewer can tell at a glance who can see the page, the thing the raw served HTML never surfaced. All values come from metadata the Worker already stamps (`visibility`, `owner`/handle, `org_domain`, `agent`, `date`); no new publish-time metadata and no CLI change, so it ships with `agents artifacts share update`. Non-HTML assets (images, JSON, the OG cover) are served byte-for-byte; metadata is HTML-escaped; the rewritten body drops the now-stale R2 etag. Source: `cli/src/lib/share/worker-template.ts`.
240
+
3
241
  ## 1.22.51
4
242
 
5
243
  - **`publish-computer-helper-mac.sh` cuts the helper's own tag (PHNX-3228).**
package/README.md CHANGED
@@ -1085,7 +1085,7 @@ agents harness add deepinfra --account deepinfra
1085
1085
 
1086
1086
  One provider account **is** one `agents secrets` bundle -- `agents accounts add` creates it with secrets policy `never`, so a background agent launch on that account never raises Touch ID. `agents accounts` (no subcommand) lists provider bundles next to harness-native signed-in identities so you see both kinds of credential together; `accounts list` / `inspect <name>` / `set-key <name>` (rotate) / `rename` / `remove` manage a bundle by its stable id, independent of its current label. `accounts switch <harness>` is the fast picker over that default -- it writes the same binding as `set-default`, and balanced rotation already honors it.
1087
1087
 
1088
- Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. Native `accounts name` / `attach` is only for harnesses agents-cli can isolate today (claude, codex, grok; muse when an email is present). A device-scoped login (kimi, droid, …) is refused with a named reason; provider `accounts add --provider` is unrestricted. `accounts sync <name> --device <device>` is the only way a provider account crosses machines, and it's explicit: nothing syncs automatically. Selection order for a run is explicit `--account`, then `accounts set-default` / `switch` for that harness, then the harness's native/balanced account behavior.
1088
+ Harness-native OAuth logins (Claude Code's own `/login`, `codex login`, and so on) stay exactly where the harness put them -- agi-cli discovers and displays them but never copies, renames, or converts them into a provider bundle. Native `accounts name` / `attach` is only for harnesses agents-cli can isolate today (claude, codex, grok; muse when an email is present). A device-scoped login (kimi, droid, …) is refused with a named reason; provider `accounts add --provider` is unrestricted. Native **labels** (`agents accounts label codex personal`) bind to a stable identityKey on the central account row in `agents.yaml`, which `agents repo push/pull` already syncs fleet-wide, so `codex#personal` selects the same login on every box. `accounts sync <name> --device <device>` is the only way a **provider** account credential crosses machines, and it's explicit: nothing copies OAuth automatically. Selection order for a run is explicit `--account`, then `accounts set-default` / `switch` for that harness, then the harness's native/balanced account behavior.
1089
1089
 
1090
1090
  ---
1091
1091
 
@@ -464,7 +464,7 @@ export function registerAccountsCommand(program) {
464
464
  agents accounts label codex@0.146.0 personal
465
465
  agents accounts label codex work --account you@example.com
466
466
  agents run codex#work`,
467
- notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
467
+ notes: 'The label binds to the signed-in account identity, not the version — codex#work keeps selecting that account after it moves to a newer install. Labels live on the central account rows in agents.yaml, which `agents repo push/pull` already syncs fleet-wide, keyed by (agent, identityKey). One signed-in login needs no selector; with several, an interactive terminal opens a picker, while scripts pass --account or <harness>@<version>.',
468
468
  });
469
469
  accounts.command('attach <account> <target>')
470
470
  .description('Attach a named account to a native installation or custom harness')
@@ -3,6 +3,7 @@ import { runOnPeer } from '../lib/session/remote-list.js';
3
3
  import { sessionRecoveryPeer } from '../lib/session/recovery.js';
4
4
  import { resolveSessionMetadataValue, resumeSessionInPlace } from './sessions.js';
5
5
  import { readDetachRecord, clearDetachRecord, isHeadlessAlive } from '../lib/session/detached.js';
6
+ import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
6
7
  export function registerAttachCommand(program) {
7
8
  // Deprecated: superseded by `agents sessions resume`, which detects the state
8
9
  // (live pane / headless / ended) instead of making the caller pick a verb.
@@ -19,6 +20,12 @@ export function registerAttachCommand(program) {
19
20
  });
20
21
  }
21
22
  export async function attachAction(id) {
23
+ // PHNX-3292: this deprecated spelling still gets the local-pane fast path —
24
+ // a live tmux alias/short id on THIS box attaches with zero SSH, matching
25
+ // `sessions resume`. `attach` takes no `--device`, so hosts is always empty.
26
+ if (await attachLocalLiveSelector(id.trim(), [])) {
27
+ return;
28
+ }
22
29
  const outcome = await resolveSessionMetadataValue(id);
23
30
  if (outcome.kind === 'partial') {
24
31
  // RUSH-2492: an unreachable peer is a warning, not a hard failure. The
@@ -14,7 +14,7 @@ import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/b
14
14
  import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
15
15
  import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
16
16
  import { parseTargetFilter } from '../lib/browser/service.js';
17
- import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, } from '../lib/browser/ipc.js';
17
+ import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, resetBrowserDaemon, } from '../lib/browser/ipc.js';
18
18
  import { bindTask, getTaskBinding, honorScreenshotOutput, REJECT_DEVICE_MESSAGE, resolveTaskRoute, unbindTask, unbindTasksForProfile, updateTaskBinding, } from '../lib/browser/task-index.js';
19
19
  import { isSelfHost } from '../lib/devices/self-host.js';
20
20
  import { resolveHost } from '../lib/hosts/registry.js';
@@ -294,7 +294,7 @@ export function registerBrowserCommand(program) {
294
294
  # Create a Chrome profile pointed at a CDP endpoint
295
295
  agents browser profiles create work --browser chrome --endpoint http://localhost:9222
296
296
 
297
- # Start a session — auto-picks the first installed Chromium-family browser
297
+ # Start a session on this machine's default browser (set it once with: agents browser use)
298
298
  agents browser start
299
299
 
300
300
  # Or pin to a specific profile
@@ -323,6 +323,11 @@ export function registerBrowserCommand(program) {
323
323
 
324
324
  # Close tabs the daemon's own reaper would have caught on its next 5-min tick
325
325
  agents browser prune --dry-run
326
+
327
+ # Recover a wedged daemon ("Timeout waiting for browser daemon socket"):
328
+ # stop it and clear a stale socket, then start clean
329
+ agents browser stop --daemon
330
+ agents browser start
326
331
  `,
327
332
  notes: `
328
333
  Most agent workflows should use the 'browser' skill instead of raw subcommands.
@@ -363,7 +368,7 @@ export function buildBrowserUseChoices(profiles, installed, current) {
363
368
  export async function runBrowserUse(name, opts, interactive = isInteractiveTerminal()) {
364
369
  if (opts.unset || name === 'auto') {
365
370
  unsetConfigValue('browser.profile');
366
- console.log('Default browser profile cleared. `agents browser start` will auto-detect an installed Chromium-family browser.');
371
+ console.log('Default browser profile cleared. Pick one with `agents browser use <name>` (or `agents setup`); until then `agents browser start` has no default to launch here.');
367
372
  return true;
368
373
  }
369
374
  let selectedName = name;
@@ -372,7 +377,7 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
372
377
  if (!interactive) {
373
378
  console.log(current
374
379
  ? `Default browser profile (this machine): ${current}`
375
- : 'Default browser profile (this machine): auto-detect');
380
+ : 'Default browser profile (this machine): none — pick one with `agents browser use <name>`');
376
381
  const hub = getConfigValue('browser.device').value;
377
382
  if (hub) {
378
383
  console.log(isSelfHost(hub)
@@ -421,7 +426,7 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
421
426
  function configureBrowserUseCommand(command, deprecated = false) {
422
427
  command
423
428
  .description('Pick the profile `agents browser start` uses when no --profile is passed. No name opens a picker on a TTY or prints the current default headlessly.')
424
- .option('--unset', 'Clear the configured default (revert to auto-detecting an installed browser)')
429
+ .option('--unset', 'Clear the configured default (leaves this machine with no default until you pick one)')
425
430
  .action(async (name, opts) => {
426
431
  if (deprecated) {
427
432
  console.warn(chalk.yellow('Deprecation: `agents browser profiles set-default` is replaced by `agents browser use`.'));
@@ -942,7 +947,7 @@ function registerProfilesCommands(browser) {
942
947
  (removed > 0 ? ` (and ${removed} cache dir${removed === 1 ? '' : 's'})` : ''));
943
948
  if (getConfiguredDefaultProfileName() === name) {
944
949
  console.error(`warning: "${name}" was this machine's default browser profile; ` +
945
- `\`agents browser start\` will auto-detect until you run: agents browser use <name>`);
950
+ `\`agents browser start\` has no default here until you run: agents browser use <name>`);
946
951
  }
947
952
  });
948
953
  profiles
@@ -1025,13 +1030,13 @@ function registerProfilesCommands(browser) {
1025
1030
  with \`agents browser profiles claim\` on the machine that hosts the browser.
1026
1031
 
1027
1032
  In \`list\`, the \`*\` marker means "this machine's configured default"
1028
- (\`agents browser start\` with no --profile). The auto-detected profile is
1029
- named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
1033
+ (\`agents browser start\` with no --profile). A profile a setup wizard pins
1034
+ is named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
1030
1035
  this machine is configured to use, resolved the same way by every command.
1031
1036
 
1032
1037
  \`prune\` only considers profiles this device declares. It never removes a
1033
- profile that is in use, the configured default, or the auto-detected
1034
- \`auto-chrome\`.
1038
+ profile that is in use, the configured default, or the \`auto-chrome\`
1039
+ profile a setup wizard created.
1035
1040
  `,
1036
1041
  });
1037
1042
  profiles
@@ -1371,8 +1376,8 @@ function registerTaskCommands(browser) {
1371
1376
  });
1372
1377
  browser
1373
1378
  .command('start')
1374
- .description('Start a browser task. Pass --profile <name>; omit to use your configured default (`agents browser use <name>`), else auto-pick an installed Chromium-family browser. Page verbs (navigate/screenshot/…) create a task implicitly when none exists — start is for --profile/--url/--record/--title.')
1375
- .option('-p, --profile <name>', 'Browser profile to use (omit to use the configured default, else auto-pick an installed Chromium-family browser)')
1379
+ .description('Start a browser task. Pass --profile <name>; omit to use your configured default (set it with `agents browser use <name>` or `agents setup`). Page verbs (navigate/screenshot/…) create a task implicitly when none exists — start is for --profile/--url/--record/--title.')
1380
+ .option('-p, --profile <name>', 'Browser profile to use (omit to use this machine\'s configured default; set it with `agents browser use <name>`)')
1376
1381
  .option(TASK_OPTION_FLAG, 'Task name (auto-generated short id if omitted)')
1377
1382
  .option('--title <label>', 'Human label shown in `browser status` (defaults to first navigated host)')
1378
1383
  .option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
@@ -1445,13 +1450,85 @@ function registerTaskCommands(browser) {
1445
1450
  return;
1446
1451
  }
1447
1452
  }
1453
+ // Resolve an EXPLICIT --device BEFORE any local profile/browser
1454
+ // resolution (PHNX-3289). The task lives on the TARGET device, so its
1455
+ // browser binary and profile must resolve THERE. Resolving locally first
1456
+ // made a browserless box fail with a misleading "No supported browser
1457
+ // found" (bare start) or "Profile <x> not found" (a profile that lives
1458
+ // only on the target) before the start ever routed to --device. This
1459
+ // mirrors the hub path above: the target is the authority on its own
1460
+ // profiles, so forward the whole start and let it validate. `local`/`self`
1461
+ // force a local run; a fleet-remote re-exec is already ON the target and
1462
+ // falls through to the local launch below.
1463
+ const deviceName = opts.device === 'local' || opts.device === 'self' ? undefined : opts.device;
1464
+ if (deviceName) {
1465
+ const lowered = deviceName.toLowerCase();
1466
+ if (lowered === 'all' || lowered === 'auto') {
1467
+ console.error(`--device ${deviceName} is not valid on \`agents browser start\`: a task lives on one device.\n` +
1468
+ `Next: agents browser start --task <name> --device <device>`);
1469
+ process.exit(1);
1470
+ }
1471
+ if (!isSelfHost(deviceName) && !isFleetRemoteInvocation()) {
1472
+ // Resolve the profile NAME (never a local browser auto-pick) so a
1473
+ // named profile can still be validated against the target's
1474
+ // declarations before we round-trip. A bare start carries no name to
1475
+ // validate — the target picks its own default — so it forwards
1476
+ // straight through, which is exactly what unblocks a browserless box.
1477
+ // Only resolve when the caller actually named a profile: passing an
1478
+ // undefined ref would let `resolveProfileRef` fall back to THIS box's
1479
+ // configured/auto-detected default (matching the ~4 other call sites
1480
+ // in this file), and validating that local name against the target
1481
+ // re-introduces the exact false failure this reorder fixes.
1482
+ const forwardProfile = opts.profile ? await resolveProfileRef(opts.profile) : undefined;
1483
+ if (forwardProfile) {
1484
+ try {
1485
+ assertDeviceDeclaresProfile(deviceName, forwardProfile);
1486
+ }
1487
+ catch (err) {
1488
+ console.error(err instanceof Error ? err.message : String(err));
1489
+ process.exit(1);
1490
+ }
1491
+ }
1492
+ try {
1493
+ const result = await dispatchBrowserToDevice(deviceName, browserForwardedArgv(), 'capture');
1494
+ process.stdout.write(result.stdout);
1495
+ process.stderr.write(result.stderr);
1496
+ if (result.code !== 0)
1497
+ process.exit(result.code);
1498
+ const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
1499
+ if (!taskName) {
1500
+ console.error(`Remote start on ${deviceName} produced no task name.`);
1501
+ process.exit(1);
1502
+ }
1503
+ bindTask(taskName, {
1504
+ device: deviceName,
1505
+ // Only the caller-named profile; the target's own default (bare
1506
+ // start) is opaque here, exactly as in the hub path.
1507
+ profile: forwardProfile ?? opts.profile,
1508
+ url: opts.url,
1509
+ sessionId: callerSessionId(),
1510
+ launchId: callerLaunchId(),
1511
+ createdAt: Date.now(),
1512
+ });
1513
+ if (!result.stderr.includes(`started on ${deviceName}`)) {
1514
+ console.error(`Task "${taskName}" started on ${deviceName}.`);
1515
+ }
1516
+ }
1517
+ catch (err) {
1518
+ console.error(err instanceof Error ? err.message : String(err));
1519
+ process.exit(1);
1520
+ }
1521
+ return;
1522
+ }
1523
+ }
1448
1524
  // One resolution order for every command (RUSH-2709): `--profile default`
1449
1525
  // means the same profile here as it does in stop / status / navigate.
1450
1526
  // `start` is the one command that LAUNCHES, so its implicit path goes
1451
1527
  // through ensureDefaultBrowserProfile, which additionally verifies the
1452
- // resolved default can launch on THIS machine and regenerates it when it
1453
- // can't (the "Custom binary not found" repair) — a filter-only command
1454
- // must not warn or rewrite a profile, so that check lives only here.
1528
+ // resolved default can launch on THIS machine and, when nothing launchable
1529
+ // resolves, throws an actionable "pick a browser in `agents setup`" error
1530
+ // (PHNX-3296 — it no longer silently mints an auto-chrome). A filter-only
1531
+ // command must not warn about the config, so that check lives only here.
1455
1532
  let profileName;
1456
1533
  try {
1457
1534
  profileName = await resolveProfileRefForStart(opts.profile);
@@ -1481,17 +1558,12 @@ function registerTaskCommands(browser) {
1481
1558
  process.exit(1);
1482
1559
  }
1483
1560
  }
1484
- // The `browser.device` hub default is applied above (bare/--profile, no
1485
- // --device). Here only an EXPLICIT --device remains — with `local`/`self` as
1486
- // the escape that forces a local run when a hub is configured.
1487
- const deviceName = opts.device === 'local' || opts.device === 'self' ? undefined : opts.device;
1561
+ // Only the LOCAL launch reaches here — an explicit remote --device already
1562
+ // forwarded and returned above. A residual `deviceName` means an explicit
1563
+ // self target (or a fleet-remote re-exec on the target); its profile
1564
+ // resolved locally, so verify this box actually declares it (the
1565
+ // `profiles claim` hint lives in `assertDeviceDeclaresProfile`).
1488
1566
  if (deviceName) {
1489
- const lowered = deviceName.toLowerCase();
1490
- if (lowered === 'all' || lowered === 'auto') {
1491
- console.error(`--device ${deviceName} is not valid on \`agents browser start\`: a task lives on one device.\n` +
1492
- `Next: agents browser start --task <name> --device <device>`);
1493
- process.exit(1);
1494
- }
1495
1567
  try {
1496
1568
  assertDeviceDeclaresProfile(deviceName, profileName);
1497
1569
  }
@@ -1499,36 +1571,6 @@ function registerTaskCommands(browser) {
1499
1571
  console.error(err instanceof Error ? err.message : String(err));
1500
1572
  process.exit(1);
1501
1573
  }
1502
- if (!isSelfHost(deviceName) && !isFleetRemoteInvocation()) {
1503
- try {
1504
- const result = await dispatchBrowserToDevice(deviceName, browserForwardedArgv(), 'capture');
1505
- process.stdout.write(result.stdout);
1506
- process.stderr.write(result.stderr);
1507
- if (result.code !== 0)
1508
- process.exit(result.code);
1509
- const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
1510
- if (!taskName) {
1511
- console.error(`Remote start on ${deviceName} produced no task name.`);
1512
- process.exit(1);
1513
- }
1514
- bindTask(taskName, {
1515
- device: deviceName,
1516
- profile: profileName,
1517
- url: opts.url,
1518
- sessionId: callerSessionId(),
1519
- launchId: callerLaunchId(),
1520
- createdAt: Date.now(),
1521
- });
1522
- if (!result.stderr.includes(`started on ${deviceName}`)) {
1523
- console.error(`Task "${taskName}" started on ${deviceName} (profile: ${profileName}).`);
1524
- }
1525
- }
1526
- catch (err) {
1527
- console.error(err instanceof Error ? err.message : String(err));
1528
- process.exit(1);
1529
- }
1530
- return;
1531
- }
1532
1574
  }
1533
1575
  // Grounded login guardrail: if the opening URL is a known login-gated
1534
1576
  // service and the resolved profile has no session for it, say so on stderr
@@ -1641,11 +1683,31 @@ function registerTaskCommands(browser) {
1641
1683
  });
1642
1684
  browser
1643
1685
  .command('stop')
1644
- .description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection)')
1686
+ .description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection); with --daemon, stop the browser daemon and clear a wedged socket')
1645
1687
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1646
1688
  .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1647
1689
  .option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
1690
+ .option('--daemon', 'Stop the browser daemon entirely and clear a stale/wedged socket so the next start comes up clean (recovery for "Timeout waiting for browser daemon socket")')
1648
1691
  .action(async (opts) => {
1692
+ if (opts.daemon) {
1693
+ if (opts.profile || opts.task) {
1694
+ console.error('--daemon stops the whole browser daemon; do not combine it with --profile or --task.');
1695
+ process.exit(1);
1696
+ }
1697
+ try {
1698
+ const result = await resetBrowserDaemon();
1699
+ const parts = [];
1700
+ parts.push(result.wasRunning ? 'Stopped browser daemon' : 'Browser daemon was not running');
1701
+ if (result.socketCleared)
1702
+ parts.push('cleared stale socket');
1703
+ console.log(`${parts.join('; ')}. It restarts on the next browser command.`);
1704
+ }
1705
+ catch (err) {
1706
+ console.error(err instanceof Error ? err.message : String(err));
1707
+ process.exit(1);
1708
+ }
1709
+ return;
1710
+ }
1649
1711
  if (opts.profile) {
1650
1712
  const profile = (await resolveProfileRef(opts.profile)) ?? opts.profile;
1651
1713
  const response = await sendIPCRequest({
@@ -16,6 +16,8 @@
16
16
  */
17
17
  import type { Command } from 'commander';
18
18
  import { type DaemonServiceId } from '../lib/daemon-services.js';
19
+ /** Elapsed wall-clock seconds since `pid` started, or null if unavailable (best-effort, POSIX only). */
20
+ export declare function uptimeSeconds(pid: number): number | null;
19
21
  /** One row of the roster every registered daemon service — supervisor-managed or legacy — renders in `agents daemon services`. */
20
22
  export interface DaemonServiceRow {
21
23
  id: DaemonServiceId;
@@ -30,6 +30,7 @@ import { parseDuration } from '../lib/hooks/cache.js';
30
30
  import { registerFunnelCommand } from './funnel.js';
31
31
  import { DEFAULT_WEBHOOK_PORT, DEFAULT_WEBHOOK_RATE_LIMIT, addHostedReceiver, getDaemonWebhooksConfigPath, hostedReceiverPort, readDaemonWebhooksConfig, removeHostedReceiver, } from '../lib/daemon-webhooks.js';
32
32
  import { parseFunnelPort } from '../lib/funnel.js';
33
+ import { parseEtimeToSeconds } from '../lib/secrets/reaper.js';
33
34
  /**
34
35
  * Provably absent, or null when unknowable.
35
36
  *
@@ -256,13 +257,16 @@ function registryScopedDuplicates(processes, ownerPid) {
256
257
  return processes.filter((p) => registered.has(p.pid));
257
258
  }
258
259
  /** Elapsed wall-clock seconds since `pid` started, or null if unavailable (best-effort, POSIX only). */
259
- function uptimeSeconds(pid) {
260
+ export function uptimeSeconds(pid) {
260
261
  if (process.platform === 'win32')
261
262
  return null;
262
263
  try {
263
- const out = execFileSync('ps', ['-o', 'etimes=', '-p', String(pid)], { encoding: 'utf-8' }).trim();
264
- const n = parseInt(out, 10);
265
- return isNaN(n) ? null : n;
264
+ // `-o etimes=` is a GNU/procps keyword macOS/BSD `ps` rejects with a
265
+ // non-zero exit (`ps: etimes: keyword not found`), so `agents daemon status`
266
+ // errored out entirely on macOS. `etime` (`[[dd-]hh:]mm:ss`) is the portable
267
+ // POSIX field; `parseEtimeToSeconds` (shared with the keychain reaper) parses it.
268
+ const out = execFileSync('ps', ['-o', 'etime=', '-p', String(pid)], { encoding: 'utf-8' }).trim();
269
+ return parseEtimeToSeconds(out);
266
270
  }
267
271
  catch {
268
272
  return null;