@phnx-labs/agents-cli 1.22.68 → 1.22.70

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 (132) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +17 -14
  3. package/dist/bootstrap.js +3 -0
  4. package/dist/commands/accounts.js +23 -7
  5. package/dist/commands/exec.js +33 -23
  6. package/dist/commands/feed.js +20 -7
  7. package/dist/commands/import.js +2 -2
  8. package/dist/commands/models.js +2 -2
  9. package/dist/commands/monitors.js +3 -0
  10. package/dist/commands/permissions.js +2 -2
  11. package/dist/commands/projects.d.ts +26 -6
  12. package/dist/commands/projects.js +55 -22
  13. package/dist/commands/rules.js +1 -1
  14. package/dist/commands/send.js +35 -2
  15. package/dist/commands/sessions-inject.d.ts +58 -0
  16. package/dist/commands/sessions-inject.js +143 -7
  17. package/dist/commands/sessions-picker.js +1 -0
  18. package/dist/commands/share.js +43 -14
  19. package/dist/commands/ssh.js +205 -2
  20. package/dist/commands/traces.js +1 -1
  21. package/dist/lib/account-capabilities.js +0 -2
  22. package/dist/lib/account-registry.d.ts +3 -3
  23. package/dist/lib/account-registry.js +25 -7
  24. package/dist/lib/accounting/usage.d.ts +7 -2
  25. package/dist/lib/accounting/usage.js +142 -10
  26. package/dist/lib/acp/client.d.ts +1 -1
  27. package/dist/lib/acp/client.js +12 -1
  28. package/dist/lib/acp/harnesses.js +1 -1
  29. package/dist/lib/add-dir.js +0 -2
  30. package/dist/lib/agent-cli-commands.js +0 -2
  31. package/dist/lib/agent-spec/agents.d.ts +1 -1
  32. package/dist/lib/agent-spec/agents.js +2 -83
  33. package/dist/lib/boot-profile.d.ts +14 -0
  34. package/dist/lib/boot-profile.js +66 -0
  35. package/dist/lib/browser/ipc.js +9 -1
  36. package/dist/lib/browser/remote-control.d.ts +6 -3
  37. package/dist/lib/browser/remote-control.js +6 -3
  38. package/dist/lib/browser/service.d.ts +4 -1
  39. package/dist/lib/browser/service.js +7 -1
  40. package/dist/lib/channels/providers/desktop.d.ts +5 -4
  41. package/dist/lib/channels/providers/desktop.js +5 -4
  42. package/dist/lib/channels/registry.d.ts +6 -0
  43. package/dist/lib/channels/send.d.ts +11 -2
  44. package/dist/lib/channels/send.js +11 -2
  45. package/dist/lib/claude-account-token.js +108 -4
  46. package/dist/lib/cloud/rush.d.ts +10 -2
  47. package/dist/lib/cloud/rush.js +15 -9
  48. package/dist/lib/daemon/daemon.js +28 -8
  49. package/dist/lib/daemon/runner.js +6 -2
  50. package/dist/lib/devices/health.d.ts +38 -2
  51. package/dist/lib/devices/health.js +43 -5
  52. package/dist/lib/devices/worker-pick.d.ts +1 -1
  53. package/dist/lib/devices/worker-pick.js +4 -1
  54. package/dist/lib/exec.d.ts +2 -2
  55. package/dist/lib/exec.js +7 -34
  56. package/dist/lib/feed-broadcast.d.ts +47 -5
  57. package/dist/lib/feed-broadcast.js +127 -24
  58. package/dist/lib/hooks/install.js +0 -87
  59. package/dist/lib/installations/strategies.js +1 -1
  60. package/dist/lib/mcp-registry.js +0 -13
  61. package/dist/lib/mcp.js +2 -2
  62. package/dist/lib/model-tiers.js +1 -1
  63. package/dist/lib/models.js +0 -63
  64. package/dist/lib/monitors/engine.js +18 -0
  65. package/dist/lib/monitors/sources/command.js +13 -3
  66. package/dist/lib/monitors/sources/failure.d.ts +32 -0
  67. package/dist/lib/monitors/sources/failure.js +52 -0
  68. package/dist/lib/monitors/sources/types.d.ts +9 -0
  69. package/dist/lib/notify.d.ts +11 -0
  70. package/dist/lib/notify.js +17 -4
  71. package/dist/lib/owner-message.d.ts +55 -0
  72. package/dist/lib/owner-message.js +64 -0
  73. package/dist/lib/permissions-registry.d.ts +0 -2
  74. package/dist/lib/permissions-registry.js +3 -50
  75. package/dist/lib/permissions.d.ts +3 -17
  76. package/dist/lib/permissions.js +4 -73
  77. package/dist/lib/run-trace-sync.d.ts +15 -0
  78. package/dist/lib/run-trace-sync.js +43 -21
  79. package/dist/lib/rush-session.d.ts +19 -0
  80. package/dist/lib/rush-session.js +24 -0
  81. package/dist/lib/secrets/drivers/rush.js +2 -1
  82. package/dist/lib/secrets/filestore.d.ts +4 -0
  83. package/dist/lib/secrets/filestore.js +164 -3
  84. package/dist/lib/session/active.d.ts +10 -0
  85. package/dist/lib/session/active.js +3 -0
  86. package/dist/lib/session/cloud.js +2 -1
  87. package/dist/lib/session/db.d.ts +12 -1
  88. package/dist/lib/session/db.js +20 -1
  89. package/dist/lib/session/discover.js +81 -1
  90. package/dist/lib/session/linear.d.ts +13 -0
  91. package/dist/lib/session/linear.js +44 -0
  92. package/dist/lib/session/live-metadata.js +1 -0
  93. package/dist/lib/session/parse.js +2 -3
  94. package/dist/lib/session/prompt.d.ts +7 -1
  95. package/dist/lib/session/prompt.js +12 -2
  96. package/dist/lib/session/recovery.d.ts +21 -12
  97. package/dist/lib/session/recovery.js +29 -11
  98. package/dist/lib/session/remote/watch.js +5 -2
  99. package/dist/lib/session/state.js +11 -13
  100. package/dist/lib/share/backend.d.ts +2 -2
  101. package/dist/lib/share/backend.js +20 -9
  102. package/dist/lib/share/delete.d.ts +5 -1
  103. package/dist/lib/share/delete.js +7 -2
  104. package/dist/lib/share/http-error.d.ts +52 -0
  105. package/dist/lib/share/http-error.js +65 -0
  106. package/dist/lib/share/publish.d.ts +13 -3
  107. package/dist/lib/share/publish.js +19 -15
  108. package/dist/lib/share/worker-template.js +5 -1
  109. package/dist/lib/sink-format.d.ts +34 -0
  110. package/dist/lib/sink-format.js +17 -0
  111. package/dist/lib/smart-launch.js +27 -4
  112. package/dist/lib/staleness/detectors/permissions.js +0 -20
  113. package/dist/lib/staleness/writers/commands.js +1 -1
  114. package/dist/lib/staleness/writers/hooks.js +2 -2
  115. package/dist/lib/storage/index.d.ts +14 -0
  116. package/dist/lib/storage/index.js +14 -0
  117. package/dist/lib/storage/selection.d.ts +48 -0
  118. package/dist/lib/storage/selection.js +39 -0
  119. package/dist/lib/storage/visibility.d.ts +82 -0
  120. package/dist/lib/storage/visibility.js +99 -0
  121. package/dist/lib/subagents-registry.js +2 -12
  122. package/dist/lib/subagents.d.ts +0 -10
  123. package/dist/lib/subagents.js +0 -37
  124. package/dist/lib/teams/agents.js +3 -1
  125. package/dist/lib/teams/placement-probe.js +1 -0
  126. package/dist/lib/teams/scheduler.d.ts +8 -1
  127. package/dist/lib/teams/scheduler.js +4 -1
  128. package/dist/lib/traces/backend.js +13 -2
  129. package/dist/lib/types.d.ts +1 -1
  130. package/dist/lib/worktree/held.d.ts +166 -0
  131. package/dist/lib/worktree/held.js +368 -0
  132. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.70
4
+
5
+ - **`agents browser stop --profile` is now gated by the remote-control consent gate (PHNX-3317).** `stop --profile <name>` (no `--task`) is handled entirely by `bindTask`'s early return in `ipc.ts`, so it never reached `resolveOrCreateTask` — the one chokepoint every other page/close/attach verb's `assertRemoteControlAllowedForRequest` gate runs through — and `case 'stop'` called `BrowserService.stopProfile()` directly with no consent check at all. That left one destructive fleet-remote path (kills the profile's browser process, clears its runtime dir) reachable from `browser --device <device> stop --profile` even with `remote-control off` on the target. `stopProfile()` now takes the same `{ fleetRemote, actor }` opts as `start`/`showUrl`/`resolveOrCreateTask` and asserts the gate first; a local (non-fleet-remote) call is unaffected. Source: `cli/src/lib/browser/service.ts`, `cli/src/lib/browser/ipc.ts`.
6
+
7
+ - **The PHNX-2939 headless-account regression test now genuinely exercises the headless path (PHNX-3352).** The real-macOS-keychain test in `account-registry.keychain.test.ts` set `AGENTS_RUNTIME='test-headless-account-launch'`, a made-up value `isHeadlessSecretsContext()` never matches — its set is exactly `headless`/`teams`/`terminal` — so the resolve never entered the headless keychain guard and the test passed identically with OR without the PHNX-2939 biometry-ACL fix, reproducing nothing. It now sets `AGENTS_RUNTIME='headless'` and asserts `isHeadlessSecretsContext()` is `true` before resolving, so the test fails against the pre-fix code (a direct `getKeychainToken` read of the policy-`never`, no-ACL account item is rejected as if it required Touch ID) and passes only because the fix routes through the bundle path that attests `silentNoAcl`. Source: `cli/src/lib/account-registry.keychain.test.ts`.
8
+
9
+ - **`agents daemon stop` reclaims a stale `browser.sock` when the daemon vanishes mid-stop (PHNX-3618).** `stopDaemonLocked` captured the browser IPC socket's inode ONLY when a live pid resolved (`pid !== null && browserSock ? …`). If the daemon died in the window between the CLI liveness precheck (`isDaemonRunning()` was true) and the locked read, `resolveLiveDaemonPid()` returned null, so the owner was never captured — and the socket the daemon's ungraceful death left on disk hit the else branch, was reported "browser IPC socket ownership could not be verified", and was left behind, so the next daemon start raced a dead binding. The socket inode is now captured **independently of the pid**, and the stale binding is reclaimed once no live daemon — the signalled target OR any successor for this state dir — is proven to own it. PHNX-3607's never-delete-a-successor invariant is preserved: a successor that rebinds during the stop gets a new inode (identity no longer matches) or shows up as a survivor, and in either case its socket is left untouched. Source: `cli/src/lib/daemon/daemon.ts`.
10
+
11
+ - **`agents run --cloud` no longer rejects a non-expiring Phoenix bearer (PHNX-3645).** A Rush session stores `expires_at: 0` for an opaque Phoenix `pid_` bearer, which by contract means NON-EXPIRING. Three freshness checks read that `0` as an absolute timestamp — `new Date(0)` = 1970-01-01 — and refused a valid, fresh session with `Rush session expired at 1970-01-01`, so no Phoenix-authed box could dispatch a cloud run, push/pull secrets over Rush, or list cloud sessions. The `expires_at === 0` (and missing) special-case now lives in one place, `isRushSessionExpired` (`cli/src/lib/rush-session.ts`), which cloud dispatch, the secrets Rush driver, and the cloud session source all route through — mirroring rush/cli's own `isFresh` (RUSH-1310, PHNX-3637). Source: `cli/src/lib/rush-session.ts`, `cli/src/lib/cloud/rush.ts`, `cli/src/lib/secrets/drivers/rush.ts`, `cli/src/lib/session/cloud.ts`.
12
+
13
+ - **Owner alerts now render per destination — Slack gets tappable links, iMessage stays plain (PHNX-3698).** `agents notify` / `agents send --to owner` (and an important `feed post` that fans out to the owner) resolve every channel in `owner.policy.normal` and composed ONE plain string for all of them, so a Slack owner destination received a wall of prose with no `<url|label>` links and no tappable session crumb — the labeled-link support already in `composeBroadcastMessage` never fired on the path the owner actually uses. The owner fan-out (`sendToOwner`) now re-renders the body PER destination, keyed on each channel's resolved provider (the same `notify.transports` remap delivery uses): a Slack destination turns every `TEAM-N` key the post names into `<https://linear.app/<ws>/issue/<KEY>|<KEY>>` and the `Sent from claude/<id> on <host>` crumb into `<https://prix.dev/console/sessions/<full-id>|claude/<id>>` (the 8-char crumb upgraded to the full indexed id first), while iMessage / owner-scoped rush / desktop / `command:` sinks stay the plain human sentence with no URLs. Each delivery echoes the exact body it sent, so `agents notify --dry-run --json` now shows the two different bodies. Source: `cli/src/lib/notify.ts`, `cli/src/lib/owner-message.ts`, `cli/src/lib/sink-format.ts`, `cli/src/lib/feed-broadcast.ts`, `cli/src/lib/channels/send.ts`, `cli/src/commands/send.ts`.
14
+
15
+ - **A release no longer starves waiting for the tip of `main` to be attested (PHNX-3705).** `release.sh` phase 2 required an attestation for `origin/main`'s tree *as of the instant the release ran*, but `attest-main.yml` produces that by running the full suite — minutes of work. On a repo where agents merge continuously, main advances faster than attestation completes, so every release lost the race and died at `missing exact attestation key`. Measured 2026-09-01: six merges to `main` in ~30 minutes with attest-main runs still pending or superseded; the tip was never attested long enough to release from, which also makes the R2 bar (ordinary release under 60s) unreachable by construction. The release does not need *the tip* — it needs a green base to cut the release commit from and derive the release-tree attestation against (PHNX-3696), and any attested **ancestor** serves. Phase 2 now requires an attestation for the tree it is **actually releasing from** (`$BASE_SHA`) rather than the live remote tip, and `derive_release_attestation` inherits from that same commit — the release commit's real parent — instead of the tip. New `cli/scripts/release-attested-base.sh` resolves the newest attested commit at or below `origin/<default>` (bounded lookback, fails closed when none is found), `release-worktree.sh` cuts the release worktree from it and says so on stderr when it is behind the tip, and `release.sh`'s base check now requires an *ancestor* of the remote tip rather than the tip exactly. The supply-chain guarantee is unchanged: the published tarball is still bound to a tree whose suite passed, and `derive`'s allowlist still fails closed on any code file — the release simply ships from a slightly older tree than the tip, which was always true of an attested tree. Source: `cli/scripts/release-attested-base.sh`, `cli/scripts/release-worktree.sh`, `cli/scripts/release.sh`.
16
+
17
+ - **Docs: sign-in unlocks managed sharing, not only team spaces; `traces setup` marked advanced/self-host.** The README said signing in unlocks "the one thing" — team spaces — which predated managed, private-by-default sharing; it now names both hosted surfaces (team spaces AND `agents artifacts share`/`agents traces sync` on our storage with zero Cloudflare setup, private by default when signed in). The `agents traces setup` command description no longer reads as a normal user step — it is now labeled advanced/self-host, since `agents auth login` syncs to the managed endpoint with no setup. Source: `README.md`, `cli/src/commands/traces.ts`, `cli/docs/command-index.{md,json}`.
18
+
19
+ ## 1.22.69
20
+
21
+ - **`agents monitors` no longer fires an action on a poll FAILURE (PHNX-3510).** An `[on-change]` monitor treated a failing poll — a `gh pr list … | jq` that intermittently printed `GraphQL: API rate limit already exceeded …` — as a value change: the empty→error→empty flap read as two changes and dispatched a full agent run each time on a premise that was false (~10 sessions burned re-verifying two already-merged PRs). A command/poll source now flags a snapshot that exited non-zero, or whose output matches a transport/auth/rate-limit error shape (caught even on exit 0, since a pipe to jq swallows the failing half's exit code), as an *observation failure*: the engine skips it, leaves watched-state untouched, does not fire, and records it as a failed check so a sustained streak escalates to the owner as a drought. `agents monitors test` labels a failed poll and reports `Would fire: no`. Note: a non-zero exit is now an observation failure rather than a watchable value — a monitor that wants to watch a command's success/failure should emit a stable token (`… && echo UP || echo DOWN`, always exit 0) so the state it cares about diffs as a value; sustained failures still reach the owner via the drought escalation. Source: `cli/src/lib/monitors/sources/failure.ts`, `cli/src/lib/monitors/sources/command.ts`, `cli/src/lib/monitors/engine.ts`, `cli/docs/automation.md`.
22
+
23
+ - **`agents fleet worktrees` surfaces the worktree-sweep's held set as buckets, not a silent count (PHNX-3520).** The nightly `worktree-sweep` (PHNX-3503, in `phnx-labs/.agents`) reclaims merged agent worktrees and correctly HOLDS anything dirty, unmerged, or undeterminable — but it only ever emitted a `held=<n>` count, so the held set became a permanent, growing residue nothing resolved. Measured 2026-08-30 it is consistently LARGER than the reclaimed set (~800 worktrees fleet-wide), and one bucket inside it is real stranded work: a worktree whose branch carries commits on no remote (the PHNX-2951 / PHNX-2732 class — an agent finished, its commits never reached a remote, nothing surfaced them). The new read-only command classifies every held worktree into its three buckets — **`unmerged-commits`** (the one that matters), **`uncommitted-changes`**, and **`undeterminable`** (`status-unreadable` / `merge-state-unknown`, failing closed exactly as the sweep does) — and reports each with device/repo/worktree/branch/reason/age/size. `--json` is the structured form for machine callers; `--fleet` fans out over every online device and aggregates; `--bucket <name>` filters. The safe recovery action for the stranded bucket is `--push`: it PUBLISHES each `unmerged-commits` branch that is on no remote (re-reading the merge state immediately before pushing and refusing anything already on origin), making the work visible — it never removes a worktree or a branch. The destructive reclaim stays in the sweep; this is the surfacing half the sweep can consume. Source: `cli/src/lib/worktree/held.ts`, `cli/src/commands/ssh.ts`, `cli/src/lib/worktree/held.test.ts`.
24
+
25
+ - **`agents run` starts ~0.6s faster on a box with many secrets bundles (PHNX-3585).** The AGI EXT "New Claude" boot runs `agents run claude --interactive`, and the whole wrapper cost is spent BEFORE the harness prints anything. Profiling that pre-exec window (new `AGENTS_PROFILE_BOOT=1` per-stage tracer, `cli/src/lib/boot-profile.ts`) showed the dominant stage was `--strategy balanced` account rotation: `readAccountRegistry` → `listBundles` decrypted EVERY file-store bundle's metadata to find the account bundles, and each decrypt runs a fresh scrypt KDF (~12ms) because every `.enc` file carries its own salt — ~690ms of pure KDF on a box with ~40 bundles, on every launch. Bundle metadata is non-secret by contract (it holds `keychain:`/`env:`/literal refs, never the secret bytes, and is already stored no-ACL so `secrets list` enumerates without Touch ID), so the file store now caches the decrypted metadata JSON keyed by each `.enc` file's `(mtime,size)` plus a passphrase fingerprint. Any bundle write (new mtime/size) or passphrase change (new fingerprint; rotation re-writes every file) misses and re-derives — no staleness, self-healing on any miss — and secret VALUE items never enter the cache. Measured on a worker box: `agents run claude` pre-exec `resolve-version` dropped from ~690ms to ~31ms (warm), total pre-exec ~1040ms → ~370ms. A committed `cli/scripts/bench-boot.sh` keeps it measured. Source: `cli/src/lib/secrets/filestore.ts`.
26
+
27
+ - **Grok's cheap scan now fills `firstUserMessage` from a bounded `chat_history.jsonl` prefix read (PHNX-3621).** The summary-only path never parsed the log. The prefix reader skips Grok `<user_info>` / `synthetic_reason` scaffolding, prefers the first `prompt_index` user turn, and unwraps `<user_query>`. Existing Grok rows backfill through `CONTENT_INDEX_VERSION` v4. Source: `cli/src/lib/session/discover.ts`.
28
+
29
+ - **Worker homes provisioned before seed-on-attach self-heal their account identity at resolve time (PHNX-3660).** `resolveClaudeSetupToken` required the version home's `.claude.json` to carry `oauthAccount.emailAddress`; homes whose `.oauth_token` was written by a pre-fix `accounts attach` had none, so exec-env injection and per-account usage reads were silently inert for them (they authenticated only via the shim's `.oauth_token` fallback). The resolver now recovers the email by matching the home's `.oauth_token` value against the `auth` bundle (re-encode-guarded slug decode, so a lossy or unmatched token can never invent a mapping), writes it back with `seedClaudeWorkerHomeIdentity` so the home converges after one read, and still fails closed — writing nothing — when the token matches no bundle key. Ambient no-home resolves never rewrite the operator's real `~/.claude.json`. Source: `cli/src/lib/claude-account-token.ts`.
30
+
31
+ - **Resume `/continue` no longer launches the exhausted origin login when the pick is a provider account (PHNX-3674).** After native-first rotation (PHNX-3626), a usage-limited origin whose transcript is not in the origin home (trash/backup/reinstall, or local fallback from an unreachable peer) fell through to `/continue` on the provider-inclusive pool without carrying `RecoveryAccount`, so exec authenticated as the rate-limited native login. That continue target now injects the healthy provider the same way native rotation does. Source: `cli/src/lib/session/recovery.ts`, `cli/src/commands/exec.ts`.
32
+
33
+ - **Private share OG covers are no longer cacheable by shared proxies (PHNX-3676).** Token-gated `visibility=private` pages already served HTML as `Cache-Control: private, no-store`, but the generated `/user/slug.png` cover used `public, max-age=31536000, immutable` after the token gate. RFC 9111 lets a shared cache reuse a `public` Authorization response for later unauthenticated requests keyed on the unauthenticated URL, so a Bearer fetch of the cover could leak the preview. `managedCoverHeaders` now treats `private` like `me`/`org` (`private, no-store` + `X-Robots-Tag: noindex`). Source: `cli/src/lib/share/worker-template.ts`.
34
+
35
+ - **`agents sessions inject` can nudge a live remote tmux session that has no session id, and no longer crashes on `--pane --device` (PHNX-3688).** Two bugs are fixed. (1) A live session whose `agents sessions --active` id column shows `-` — only a tmux label like `ag-claude-214edaae:0.0` is known — was untargetable: `inject <shortid>` failed `No active session matches`. The lookup now matches the `ag-<agent>-<shortid>` tmux name's suffix (and the full name / pane id), the only selector such a row exposes, and the discovered row carries its `tmuxName` so the match has something to key on. (2) `inject _ "text" --pane %122 --device <box>` crashed with `TypeError: host.startsWith is not a function`: under `optsWithGlobals()` the parent `sessions` command's variadic `-D, --device <target...>` shadows this subcommand's scalar `--device`, so a single `--device box` arrived as `['box']` and the array flowed straight into `sshExec`. It is now coerced to one host (and fails loud on more than one, since inject delivers to exactly one terminal). A bare session id combined with `--device` now resolves ON that device — its tmux panes live there, not on the box you typed the command from — by re-running the inject over SSH, the tool-native form of the old `agents ssh <box> "agents sessions inject <id> …"` workaround. Source: `cli/src/commands/sessions-inject.ts`, `cli/src/lib/session/active.ts`.
36
+
37
+ - **`agents` releases no longer wedge waiting for a human to mint an attestation (PHNX-3696).** RUSH-2666 made an exact-tree attestation for the *release commit* mandatory but shipped no producer for it, so every `release.sh --apply` since 2026-08-15 stopped at `missing exact attestation key` and required an operator to hand-run `release-attestation-produce.sh` and copy the tarball into the store. `attest-main.yml` already attests every push to `main`; the release commit differs from that base only by the version bump, the folded changelog, and the regenerated command index — exactly the set `release-attestation.sh derive` allowlists — so `release.sh` now derives the record itself (`derive_release_attestation`, inheriting the green base's suite via `--inherit-suite-from`, no suite re-run). Soundness is unchanged: `derive` still fails closed if the tree diff touches anything outside that allowlist, so a code change can never inherit a stale pass, and the derive is best-effort — any failure falls through to the previous poll-then-`require`, which still fails loud (the call site guards it with `|| true`, which is load-bearing under `set -euo pipefail`: a bare call returning non-zero would abort the release before that fallback ran). The regression survived review because `release.test.ts` asserted against `release.sh` as *text*; the new coverage executes the real `derive_release_attestation` body against a real git repo and store. Source: `cli/scripts/release.sh`, `cli/scripts/release.test.ts`, `cli/AGENTS.md`.
38
+
39
+ - **Owner phone pings are short and tappable — but only where a sink can render a link (PHNX-3698).** Every owner-bound message goes through one composer (`composeBroadcastMessage`), and the shared `{message}` now surfaces its links per-sink instead of dumping naked URLs after the footer. A **Slack `channel:` sink** gets mrkdwn labeled links `<url|label>`: the `Sent from claude/6fc1db18 on zion` crumb becomes `<https://prix.dev/console/sessions/<full-id>|claude/6fc1db18>` (an 8-char crumb is first upgraded to the full indexed id, since a truncated id would 404), and every `TEAM-N` key the title or body *names* — not just the session's own `ticketId`, so a ping that only mentions `PHNX-3689` in prose is still tappable — becomes `<https://linear.app/<workspace>/issue/<KEY>|<KEY>>` in place. **iMessage, the owner-scoped rush message, `command:` sinks, and every other channel stay plain** — they cannot render a labeled link and a dumped naked URL reads as noise, so the message is the human sentence with no URLs (keys and crumb as text); there is never a trailing URL line on any sink. `agents notify` and `agents send --to owner` route through that same composer on the owner-scoped (plain) path instead of shipping the raw body dump; a non-owner `agents send` is still delivered verbatim. An important/`--blocked` post (and an owner send) fires a best-effort, opt-in background `agents traces sync` so the linked console page exists when a Slack crumb is tapped (gated exactly like the run-exit arm; `AGENTS_NO_TRACE_SYNC=1` opts out). The Linear-key detector is canonical in one place shared with transcript ticket detection (`session/linear.ts`). Source: `cli/src/lib/feed-broadcast.ts`, `cli/src/lib/owner-message.ts`, `cli/src/lib/session/linear.ts`, `cli/src/lib/session/db.ts`, `cli/src/lib/run-trace-sync.ts`, `cli/src/commands/send.ts`, `cli/src/commands/feed.ts`.
40
+
41
+ - **An ordinary CLI release no longer signs or notarizes anything on macOS, and the macOS attestation producer works again (PHNX-3699, PHNX-3631).** `release-attestation-produce.sh` gated its sign+notarize block on `uname == Darwin` alone, with no `--with-helpers` condition — so a CLI-only attestation produced on a Mac codesigned the CLI binary and rebuilt + signed both helper `.app`s, none of which ship in the tarball (RUSH-3026 removed the binary, RUSH-3100 the bundles). That violates the R3 requirement ("no signing, no notarization on the ordinary path") and, because `agents secrets exec apple.com` cannot unlock a Touch-ID-gated bundle headlessly, it *killed* the release outright — hit live cutting 1.22.69 right after PHNX-3696 made `release.sh` produce this attestation itself. The block is now gated on `--with-helpers`, so cutting a helper release still signs and an ordinary CLI release does not. Shipped alongside the **PHNX-3631** fix in the same path: `ATTEST_TMP` used `mktemp ".../agents-cli-attest.XXXXXX.json"`, but BSD/macOS `mktemp` only substitutes trailing X's and treats a template with a suffix after them as a literal filename — so the first call created that exact file and every later call died with `mkstemp failed … File exists`. Now `agents-cli-attest.json.XXXXXX`, which GNU accepts identically; the same pattern in `remote-sign-mac.sh` is fixed too. Together these take `release-attestation-produce.test.ts` on macOS from 22-of-29 failing to 31/31 passing. Source: `cli/scripts/release-attestation-produce.sh`, `cli/scripts/remote-sign-mac.sh`.
42
+
43
+ - **`agents projects view <path>` auto-detects the cwd's project; `projects for-cwd` is gone (PHNX-3704).** `agents projects view [nameOrPath]` now accepts a DIRECTORY as well as a project name. A path-shaped argument — `.`, `..`, a `~`-prefixed value, a `/`-containing value, or an explicit `--path [dir]` (bare `--path` = cwd) — auto-detects the project that contains that directory via the existing path-based `projectNameForCwd` (def-root containment, longest wins); a bare token stays a project name. `view <path> --json` prints `{name, linear:{name,projectId}, root}` in ONE call, so a consumer gets the project name AND its Linear binding without a second `projects list` round-trip. Fail-open: an all-null shape when nothing contains the path, exit 0, never throws. The `projects for-cwd` subcommand and its `formatForCwdOutput` helper are deleted — `view <path> --json` subsumes them. Source: `cli/src/commands/projects.ts`.
44
+
45
+ - **`agents artifacts share` is private by default when signed in, and storage + visibility now live in one shared module.** A publish with no `--visibility` flag is now `me` (owner-only, Phoenix-gated) instead of `public` — signed in, your share is private unless you opt into `--visibility org` (everyone at your email domain, no org to create) or `--visibility public` (the gallery). A BYO (signed-out) publish still defaults to `public`, since the Worker refuses `me`/`org` without a Phoenix owner. The managed-vs-BYO selection policy and the visibility model are now the single `cli/src/lib/storage/` module (`selectStorageBackendKind` + `publishVisibility`/`defaultVisibilityForBackend`), consumed by both `agents artifacts share` (`lib/share/backend.ts`) and `agents traces sync` (`lib/traces/backend.ts`) instead of each re-deriving it — the seam the `sessions` backup surface reuses next. Failed share calls (publish/list/revisions/delete) now surface the Worker's own error — a 400/413/429 shows its `{"error":"…"}` reason and a 429's `Retry-After` — via one bounded extractor (`lib/share/http-error.ts`) that never dumps an arbitrary body. Source: `cli/src/lib/storage/`, `cli/src/lib/share/{backend,publish,delete,http-error}.ts`, `cli/src/commands/share.ts`.
46
+
47
+ - **`agents view grok` shows the last-known usage % again instead of a numberless "run grok once to refresh usage".** Two bugs compounded. (1) `getGrokUsageInfo` read `unified.jsonl` from the home it was given, but `agents view` fetches usage per INSTALLED VERSION, passing each version's isolated home (`~/.agents/.history/versions/grok/<ver>`) — whose `.grok/logs/unified.jsonl` never exists, because Grok writes its billing log only to the user's shared real home `~/.grok`. So every version fell back to the refresh hint while the real reading (e.g. week 42%) sat unread in the shared home. Grok usage now resolves the requested home's log first and falls back to the shared `~/.grok` log where Grok actually writes (a per-version log, if one ever appears, still wins). The shared last line is attributed only to the identity that owns `~/.grok` (matching `auth.json`, or the real home itself) — other version-scoped Grok accounts stay as no-recent-usage rather than inheriting that meter. (2) The usage cache serializer persisted only fresh `windows`, dropping the `staleWindows` Grok's collector pre-partitions an ended-period reading onto — so the daemon-refreshed cache the plain `agents view grok` reads rendered the plan alone (no bar), even right after a `--refresh` had shown "W: 42% · stale". The serializer now persists the union of fresh and stale windows; `deserializeClaudeUsageSnapshot` re-runs the freshness gate on read, so the round-trip is preserved for every collector (Claude, which returns raw windows, is unchanged). Source: `cli/src/lib/accounting/usage.ts`.
48
+
3
49
  ## 1.22.68
4
50
 
5
51
  Fix OpenCode session history to index and backfill the genuine first user request.
package/README.md CHANGED
@@ -34,7 +34,6 @@
34
34
  &nbsp;&nbsp;&nbsp;&nbsp;
35
35
  <a href="https://dev.meta.ai/docs/muse-code" title="Meta Muse Code"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/harnesses/muse.svg"><img src="assets/harnesses/muse-light.svg" height="32" alt="Meta Muse Code" /></picture></a>
36
36
  &nbsp;&nbsp;&nbsp;&nbsp;
37
- <a href="https://omp.sh" title="Oh My Pi"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/harnesses/pi.svg"><img src="assets/harnesses/pi-light.svg" height="32" alt="Oh My Pi" /></picture></a>
38
37
  &nbsp;&nbsp;&nbsp;&nbsp;
39
38
  <a href="https://docs.warp.dev/reference/cli" title="Warp Agent CLI (Oz)"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/harnesses/warp.svg"><img src="assets/harnesses/warp-light.svg" height="32" alt="Warp Agent CLI" /></picture></a>
40
39
  </p>
@@ -54,7 +53,7 @@ agents setup status # readiness for browser, computer, fleet,
54
53
  agents run claude "explain this repo" # run any agent on your existing subscription
55
54
  ```
56
55
 
57
- Everything here — and every other command in this README — is free and needs no account; the optional `agents auth login` exists only for [team spaces](#sign-in). `agents setup` is interactive and idempotent -- safe to re-run on any machine. Once core setup exists, it opens a status-aware menu for browser, computer, secrets, fleet, share, watchdog, and device preferences; each choice delegates to the same wizard available under `agents setup <capability>`. In CI or another non-TTY, bare setup prints the checklist without prompting. The `agi-cli.sh` one-liner installs this same canonical `@phnx-labs/agents-cli` package. Prefer bun? `bun install -g @phnx-labs/agents-cli` works too.
56
+ Everything here — and every other command in this README — is free and needs no account; the optional `agents auth login` unlocks the hosted surfaces — [team spaces](#sign-in) and managed, private-by-default sharing (`agents artifacts share` / `agents traces sync` with no Cloudflare setup). `agents setup` is interactive and idempotent -- safe to re-run on any machine. Once core setup exists, it opens a status-aware menu for browser, computer, secrets, fleet, share, watchdog, and device preferences; each choice delegates to the same wizard available under `agents setup <capability>`. In CI or another non-TTY, bare setup prints the checklist without prompting. The `agi-cli.sh` one-liner installs this same canonical `@phnx-labs/agents-cli` package. Prefer bun? `bun install -g @phnx-labs/agents-cli` works too.
58
57
 
59
58
  The command surface teaches setup through `agents setup` and group-level `--help`.
60
59
  The durable system model starts at [`cli/docs/README.md`](cli/docs/README.md).
@@ -416,11 +415,11 @@ agents sessions resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # original harness/
416
415
  agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its account is unavailable
417
416
  ```
418
417
 
419
- `agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents sessions resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and recover on the source device. If the origin version is installed, signed in, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories. Otherwise a healthy version of the **same harness** starts with `/continue <id>`, which reads the indexed transcript even when the old version home is retained under version trash or the same version number was reinstalled into a new home. It never native-resumes from a different isolated home. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
418
+ `agents sessions resume` reopens several sessions in whatever terminal you're in -- auto-detected across iTerm, Ghostty, tmux, and the VSCodium agent-terminal, or forced with `--iterm` / `--ghostty` / `--tmux` / `--vscodium`. `agents sessions resume <id>` resumes one session without requiring you to name its harness: exact IDs take a local SQLite fast path, then resolve fleet-wide and recover on the source device. If the origin version is installed, signed in, healthy, and still owns the indexed transcript, its isolated home performs native resume. Claude launches that native resume from the original project directory recorded before the first turn, so its `projects/<cwd-key>` lookup reaches the conversation even when the session later changed directories. When that origin login is usage-limited but the home still owns the transcript, resume stays native and rotates to a healthy injectable provider account of the **same harness**. `/continue <id>` is the last-resort fallback — a signed-out, revoked, trashed, backup-only, or same-number-reinstalled origin, or a limited origin whose transcript is no longer in that home — and still authenticates as a healthy same-harness account (injecting a provider credential when that is the pick, so it never launches the exhausted native login). It never native-resumes from a different isolated home. Back them with **tmux** and the runs turn durable: detach, close your editor, reboot the GUI -- the session is still alive to `agents tmux attach`. The whole `agents tmux` subsystem (persistent multiplexer sessions that survive editor restarts and can be shared with other tools) sits underneath.
420
419
 
421
420
  ### Send an agent to the background — and bring it back
422
421
 
423
- Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact healthy origin home, or same-harness `/continue` when that home is unavailable, with the full indexed history (including whatever it did while backgrounded).
422
+ Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the exact origin home (healthy login, or a rotated injectable account on a usage limit), or same-harness `/continue` as last resort when that home is unavailable, with the full indexed history (including whatever it did while backgrounded).
424
423
 
425
424
  ```
426
425
  agents sessions detach a1b2c3d4 # go headless in the background, keep working
@@ -658,6 +657,9 @@ agents fleet status # online/offline rollup + NEEDS ATTENTIO
658
657
  agents fleet status --verbose # full per-device auth/CLI/sync/version grid
659
658
  agents fleet status --live # force a live resource probe (alias of --refresh)
660
659
  agents fleet status --json --strict # scriptable fleet health gate
660
+ agents fleet worktrees # held agent worktrees by bucket: stranded (unmerged) · dirty · undeterminable
661
+ agents fleet worktrees --fleet # aggregate the held set across every online device
662
+ agents fleet worktrees --push # publish stranded on-no-remote branches (recovers work; never deletes)
661
663
  agents devices harnesses # per device: agent@version · account · signed · quota · ready
662
664
  agents devices accounts # same, one row per account (which harnesses share it)
663
665
  agents devices harnesses --agents claude,codex --json # scoped, machine-readable
@@ -1087,7 +1089,7 @@ agents browser profiles create cloud \
1087
1089
 
1088
1090
  ## Sign in
1089
1091
 
1090
- Signing in is **optional**. Every local feature — `agents run`, sessions, teams, fleet dispatch, secrets, browser, computer — works with no account. The one thing an account unlocks is **team spaces**, the hosted surface under `agents auth space`. `agents auth` signs this machine in to **Phoenix ID**, the Phoenix Labs account layer behind team spaces. Sign-in is Google-only and runs a device-code flow: the CLI shows a code, your browser confirms it, and the CLI picks the session up.
1092
+ Signing in is **optional**. Every local feature — `agents run`, sessions, teams, fleet dispatch, secrets, browser, computer — works with no account. Signing in unlocks the hosted surfaces: **team spaces** (`agents auth space`) and **managed sharing** — `agents artifacts share` and `agents traces sync` publish to our storage with **zero Cloudflare setup**, and a signed-in `agents artifacts share` is **private by default** (`me`, owner-only) unless you pass `--visibility org`/`public`. `agents auth` signs this machine in to **Phoenix ID**, the Phoenix Labs account layer behind these hosted surfaces. Sign-in is Google-only and runs a device-code flow: the CLI shows a code, your browser confirms it, and the CLI picks the session up.
1091
1093
 
1092
1094
  ```bash
1093
1095
  agents auth login # shows a code, opens a Phoenix-branded page
@@ -1322,6 +1324,7 @@ Sources: a command's stdout (`--watch` / `--poll`), an HTTP endpoint (`--poll-ht
1322
1324
  # Signed in? Just publish — no Cloudflare setup.
1323
1325
  agents auth login
1324
1326
  agents artifacts share plan.html --visibility unlisted # → https://share.agents-cli.sh/<handle>/<slug>-<id>
1327
+ agents artifacts share secret.html --protected # → https://share.agents-cli.sh/<handle>/<slug>-<id>?k=<token>
1325
1328
 
1326
1329
  # Or provision your own Cloudflare R2 (~$0).
1327
1330
  agents artifacts setup # once: provision bucket + Worker on your CF
@@ -1331,7 +1334,7 @@ agents artifacts share plan.html --label "Q3 fleet plan" --meta kind=plan # hu
1331
1334
  agents artifacts share plan.html --json # URL object for plan-render hooks
1332
1335
  agents artifacts share list --agent claude # public gallery, filterable
1333
1336
  agents artifacts share list --meta kind=plan # exact, repeatable metadata filters
1334
- agents artifacts share list --all # include hidden unlisted/me/org pages
1337
+ agents artifacts share list --all # include hidden unlisted/private/me/org pages
1335
1338
  agents artifacts share list --scope me # just the owner-only pages
1336
1339
  agents artifacts share edit fleet --label "Final fleet plan" --meta status=final
1337
1340
  agents artifacts share revisions fleet # prior versions kept under a slug
@@ -1353,7 +1356,10 @@ Worker lazily renders and caches the branded 1200×630 Open Graph card at `<slug
1353
1356
  so publishing from Linux does not require a local Chromium; BYO endpoints keep the
1354
1357
  local screenshot fallback. `--visibility unlisted` (hidden aliases
1355
1358
  `--unlisted` / `--private`) is a capability URL: GET still works, the gallery hides it,
1356
- and the Worker sends `X-Robots-Tag: noindex`. `--visibility me` is visible only to
1359
+ and the Worker sends `X-Robots-Tag: noindex`. `--protected` (`--visibility private`)
1360
+ is token-gated: the published URL carries a secret `?k=` key (`Authorization: Bearer`
1361
+ is accepted too) and GET returns 404 without it — treat the whole link as a secret.
1362
+ `--visibility me` is visible only to
1357
1363
  you (the signed-in owner); `--visibility org` is visible to anyone at your email
1358
1364
  **domain** — derived from your own address, so it needs a **workspace** Google
1359
1365
  account and is refused on a public-inbox domain (`gmail.com`, `outlook.com`,
@@ -1373,8 +1379,9 @@ single **Phoenix ID** (Google-only device-code OAuth, `agents auth login`) — s
1373
1379
  a Cloudflare API token from your `cloudflare.com` secrets bundle (or `--token`), creates
1374
1380
  an R2 bucket, uploads a tiny Worker, and enables the free `*.workers.dev` subdomain (or
1375
1381
  maps `--domain share.example.com` when the token owns the zone). Writes are bearer-gated
1376
- **through** the Worker (Phoenix bearer or static `WRITE_TOKEN`); reads are **public**, so
1377
- a link outlives the agent. R2 has zero egress + a 10 GB free tier, so BYO is still
1382
+ **through** the Worker (Phoenix bearer or static `WRITE_TOKEN`); public and unlisted
1383
+ reads are **public** so a link outlives the agent, while `--protected` token-gates
1384
+ reads on BYO too. R2 has zero egress + a 10 GB free tier, so BYO is still
1378
1385
  effectively free.
1379
1386
 
1380
1387
  **Fleet mode:** provision one endpoint, then every fleet / cloud / ephemeral agent
@@ -1594,7 +1601,6 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
1594
1601
  | OpenCode | yes | no | yes | >= 1.1.1 | yes | yes | no | no | `AGENTS.md` | no |
1595
1602
  | Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1596
1603
  | Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1597
- | Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
1598
1604
  | Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
1599
1605
  | Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1600
1606
  | Droid | yes | yes | yes | >= 0.57.5 | >= 0.26.0 | yes | yes | yes | `AGENTS.md` | no |
@@ -1615,17 +1621,14 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
1615
1621
  | Antigravity | -- | yes | -- |
1616
1622
  | Copilot | -- | -- | yes |
1617
1623
  | OpenClaw, Amp, Goose, Roo | -- | -- | -- |
1618
- | Kiro | -- | -- | -- |
1619
1624
 
1620
1625
  ### Version-gated sync
1621
1626
 
1622
1627
  | Capability | Agent | Gate |
1623
1628
  |------------|-------|------|
1624
1629
  | Hooks | Codex | >= 0.116.0 |
1625
- | Subagents | Kiro | >= 1.23.0 |
1626
1630
  | Skills | Droid | >= 0.26.0 |
1627
1631
  | Permissions | Droid | >= 0.57.5 |
1628
- | Permissions | Kiro | >= 2.8.0 |
1629
1632
  | File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
1630
1633
  | Plugins | Codex | >= 0.128.0 |
1631
1634
 
@@ -1641,7 +1644,7 @@ Claude Code, Codex CLI, Antigravity, Grok Build, and others each have their own
1641
1644
 
1642
1645
  ### Is it free?
1643
1646
 
1644
- Yes — every feature, with no account and no signup. `agents run`, sessions, teams, fleet dispatch, secrets, browser, and computer automation all work the moment you install. The optional `agents auth login` exists only to unlock hosted team spaces (`agents auth space`); there are no paid tiers. This developer tool is entirely free because we believe developers should have the best tools — fast and robust — so they can create the best products for their users.
1647
+ Yes — every feature, with no account and no signup. `agents run`, sessions, teams, fleet dispatch, secrets, browser, and computer automation all work the moment you install. The optional `agents auth login` unlocks the hosted surfaces — team spaces (`agents auth space`) and managed, private-by-default sharing (`agents artifacts share` / `agents traces sync`, no Cloudflare setup); there are no paid tiers. This developer tool is entirely free because we believe developers should have the best tools — fast and robust — so they can create the best products for their users.
1645
1648
 
1646
1649
  ### Is this like `nvm` / `mise` / `asdf` for AI agents?
1647
1650
 
package/dist/bootstrap.js CHANGED
@@ -19,6 +19,7 @@ import * as path from 'path';
19
19
  import { fileURLToPath } from 'url';
20
20
  import { detectDevBuild } from './lib/startup/dev-build.js';
21
21
  import { configureRootCommand } from './lib/startup/root-command.js';
22
+ import { bootMark } from './lib/boot-profile.js';
22
23
  // `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
23
24
  // modules are imported dynamically at their use sites: they are needed only on
24
25
  // interactive / update / shim-repair paths, never for fast commands like
@@ -1029,6 +1030,7 @@ if (helpAllRequested) {
1029
1030
  // immediately: skip the update check (PATH scan + cache read) and the detached
1030
1031
  // background sync (spawns a child process) that every other invocation runs.
1031
1032
  if (!isDocumentationRequest) {
1033
+ bootMark('bootstrap:evaluated');
1032
1034
  // Run update check before parsing so the upgrade notice/prompt precedes output.
1033
1035
  await checkForUpdates();
1034
1036
  // Fire-and-forget the background sync. System repo gets a real fast-forward
@@ -1157,6 +1159,7 @@ if (passedArgs.length === 0) {
1157
1159
  }
1158
1160
  try {
1159
1161
  await maybeBootstrapShimIntegration(requestedCommand, isDocumentationRequest, verboseStartup);
1162
+ bootMark('bootstrap:pre-parse');
1160
1163
  await program.parseAsync();
1161
1164
  }
1162
1165
  catch (err) {
@@ -336,7 +336,10 @@ export async function listSwitchableAccounts(agent) {
336
336
  */
337
337
  export function setDefaultAccount(agentRaw, name) {
338
338
  const agent = parseHarness(agentRaw);
339
- const account = findUnifiedAccount(name, readMeta());
339
+ // Scope to the harness this default is FOR: a bare identity (`<email>`) matches
340
+ // every harness that identity is signed into, so an un-scoped lookup resolved
341
+ // the wrong row and then rejected it as "is a <other> login" just below.
342
+ const account = findUnifiedAccount(name, readMeta(), undefined, agent);
340
343
  if (!account)
341
344
  throw new Error(`Unknown account '${name}'.`);
342
345
  if (account.kind === 'provider') {
@@ -519,14 +522,19 @@ agents run codex#work`,
519
522
  .action(async (name, target, _o, command) => {
520
523
  await runAccountsAction(command, async () => {
521
524
  const meta = readMeta();
522
- const account = findUnifiedAccount(name, meta);
525
+ // Validate the target exists before mutating any binding — and resolve it
526
+ // FIRST so the account lookup can be scoped to the harness being attached
527
+ // to. `identityLabel` defaults to the login's email, so a bare identity
528
+ // (`muqsitnawaz@gmail.com`) matches every harness that identity is signed
529
+ // into; un-scoped this resolved whichever row the store ordered first and
530
+ // then rejected it below as "is a <other> login".
531
+ const t = classifyAttachTarget(target);
532
+ const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
533
+ const account = findUnifiedAccount(name, meta, undefined, targetAgent);
523
534
  if (!account)
524
535
  throw new Error(`Unknown account '${name}'.`);
525
536
  if (account.kind === 'native')
526
537
  assertNativeAccountNameable(account.agent);
527
- // Validate the target exists before mutating any binding.
528
- const t = classifyAttachTarget(target);
529
- const targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
530
538
  if (account.kind === 'native') {
531
539
  // A provider-backed profile injects provider env at spawn, which would run
532
540
  // under a different credential than the native identity claims — refuse it.
@@ -572,7 +580,7 @@ agents run codex#work`,
572
580
  // Provider account: it must be able to authenticate the target's harness.
573
581
  getAccountProvider(account.provider).envFor(targetAgent, account.auth);
574
582
  }
575
- bindAccount(name, target);
583
+ bindAccount(name, target, targetAgent);
576
584
  writeClaudeInteractiveOauthToken(t, targetAgent, account.kind === 'native' && account.agent === 'claude' ? account.identityLabel : undefined);
577
585
  console.log(chalk.green(`Attached ${account.name} to ${target}.`));
578
586
  });
@@ -581,7 +589,15 @@ agents run codex#work`,
581
589
  .description('Remove one account attachment')
582
590
  .action(async (name, target, _o, command) => {
583
591
  await runAccountsAction(command, () => {
584
- unbindAccount(name, target);
592
+ // Classify the target first so the unbind is scoped to the right harness
593
+ // for a colliding identity selector (same as the attach path).
594
+ let targetAgent;
595
+ try {
596
+ const t = classifyAttachTarget(target);
597
+ targetAgent = t.kind === 'profile' ? t.profile.host.agent : t.agent;
598
+ }
599
+ catch { /* an unresolvable target still unbinds by whatever row matches */ }
600
+ unbindAccount(name, target, targetAgent);
585
601
  // With the binding gone, no setup-token resolves for this version home, so this
586
602
  // clears any .oauth_token the attach left behind (else interactive runs would keep
587
603
  // authenticating as the just-detached account).
@@ -27,6 +27,7 @@ import { randomUUID } from 'crypto';
27
27
  import { isSessionTrackedAgent } from '../lib/session/types.js';
28
28
  import { applyActiveRulesPresetAtRun } from '../lib/rules/run-sync.js';
29
29
  import { handleBroadcast } from './run-broadcast.js';
30
+ import { bootMark } from '../lib/boot-profile.js';
30
31
  /** Distinguish a terminal account-picker marker from an explicit @version pin. */
31
32
  export function parseRunAccountPickerRequest(agentSpec) {
32
33
  const requested = agentSpec.endsWith('@');
@@ -690,6 +691,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
690
691
  `,
691
692
  });
692
693
  runCmd.action(async (agentSpec, prompt, options, command) => {
694
+ bootMark('run-action:enter');
693
695
  // Capture everything after -- as passthrough args forwarded verbatim to the
694
696
  // underlying CLI. Commander strips the literal `--` and folds what follows
695
697
  // into the positional operands (so `agents run codex -- --yolo` would parse
@@ -1806,6 +1808,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1806
1808
  import('../lib/capabilities.js'),
1807
1809
  import('../lib/share/config.js'),
1808
1810
  ]);
1811
+ bootMark('run-deps:imported');
1809
1812
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
1810
1813
  // Parse agent@version#label. The label selects native auth without pinning
1811
1814
  // the binary version; --account remains the equivalent flag form.
@@ -2241,7 +2244,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2241
2244
  const { pickSessionInteractive } = await import('./sessions.js');
2242
2245
  const { buildContinuePrompt } = await import('../lib/loop.js');
2243
2246
  // Freshen the index for this agent before any lookup (incremental, cached).
2244
- // AgentId is wider than SessionAgentId (amp/kiro/goose/copilot keep no transcripts);
2247
+ // AgentId is wider than SessionAgentId (amp/goose/copilot keep no transcripts);
2245
2248
  // those simply yield no matches and fall through to the not-found error.
2246
2249
  const sessionAgent = agent;
2247
2250
  if (!resolvedResumeSource)
@@ -2345,6 +2348,25 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2345
2348
  process.exit(1);
2346
2349
  }
2347
2350
  version = resolvedRecoveryTarget.version;
2351
+ // Account rotation (PHNX-3626 / PHNX-3674): recovery picked a healthy
2352
+ // provider of the SAME harness. Inject that credential through the
2353
+ // `--account` path so spawn does not authenticate as the version home's
2354
+ // native login (the exhausted origin when the continue pick is a
2355
+ // provider). An explicit --account (configuredAccount) always wins.
2356
+ const rotatedAccount = resolvedRecoveryTarget.account;
2357
+ if (rotatedAccount && !configuredAccount) {
2358
+ try {
2359
+ const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
2360
+ if (picked?.kind === 'provider')
2361
+ accountEnv = picked.env;
2362
+ }
2363
+ catch {
2364
+ // Account-registry errors can carry provider credential material;
2365
+ // never relay them onto the terminal from this automatic path.
2366
+ console.error(chalk.red('Could not prepare the rotated provider account for session recovery.'));
2367
+ process.exit(1);
2368
+ }
2369
+ }
2348
2370
  if (resolvedRecoveryTarget.mode === 'native') {
2349
2371
  version = session.version;
2350
2372
  resumeNative = true;
@@ -2355,27 +2377,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2355
2377
  // differ from the earlier cwd that selected projects/<cwd-key>.
2356
2378
  if (!options.cwd && resolvedRecoveryTarget.cwd)
2357
2379
  options.cwd = resolvedRecoveryTarget.cwd;
2358
- // Account rotation on a limit (PHNX-3626): recovery kept resume NATIVE
2359
- // in the origin home but rotated to a healthy provider account of the
2360
- // SAME harness. Inject that credential through the same `--account`
2361
- // path an explicit selection uses, so exec authenticates as the
2362
- // rotated account while reading the origin transcript. An explicit
2363
- // --account (configuredAccount) always wins — never rotate over it.
2364
- const rotatedAccount = resolvedRecoveryTarget.account;
2365
- if (rotatedAccount && !configuredAccount) {
2366
- try {
2367
- const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
2368
- if (picked?.kind === 'provider')
2369
- accountEnv = picked.env;
2370
- }
2371
- catch {
2372
- // Account-registry errors can carry provider credential material;
2373
- // never relay them onto the terminal from this automatic path.
2374
- console.error(chalk.red('Could not prepare the rotated provider account for native resume.'));
2375
- process.exit(1);
2376
- }
2377
- if (!options.quiet)
2378
- process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
2380
+ if (rotatedAccount && !configuredAccount && !options.quiet) {
2381
+ process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
2379
2382
  }
2380
2383
  if (!options.quiet)
2381
2384
  process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
@@ -2386,6 +2389,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2386
2389
  prompt = buildContinuePrompt(session.id, prompt);
2387
2390
  if (prompt.trim() === `/continue ${session.id}`)
2388
2391
  forceInteractive = true;
2392
+ if (rotatedAccount && !configuredAccount && !options.quiet) {
2393
+ process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (/continue)\n`));
2394
+ }
2389
2395
  if (!options.quiet)
2390
2396
  process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)${version ? ` @${version}` : ''}\n`));
2391
2397
  }
@@ -2443,7 +2449,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2443
2449
  // ones sitting in a version home (RUSH-3182). Run-path only — the
2444
2450
  // picker's other callers keep the native-only collector.
2445
2451
  const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
2452
+ bootMark('resolve-version:start');
2446
2453
  const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun);
2454
+ bootMark('resolve-version:done');
2447
2455
  if (resolved.exhausted) {
2448
2456
  // Zero healthy accounts splits two ways, and conflating them is what
2449
2457
  // stranded a logged-out harness with no way in at all (RUSH-2334):
@@ -2610,6 +2618,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2610
2618
  version = healed;
2611
2619
  }
2612
2620
  }
2621
+ bootMark('ensure-runnable:done');
2613
2622
  // The harness may simply not be on this machine. The self-heal above only
2614
2623
  // runs when a managed version resolved, so with nothing installed we used
2615
2624
  // to fall through and spawn the bare `cliCommand`, which dies as
@@ -2646,6 +2655,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2646
2655
  if (defaultVersion) {
2647
2656
  applyActiveRulesPresetAtRun(agent, defaultVersion, getVersionHomePath(agent, defaultVersion));
2648
2657
  }
2658
+ bootMark('rules-sync:done');
2649
2659
  // Login preflight (advisory, warn + continue). On a local INTERACTIVE
2650
2660
  // launch, probe whether this agent's account has a credential and print a
2651
2661
  // one-line warning if it looks logged out — so you find out BEFORE the TUI
@@ -2713,7 +2723,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2713
2723
  process.exit(1);
2714
2724
  }
2715
2725
  // Default CLI mode is the generic 'plan'. Agents without a read-only
2716
- // mode (antigravity, kiro, …) degrade via resolveMode to their
2726
+ // mode (antigravity, …) degrade via resolveMode to their
2717
2727
  // safest native mode (modes[0], typically edit). That covers both the
2718
2728
  // implicit default and an explicit `--mode plan`, so multi-agent
2719
2729
  // scripts can pass a uniform plan flag without per-agent branching.
@@ -5,7 +5,8 @@ import { projectKeyFromCwd } from '../lib/project-key.js';
5
5
  import { postFeedStatus } from '../lib/feed-post.js';
6
6
  import { linearIssueUrl } from '../lib/session/linear.js';
7
7
  import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, withDesktopNotify, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
8
- import { getSessionById } from '../lib/session/db.js';
8
+ import { getSessionById, resolveFullSessionId } from '../lib/session/db.js';
9
+ import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
9
10
  import { readMeta } from '../lib/state.js';
10
11
  import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
11
12
  import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
@@ -692,7 +693,10 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
692
693
  const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, level, meta), notify);
693
694
  if (!config)
694
695
  return [];
695
- const ticket = getSessionById(event.sessionId)?.ticketId;
696
+ // Upgrade an 8-char footer crumb to the full indexed id so the console session
697
+ // URL resolves instead of 404ing, and so the ticket join hits the right row.
698
+ const session = resolveFullSessionId(event.sessionId) ?? event.sessionId;
699
+ const ticket = getSessionById(session)?.ticketId;
696
700
  const planned = planFeedBroadcast(config, {
697
701
  title: event.title,
698
702
  text: event.detail ?? '',
@@ -702,11 +706,16 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
702
706
  project: event.project,
703
707
  agent: event.agent,
704
708
  host: event.host,
705
- session: event.sessionId,
709
+ session,
706
710
  links: (event.attachments ?? [])
707
711
  .map((a) => a.href)
708
712
  .filter((href) => /^https?:\/\//i.test(href)),
709
- });
713
+ }, meta);
714
+ // An important post links the session's console page; fire the trace sync now
715
+ // so that page exists when the owner taps it (trace sync otherwise waits for
716
+ // run exit — PHNX-3628/PHNX-3698). Gated + best-effort, never blocks the post.
717
+ if (level === 'important')
718
+ fireTraceSyncInBackground();
710
719
  return runFeedBroadcast(planned, meta);
711
720
  }
712
721
  /**
@@ -722,9 +731,13 @@ async function broadcastBlock(block, extras, meta, notify = false) {
722
731
  const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, 'important', meta), notify);
723
732
  if (!config)
724
733
  return [];
725
- const ticket = getSessionById(block.sessionId)?.ticketId;
726
- const ctx = blockBroadcastContext({ ...block, ticket: block.ticket ?? ticket }, extras);
727
- return runFeedBroadcast(planFeedBroadcast(config, ctx), meta);
734
+ const sessionId = resolveFullSessionId(block.sessionId) ?? block.sessionId;
735
+ const ticket = getSessionById(sessionId)?.ticketId;
736
+ const ctx = blockBroadcastContext({ ...block, sessionId, ticket: block.ticket ?? ticket }, extras);
737
+ // A block is always important and links the session's console page — fire the
738
+ // trace sync so the page exists when tapped (best-effort, gated, non-blocking).
739
+ fireTraceSyncInBackground();
740
+ return runFeedBroadcast(planFeedBroadcast(config, ctx, meta), meta);
728
741
  }
729
742
  /** One line per sink that ran. Silent when nothing is configured. */
730
743
  function reportBroadcast(outcomes) {
@@ -56,7 +56,7 @@ async function runImport(agentArg, opts) {
56
56
  if (!opts.isolated) {
57
57
  assertIsolationBoundary(agentId, 'adopt your existing install');
58
58
  }
59
- // installScript-based agents (Grok, Antigravity, Cursor, Kiro, Goose, Roo)
59
+ // installScript-based agents (Grok, Antigravity, Cursor, Goose, Roo)
60
60
  // don't have an npm package; their binary lives wherever the curl/brew
61
61
  // installer dropped it. We adopt by symlinking that PATH binary directly
62
62
  // into the version's `node_modules/.bin/`. No package.json walk.
@@ -345,7 +345,7 @@ When to use:
345
345
  pointing at the existing install — nothing is copied or moved (except the
346
346
  agent's config dir, which is moved into the version's home). Works for both
347
347
  npm-style packages (claude, codex, opencode, openclaw) and
348
- installScript-based agents (grok, antigravity, cursor, kiro, goose).
348
+ installScript-based agents (grok, antigravity, cursor, goose).
349
349
  `)
350
350
  .action((...args) => withIsolationBoundary(() => runImport(...args)));
351
351
  }
@@ -18,13 +18,13 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
18
18
  import { wrapJoined } from './inspect.js';
19
19
  import { setHelpSections } from '../lib/help.js';
20
20
  import { formatRunDefaultEntry, listRunDefaults, parseRunDefaultSelector, setRunDefault, } from '../lib/run-defaults.js';
21
- const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid', 'pi'];
21
+ const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi', 'grok', 'droid'];
22
22
  /**
23
23
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
24
24
  * via a curl script). For these, fall back to the PATH binary and synthesize
25
25
  * a version label from the install path so cache keys stay stable.
26
26
  */
27
- const PATH_ONLY_AGENTS = new Set(['cursor', 'pi']);
27
+ const PATH_ONLY_AGENTS = new Set(['cursor']);
28
28
  /** Derive a version label from the PATH-installed binary location for agents without managed versions. */
29
29
  function fallbackPathVersion(agent) {
30
30
  const src = locateModelSource(agent, 'unresolved');
@@ -947,6 +947,9 @@ export function registerMonitorsCommands(program) {
947
947
  console.log(observation.raw.split('\n').slice(0, 20).map((l) => ` ${l}`).join('\n'));
948
948
  if (observation.meta)
949
949
  console.log(chalk.gray(` meta: ${JSON.stringify(observation.meta)}`));
950
+ if (observation.failed) {
951
+ console.log(chalk.yellow(` poll failed (${observation.failureReason ?? 'observation failure'}) — not a value change; skipped`));
952
+ }
950
953
  console.log('');
951
954
  console.log(`Would fire: ${wouldFire ? chalk.green('yes') : chalk.gray('no')}`);
952
955
  if (decision?.event) {
@@ -377,8 +377,8 @@ Examples:
377
377
  //
378
378
  // This used to hardcode `.json`/`.jsonc`/`.toml` plus a `.claude`/
379
379
  // `.opencode`/`.codex` substring, which excluded the ten other harnesses
380
- // the CLI writes -- and excluded kiro/hermes twice over, since
381
- // their configs are YAML. The registry already answers this by matching
380
+ // the CLI writes -- and excluded hermes twice over, since
381
+ // its config is YAML. The registry already answers this by matching
382
382
  // each harness's own declared path, so ask it (RUSH-2676).
383
383
  const isAgentConfig = detectPermissionAgentFromPath(localPath) !== null;
384
384
  if (isAgentConfig) {
@@ -13,13 +13,33 @@ import { type LinearMilestone } from '../lib/linear-project-counts.js';
13
13
  /** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
14
14
  export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
15
15
  /**
16
- * Render `agents projects for-cwd`'s output for a resolved (or absent)
17
- * project name — `--json` always prints `{"name": ...}` even on no match, so
18
- * a scripted caller can distinguish "ran and found nothing" from a crash;
19
- * the plain-text form prints nothing on no match. Returns '' when nothing
20
- * should be printed.
16
+ * A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
17
+ * value, or anything containing a path separator — means "auto-detect the
18
+ * project for this DIRECTORY", not "a project named literally this". A bare
19
+ * token (no separators) stays a project name, exactly as before.
21
20
  */
22
- export declare function formatForCwdOutput(name: string | undefined, json: boolean): string;
21
+ export declare function looksLikePath(token: string): boolean;
22
+ /** Machine-readable shape of `agents projects view <path> --json`. */
23
+ export interface ViewDetection {
24
+ /** Detected project name, or null when no definition contains the path. */
25
+ name: string | null;
26
+ /** The detected project's Linear binding; fields are null when unbound. */
27
+ linear: {
28
+ name: string | null;
29
+ projectId: string | null;
30
+ };
31
+ /** The detected project's repo/monorepo root (home-relative), or null. */
32
+ root: string | null;
33
+ }
34
+ /**
35
+ * Best-effort cwd->project detection for `agents projects view <path>`. Delegates
36
+ * to the shared {@link projectNameForCwd} (def-root containment, longest wins),
37
+ * then reads the matched definition's Linear binding and root so ONE call yields
38
+ * the name AND the Linear projectId. Fail-open: an all-null shape when nothing
39
+ * contains the path — never throws, so a scripted caller stays unscoped rather
40
+ * than crashing. This subsumes the removed `agents projects for-cwd`.
41
+ */
42
+ export declare function detectProjectForPath(cwd: string, defs: ProjectDef[]): ViewDetection;
23
43
  /**
24
44
  * One compact trailing note for peers that didn't answer the `--fleet`
25
45
  * fan-out — unreachable, running an agents-cli too old to carry `projects