@phnx-labs/agents-cli 1.22.22 → 1.22.23

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 (91) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +8 -2
  3. package/dist/commands/doctor.js +15 -7
  4. package/dist/commands/exec.js +20 -6
  5. package/dist/commands/focus.d.ts +76 -4
  6. package/dist/commands/focus.js +219 -40
  7. package/dist/commands/fork.d.ts +20 -2
  8. package/dist/commands/fork.js +91 -64
  9. package/dist/commands/go.d.ts +25 -0
  10. package/dist/commands/go.js +63 -2
  11. package/dist/commands/harness-wizard.d.ts +206 -0
  12. package/dist/commands/harness-wizard.js +403 -0
  13. package/dist/commands/harness.d.ts +12 -0
  14. package/dist/commands/harness.js +97 -101
  15. package/dist/commands/resume.js +10 -3
  16. package/dist/commands/secrets.js +25 -30
  17. package/dist/commands/sessions-resume.d.ts +21 -3
  18. package/dist/commands/sessions-resume.js +50 -11
  19. package/dist/commands/sessions.d.ts +81 -5
  20. package/dist/commands/sessions.js +325 -66
  21. package/dist/commands/watchdog.js +13 -2
  22. package/dist/lib/agents.d.ts +1 -1
  23. package/dist/lib/agents.js +132 -0
  24. package/dist/lib/codex-policy.d.ts +17 -0
  25. package/dist/lib/codex-policy.js +48 -0
  26. package/dist/lib/crabbox/lease.d.ts +25 -0
  27. package/dist/lib/crabbox/lease.js +62 -0
  28. package/dist/lib/daemon.js +70 -0
  29. package/dist/lib/exec.d.ts +4 -0
  30. package/dist/lib/exec.js +88 -54
  31. package/dist/lib/feed-broadcast.d.ts +1 -20
  32. package/dist/lib/feed-broadcast.js +31 -1
  33. package/dist/lib/hooks.js +12 -2
  34. package/dist/lib/mcp.js +44 -0
  35. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  36. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  38. package/dist/lib/models.d.ts +0 -5
  39. package/dist/lib/models.js +48 -0
  40. package/dist/lib/plugin-marketplace.js +9 -0
  41. package/dist/lib/pricing/prices.json +119 -92
  42. package/dist/lib/pricing/table.js +13 -0
  43. package/dist/lib/remote-agents-json.d.ts +29 -1
  44. package/dist/lib/remote-agents-json.js +47 -10
  45. package/dist/lib/resources/mcp.js +2 -0
  46. package/dist/lib/resources/permissions.js +3 -0
  47. package/dist/lib/resources/types.d.ts +2 -1
  48. package/dist/lib/runner.js +28 -19
  49. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  50. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  51. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  53. package/dist/lib/secrets/index.d.ts +15 -1
  54. package/dist/lib/secrets/index.js +118 -49
  55. package/dist/lib/secrets/reaper.d.ts +87 -0
  56. package/dist/lib/secrets/reaper.js +184 -0
  57. package/dist/lib/secrets/remote.d.ts +29 -0
  58. package/dist/lib/secrets/remote.js +37 -1
  59. package/dist/lib/session/active.d.ts +36 -1
  60. package/dist/lib/session/active.js +60 -19
  61. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  62. package/dist/lib/session/actor-sidecar.js +67 -4
  63. package/dist/lib/session/db.d.ts +1 -1
  64. package/dist/lib/session/db.js +32 -1
  65. package/dist/lib/session/discover.js +168 -0
  66. package/dist/lib/session/parse.d.ts +10 -0
  67. package/dist/lib/session/parse.js +98 -0
  68. package/dist/lib/session/remote-list.d.ts +10 -1
  69. package/dist/lib/session/remote-list.js +2 -8
  70. package/dist/lib/session/remote.d.ts +57 -3
  71. package/dist/lib/session/remote.js +90 -26
  72. package/dist/lib/session/resume-command.d.ts +6 -0
  73. package/dist/lib/session/resume-command.js +8 -0
  74. package/dist/lib/session/session-cache.d.ts +173 -0
  75. package/dist/lib/session/session-cache.js +399 -0
  76. package/dist/lib/session/types.d.ts +1 -1
  77. package/dist/lib/session/types.js +1 -1
  78. package/dist/lib/session/width.d.ts +1 -1
  79. package/dist/lib/session/width.js +12 -2
  80. package/dist/lib/shims.d.ts +2 -2
  81. package/dist/lib/shims.js +40 -5
  82. package/dist/lib/staleness/writers/hooks.js +1 -1
  83. package/dist/lib/types.d.ts +1 -1
  84. package/dist/lib/usage.d.ts +13 -0
  85. package/dist/lib/usage.js +215 -0
  86. package/dist/lib/versions.js +13 -2
  87. package/package.json +1 -1
  88. package/dist/bin/agents +0 -0
  89. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  90. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,214 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.23
4
+
5
+ - **Daemon-warmed cross-surface session-status cache (RUSH-2062).** Menubar,
6
+ Factory, watchdog, and CLI used to each re-run a full `sessions --active`
7
+ gather (~9s / ~170MB) with no sharing. The daemon now warms a local active-
8
+ session snapshot every 15s; those surfaces read the warm file when fresh.
9
+ `session/remote.ts` is cache-first too — a reachable host skips SSH while its
10
+ cache is within the freshness window (it used to replay only on
11
+ unreachable). Immutable per-session fields (topic, label, cwd, …) are memoized
12
+ by transcript mtime; live status never rides that memo. Source:
13
+ `apps/cli/src/lib/session/session-cache.ts`, `apps/cli/src/lib/session/remote.ts`,
14
+ `apps/cli/src/lib/daemon.ts`.
15
+
16
+ - **`agents sessions <id>` / `agents resume <id>` resolve across the fleet fast, with
17
+ early-exit and SSH cancellation (RUSH-2203).** The cross-machine resolve fanned out
18
+ with `await Promise.all` and no cancellation, so one slow or unreachable peer stalled
19
+ the whole lookup to the 12s per-peer timeout even after a fast peer already returned
20
+ the exact match — a full-UUID resolve on a fleet with an offline box took ~16-40s. A
21
+ **full-UUID** resolve (globally unique) now opts into a cancellable, early-exit
22
+ fan-out: the first peer holding it resolves the sweep and SIGTERMs every
23
+ still-outstanding peer, and a local UUID hit resolves with **zero** SSH.
24
+ `agents resume <label>` auto-resumes the one exact-label match; labels are not globally
25
+ unique, so they stay all-settle (a same-label session may live on another peer) — a
26
+ unique label resumes, a cross-machine collision surfaces as an ambiguity. The not-found
27
+ message reports the actual sweep result (devices searched, which were unreachable)
28
+ instead of the misleading "search with `--device <host>`" — fleet search is already the
29
+ default. `--device all` / `--devices` are accepted and mean "the whole fleet". The
30
+ all-settle default is unchanged for the shared fan-out's other callers (tool-search,
31
+ program-count) and for labels / ambiguous short-id prefixes, which still wait for every
32
+ peer. Source:
33
+ `apps/cli/src/lib/remote-agents-json.ts`, `apps/cli/src/lib/session/remote-list.ts`,
34
+ `apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/resume.ts`.
35
+
36
+ - **`agents sessions focus` gains device + live-state scoping and multi-select tabs
37
+ (RUSH-2206).** `focus` now honors `--device/--host <name>` (scope the picker to a
38
+ device's live sessions) and the same live-state filters `sessions --active` defines
39
+ (`--orphan`/`--crashed`/`--waiting`/`--idle`/`--working`/`--closed`/`--abandoned`/
40
+ `--queued`/`--unknown`) — they compose, so `agents sessions focus --orphan --device
41
+ yosemite-s0` lists only that host's orphans. The interactive picker is now
42
+ multi-select: check several sessions and each opens as a new tab in the terminal
43
+ you're in (Ghostty / iTerm / tmux, auto-detected), reusing `resume`'s batch open and
44
+ flood guard. Per tab, live semantics hold — a tmux session is joined (a second
45
+ client, no fork), local or remote over SSH; a session with no attach rail resumes a
46
+ copy in the tab, reported never silently dropped. `focus <id>` direct-jump and
47
+ `--attach-only` (old `go`) behavior are unchanged. Source:
48
+ `apps/cli/src/commands/focus.ts`, `apps/cli/src/commands/go.ts`.
49
+
50
+ - **`agents harness edit` is now wizard-capable, on a shared step engine (RUSH-2219).**
51
+ Run `agents harness edit <name>` with no flags in a terminal and it walks each field
52
+ (model, endpoint, auth, version, fallback, description) pre-filled with the harness's
53
+ current values — where before it was flag-only and errored without one. Both `add`/`fork`
54
+ and `edit` now drive one shared step engine: every step is skippable by the matching flag,
55
+ so scripting stays non-interactive and a flagless non-interactive `edit` still errors as
56
+ before. Endpoint and version prompts are gated by the host's API format (a host with no
57
+ custom-endpoint slot, or one that self-updates, shows the field disabled with a reason
58
+ instead of silently accepting a value the run would drop). This is the foundation the
59
+ model catalog, connection test, edit matrix, and cross-host portability build on.
60
+ Source: `apps/cli/src/commands/harness-wizard.ts`, `apps/cli/src/commands/harness.ts`.
61
+
62
+ - **Codex launches now default to safe writable access instead of an offline
63
+ read-only sandbox.** When no configured run default exists, omitting `--mode`
64
+ selects a managed workspace profile with network access, on-request approvals,
65
+ `~/.agents`, and regenerable build caches. Explicit `--mode plan` remains
66
+ filesystem-read-only but now retains network access. The same policy covers
67
+ `agents run`, fallback attempts, resumes, routines, direct `codex` shims,
68
+ versioned aliases, and Windows shim passthrough; only explicit `skip` disables
69
+ the sandbox and approvals. Source: `apps/cli/src/lib/codex-policy.ts`.
70
+
71
+ - **Phone forwards are shaped to a text, not a wall.** `agents feed post`
72
+ (`--level important` / `--blocked`), `agents notify`, and `agents send --to owner`
73
+ all forward to the owner's phone through one seam, `composeBroadcastMessage`; it now
74
+ keeps the post **title** (the scannable headline) and truncates a long **body** to a
75
+ short excerpt marked `… (full in feed)` (caps: 500 chars / 8 lines). The full post is
76
+ untouched in the feed — only the outbound phone copy is shortened — so a requested
77
+ long write is not lost, and enforcing it at this seam covers every sink (owner alias,
78
+ in-process `channel:`, spawned `command:` via `{message}`), which a per-command shell
79
+ hook cannot. Source: `apps/cli/src/lib/feed-broadcast.ts` (`truncateBroadcastBody`).
80
+
81
+ - **`agents run --lease` self-cleans its warm pool.** Each lease now auto-stops **expired, idle** boxes in the run's profile pool (an expired box can never be reused, so it was pure cost the 1h-idle `agents lease gc` window left running). Conservative: only a box that is running, in the same profile+netMode pool, past its lease expiry, and untouched for a grace window is stopped — a mid-boot or in-use box is never touched. Skipped when an explicit `--box <slug>` is named. Source: `apps/cli/src/lib/crabbox/lease.ts`.
82
+
83
+ - **Muse Code (Meta) harness support.** `muse` is a first-class agent: install via
84
+ `curl -fsSL https://dev.meta.ai/install.sh | sh`, run with `agents run muse`,
85
+ and use in teams. Modes map to Muse safety flags (`--disable-write` /
86
+ `--disable-approval` / `--yolo`); `--model` and `--reasoning-effort` are
87
+ forwarded; headless is `muse exec` with `--json`; interactive resume is
88
+ `muse resume <id>` (id immediately after the verb), headless resume is
89
+ `--session-id`. Sessions under `~/.local/share/muse/sessions/` (plus version
90
+ homes) are discovered and parsed; usage shows Meta Model API rate limits when
91
+ a key is present, otherwise local 7-day token totals. MCP writes to
92
+ `~/.config/muse/settings.json` (`mcp_servers`, `schema_version: 1`). Model
93
+ catalog: `muse-spark-1.2` (default), `1.1`, `1.2-contributor` with offline
94
+ pricing. Aliases: `muse-code`, `meta-muse`. Hooks write Claude-shaped
95
+ matcher groups into `~/.config/muse/settings.json` (`schema_version: 1`);
96
+ plugins use the Claude marketplace layout under the XDG data plugin store
97
+ (`~/.local/share/muse/plugins`) with `.muse-plugin` manifests. Allowlist
98
+ stays false (Muse uses approval-mode + sandbox, not tool-name allow/deny).
99
+ Source: `apps/cli/src/lib/{agents,exec,models,usage,mcp,hooks,plugins,
100
+ plugin-marketplace,runner,shims}.ts`, `apps/cli/src/lib/session/*`.
101
+
102
+ - **Release CI no longer re-runs the six-job cross-platform matrix on `v*` tags.**
103
+ The expensive `ci.yml` matrix (ubuntu + macOS + Windows × Node 22/24) still
104
+ runs on `release/**` branch pushes and manual `workflow_dispatch`, but not when
105
+ a version tag is pushed. `release.sh` tags the exact commit that already passed
106
+ the release-branch matrix, so the post-tag matrix was pure cost. Source:
107
+ `.github/workflows/ci.yml`.
108
+
109
+ - **`claude-opus-5` and `claude-sonnet-5` were unpriced, so every session using them
110
+ cost $0.** The pricing table carried `claude-opus-4`, `claude-sonnet-4`,
111
+ `claude-fable-5` and `claude-mythos-5` but not the Opus/Sonnet 5 line. Matching is
112
+ dash-bounded (`getModelPricing`), so `claude-opus-5` cannot fall back to the
113
+ `claude-opus-4` entry — it resolved to null, and an unpriced model contributes
114
+ nothing rather than erroring. On one real index that silently zeroed **526 sessions**,
115
+ 478 of them the current default model, understating `agents cost`, `agents output`
116
+ and `agents insights` alike. Rates from the published table: Opus 5 $5/$25 per MTok
117
+ (cache write $6.25, cache read $0.50); Sonnet 5 $2/$10 (cache write $2.50, cache read
118
+ $0.20). Source: `apps/cli/src/lib/pricing/prices.json`.
119
+
120
+ - **Schema v34 reprices the sessions that were zeroed.** Adding prices alone fixes
121
+ nothing already indexed: `cost_usd` is computed at scan time and the scanner skips any
122
+ transcript whose `(file_mtime_ms, file_size)` is unchanged, so those rows would keep
123
+ their NULL forever. They cannot be repaired in place either — the row stores
124
+ `token_count` and `output_tokens` but not the uncached-input / cache-read / cache-write
125
+ split the price table needs. So v34 flushes `scan_ledger`, the same remedy v5 → v6 used
126
+ for this exact column when cost was introduced. One slower scan, then correct numbers.
127
+ Source: `apps/cli/src/lib/session/db.ts`.
128
+
129
+ **Sonnet 5's $2/$10 is introductory pricing that ends 2026-08-31.** From 2026-09-01 the
130
+ standard rate is $3/$15. The table holds one current rate per model with no notion of an
131
+ effective date, so that entry must be updated then or Sonnet 5 spend reads ~33% low.
132
+ Called out at the top of `table.ts`.
133
+
134
+ - **`agents sessions --orphan`/`--active`: cross-harness live discovery + richer rows (RUSH-2205).**
135
+ Two fixes. (1) The headless `ps`-scan recognized only 6 agent executables, so a
136
+ bare-headless `grok`/`kimi`/`antigravity`/`openclaw`/`hermes`/`rush` run was
137
+ silently dropped from the live views; the comm→kind map is now derived from
138
+ `SESSION_AGENTS` × the AGENTS registry (`cliCommand`), so every discoverable
139
+ harness surfaces. (2) Each live row now shows the agent **version** and a human
140
+ **created · idle** time cell, and backfills the **ticket/PR/label** from the
141
+ indexed session history onto orphan rows that lack them, with the label/topic on
142
+ its own line — grouped-by-directory layout unchanged, rows stay width-safe.
143
+ Source: `apps/cli/src/lib/session/active.ts`, `apps/cli/src/commands/sessions.ts`.
144
+
145
+ - **`agents doctor --check` verdict lines now carry a `check:` prefix and a total
146
+ version count**, so CI logs and log-scrapers get one consistent, grep-alike shape
147
+ whether the result is clean or drifted: `check: ok — 3 version(s) in sync` /
148
+ `check: drift — 2 stale, 1 never-synced across 3 version(s)`. The per-version
149
+ status badge for a never-synced version now reads `never-synced` (its real
150
+ status) instead of the unrelated `cold` label, and all three badges
151
+ (`stale`/`never-synced`/`unwired`) share one fixed-width column so the rows
152
+ align. Source: `apps/cli/src/commands/doctor.ts` (`runCheckGate`).
153
+
154
+ - **`agents secrets export --host --remote-backend file` no longer requires
155
+ `AGENTS_SECRETS_PASSPHRASE`.** Since the file store became passphrase-free
156
+ (auto-provisioning a 0600 machine-local key under `~/.agents/.secrets-key/`), the
157
+ remote `import --backend file` reads headlessly under that key with no passphrase
158
+ and no Touch ID — but the export path still hard-failed unless a passphrase was
159
+ set, and forcing one made the remote bundle require that shared passphrase to
160
+ read, defeating the headless use it was for. The passphrase is now **optional**:
161
+ with none set, the remote command carries no `AGENTS_SECRETS_PASSPHRASE`
162
+ read/export prologue and the .env is the only stdin, so the remote keys the bundle
163
+ under its own machine-local key (headless reads); set `AGENTS_SECRETS_PASSPHRASE`
164
+ locally only to opt into a shared off-disk key, which is still forwarded over ssh
165
+ stdin (never argv). This unblocks hands-off Linux-driven releases that provision
166
+ `apple.com` on a headless macOS sign host. Windows targets are still refused
167
+ cleanly. Source: `apps/cli/src/lib/secrets/remote.ts`,
168
+ `apps/cli/src/commands/secrets.ts`.
169
+
170
+ - **Docs: the per-command secrets Touch-ID contract is now written down and
171
+ accurate.** The `view --reveal` (1.22.14) and `exec` (1.22.21) interactive-unlock
172
+ changes shipped in code + CHANGELOG only; the reference docs still implied every
173
+ value command behaves the same. `docs/secrets.md` and the `secrets` skill now
174
+ carry an explicit Touch-ID matrix (deliberate `view --reveal`/`exec` at a real
175
+ terminal → one sheet on a locked bundle; `get`/`export` automation primitives →
176
+ never prompt, fail closed to `agents secrets unlock`; anything an agent launches →
177
+ broker-only), `specifications.md` adds **SEC-13b** + a `Prompts?` column on the
178
+ materialization table + `GWT-S2b`, and the skill's stale hold-window facts are
179
+ corrected (7-day default, screen-lock does not drop the hold). Docs-only; no
180
+ behavior change. Source: `apps/cli/docs/secrets.md`,
181
+ `apps/cli/docs/specifications.md`, `skills/secrets/SKILL.md`.
182
+
183
+ - **Resume session identities through one live-first path across the fleet.**
184
+ `agents sessions resume <selector>` accepts a full UUID, unique UUID prefix,
185
+ durable tmux name (`ag-codex-c1f3d813`), or unique alias suffix (`c1f3d813`). It
186
+ resolves the owning device, rechecks whether the process and pane are alive,
187
+ attaches a live pane, and otherwise resumes the harness-native conversation with
188
+ its recorded version, cwd, and mode. Retained dead tmux panes no longer count as
189
+ attachable, and native resume no longer collides with an existing live wrapper.
190
+ Source: `apps/cli/src/commands/focus.ts`,
191
+ `apps/cli/src/commands/sessions-resume.ts`, `apps/cli/src/lib/exec.ts`.
192
+
193
+ - **`agents fork` is now `agents sessions fork`.** Forking is a session operation, so
194
+ it lives under the `sessions` group next to `resume`/`focus` — `agents sessions fork
195
+ <id>` branches a conversation into a new, independent copy you can continue separately,
196
+ leaving the original untouched. The old top-level `agents fork` keeps working as a
197
+ hidden alias, so nothing that scripted it breaks. For a harness without a native copy
198
+ (anything but Claude today), the command now fails loud and names the manual branch —
199
+ start a fresh agent and seed it with `/continue <id>` — instead of only saying
200
+ "unsupported". Source: `apps/cli/src/commands/fork.ts`,
201
+ `apps/cli/src/commands/sessions.ts`.
202
+
203
+ - **Muse Code global binary is visible to `agents view` / `isVersionInstalled`.**
204
+ Muse installs a single self-updating launcher at `~/.local/bin/muse` (same
205
+ shape as droid), but `getBinaryPath` still resolved a version-home
206
+ `node_modules/.bin/muse` that never exists. After `agents import muse` /
207
+ `agents add muse`, version dirs now resolve to that global path so managed
208
+ view, collapse, and live-version bookkeeping match what actually executes;
209
+ install no longer writes a self-referential shim symlink. Source:
210
+ `apps/cli/src/lib/versions.ts`.
211
+
3
212
  ## 1.22.22
4
213
 
5
214
  - **New: `agents insights` — how you work, split by the Claude account that did the
package/README.md CHANGED
@@ -32,6 +32,8 @@
32
32
  &nbsp;&nbsp;&nbsp;&nbsp;
33
33
  <a href="https://factory.ai" title="Factory AI Droid"><strong>Droid</strong></a>
34
34
  &nbsp;&nbsp;&nbsp;&nbsp;
35
+ <a href="https://dev.meta.ai/docs/muse-code" title="Meta Muse Code"><strong>Muse</strong></a>
36
+ &nbsp;&nbsp;&nbsp;&nbsp;
35
37
  <a href="https://omp.sh" title="Oh My Pi"><strong>Pi</strong></a>
36
38
  </p>
37
39
 
@@ -218,7 +220,11 @@ otherwise the first permission option offered by the server. The same last-resor
218
220
  warning applies.
219
221
 
220
222
  Codex has no native smart-classifier mode, so `agents run codex --mode auto` resolves
221
- to sandboxed `edit` and can still prompt. `agents run codex --mode skip` is different:
223
+ to sandboxed `edit` and can still prompt. When `--mode` is omitted for Codex, the same
224
+ safe writable mode is used: the workspace, `~/.agents`, and regenerable toolchain caches
225
+ are writable; network access is enabled; and approvals are requested on demand. An
226
+ explicit `--mode plan` keeps the filesystem read-only while leaving network access on.
227
+ `agents run codex --mode skip` is different:
222
228
  it bypasses approvals **and** removes the sandbox. `full` remains an alias for `skip`,
223
229
  but new scripts should use the explicit `skip` name.
224
230
 
@@ -684,7 +690,7 @@ tools:
684
690
  ---
685
691
  ```
686
692
 
687
- Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit`, or `--mode auto` on Claude Code and GitHub Copilot. Reserve `--mode skip` (legacy alias: `full`) for last-resort bypasses. `agents run` defaults to `--mode plan` (read-only), which deadlocks at `ExitPlanMode` in headless runs.
693
+ Workflows that need to write — post PR comments, edit files, send Slack — should run with `--mode edit`, or `--mode auto` on Claude Code and GitHub Copilot. Reserve `--mode skip` (legacy alias: `full`) for last-resort bypasses. `agents run` defaults to `--mode plan` for other harnesses; Codex defaults to its safe writable profile. An explicit Codex `--mode plan` is read-only with network access.
688
694
 
689
695
  Resolution is project > user > system: a `<repo>/.agents/workflows/<name>/` overrides a same-named workflow in `~/.agents/workflows/`. Commit project workflows with your repo so teammates get the same pipeline.
690
696
 
@@ -1022,10 +1022,13 @@ function runCheckGate(opts, cwd) {
1022
1022
  const orphanNote = drift.orphanVersionCount > 0
1023
1023
  ? chalk.gray(` (${drift.orphanVersionCount} version(s) carry orphans — run \`agents prune cleanup\`)`)
1024
1024
  : '';
1025
- console.log(`${chalk.green('ok')} ${drift.syncRows.length} version(s) in sync${orphanNote}`);
1025
+ console.log(`${chalk.gray('check:')} ${chalk.green('ok')}${drift.syncRows.length} version(s) in sync${orphanNote}`);
1026
1026
  process.exit(0);
1027
1027
  }
1028
- // Drift: one-line verdict always, per-version detail unless --quiet.
1028
+ // Drift: one-line verdict always, per-version detail unless --quiet. The
1029
+ // `check:` prefix and total count make every verdict line grep/parse-alike
1030
+ // whether it's clean or drifted, instead of the bare `drift <parts>` shape
1031
+ // that gave CI logs no anchor to search on.
1029
1032
  const parts = [];
1030
1033
  if (drift.staleCount > 0)
1031
1034
  parts.push(`${drift.staleCount} stale`);
@@ -1035,16 +1038,21 @@ function runCheckGate(opts, cwd) {
1035
1038
  parts.push(`${drift.unwiredHookVersions} with unwired hooks`);
1036
1039
  if (drift.sourceBehind.length > 0)
1037
1040
  parts.push(`${drift.sourceBehind.length} source layer(s) behind origin`);
1038
- console.error(`${chalk.red('drift')} ${parts.join(', ')}`);
1041
+ console.error(`${chalk.gray('check:')} ${chalk.red('drift')}${parts.join(', ')} across ${drift.syncRows.length} version(s)`);
1039
1042
  if (!opts.quiet) {
1043
+ // Fixed-width, left-aligned so rows form a real column; the badge text is
1044
+ // the actual status name ('never-synced'), not the unrelated 'cold' label
1045
+ // that used to hide it. Width is 'never-synced'.length + a 2-space gap so
1046
+ // the longest badge still separates cleanly from the label that follows.
1047
+ const STATUS_BADGE_WIDTH = 'never-synced'.length + 2;
1040
1048
  for (const row of drift.syncRows) {
1041
1049
  const unwired = (row.unwiredHooks ?? 0) > 0;
1042
1050
  if (row.status === 'fresh' && !unwired)
1043
1051
  continue;
1044
- const tag = row.status === 'stale' ? chalk.yellow('stale')
1045
- : row.status === 'never-synced' ? chalk.gray('cold ')
1046
- : chalk.red('unwired'); // fresh but hooks not wired into settings.json
1047
- console.error(` ${tag} ${checkLabel(row)}`);
1052
+ const tag = row.status === 'stale' ? chalk.yellow('stale'.padEnd(STATUS_BADGE_WIDTH))
1053
+ : row.status === 'never-synced' ? chalk.gray('never-synced'.padEnd(STATUS_BADGE_WIDTH))
1054
+ : chalk.red('unwired'.padEnd(STATUS_BADGE_WIDTH)); // fresh but hooks not wired into settings.json
1055
+ console.error(` ${tag}${checkLabel(row)}`);
1048
1056
  for (const line of row.divergence ?? []) {
1049
1057
  console.error(chalk.gray(` ${line}`));
1050
1058
  }
@@ -516,7 +516,7 @@ export function registerRunCommand(program) {
516
516
  const runCmd = program
517
517
  .command('run <agent> [prompt]')
518
518
  .description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively.')
519
- .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). \'full\' accepted as alias for skip.', 'plan')
519
+ .option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
520
520
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto (claude and codex only)', 'auto')
521
521
  .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete model id; tiers resolve per harness+version to a supported model')
522
522
  .option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
@@ -882,8 +882,10 @@ export function registerRunCommand(program) {
882
882
  // the normal new-run default as a real value, so consult its provenance
883
883
  // rather than mistaking the default for an explicit override.
884
884
  if (command.getOptionValueSource('mode') === 'default') {
885
- if (resolvedResumeSource.mode)
885
+ if (resolvedResumeSource.mode) {
886
886
  options.mode = resolvedResumeSource.mode;
887
+ command.setOptionValueWithSource('mode', resolvedResumeSource.mode, 'implied');
888
+ }
887
889
  else if (!options.quiet)
888
890
  process.stderr.write(chalk.yellow(`[agents] session ${resolvedResumeSource.shortId} predates stored launch modes; using --mode ${options.mode}\n`));
889
891
  }
@@ -1540,13 +1542,15 @@ export function registerRunCommand(program) {
1540
1542
  });
1541
1543
  }
1542
1544
  const isRaw = options.raw || options.tmux === false || options.disableTmux === true;
1545
+ const { modeForRemoteDispatch } = await import('../lib/codex-policy.js');
1546
+ const forwardedMode = modeForRemoteDispatch(options.mode, command.getOptionValueSource('mode'));
1543
1547
  const exitCode = await runInteractiveOnHost(host, {
1544
1548
  agent: runAgent,
1545
1549
  version: resumeId ? undefined : runVersion,
1546
1550
  strategy: resumeId ? undefined : runStrategy,
1547
1551
  fallback: options.fallback,
1548
1552
  prompt,
1549
- mode: options.mode,
1553
+ mode: forwardedMode,
1550
1554
  model: options.model,
1551
1555
  effort: options.effort,
1552
1556
  env: hostEnv,
@@ -1625,13 +1629,15 @@ export function registerRunCommand(program) {
1625
1629
  }
1626
1630
  // Session-id mint, detached dispatch, and local session-index
1627
1631
  // registration all live in the shared helper (lib/hosts/run-target.ts).
1632
+ const { modeForRemoteDispatch } = await import('../lib/codex-policy.js');
1633
+ const forwardedMode = modeForRemoteDispatch(options.mode, command.getOptionValueSource('mode'));
1628
1634
  const { task, exitCode } = await dispatchPromptToHost(host, {
1629
1635
  agent: runAgent,
1630
1636
  version: resumeId ? undefined : runVersion,
1631
1637
  strategy: resumeId ? undefined : runStrategy,
1632
1638
  fallback: options.fallback,
1633
1639
  prompt,
1634
- mode: options.mode,
1640
+ mode: forwardedMode,
1635
1641
  model: options.model,
1636
1642
  effort: options.effort,
1637
1643
  env: options.env,
@@ -1770,7 +1776,7 @@ export function registerRunCommand(program) {
1770
1776
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
1771
1777
  process.exit(resumeExit);
1772
1778
  }
1773
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
1779
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
1774
1780
  import('../lib/exec.js'),
1775
1781
  import('../lib/agents.js'),
1776
1782
  import('../lib/profiles.js'),
@@ -2407,7 +2413,14 @@ export function registerRunCommand(program) {
2407
2413
  // `skip` still hard-fails when unsupported — pretending we bypassed
2408
2414
  // permissions would be unsafe.
2409
2415
  const modeIsDefault = modeSource === 'default';
2410
- const requestedMode = normalizeMode(mode);
2416
+ let requestedMode = normalizeMode(mode);
2417
+ // Codex's intrinsic omitted-mode default is safe writable: workspace plus
2418
+ // common caches, network enabled, approvals on request. An explicit
2419
+ // --mode plan and a configured run default remain read-only.
2420
+ const { modeWasImplicit } = await import('../lib/codex-policy.js');
2421
+ const wasModeImplicit = modeWasImplicit(modeSource, modeFromRunDefault);
2422
+ if (wasModeImplicit)
2423
+ requestedMode = implicitModeFor(agent);
2411
2424
  let resolvedMode;
2412
2425
  try {
2413
2426
  resolvedMode = resolveMode(agent, requestedMode);
@@ -2531,6 +2544,7 @@ export function registerRunCommand(program) {
2531
2544
  prompt,
2532
2545
  interactive: options.interactive || forceInteractive,
2533
2546
  mode: requestedMode,
2547
+ modeWasImplicit: wasModeImplicit,
2534
2548
  effort,
2535
2549
  cwd: options.cwd,
2536
2550
  model,
@@ -16,6 +16,29 @@ import type { Command } from 'commander';
16
16
  import { type UnreachableFallback } from './go.js';
17
17
  import type { ActiveSession } from '../lib/session/active.js';
18
18
  import type { SessionMeta } from '../lib/session/types.js';
19
+ import { type LiveStatusFilter } from './sessions.js';
20
+ import { openSurfaces, type Backend } from '../lib/terminal/index.js';
21
+ /** Options for `sessions focus` — device/host scope + the `--active` live-state filters. */
22
+ export interface FocusOptions {
23
+ local?: boolean;
24
+ attachOnly?: boolean;
25
+ host?: string[];
26
+ device?: string[];
27
+ working?: boolean;
28
+ idle?: boolean;
29
+ waiting?: boolean;
30
+ orphan?: boolean;
31
+ orphaned?: boolean;
32
+ crashed?: boolean;
33
+ closed?: boolean;
34
+ abandoned?: boolean;
35
+ queued?: boolean;
36
+ unknown?: boolean;
37
+ }
38
+ /** `--device` is an alias of `--host`; both are repeatable. Merge into one host list. */
39
+ export declare function mergeFocusHosts(opts: FocusOptions): string[];
40
+ /** Picker header that reflects the active filter + device, e.g. "Focus orphaned sessions on yosemite-s0:". */
41
+ export declare function focusHeader(statuses: LiveStatusFilter[], hosts: string[]): string;
19
42
  export declare function registerFocusCommand(program: Command): void;
20
43
  /**
21
44
  * Which fallback fires when a session has no attach rail. `--attach-only` (the old
@@ -23,9 +46,58 @@ export declare function registerFocusCommand(program: Command): void;
23
46
  * without touching `jumpTo`'s side effects.
24
47
  */
25
48
  export declare function selectFallback(attachOnly: boolean | undefined): UnreachableFallback;
26
- export declare function focusAction(id: string | undefined, opts: {
27
- local?: boolean;
28
- attachOnly?: boolean;
29
- }): Promise<void>;
49
+ export declare function focusAction(id: string | undefined, opts: FocusOptions): Promise<void>;
50
+ /** A retained pane is not attachable merely because tmux can still display it. */
51
+ export declare function isAttachableLiveSession(session: ActiveSession): boolean;
52
+ /**
53
+ * How a single selected live session opens in its new tab.
54
+ * - `attach` — a live tmux pane joined in the tab (a second client, no fork),
55
+ * local (`sh -c`) or remote (`ssh -tt`). tmux is the only rail that can be
56
+ * *joined* without forking (see the file header).
57
+ * - `resume` — no attach rail, so resume a copy in the tab (the original keeps
58
+ * running); never a silent drop.
59
+ * - `skip` — not resumable AND no rail: reported, not dropped.
60
+ */
61
+ export type FocusSurfacePlan = {
62
+ kind: 'attach';
63
+ command: string[];
64
+ note: string;
65
+ } | {
66
+ kind: 'resume';
67
+ command: string[];
68
+ note: string;
69
+ } | {
70
+ kind: 'skip';
71
+ note: string;
72
+ };
73
+ /**
74
+ * Shell that resolves a tmux pane to its session and attaches it — the exact form
75
+ * `jumpTo` uses, minus the pre-select-window nicety, so a batch tab joins the live
76
+ * session (a second client) without forking. Reused for local and (wrapped in ssh)
77
+ * remote panes.
78
+ */
79
+ export declare function tmuxAttachScript(mux: {
80
+ socket?: string;
81
+ pane: string;
82
+ }): string;
83
+ /**
84
+ * Decide how one live session opens as a tab. Pure over the session + a
85
+ * `resumeCommandFor` resolver (injected so the local version-pinned resume command
86
+ * and the tests stay decoupled from `discoverSessions`).
87
+ */
88
+ export declare function planFocusSurface(s: ActiveSession, self: string, resumeCommandFor: (s: ActiveSession) => string[] | null): FocusSurfacePlan;
89
+ /** The engine seam — real `openSurfaces`, overridable in tests to assert the tab requests. */
90
+ export type OpenSurfacesFn = typeof openSurfaces;
91
+ /** Test seams for `openFocusTabs`: inject the engine boundary + force a backend. */
92
+ export interface OpenFocusTabsDeps {
93
+ open?: OpenSurfacesFn;
94
+ /** Skip `resolveBackend` (which needs a live terminal) when set — tests pass 'tmux'. */
95
+ backend?: Backend | 'inplace';
96
+ }
97
+ /**
98
+ * Open each selected live session as a tab: attach its live pane where one exists,
99
+ * else resume a copy. Reuses `resume`'s backend resolution + flood guard.
100
+ */
101
+ export declare function openFocusTabs(targets: ActiveSession[], self: string, deps?: OpenFocusTabsDeps): Promise<void>;
30
102
  /** Minimal SessionMeta for a live session, enough for `buildResumeCommand` + placement. */
31
103
  export declare function metaFromActive(s: ActiveSession): SessionMeta;