@phnx-labs/agents-cli 1.22.69 → 1.22.71

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 (130) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +34 -9
  3. package/dist/bootstrap.js +4 -4
  4. package/dist/commands/accounts.js +23 -7
  5. package/dist/commands/exec.js +2 -2
  6. package/dist/commands/import.js +2 -2
  7. package/dist/commands/models.js +2 -2
  8. package/dist/commands/permissions.js +2 -2
  9. package/dist/commands/repo.js +2 -2
  10. package/dist/commands/rules.js +1 -1
  11. package/dist/commands/send.js +9 -3
  12. package/dist/commands/sessions-export.d.ts +5 -1
  13. package/dist/commands/sessions-export.js +100 -24
  14. package/dist/commands/sessions-import.d.ts +2 -1
  15. package/dist/commands/sessions-import.js +85 -21
  16. package/dist/commands/traces.js +1 -1
  17. package/dist/lib/account-capabilities.js +0 -2
  18. package/dist/lib/account-registry.d.ts +3 -3
  19. package/dist/lib/account-registry.js +25 -7
  20. package/dist/lib/accounting/usage-sync.d.ts +1 -1
  21. package/dist/lib/accounting/usage-sync.js +3 -3
  22. package/dist/lib/acp/client.d.ts +1 -1
  23. package/dist/lib/acp/client.js +12 -1
  24. package/dist/lib/acp/harnesses.js +1 -1
  25. package/dist/lib/add-dir.js +0 -2
  26. package/dist/lib/agent-cli-commands.js +0 -2
  27. package/dist/lib/agent-spec/agents.d.ts +1 -1
  28. package/dist/lib/agent-spec/agents.js +2 -83
  29. package/dist/lib/browser/ipc.d.ts +34 -0
  30. package/dist/lib/browser/ipc.js +149 -20
  31. package/dist/lib/browser/remote-control.d.ts +6 -3
  32. package/dist/lib/browser/remote-control.js +6 -3
  33. package/dist/lib/browser/service.d.ts +4 -1
  34. package/dist/lib/browser/service.js +7 -1
  35. package/dist/lib/browser/types.d.ts +3 -1
  36. package/dist/lib/channels/registry.d.ts +6 -0
  37. package/dist/lib/channels/send.d.ts +11 -2
  38. package/dist/lib/channels/send.js +11 -2
  39. package/dist/lib/cloud/rush.d.ts +10 -2
  40. package/dist/lib/cloud/rush.js +15 -9
  41. package/dist/lib/daemon/auth-sync-service.js +1 -1
  42. package/dist/lib/daemon/browser-task-reap-service.js +1 -1
  43. package/dist/lib/daemon/daemon.js +41 -11
  44. package/dist/lib/daemon/heartbeat-service.js +3 -3
  45. package/dist/lib/daemon/keychain-reap-service.js +1 -1
  46. package/dist/lib/daemon/runner.d.ts +18 -1
  47. package/dist/lib/daemon/runner.js +237 -80
  48. package/dist/lib/daemon/self-heal-service.js +13 -3
  49. package/dist/lib/daemon/self-update-service.d.ts +174 -0
  50. package/dist/lib/daemon/self-update-service.js +353 -0
  51. package/dist/lib/daemon/state-dir-check-service.js +3 -3
  52. package/dist/lib/daemon/usage-sync-service.js +1 -1
  53. package/dist/lib/daemon/watchdog-service.js +4 -4
  54. package/dist/lib/daemon-services.d.ts +1 -1
  55. package/dist/lib/daemon-services.js +5 -0
  56. package/dist/lib/device-config.d.ts +12 -1
  57. package/dist/lib/device-config.js +63 -13
  58. package/dist/lib/exec-bounded.d.ts +52 -0
  59. package/dist/lib/exec-bounded.js +113 -0
  60. package/dist/lib/exec.d.ts +2 -2
  61. package/dist/lib/exec.js +3 -34
  62. package/dist/lib/feed/events.d.ts +22 -14
  63. package/dist/lib/feed/events.js +84 -44
  64. package/dist/lib/feed-broadcast.d.ts +12 -29
  65. package/dist/lib/feed-broadcast.js +28 -27
  66. package/dist/lib/fleet-shared-state.d.ts +12 -5
  67. package/dist/lib/fleet-shared-state.js +50 -20
  68. package/dist/lib/fs-atomic.d.ts +11 -0
  69. package/dist/lib/fs-atomic.js +60 -0
  70. package/dist/lib/hooks/install.js +0 -87
  71. package/dist/lib/hosts/reconcile.d.ts +11 -4
  72. package/dist/lib/hosts/reconcile.js +31 -5
  73. package/dist/lib/installations/strategies.js +1 -1
  74. package/dist/lib/mcp-registry.js +0 -13
  75. package/dist/lib/mcp.js +2 -2
  76. package/dist/lib/model-tiers.js +1 -1
  77. package/dist/lib/models.js +0 -63
  78. package/dist/lib/notify.d.ts +11 -0
  79. package/dist/lib/notify.js +17 -4
  80. package/dist/lib/owner-message.d.ts +46 -3
  81. package/dist/lib/owner-message.js +26 -6
  82. package/dist/lib/permissions-registry.d.ts +0 -2
  83. package/dist/lib/permissions-registry.js +3 -50
  84. package/dist/lib/permissions.d.ts +3 -17
  85. package/dist/lib/permissions.js +4 -73
  86. package/dist/lib/project-resources.d.ts +12 -0
  87. package/dist/lib/project-resources.js +129 -0
  88. package/dist/lib/routine-process-cleanup.d.ts +2 -2
  89. package/dist/lib/routine-process-cleanup.js +45 -34
  90. package/dist/lib/rush-session.d.ts +19 -0
  91. package/dist/lib/rush-session.js +24 -0
  92. package/dist/lib/secrets/drivers/rush.js +2 -1
  93. package/dist/lib/secrets/reaper.d.ts +2 -2
  94. package/dist/lib/secrets/reaper.js +13 -10
  95. package/dist/lib/secrets/reserved-sync.d.ts +1 -1
  96. package/dist/lib/secrets/reserved-sync.js +4 -4
  97. package/dist/lib/self-update.d.ts +21 -8
  98. package/dist/lib/self-update.js +54 -31
  99. package/dist/lib/session/cloud.js +2 -1
  100. package/dist/lib/session/sync/backend.d.ts +61 -0
  101. package/dist/lib/session/sync/backend.js +89 -0
  102. package/dist/lib/session/sync/managed-config.d.ts +29 -0
  103. package/dist/lib/session/sync/managed-config.js +23 -0
  104. package/dist/lib/session/sync/managed-key.d.ts +45 -0
  105. package/dist/lib/session/sync/managed-key.js +128 -0
  106. package/dist/lib/session/sync/net-client.d.ts +65 -0
  107. package/dist/lib/session/sync/net-client.js +117 -0
  108. package/dist/lib/session/sync/provision.d.ts +19 -0
  109. package/dist/lib/session/sync/provision.js +38 -0
  110. package/dist/lib/session/sync/r2.d.ts +5 -2
  111. package/dist/lib/session/sync/r2.js +5 -2
  112. package/dist/lib/session/sync/worker-template.d.ts +6 -0
  113. package/dist/lib/session/sync/worker-template.js +847 -0
  114. package/dist/lib/sink-format.d.ts +34 -0
  115. package/dist/lib/sink-format.js +17 -0
  116. package/dist/lib/staleness/detectors/permissions.js +0 -20
  117. package/dist/lib/staleness/writers/commands.js +1 -1
  118. package/dist/lib/staleness/writers/hooks.js +2 -2
  119. package/dist/lib/subagents-registry.js +2 -12
  120. package/dist/lib/subagents.d.ts +0 -10
  121. package/dist/lib/subagents.js +0 -37
  122. package/dist/lib/tmux/orphan-reap.js +6 -4
  123. package/dist/lib/tmux/session.js +4 -1
  124. package/dist/lib/traces/classify.d.ts +8 -1
  125. package/dist/lib/traces/insights.d.ts +13 -1
  126. package/dist/lib/traces/insights.js +78 -3
  127. package/dist/lib/traces/sync.js +8 -3
  128. package/dist/lib/traces/worker-template.js +9 -5
  129. package/dist/lib/types.d.ts +1 -1
  130. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.71
4
+
5
+ - **The daemon now keeps itself current instead of running stale code for days (PHNX-3695).** The long-running daemon (`agents __daemon-run`) deliberately opted out of the interactive CLI's auto-update (`AGENTS_CLI_DISABLE_AUTO_UPDATE=1` forced on for `__daemon-run`), so new agents-cli code never reached a running daemon until a human ran `agents daemon restart` — R5 ("the installed CLI auto-updates") silently did not hold for the one process that runs unattended the longest. A new supervised `self-update` service checks npm roughly every 75 minutes, installs and byte-verifies a newer version with the same primitives `agents upgrade` already uses (never a bare `npm install -g`), best-effort pulls the `.system` companion repo and reconciles with `agents sync --local`, then exits so the OS supervisor (launchd `KeepAlive` / systemd `Restart=always`) relaunches it onto the new code — clients reconnect on their own, and the scheduler's atomic `(routine, scheduledFor)` claim dedupes any routine mid-fire across the restart. It fails closed on every step: an install or verify failure leaves the running daemon untouched and retries next tick, and it no-ops on a dev build or a shadowed install. A version-skewed browser-IPC client no longer just prints "run agents daemon restart" — `reconcileDaemonVersion` now asks the daemon to run this same fail-closed path on demand (`request-self-update`), sharing one in-flight attempt with the periodic tick so concurrent requests can't race two installs. That on-demand trigger is DECOUPLED from the install: because every version-skewed `agents browser <verb>` routes through it, the daemon kicks the self-update off in the background and answers "triggered" immediately rather than making the browser verb wait out the whole check→download→install→verify (tens of seconds, worst case ~15 min) — the daemon installs, verifies, and exits on its own, and the browser reconnects. Source: `cli/src/lib/daemon/self-update-service.ts`, `cli/src/lib/browser/ipc.ts`.
6
+
7
+ - **The daemon's event loop is no longer starved by synchronous subprocess spawns on its service ticks (PHNX-3695).** Every daemon background service runs on one Node event loop, so a synchronous `execFileSync`/`readFileSync` inside a service tick froze that loop for the whole call — and while frozen the supervisor's per-tick deadline timer could not fire and the browser IPC server could not answer, which is the "accept but never reply" wedge the trivial `version` probe (`cli/src/lib/browser/ipc.ts`) surfaces. A new async, deadline-bounded, process-group-killable exec helper (`cli/src/lib/exec-bounded.ts` `execFileBounded`) replaces the tick-reachable sync spawns. The heartbeat tick is now fully async: it reaps exited routine children through `reapExitedRunningJobs` (an async twin of the sync `monitorRunningJobs`, sharing one reconciliation core so the CLI `routines list/status` builders keep their synchronous, off-loop path) with an `execFileBounded`-bounded `ps` identity probe and an async `sshExecAsync` read of a `host:`-placed run's remote exit (a synchronous `ssh` there would freeze the loop on a host timeout), plus the async terminal-routine reaper; the self-heal and state-dir-check service ticks moved their `existsSync`/`readFileSync` to `fs/promises`. The worst halt was the event-log **file lock** (`withFileLock` → `lockfile.lockSync` + `Atomics.wait`, up to 30s), reached from every `ctx.log` on every tick as well as the watchdog and routine-reaper emits; a new `withFileLockAsync` + `emitAsync` (`cli/src/lib/fs-atomic.ts`, `cli/src/lib/feed/events.ts`) acquire it without freezing the loop. The other unconditional every-tick halts are converted the same way: usage-sync/auth-sync publish through `updateFleetSharedDeviceStateAsync`, keychain-reap's whole-process-table `ps` uses `execFileBounded`, watchdog/browser-task-reap read config via async paths, and tmux-reap's `/proc` scan uses `fs/promises`. A structural guard test (`cli/src/lib/daemon/guard-no-sync-io.test.ts`) scans every `*-service.ts` tick/start body for a synchronous fs/exec/lock call AND pins that each hot tick call site uses its async variant (failing if a fix is reverted to its sync twin). Startup and stop/uninstall lifecycle code (pid/lock, `launchctl`/`systemctl`, the `ps` daemon-identity probe) stays synchronous — it runs in a short-lived CLI process, never on the served loop. Source: `cli/src/lib/{exec-bounded,fs-atomic,device-config,fleet-shared-state}.ts`, `cli/src/lib/feed/events.ts`, `cli/src/lib/secrets/{reaper,reserved-sync}.ts`, `cli/src/lib/accounting/usage-sync.ts`, `cli/src/lib/tmux/{session,orphan-reap}.ts`, `cli/src/lib/daemon/{runner,daemon,heartbeat-service,watchdog-service,usage-sync-service,auth-sync-service,keychain-reap-service,browser-task-reap-service,self-heal-service,state-dir-check-service}.ts`, `cli/src/lib/hosts/reconcile.ts`, `cli/src/lib/routine-process-cleanup.ts`, `cli/src/lib/daemon/AGENTS.md`.
8
+
9
+ - **The required PR check no longer reinstalls 273 packages on every run.** Measured across recent Linux required runs, `bun install` took 14–22s on *every* run — whether the impact plan selected 78 test files or 2 — while a release check's actual test execution is 0.27s. The install was a fixed cost every pull request paid, and the largest single item in the required-check budget. `tests.yml` now restores `~/.bun/install/cache`, `cli/node_modules`, and `packages/session-tracker/node_modules` from an `actions/cache` keyed on **both** `cli/bun.lock` and `packages/session-tracker/bun.lock` — keying on the CLI lockfile alone would have left the session-tracker tree outside its own key. The key also carries a `node -v`/`bun -v` fingerprint, because `cli/package.json` trusts a native addon (`@homebridge/node-pty-prebuilt-multiarch`) whose prebuilt binary is Node-ABI-specific and nothing pins the runner's Node. `restore-keys` is deliberately omitted: a prefix fallback would let bun install on top of a *different* lockfile's modules, and `--frozen-lockfile` only guarantees the tree matches the lock from a clean or exact-match state — correctness beats a warmer cache when an attestation binds the tested tree to the published tarball. A dependency change misses the cache and reinstalls, which is exactly the previous behavior, so a miss can never be slower than not caching. Source: `.github/workflows/tests.yml`.
10
+
11
+ - **The CI dependency cache now actually hits.** The cache added a change earlier was dead weight in practice: GitHub scopes caches per ref — a run can restore only from its own branch or the default branch — and the required `test` job runs on `pull_request` only, so every entry it saved landed under `refs/pull/<N>/merge`, invisible to every other pull request. Measured after it shipped: `deps-cache-hit=0` with `packages installed [21.63s]` on unrelated PRs, i.e. a permanent cold miss for everyone except a re-run of the same PR. A push-to-`main` `warm-dep-cache` job now writes the identical key and paths, so the first PR after any lockfile or toolchain change pays the install once and every later PR restores it. The job is push-only and never joins the required check identity. `tests-gate.test.ts` pins the two keys equal, because drift between them would be silent — the warm job would populate an entry the required job never asks for, and the cache would look healthy while never hitting. Source: `.github/workflows/tests.yml`.
12
+
13
+ - **Managed session backup: harden the storage boundary (PHNX-3726).** Follow-up security fixes to the managed sessions Worker landed in the same ticket. The Worker is now **Phoenix-only** — the static `WRITE_TOKEN` principal is removed (and no longer provisioned): the zero-knowledge `--byo` path talks to your own R2 bucket directly and never reaches this Worker, so a token here was only a Phoenix-and-quota bypass. Mandatory encryption is now enforced at the storage boundary, not just client-side: a PUT whose body is not an encrypted bundle is rejected `422`, and a managed restore fails loud on any plaintext object instead of importing it. DELETE now refunds the per-user quota ledger via a delta CAS applied BEFORE the object is removed, and fails loud (503, object left intact and charged) when that CAS stays contended — instead of deleting first and silently dropping the refund on a contended ledger. Source: `cli/src/lib/session/sync/{worker-template,provision}.ts`, `cli/src/commands/sessions-import.ts`, `cli/docs/specifications.md`.
14
+
15
+ - **Signed-in users back sessions up off-box with NO `r2.backups` bucket to set up (PHNX-3726).** `agents sessions export --to-r2` / `import --from-r2` now default to a managed, Phoenix-gated store (`sessions.agents-cli.sh`) when you are signed in (`agents auth login`) — the same zero-setup pattern `agents artifacts share` and `agents traces sync` already use, resolved through the one shared `selectStorageBackendKind` policy. It is **managed-first**: a signed-in user backs up to managed even with a stale `r2.backups` bundle present; only `--byo` / `AGENTS_SESSIONS_BACKEND=byo` flips to your own bucket. Every managed backup is **mandatorily** AES-256-GCM encrypted under a per-account key that is minted once, cached at `~/.agents/.cache/state/sessions-backup-key.json` (mode 0600), and escrowed at the bearer-gated, immutable Worker key `<userId>/__key/backup-dek` so a fresh box signing in recovers it and decrypts prior backups with zero setup — the managed path never uploads plaintext. Both the command and Worker reject plaintext/malformed managed objects, and restore revalidates every at-rest bundle before decryption. Honest trust boundary: confidential vs a raw R2/Cloudflare bucket read (ciphertext at rest) but NOT zero-knowledge vs Phoenix (the DEK is escrowed on Phoenix infra); `--byo` keeps the zero-knowledge own-bucket path (the key stays only in your `r2.backups` bundle). The isolated Worker verifies the Phoenix bearer on every PUT/GET/LIST/DELETE, enforces `segments[0] == userId` (401/403, no public GET), and meters a per-user CAS quota (`__usage`, 413 over quota). Same-key managed PUT/DELETE operations share an expiring R2 lease; every object change is etag-conditioned, DELETE leaves a GET-404/LIST-hidden tombstone, and expired recovery fences the predecessor's etag before settling quota. The ledger retains an idempotent pending delta plus a terminal generation per path in the same CAS object, preventing late predecessor charges, replacement deletes, double refunds, stranded paths, and phantom quota. A CAS-reserved historical-path budget is consumed before persistent per-path state is created and is not refunded on DELETE, so authenticated create/delete churn cannot grow tombstones, leases, or ledger history past the hard cap. Reserved `__usage`/`__key` prefixes are never listed. The BYO object layout is byte-for-byte unchanged (the `<userId>/` prefix is added inside the client) and the Worker is integration-tested against real workerd. Source: `cli/src/lib/session/sync/{backend,managed-config,managed-key,net-client,provision,worker-template}.ts`, `cli/src/commands/sessions-export.ts`, `cli/src/commands/sessions-import.ts`, `cli/docs/{sessions,specifications}.md`.
16
+
17
+ - **Daemon: a host-placed run finishing on the heartbeat tick no longer freezes the event loop (PHNX-3727).** Fast-follow to PHNX-3695. `finalizeHostRunAsync` (the async, tick-reachable finalize path reached via `reapExitedRunningJobs`) called `applyHealedHostRun`, which ended by emitting routine-end through the **synchronous** `emitRoutineEnd` — whose event-log file lock (`withFileLock` → `lockSync` + `Atomics.wait`, up to 30s under contention) is exactly the loop-freeze PHNX-3695 removed elsewhere, on a path `guard-no-sync-io` couldn't see (it lives in `runner.ts`, not a `*-service.ts`). So a `host:`-placed routine run completing could still wedge the browser-IPC "accept but never reply" state under lock contention. `applyHealedHostRun` now takes an injected emitter: `finalizeHostRun` (sync CLI path) keeps the synchronous emit, while `finalizeHostRunAsync` (the tick) passes `emitRoutineEndAsync` (`withFileLockAsync`), so no daemon tick reaches the synchronous lock. `guard-no-sync-io` gains a call-site pin so reverting it fails loud. Source: `cli/src/lib/daemon/runner.ts`, `cli/src/lib/daemon/guard-no-sync-io.test.ts`.
18
+
19
+ - **`agents traces sync` now surfaces silent behavioral failures as ranked issues, not just a friction counter (RUSH-2988).** A session where the agent went idle after its last event and a human had to nudge it is a real failure with **no error code**, so it never clustered through `computeInsights` (which only groups `outcome === 'error'` tool calls) and was previously only a `needsAttention` severity input. The per-session silent-stall friction (`silent stall: 5-15m/15-60m/1h+`, already computed by `computeInsightFacets`) is now promoted into cross-session `FailurePattern`s with a new `behavioral` failure cause (`cli/src/lib/traces/insights.ts` `computeBehavioralPatterns`), ranked into the same top-K `failurePatterns` by wasted idle time (a bucket's midpoint, bounded by the same 30-min `MAX_GAP_ATTRIBUTION_MS` cap the tool-error path uses) and summed into `wastedMsTotal`. `TraceFailureCause` gains `behavioral` (`cli/src/lib/traces/classify.ts`); it is producer-derived and never returned by `classifyCause`, so the `failures.byCause` tool-error split reports it as `0`. The console renders the new cause in a follow-up web change.
20
+
21
+ ## 1.22.70
22
+
23
+ - **`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`.
24
+
25
+ - **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`.
26
+
27
+ - **`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`.
28
+
29
+ - **`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`.
30
+
31
+ - **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`.
32
+
33
+ - **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`.
34
+
35
+ - **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}`.
36
+
3
37
  ## 1.22.69
4
38
 
5
39
  - **`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`.
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).
@@ -403,6 +402,31 @@ Sharing a session uses `agents sessions render <id> -o session.md`, not the raw
403
402
 
404
403
  `agents sessions share <id>` goes one step further and publishes that document as a self-contained web page on your own share endpoint, printing the link. It is **unlisted** unless you pass `--public` — a transcript carries file paths, command output, and error text that a plan does not, so it stays out of your public gallery by default, and emails are masked on top of the render's own redaction. The slug is `session-<shortId>`, so re-sharing one session updates one URL.
405
404
 
405
+ ### Back up sessions off-box
406
+
407
+ Signed-in Phoenix users can back sessions up without creating a Cloudflare bucket or
408
+ an `r2.backups` secrets bundle:
409
+
410
+ ```bash
411
+ agents sessions export --since 30d --to-r2
412
+ agents sessions import --from-r2 --dry-run
413
+ agents sessions import --from-r2
414
+ ```
415
+
416
+ The managed path encrypts every transcript body locally with AES-256-GCM before
417
+ upload. Its per-account key is escrowed behind the same Phoenix bearer so another
418
+ signed-in device can restore with zero setup. This protects transcript contents from
419
+ a raw storage-bucket read, but it is not zero-knowledge against Phoenix because
420
+ Phoenix-operated infrastructure stores the recovery key. Use `--byo` with your own
421
+ `r2.backups` bundle and `R2_SYNC_ENC_KEY` when Phoenix must never possess the key:
422
+
423
+ ```bash
424
+ agents sessions export --since 30d --to-r2 --byo
425
+ agents sessions import --from-r2 --byo
426
+ ```
427
+
428
+ Both targets are on-demand backups only; neither enables background session sync.
429
+
406
430
  ### Resume anywhere — and stay resumed
407
431
 
408
432
  Pick up any past conversation and drop it back into a terminal:
@@ -1090,7 +1114,7 @@ agents browser profiles create cloud \
1090
1114
 
1091
1115
  ## Sign in
1092
1116
 
1093
- 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.
1117
+ 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.
1094
1118
 
1095
1119
  ```bash
1096
1120
  agents auth login # shows a code, opens a Phoenix-branded page
@@ -1262,8 +1286,13 @@ agents daemon doctor # one-shot health check; non-zero ex
1262
1286
  ```
1263
1287
 
1264
1288
  Each hosted responsibility (secrets broker, browser IPC, scheduler, monitors,
1265
- watchdog, device probe, self-heal, keychain reap, account-state refresh,
1289
+ watchdog, device probe, self-heal, self-update, keychain reap, account-state refresh,
1266
1290
  state-dir checks) is an independent toggle in `~/.agents/daemon/services.yaml`.
1291
+ Self-update checks npm on its own schedule, installs + verifies a newer
1292
+ agents-cli with the same primitives `agents upgrade` uses, then exits so the OS
1293
+ supervisor relaunches the daemon onto the new code — the daemon used to run
1294
+ with auto-update forced off and only picked up new code on a manual
1295
+ `agents daemon restart`.
1267
1296
  `agents daemon services list` shows every service; `enable|disable <id>` flips
1268
1297
  one. Missing keys default to enabled, so upgrades are no-ops. Most services take
1269
1298
  effect on the next daemon start; browser IPC is registered even when boot-disabled
@@ -1602,7 +1631,6 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
1602
1631
  | OpenCode | yes | no | yes | >= 1.1.1 | yes | yes | no | no | `AGENTS.md` | no |
1603
1632
  | Copilot | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1604
1633
  | Amp | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1605
- | Kiro | yes | no | yes | >= 2.8.0 | yes | yes | no | >= 1.23.0 | `AGENTS.md` | no |
1606
1634
  | Goose | yes | no | yes | no | no | no | no | no | `AGENTS.md` | no |
1607
1635
  | Roo Code | yes | no | yes | no | yes | yes | no | no | `AGENTS.md` | no |
1608
1636
  | Droid | yes | yes | yes | >= 0.57.5 | >= 0.26.0 | yes | yes | yes | `AGENTS.md` | no |
@@ -1623,17 +1651,14 @@ Which DotAgents resources each agent CLI can load. Source of truth: [src/lib/age
1623
1651
  | Antigravity | -- | yes | -- |
1624
1652
  | Copilot | -- | -- | yes |
1625
1653
  | OpenClaw, Amp, Goose, Roo | -- | -- | -- |
1626
- | Kiro | -- | -- | -- |
1627
1654
 
1628
1655
  ### Version-gated sync
1629
1656
 
1630
1657
  | Capability | Agent | Gate |
1631
1658
  |------------|-------|------|
1632
1659
  | Hooks | Codex | >= 0.116.0 |
1633
- | Subagents | Kiro | >= 1.23.0 |
1634
1660
  | Skills | Droid | >= 0.26.0 |
1635
1661
  | Permissions | Droid | >= 0.57.5 |
1636
- | Permissions | Kiro | >= 2.8.0 |
1637
1662
  | File-based commands | Codex | < 0.117.0 (0.117+ uses command-as-skill) |
1638
1663
  | Plugins | Codex | >= 0.128.0 |
1639
1664
 
@@ -1649,7 +1674,7 @@ Claude Code, Codex CLI, Antigravity, Grok Build, and others each have their own
1649
1674
 
1650
1675
  ### Is it free?
1651
1676
 
1652
- 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.
1677
+ 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.
1653
1678
 
1654
1679
  ### Is this like `nvm` / `mise` / `asdf` for AI agents?
1655
1680
 
package/dist/bootstrap.js CHANGED
@@ -328,7 +328,7 @@ async function installResolvedPackage(metadata) {
328
328
  // every subsequent upgrade dead-ends there forever. bun does not use
329
329
  // npm's retire-path staging scheme, so this only needs to run once, ahead
330
330
  // of both package-manager branches below.
331
- sweepStaleInstallStaging(packageRoot);
331
+ await sweepStaleInstallStaging(packageRoot);
332
332
  // Upgrade with the package manager that owns this install. A bun global
333
333
  // install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
334
334
  // `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
@@ -349,8 +349,8 @@ async function installResolvedPackage(metadata) {
349
349
  /* leave it for the OS temp sweep */
350
350
  }
351
351
  }
352
- verifyInstalledVersion(packageRoot, metadata.version);
353
- refreshAliasShims(packageRoot);
352
+ await verifyInstalledVersion(packageRoot, metadata.version);
353
+ await refreshAliasShims(packageRoot);
354
354
  // PHNX-2768: the npm install above can leave the package at the new version
355
355
  // but the global bin links GONE — the state that stranded zion (package at
356
356
  // 1.22.40, `/opt/homebrew/bin/{agents,ag,browser,computer}` missing, every
@@ -361,7 +361,7 @@ async function installResolvedPackage(metadata) {
361
361
  // own bin shims and are out of scope.
362
362
  if (detectPackageManager(packageRoot) !== 'bun' && process.platform !== 'win32') {
363
363
  const prefix = deriveGlobalPrefix(packageRoot);
364
- const repairs = ensureGlobalBinLinks(packageRoot, prefix);
364
+ const repairs = await ensureGlobalBinLinks(packageRoot, prefix);
365
365
  const repaired = repairs.filter((r) => r.action === 'repaired');
366
366
  const failed = repairs.filter((r) => r.action === 'failed');
367
367
  if (repaired.length > 0) {
@@ -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).
@@ -2244,7 +2244,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2244
2244
  const { pickSessionInteractive } = await import('./sessions.js');
2245
2245
  const { buildContinuePrompt } = await import('../lib/loop.js');
2246
2246
  // Freshen the index for this agent before any lookup (incremental, cached).
2247
- // AgentId is wider than SessionAgentId (amp/kiro/goose/copilot keep no transcripts);
2247
+ // AgentId is wider than SessionAgentId (amp/goose/copilot keep no transcripts);
2248
2248
  // those simply yield no matches and fall through to the not-found error.
2249
2249
  const sessionAgent = agent;
2250
2250
  if (!resolvedResumeSource)
@@ -2723,7 +2723,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2723
2723
  process.exit(1);
2724
2724
  }
2725
2725
  // Default CLI mode is the generic 'plan'. Agents without a read-only
2726
- // mode (antigravity, kiro, …) degrade via resolveMode to their
2726
+ // mode (antigravity, …) degrade via resolveMode to their
2727
2727
  // safest native mode (modes[0], typically edit). That covers both the
2728
2728
  // implicit default and an explicit `--mode plan`, so multi-agent
2729
2729
  // scripts can pass a uniform plan flag without per-agent branching.
@@ -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');
@@ -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) {
@@ -60,7 +60,7 @@ function syncMarketplacesForDefaults() {
60
60
  /** Write this device's account metadata before the user repo is committed. */
61
61
  async function publishUserRepoAccountState(provisionAuth) {
62
62
  const { publishUsageSnapshotToSharedStore } = await import('../lib/accounting/usage-sync.js');
63
- const usage = publishUsageSnapshotToSharedStore();
63
+ const usage = await publishUsageSnapshotToSharedStore();
64
64
  if (usage.error)
65
65
  console.error(chalk.yellow(`Usage snapshot: ${usage.error}`));
66
66
  if (provisionAuth) {
@@ -73,7 +73,7 @@ async function publishUserRepoAccountState(provisionAuth) {
73
73
  return;
74
74
  }
75
75
  const { publishReservedAuthVerdict } = await import('../lib/secrets/reserved-sync.js');
76
- const auth = publishReservedAuthVerdict();
76
+ const auth = await publishReservedAuthVerdict();
77
77
  if (auth.error)
78
78
  console.error(chalk.yellow(`Auth verdict: ${auth.device}: ${auth.error}`));
79
79
  }
@@ -52,7 +52,7 @@ Project rules & @-imports:
52
52
  @path imports inside AGENTS.md/CLAUDE.md are resolved at session start by the agent
53
53
  itself, not by agents-cli. Support is per-agent:
54
54
  Inlined natively: claude
55
- Literal text: codex, cursor, opencode, copilot, amp, kiro, goose
55
+ Literal text: codex, cursor, opencode, copilot, amp, goose
56
56
 
57
57
  For rules that need to work across all agents, inline the content rather than using
58
58
  @-imports — the second group will load '@path/to/file.md' as a literal string.
@@ -3,7 +3,7 @@ import { die } from '../lib/format.js';
3
3
  import { setHelpSections } from '../lib/help.js';
4
4
  import { readMeta } from '../lib/state.js';
5
5
  import { sendMessage, isOwnerAlias } from '../lib/channels/send.js';
6
- import { composeOwnerMessage } from '../lib/owner-message.js';
6
+ import { ownerMessageComposer } from '../lib/owner-message.js';
7
7
  import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
8
8
  function mergeAttachments(opts) {
9
9
  const list = [...(opts.attach ?? []), ...(opts.attachment ?? [])];
@@ -30,6 +30,7 @@ async function runSend(positionalText, opts, ownerMode) {
30
30
  // the SAME composer as an important `feed post` (PHNX-3698): short-shaped body,
31
31
  // TEAM-N keys linkified, session crumb as a tappable console URL — instead of a
32
32
  // raw dump. A non-owner send (explicit --channel/--to) is delivered verbatim.
33
+ let ownerCompose;
33
34
  if (ownerMode || isOwnerAlias(opts.to)) {
34
35
  const flagged = opts.text?.trim() ?? '';
35
36
  const positional = (positionalText ?? '').trim();
@@ -38,7 +39,12 @@ async function runSend(positionalText, opts, ownerMode) {
38
39
  // loud with the "pass the message once" error rather than composing a guess.
39
40
  const bothDiffer = flagged !== '' && positional !== '' && flagged !== positional;
40
41
  if (raw && !bothDiffer) {
41
- input = { ...input, text: composeOwnerMessage(raw), positionalText: undefined };
42
+ // Resolve the context once, then let each owner destination render its own
43
+ // format: the envelope carries the plain default (for --json and dry-run
44
+ // display), while ownerCompose hands sendToOwner the mrkdwn variant for a
45
+ // Slack owner destination and plain for iMessage (PHNX-3698).
46
+ ownerCompose = ownerMessageComposer(raw);
47
+ input = { ...input, text: ownerCompose('plain'), positionalText: undefined };
42
48
  // The console URL in the composed body only resolves once this session's
43
49
  // trace shard is uploaded; fire that now so the tapped link isn't a 404.
44
50
  // A --dry-run resolves + composes but MUST NOT act (its documented contract),
@@ -46,7 +52,7 @@ async function runSend(positionalText, opts, ownerMode) {
46
52
  fireTraceSyncInBackground({ disabled: Boolean(opts.dryRun) });
47
53
  }
48
54
  }
49
- const out = await sendMessage(input, meta);
55
+ const out = await sendMessage(input, meta, ownerCompose);
50
56
  if ('error' in out) {
51
57
  die(out.error);
52
58
  }
@@ -1,5 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
+ import { type SessionsBackupClient } from '../lib/session/sync/net-client.js';
3
4
  import { type BundleHeader, type BundleRecord } from '../lib/session/bundle.js';
4
5
  export declare function registerSessionsExportCommand(sessionsCmd: Command): void;
5
6
  interface GlobalSelection {
@@ -10,6 +11,7 @@ interface GlobalSelection {
10
11
  redact?: boolean;
11
12
  encrypt?: boolean;
12
13
  toR2?: boolean;
14
+ byo?: boolean;
13
15
  output?: string;
14
16
  stdout?: boolean;
15
17
  host?: string[];
@@ -36,7 +38,9 @@ interface GlobalSelection {
36
38
  * its test drive the exact same decision.
37
39
  */
38
40
  export declare function r2ExportGateError(g: Pick<GlobalSelection, 'toR2' | 'host'>, isConfigured: boolean): string | null;
39
- export declare function uploadToR2(header: BundleHeader, records: BundleRecord[]): Promise<void>;
41
+ export declare function uploadToR2(header: BundleHeader, records: BundleRecord[], resolvedClient?: SessionsBackupClient): Promise<void>;
42
+ /** Fail-closed guard at the managed transport boundary, not only at record construction. */
43
+ export declare function managedUploadEncryptionError(header: BundleHeader, records: BundleRecord[]): string | null;
40
44
  /** R2 object key for one record — dir-shaped agents key by relKey, file-shaped by session. */
41
45
  export declare function r2KeyForRecord(rec: BundleRecord): string;
42
46
  /**