@phnx-labs/agents-cli 1.21.2 → 1.22.0

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 (102) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +35 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/exec.d.ts +27 -0
  8. package/dist/commands/exec.js +123 -6
  9. package/dist/commands/models.js +36 -1
  10. package/dist/commands/perf.d.ts +16 -0
  11. package/dist/commands/perf.js +11 -1
  12. package/dist/commands/projects.d.ts +11 -1
  13. package/dist/commands/projects.js +38 -4
  14. package/dist/commands/sessions-backfill.d.ts +32 -0
  15. package/dist/commands/sessions-backfill.js +186 -0
  16. package/dist/commands/sessions-picker.js +17 -2
  17. package/dist/commands/sessions.d.ts +22 -1
  18. package/dist/commands/sessions.js +331 -18
  19. package/dist/commands/teams.js +1 -1
  20. package/dist/commands/worktree.d.ts +3 -3
  21. package/dist/commands/worktree.js +35 -4
  22. package/dist/index.js +8 -0
  23. package/dist/lib/browser/service.js +13 -0
  24. package/dist/lib/computer/dispatch.d.ts +3 -1
  25. package/dist/lib/computer/dispatch.js +10 -2
  26. package/dist/lib/daemon.d.ts +5 -1
  27. package/dist/lib/daemon.js +63 -14
  28. package/dist/lib/devices/resolve-target.d.ts +6 -0
  29. package/dist/lib/devices/resolve-target.js +9 -3
  30. package/dist/lib/event-stream.d.ts +2 -0
  31. package/dist/lib/event-stream.js +3 -0
  32. package/dist/lib/events.d.ts +3 -1
  33. package/dist/lib/events.js +4 -2
  34. package/dist/lib/exec.js +39 -8
  35. package/dist/lib/git.d.ts +14 -0
  36. package/dist/lib/git.js +36 -0
  37. package/dist/lib/hooks/profile.js +1 -14
  38. package/dist/lib/hosts/dispatch.d.ts +12 -0
  39. package/dist/lib/hosts/dispatch.js +23 -6
  40. package/dist/lib/hosts/reconnect.d.ts +38 -0
  41. package/dist/lib/hosts/reconnect.js +85 -4
  42. package/dist/lib/hosts/run-target.js +14 -2
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/model-tiers.d.ts +54 -0
  46. package/dist/lib/model-tiers.js +229 -0
  47. package/dist/lib/models.d.ts +3 -0
  48. package/dist/lib/models.js +44 -7
  49. package/dist/lib/percentile.d.ts +12 -0
  50. package/dist/lib/percentile.js +24 -0
  51. package/dist/lib/perf/db.d.ts +1 -2
  52. package/dist/lib/perf/db.js +2 -14
  53. package/dist/lib/plugins.js +12 -1
  54. package/dist/lib/pricing/prices.json +16 -1
  55. package/dist/lib/project-focus.d.ts +42 -0
  56. package/dist/lib/project-focus.js +80 -0
  57. package/dist/lib/project-schedule.d.ts +75 -0
  58. package/dist/lib/project-schedule.js +110 -0
  59. package/dist/lib/redact.d.ts +2 -0
  60. package/dist/lib/redact.js +22 -0
  61. package/dist/lib/remote-agents-json.d.ts +2 -0
  62. package/dist/lib/remote-agents-json.js +3 -3
  63. package/dist/lib/resources.d.ts +16 -0
  64. package/dist/lib/resources.js +25 -14
  65. package/dist/lib/rotate.d.ts +84 -1
  66. package/dist/lib/rotate.js +155 -5
  67. package/dist/lib/routines.js +1 -14
  68. package/dist/lib/runner.d.ts +4 -2
  69. package/dist/lib/runner.js +21 -5
  70. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  71. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  72. package/dist/lib/session/bash-command.js +60 -9
  73. package/dist/lib/session/db.d.ts +22 -1
  74. package/dist/lib/session/db.js +516 -24
  75. package/dist/lib/session/discover.d.ts +68 -7
  76. package/dist/lib/session/discover.js +186 -84
  77. package/dist/lib/session/highlights.d.ts +24 -4
  78. package/dist/lib/session/highlights.js +52 -7
  79. package/dist/lib/session/parse.d.ts +8 -1
  80. package/dist/lib/session/parse.js +102 -35
  81. package/dist/lib/session/prompt.d.ts +19 -0
  82. package/dist/lib/session/prompt.js +43 -0
  83. package/dist/lib/session/remote-list.d.ts +71 -0
  84. package/dist/lib/session/remote-list.js +410 -2
  85. package/dist/lib/session/shell-programs.d.ts +15 -0
  86. package/dist/lib/session/shell-programs.js +359 -0
  87. package/dist/lib/session/tool-calls.d.ts +88 -0
  88. package/dist/lib/session/tool-calls.js +612 -0
  89. package/dist/lib/session/tool-index.d.ts +100 -0
  90. package/dist/lib/session/tool-index.js +773 -0
  91. package/dist/lib/session/tool-store.d.ts +15 -0
  92. package/dist/lib/session/tool-store.js +198 -0
  93. package/dist/lib/session/types.d.ts +49 -0
  94. package/dist/lib/state.d.ts +10 -1
  95. package/dist/lib/state.js +11 -2
  96. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  97. package/dist/lib/teams/remoteWorktree.js +3 -4
  98. package/dist/lib/teams/worktree.d.ts +11 -1
  99. package/dist/lib/teams/worktree.js +42 -4
  100. package/dist/lib/types.d.ts +31 -0
  101. package/dist/lib/types.js +17 -0
  102. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,106 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.0
4
+
5
+ - **`agents run auto` — full-auto dispatch (RUSH-2132).** `run auto` composes all three routing layers: host (14d launch affinity, unless `--host` is given), harness (installed CLIs weighted by best-account headroom), and account (the configured strategy). `balanced`/`available` now exit nonzero when every installed account is unhealthy — naming each excluded account, the earliest window reset, and the `--strategy pinned` escape hatch — instead of warning "falling back to defaults" and launching the exhausted pinned default. The error text is a machine-readable contract (`no healthy` + `resets <iso-time>`) the Factory watchdog tail-detects for rotate cooldowns. Source: `apps/cli/src/lib/rotate.ts`, `apps/cli/src/commands/exec.ts`, `apps/cli/src/lib/runner.ts`.
6
+
7
+ - **Bash-command summaries are faster and recognize more of what actually ran (#1830).**
8
+ `classifyBashCommand` (behind `agents sessions` / `agents activity` summaries) tokenized
9
+ the *entire* command — every pipeline segment, multi-KB heredoc bodies included — just to
10
+ read the leading executable, costing up to ~1ms on a big `cat <<HEREDOC …`. It now
11
+ tokenizes only the head of the first simple command. Coverage gaps that dumped commands
12
+ into a raw `other` pile are closed too: a `cd` prefix separated by `;` or a newline (not
13
+ just `&&`) unwraps to the real command, a path/tilde executable
14
+ (`~/.agents/skills/linear/scripts/linear`) resolves by basename, and the repo's own
15
+ toolchain (`agents`, `linear`, plus `rmdir`) is recognized — `agents` was the single top
16
+ unrecognized token. `ag` stays the silver searcher, not an `agents` alias. Source:
17
+ `apps/cli/src/lib/session/bash-command.ts`.
18
+
19
+ - **`agents computer describe` now counts toward `usedComputer`.** Every other
20
+ verb (`click`, `type`, `key`, `screenshot`, `run`, …) fires the
21
+ `computer.action` event via `emitComputerAction`; `describe` never did, so a
22
+ session that only ran `agents computer describe` read back
23
+ `usedComputer=false` — a false-negative in the sessions preview. A new
24
+ completeness-guard test pins every registered `agents computer` verb command
25
+ to a matching `emitComputerAction` call so a future verb can't ship the same
26
+ gap silently. Source: `apps/cli/src/commands/computer-actions.ts`,
27
+ `apps/cli/src/commands/computer-actions.test.ts`.
28
+
29
+ - **Pick a model by cost tier — `--model cheap|default|best|ultra` — on `agents run` and `agents teams add`.** Instead of a concrete id that churns per release and differs per harness, a tier resolves per `(harness, installed version)` to a model that version actually ships, ranked by the provider's own lineup (`opus/sonnet/haiku/fable`; Codex "frontier/balanced/fast" → Sol/Terra/Luna), then price, then size tokens. Single-model harnesses (Grok) map the tiers to reasoning effort; Droid uses a curated credit-multiplier map capped at 2x. An unsupported tier clamps to the nearest lower one; an unresolvable tier drops the flag and falls back to the harness default. Concrete model ids keep working unchanged. `agents models [agent[@version]]` now prints the per-harness tier map (with `~$/Mtok` where priced) and emits `tiers` in `--json`, and Droid joins the model-capable set. Also fixes the Claude catalog extractor returning 0 models on the newest native-binary format (a fallback id scan), and refreshes `prices.json` with the GPT-5.6 Sol/Terra/Luna series. Source: `apps/cli/src/lib/model-tiers.ts`, `apps/cli/src/lib/models.ts`, `apps/cli/src/lib/exec.ts`, `apps/cli/src/commands/models.ts`, `apps/cli/docs/model-tiers.md`.
30
+
31
+ - **`agents projects status` says what was worked on and what the dates prove.** Two new lines.
32
+ `focus` ranks the directories the window's commits landed in, read from the local checkout
33
+ with `git log --name-only` — no API call, no credential, no rate-limit budget, measured at
34
+ 0.23s over a 897-commit week. Changelog fragments and lockfiles are excluded from the
35
+ ranking: this repo files one fragment per PR, so `.changelog` otherwise ranked second and
36
+ presented PR count as an area of focus. `schedule` states what the milestone dates prove —
37
+ `overdue by N days`, `due in N days`, `N milestones, no issues filed against any`, or
38
+ `none dated`. Source: `apps/cli/src/lib/project-focus.ts`, `project-schedule.ts`.
39
+ - **The schedule line will never say "on track".** That verdict needs either project start and
40
+ target dates to interpolate expected progress, or a scope-history series to extrapolate a
41
+ finish date. Probed against a live workspace, all of them are absent (`health: null`,
42
+ `startDate`/`targetDate` null, `scopeHistory` and `completedScopeHistory` empty), so an
43
+ on-track or at-risk chip would be fabricated — and a confident wrong answer on a status card
44
+ is unfalsifiable from the card. When a human posts a Linear project health update, it is
45
+ relayed and attributed (`per Linear: atRisk`), never synthesized.
46
+
47
+ - **The `--device`/`--host` auto-reconnect loop no longer trusts a remote-origin exit code of 255 as "the SSH link dropped."** `reattachRemoteSession`'s `connected` flag is set as soon as the fast SSH preflight probe succeeds, before the actual reattach runs — so if the remote command it drives (`agents sessions focus <id> --local --attach-only`) ever exited 255 for a reason that had nothing to do with the SSH transport, that would be indistinguishable from the link itself dropping, refill the retry budget every cycle, and loop forever — printing "attempt 1/6" on every cycle and leaving the terminal full of aborted-TTY escape codes. The remote invocation is now wrapped in `bash -lc` so that whatever exit code it decides on, a 255 is remapped to 254 before this process sees it, closing that gap in the exit-code channel regardless of which remote-side path or peer `agents` version might produce it. A genuinely recurring *local* SSH failure can still refill the retry budget on every attempt by design (unchanged, tracked separately: phnx-labs/agents-cli#1884). Source: `apps/cli/src/lib/hosts/reconnect.ts`.
48
+
49
+ - **`agents sessions` can query distinct tool calls and count static Bash program occurrences locally or across the fleet.** Use `--include tools`, repeat `--query` with `tool:`, `program:`, `input:`, `output:`, `status:`, `exit:`, or `error:` fields, and add `--fleet` for live SSH fan-out. `--count` reports exact occurrence, containing-call, and session totals from ordered `wrapper`/`effective` rows without reparsing; synced mirrors are partitioned by origin so fleet evidence and totals do not duplicate sessions. Historical parsing is explicit and resumable through `agents sessions backfill tools`; normal scans index new and changed sessions once. Codex orchestration wrappers are parsed statically so only literal `tools.exec_command` commands reach the Bash AST, never wrapper code. Each device keeps a redacted, bounded relational SQLite/FTS5 cache, queries perform no transcript I/O or index writes, and no embeddings, vector database, or model calls are used. A sampling script explicitly backfills then extracts redacted shell-command origins from 50–100 sessions over the last seven days into a 16 MiB maximum artifact.
50
+
51
+ - **Local team worktrees base on freshly-fetched `origin/<default>`, not `HEAD`.**
52
+ `createWorktree` (and `agents worktree provision` for new branches) now
53
+ `git fetch origin` then `worktree add -b … origin/<default>`, matching
54
+ `createRemoteWorktree`. Previously local teammates forked from the
55
+ orchestrator's current `HEAD`, so a stale checkout made every teammate write
56
+ on old code and only surface the conflict at merge. Source:
57
+ `apps/cli/src/lib/teams/worktree.ts`, `apps/cli/src/commands/worktree.ts`,
58
+ `apps/cli/docs/teams.md`.
59
+
60
+ ## 1.21.3
61
+
62
+ - **`agents projects import --from-factory` stops printing raw git errors.** Reading each
63
+ checkout's real remote is done per registry row, and a checkout with no `origin` makes git
64
+ write `error: No such remote 'origin'` straight to the terminal — its own stderr, which the
65
+ surrounding try/catch never sees. Importing 12 rows printed two of them between the progress
66
+ lines. The probe now discards git's stderr; an absent remote is an expected answer, not
67
+ something to report. Source: `apps/cli/src/commands/projects.ts`.
68
+
69
+ - **Sessions now track browser/computer tool use and skill/plugin/slash-command
70
+ usage, queryable with `agents sessions --skill <name>` / `--plugin <name>`.**
71
+ `browser.navigate`, `browser.screenshot`, and a new `computer.action` event
72
+ fire on every `agents browser`/`agents computer` action, carrying session
73
+ identity for free. The sessions index persists `usedBrowser`/`usedComputer`
74
+ (from a scoped events-log read, not a transcript re-scan) and a new
75
+ `session_resource_usage` table records every skill and slash-command
76
+ invocation with its owning plugin, source repo, and git commit — resolved
77
+ against `resolveResource()`/`discoverPlugins()` at scan time. The sessions
78
+ picker preview surfaces both as `browser`/`computer` and `Skills:` tags.
79
+ Source: `apps/cli/src/lib/browser/service.ts`, `apps/cli/src/commands/computer-actions.ts`,
80
+ `apps/cli/src/lib/session/db.ts`, `apps/cli/src/lib/session/highlights.ts`,
81
+ `apps/cli/src/lib/session/discover.ts`, `apps/cli/src/commands/sessions.ts`.
82
+ - **`ResolvedResource` and `DiscoveredPlugin` carry provenance: `repoRoot` and a
83
+ lazily-resolved `snapshotSha`.** Every resource/plugin resolution can now
84
+ answer "which DotAgents repo, which commit" without an extra lookup; the git
85
+ shell-out is memoized per repo root and only runs when a caller actually
86
+ reads `snapshotSha`. Source: `apps/cli/src/lib/resources.ts`,
87
+ `apps/cli/src/lib/plugins.ts`, `apps/cli/src/lib/git.ts`.
88
+ - **`SessionEvent.slashCommand` captures a typed or model-invoked slash command**
89
+ (both the `<command-name>` wrapper and the `SlashCommand` tool call), and
90
+ `agents sessions`'s perf sample for `command.end` now carries the session id
91
+ and agent instead of being anonymous. Source: `apps/cli/src/lib/session/prompt.ts`,
92
+ `apps/cli/src/lib/session/parse.ts`, `apps/cli/src/index.ts`.
93
+ - **`agents routines status` no longer reports "stopped" for a live scheduler, and
94
+ `agents routines start` can't spawn a second one.** The daemon writes its pid file
95
+ once (on claim/start) but rewrites the heartbeat every tick. If the pid file was lost
96
+ while the daemon kept ticking — an earlier status check clearing a stale/reused pid, or
97
+ the file removed out from under a live daemon — `status` read only the pid file and
98
+ reported `stopped` for a scheduler that was in fact running and firing jobs, while
99
+ `claimDaemonInstance()` would start a concurrent `JobScheduler` that double-fires every
100
+ routine. `isDaemonRunning()` and the single-instance claim now also trust a fresh
101
+ heartbeat whose pid is alive, re-adopting the pid file to heal the desync.
102
+ Source: `apps/cli/src/lib/daemon.ts`.
103
+
3
104
  ## 1.21.2
4
105
 
5
106
  - **`agents trends` — resource and session analytics dashboard.** Baked recipes
package/README.md CHANGED
@@ -161,7 +161,18 @@ agents run claude@
161
161
  agents run codex@ "review this branch"
162
162
  ```
163
163
 
164
- `--strategy balanced` spreads work across available versions of the same agent -- useful when you have multiple accounts and want to avoid burning through one.
164
+ `--strategy balanced` spreads work across available versions of the same agent -- useful when you have multiple accounts and want to avoid burning through one. When every account is rate-limited, the run exits nonzero naming each excluded account and the earliest window reset (use `--strategy pinned` to force the default) -- it never launches into an exhausted account.
165
+
166
+ ### Don't care which harness? `agents run auto`
167
+
168
+ ```bash
169
+ # Picks the host (14d usage affinity), the harness (installed CLIs weighted by
170
+ # best-account headroom), and the account (balanced) -- all three layers.
171
+ agents run auto "summarize recent commits"
172
+ agents run auto --host yosemite-s0 "fix the flaky test" # pin the host layer
173
+ ```
174
+
175
+ `run auto` excludes any harness whose accounts are all rate-limited or signed out, and exits nonzero with the earliest reset time when nothing anywhere is healthy.
165
176
 
166
177
  A trailing `@` opens an account picker before either an interactive or prompt-based run. Each installed version shows its account identity, exact version, login state, plan, and every available session, weekly, or monthly limit. Logged-out, rate-limited, and out-of-credit accounts remain visible with the reason they cannot be selected; signed-in accounts whose provider does not expose quota data stay selectable and say `limits unavailable`. The choice pins only that run and does not change your default version.
167
178
 
@@ -244,11 +255,26 @@ agents sessions a1b2c3d4 --markdown
244
255
 
245
256
  # Just the last 3 turns, user messages only
246
257
  agents sessions a1b2c3d4 --last 3 --include user
258
+
259
+ # Calls in recent Codex sessions on one device
260
+ agents sessions --include tools --agent codex --device mac-mini --since 7d
261
+
262
+ # One session where two different calls match; query every online device
263
+ agents sessions --include tools \
264
+ --query 'program:git input:merge' \
265
+ --query 'program:gh output:CONFLICT' \
266
+ --fleet --json
267
+
268
+ # Count pre-indexed static git sites, containing calls, and sessions
269
+ agents sessions --include tools --query 'program:git' --count --fleet --json
270
+
271
+ # Populate historical tool rows once on each device
272
+ agents sessions backfill tools --fleet
247
273
  ```
248
274
 
249
275
  Interactive picker when you're in a terminal. Structured output (`--json`, `--markdown`, filtered by role or turn count) when piped.
250
276
 
251
- Backed by a SQLite + FTS5 index at `~/.agents/.history/sessions/sessions.db` with incremental scanning -- warm reads in ~100ms. External tools can consume `--json` output as a programmatic observability layer; see [docs/05-sessions.md](apps/cli/docs/05-sessions.md) for the schema and [docs/06-observability.md](apps/cli/docs/06-observability.md) for the consumption patterns.
277
+ Backed by a SQLite + FTS5 index at `~/.agents/.history/sessions/sessions.db` with incremental scanning -- warm reads in ~100ms. Tool-call evidence is redacted and bounded before it is cached; repeated `--query` clauses must match distinct calls in one session. Tool queries read SQLite only: `agents sessions backfill tools` performs the one-time historical parse, while normal incremental scans index new and changed sessions. The index stores ordered static Bash program sites, so `--count` reports occurrences, containing tool calls, and distinct sessions without reparsing. `--fleet` executes one origin partition per device, so synced mirrors cannot duplicate compact evidence or counts returned over SSH; transcript bodies stay on their origin machine. This uses relational SQLite rows and literal FTS5 only, with no embeddings, vector database, or model calls. External tools can consume `--json` output as a programmatic observability layer; see [docs/05-sessions.md](apps/cli/docs/05-sessions.md) for the schemas and [docs/06-observability.md](apps/cli/docs/06-observability.md) for the consumption patterns.
252
278
 
253
279
  ### Live state, and catching up fast
254
280
 
@@ -1095,9 +1121,12 @@ Conversations with Claude, Codex, legacy Gemini, and other agents scatter across
1095
1121
  ```bash
1096
1122
  agents sessions "auth middleware" # Full-text search across all agents
1097
1123
  agents sessions --agent claude --since 7d
1124
+ agents sessions --include tools --query 'program:git' --fleet --json
1125
+ agents sessions --include tools --query 'program:git' --count --fleet --json
1126
+ agents sessions backfill tools --fleet
1098
1127
  ```
1099
1128
 
1100
- The index lives at `~/.agents/.history/sessions/sessions.db` (SQLite + FTS5). Nothing leaves your machine. See [Sessions](#sessions-across-agents) for full usage.
1129
+ The index lives at `~/.agents/.history/sessions/sessions.db` (SQLite + FTS5). A local query stays on the machine; an explicit `--fleet` tool query sends only redacted, bounded match evidence or aggregate counts over SSH. Historical tool parsing is explicit via `sessions backfill tools`; queries never parse transcripts. See [Sessions](#sessions-across-agents) for full usage.
1101
1130
 
1102
1131
  ### Secrets
1103
1132
 
package/dist/bin/agents CHANGED
Binary file
@@ -82,4 +82,8 @@ export declare function shouldRaise(opts: {
82
82
  }): boolean;
83
83
  export declare function appPathIsElectron(appPath: string | null, exists?: (p: string) => boolean): boolean;
84
84
  export declare function electronWebviewTip(appLabel: string): string;
85
+ export declare function emitComputerAction(verb: string, targetPid: number | undefined, opts: {
86
+ bundle?: string;
87
+ host?: string;
88
+ }, extra?: Record<string, unknown>): void;
85
89
  export declare function registerActionCommands(program: Command): void;
@@ -11,6 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { openComputerClient, describeTransport, resolvePolicyPath, } from '../lib/computer-rpc.js';
13
13
  import { COMPUTER_INPUT_GATED_VERBS, formatComputerPermissionGrantHint, } from '../lib/permissions.js';
14
+ import { emit as emitEvent } from '../lib/events.js';
14
15
  function isComputerInputVerb(verb) {
15
16
  return typeof verb === 'string' && COMPUTER_INPUT_GATED_VERBS.includes(verb);
16
17
  }
@@ -350,6 +351,22 @@ function emit(result, json, human) {
350
351
  console.log(human());
351
352
  }
352
353
  }
354
+ // Record one `computer.action` per verb invocation, one call site per command
355
+ // below. Session/agent/machine identity is stamped for free by emitEvent's
356
+ // provenance floor (events.ts resolveProvenance) — this only carries the
357
+ // action-specific facts: which verb, against which target pid/bundle/host.
358
+ // NOTE: the field is `targetPid`, never `pid` — `pid` is a reserved envelope
359
+ // key (the emitting process's OWN pid, events.ts RESERVED_META_KEYS) that
360
+ // sanitizePayload() silently strips from the payload before it can collide.
361
+ export function emitComputerAction(verb, targetPid, opts, extra = {}) {
362
+ emitEvent('computer.action', {
363
+ command: verb,
364
+ targetPid,
365
+ bundle: opts.bundle,
366
+ host: opts.host,
367
+ ...extra,
368
+ });
369
+ }
353
370
  // Add the shared --pid/--bundle/--host target options to a verb. `--host` routes
354
371
  // the verb at a remote Windows device: the `computer` preAction hook hydrates
355
372
  // COMPUTER_HELPER_TCP from the tunnel `start --host` recorded, so withClient's
@@ -376,6 +393,7 @@ export function registerActionCommands(program) {
376
393
  await withClient(async (client) => {
377
394
  const res = unwrap(await client.call('list_apps'));
378
395
  const list = res.apps || [];
396
+ emitComputerAction('apps', undefined, opts);
379
397
  emit(res, Boolean(opts.json), () => list.length === 0
380
398
  ? '(no allow-listed apps running)'
381
399
  : list
@@ -395,6 +413,7 @@ export function registerActionCommands(program) {
395
413
  if (opts.depth != null)
396
414
  params.max_depth = opts.depth;
397
415
  const res = unwrap(await client.call('describe', params));
416
+ emitComputerAction('describe', pid, opts, { depth: opts.depth });
398
417
  // The tree is inherently structured — always JSON, pretty unless --json.
399
418
  console.log(JSON.stringify(opts.json ? res : res.tree ?? res, null, 2));
400
419
  });
@@ -422,6 +441,7 @@ export function registerActionCommands(program) {
422
441
  if (opts.background)
423
442
  params.background = true;
424
443
  const res = unwrap(await client.call('click', params));
444
+ emitComputerAction('click', pid, opts, { id: opts.id, count: opts.count });
425
445
  emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
426
446
  });
427
447
  });
@@ -440,6 +460,7 @@ export function registerActionCommands(program) {
440
460
  }
441
461
  await applyFocusPolicy(client, pid, opts);
442
462
  const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
463
+ emitComputerAction('right-click', pid, opts, { id: opts.id });
443
464
  emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
444
465
  });
445
466
  });
@@ -466,6 +487,8 @@ export function registerActionCommands(program) {
466
487
  if (opts.allowSecureField)
467
488
  params.allow_secure_field = true;
468
489
  const res = unwrap(await client.call('type', params));
490
+ // textLength, never the text itself — users type passwords/secrets into fields.
491
+ emitComputerAction('type', pid, opts, { id: opts.id, textLength: opts.text.length, committed: Boolean(res.committed) });
469
492
  emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
470
493
  });
471
494
  });
@@ -493,6 +516,7 @@ export function registerActionCommands(program) {
493
516
  params.char_delay_ms = charDelay;
494
517
  const res = unwrap(await client.call('type_text', params));
495
518
  warnIfNotFrontmost(res);
519
+ emitComputerAction('type-text', pid, opts, { textLength: opts.text.length, committed: Boolean(opts.commit) });
496
520
  emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
497
521
  });
498
522
  });
@@ -513,6 +537,7 @@ export function registerActionCommands(program) {
513
537
  params.require_frontmost = true;
514
538
  const res = unwrap(await client.call('key', params));
515
539
  warnIfNotFrontmost(res);
540
+ emitComputerAction('key', pid, opts, { keys: opts.keys });
516
541
  emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
517
542
  });
518
543
  });
@@ -548,6 +573,7 @@ export function registerActionCommands(program) {
548
573
  if (opts.background)
549
574
  params.background = true;
550
575
  const res = unwrap(await client.call('drag', params));
576
+ emitComputerAction('drag', pid, opts, { from: opts.from, to: opts.to });
551
577
  emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
552
578
  });
553
579
  });
@@ -574,6 +600,7 @@ export function registerActionCommands(program) {
574
600
  if (opts.dx != null)
575
601
  params.dx = opts.dx;
576
602
  const res = unwrap(await client.call('scroll', params));
603
+ emitComputerAction('scroll', pid, opts, { id: opts.id, dx: opts.dx, dy: opts.dy });
577
604
  emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
578
605
  });
579
606
  });
@@ -587,6 +614,7 @@ export function registerActionCommands(program) {
587
614
  await withClient(async (client) => {
588
615
  const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
589
616
  const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
617
+ emitComputerAction('ax-action', pid, opts, { id: opts.id, action: opts.action });
590
618
  emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
591
619
  });
592
620
  });
@@ -599,6 +627,7 @@ export function registerActionCommands(program) {
599
627
  await withClient(async (client) => {
600
628
  const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
601
629
  const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
630
+ emitComputerAction('focus', pid, opts, { id: opts.id });
602
631
  emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
603
632
  });
604
633
  });
@@ -615,6 +644,7 @@ export function registerActionCommands(program) {
615
644
  await withClient(async (client) => {
616
645
  const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
617
646
  const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
647
+ emitComputerAction('raise', pid, opts, { windowId: opts.windowId, title: opts.title });
618
648
  emit(res, Boolean(opts.json), () => {
619
649
  const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
620
650
  return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
@@ -644,6 +674,9 @@ export function registerActionCommands(program) {
644
674
  if (params.duration_ms == null)
645
675
  params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
646
676
  const res = unwrap(await client.call('wait', params));
677
+ emitComputerAction('wait', params.pid, opts, {
678
+ until: opts.until, durationMs: opts.duration, satisfied: Boolean(res.satisfied),
679
+ });
647
680
  emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
648
681
  });
649
682
  });
@@ -662,6 +695,7 @@ export function registerActionCommands(program) {
662
695
  if (opts.maxChars != null)
663
696
  params.max_chars = opts.maxChars;
664
697
  const res = unwrap(await client.call('get_text', params));
698
+ emitComputerAction('get-text', pid, opts, { id: opts.id });
665
699
  emit(res, Boolean(opts.json), () => String(res.text ?? ''));
666
700
  });
667
701
  });
@@ -688,6 +722,7 @@ export function registerActionCommands(program) {
688
722
  if (opts.name)
689
723
  params.name = opts.name;
690
724
  const res = unwrap(await client.call('launch_app', params));
725
+ emitComputerAction('launch', res.pid, opts, { path: opts.path, name: opts.name });
691
726
  emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
692
727
  });
693
728
  });
@@ -7,7 +7,7 @@ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketP
7
7
  import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
8
8
  import { sshExec } from '../lib/ssh-exec.js';
9
9
  import { encodePowershell } from '../lib/hosts/remote-cmd.js';
10
- import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
10
+ import { registerActionCommands, withClient, unwrap, pickTarget, emitComputerAction } from './computer-actions.js';
11
11
  import { runComputerLoop } from '../lib/computer/loop.js';
12
12
  import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
13
13
  import { makeClaudeResponder, resolveApiKey, DEFAULT_CLAUDE_MODEL, DEFAULT_CLAUDE_BASE_URL } from '../lib/computer/model.js';
@@ -301,7 +301,7 @@ function registerRunCommand(program) {
301
301
  maxTokens: opts.maxTokens,
302
302
  });
303
303
  await withClient(async (client) => {
304
- const dispatch = makeVerbDispatcher(client);
304
+ const dispatch = makeVerbDispatcher(client, { host: opts.host });
305
305
  const targetInput = opts.bundle ? { bundle: opts.bundle } : {};
306
306
  const result = await runComputerLoop({
307
307
  task: opts.task,
@@ -375,6 +375,7 @@ function registerScreenshotCommand(program) {
375
375
  // --list: enumerate windows, no image.
376
376
  if (opts.list) {
377
377
  const res = unwrap(await client.call('screenshot', { pid, list: true }));
378
+ emitComputerAction('screenshot', pid, opts, { list: true });
378
379
  const windows = res.windows || [];
379
380
  if (opts.json) {
380
381
  console.log(JSON.stringify(res, null, 2));
@@ -402,6 +403,7 @@ function registerScreenshotCommand(program) {
402
403
  console.error('helper returned no image_data');
403
404
  process.exit(1);
404
405
  }
406
+ emitComputerAction('screenshot', pid, opts, { display: opts.display, windowId: opts.windowId });
405
407
  const buf = Buffer.from(b64, 'base64');
406
408
  // Sniff the real format and correct the extension so the filename never
407
409
  // lies about its bytes (macOS -> JPEG, Windows helper -> PNG).
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { type Command } from 'commander';
9
9
  import type { ExecEffort } from '../lib/exec.js';
10
+ import { RUN_AUTO_KEYWORD } from '../lib/types.js';
10
11
  import { type SshGResult } from '../lib/hosts/ssh-config.js';
11
12
  export interface RunAccountPickerRequest {
12
13
  requested: boolean;
@@ -41,6 +42,32 @@ export declare function runAccountPickerConflicts(options: {
41
42
  on?: string;
42
43
  computer?: string;
43
44
  }): string[];
45
+ export { RUN_AUTO_KEYWORD };
46
+ /**
47
+ * Whether `run auto` should default its host layer to the affinity pick (the
48
+ * same machinery as `--device auto`). False when the caller pinned any host
49
+ * flag, and false when this process was itself dispatched by a host run — the
50
+ * dispatcher exports AGENTS_RUN_AUTO_HOST_RESOLVED=1 into the remote SHELL
51
+ * (hosts/dispatch.ts remoteRunShellPrelude) because it already resolved the
52
+ * host layer, and re-picking here would chain-hop the run across the fleet.
53
+ * Pure so the pinning matrix is unit-testable.
54
+ */
55
+ export declare function runAutoDefaultsToAffinity(options: {
56
+ host?: string;
57
+ device?: string;
58
+ on?: string;
59
+ computer?: string;
60
+ }, env?: NodeJS.ProcessEnv): boolean;
61
+ /**
62
+ * Whether an interactive host dispatch must mint a correlation launch id and
63
+ * resolve the remote session via the launch-id join (RUSH-2034), rather than
64
+ * trusting a pre-known session id. `run auto` ALWAYS joins: the harness is
65
+ * picked on the remote, so an explicit --session-id is only adopted when the
66
+ * pick lands on claude — pre-registering it would strand a stale session-index
67
+ * entry naming an id a non-claude pick never used (RUSH-2132). Pure so the
68
+ * decision matrix is unit-testable.
69
+ */
70
+ export declare function hostInteractiveNeedsCorrelationId(runAgent: string, hostSessionId: string | undefined, resumeId: string | undefined): boolean;
44
71
  /** The host descriptor fields the `--copy-creds` security gate reads. */
45
72
  export interface CopyCredsGateHost {
46
73
  name: string;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
+ import { RUN_AUTO_KEYWORD } from '../lib/types.js';
10
11
  import { setHelpSections } from '../lib/help.js';
11
12
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
12
13
  import { getUserAgentsDir } from '../lib/state.js';
@@ -66,6 +67,39 @@ export function runAccountPickerConflicts(options) {
66
67
  function isValidAgent(agent) {
67
68
  return agent in AGENTS;
68
69
  }
70
+ // Reserved `<agent>` keyword for `agents run auto` — canonical definition in
71
+ // lib/types.ts (shared with the host dispatch layer); re-exported here.
72
+ export { RUN_AUTO_KEYWORD };
73
+ /**
74
+ * Whether `run auto` should default its host layer to the affinity pick (the
75
+ * same machinery as `--device auto`). False when the caller pinned any host
76
+ * flag, and false when this process was itself dispatched by a host run — the
77
+ * dispatcher exports AGENTS_RUN_AUTO_HOST_RESOLVED=1 into the remote SHELL
78
+ * (hosts/dispatch.ts remoteRunShellPrelude) because it already resolved the
79
+ * host layer, and re-picking here would chain-hop the run across the fleet.
80
+ * Pure so the pinning matrix is unit-testable.
81
+ */
82
+ export function runAutoDefaultsToAffinity(options, env = process.env) {
83
+ if (hostTargetGiven(options).length > 0)
84
+ return false;
85
+ return env.AGENTS_RUN_AUTO_HOST_RESOLVED !== '1';
86
+ }
87
+ /**
88
+ * Whether an interactive host dispatch must mint a correlation launch id and
89
+ * resolve the remote session via the launch-id join (RUSH-2034), rather than
90
+ * trusting a pre-known session id. `run auto` ALWAYS joins: the harness is
91
+ * picked on the remote, so an explicit --session-id is only adopted when the
92
+ * pick lands on claude — pre-registering it would strand a stale session-index
93
+ * entry naming an id a non-claude pick never used (RUSH-2132). Pure so the
94
+ * decision matrix is unit-testable.
95
+ */
96
+ export function hostInteractiveNeedsCorrelationId(runAgent, hostSessionId, resumeId) {
97
+ if (resumeId)
98
+ return false;
99
+ if (runAgent === RUN_AUTO_KEYWORD)
100
+ return true;
101
+ return !hostSessionId && isSessionTrackedAgent(runAgent);
102
+ }
69
103
  /** Build a one-line banner describing which version the strategy picked. */
70
104
  function formatRotationBanner(result, verb = 'balanced') {
71
105
  const { picked, healthy, excluded } = result;
@@ -483,7 +517,7 @@ export function registerRunCommand(program) {
483
517
  .description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively.')
484
518
  .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')
485
519
  .option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto (claude and codex only)', 'auto')
486
- .option('--model <model>', 'Override the model directly (e.g., claude-opus-4-6)')
520
+ .option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete model id; tiers resolve per harness+version to a supported model')
487
521
  .option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
488
522
  .option('--secrets <bundle>', 'Inject a secrets bundle (repeatable). Values resolve from macOS Keychain at run time. See `agents secrets`.', (val, prev) => [...prev, val], [])
489
523
  .option('--no-auto-secrets', 'Skip auto-injection of secrets declared by a workflow\'s frontmatter `secrets:` field. Has no effect on bare-agent runs.')
@@ -561,6 +595,11 @@ export function registerRunCommand(program) {
561
595
  # Pick a signed-in account/version for only this run
562
596
  agents run claude@
563
597
 
598
+ # Full-auto: affinity-pick the host, then the harness with the most
599
+ # account headroom, then a balanced account on it
600
+ agents run auto "fix the flaky test" --mode edit
601
+ agents run auto --host yosemite-s0 "fix the flaky test" # pin the host
602
+
564
603
  # Open the session in a terminal tab — detected from where your sessions
565
604
  # already run (Ghostty / iTerm / Terminal.app); force one with a value
566
605
  agents run claude --terminal
@@ -599,6 +638,13 @@ export function registerRunCommand(program) {
599
638
  balanced distribute load across healthy accounts by remaining capacity (default)
600
639
  A version/account is skipped when it is rate-limited right now — any usage window (incl. the 5-hour session window) at 100%, matching the 'agents view' badge.
601
640
  --balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
641
+ Zero healthy accounts under balanced/available exits nonzero naming each
642
+ excluded account and the earliest window reset — use --strategy pinned to force.
643
+
644
+ 'auto' harness (agents run auto): picks the host (14d usage affinity,
645
+ unless --host is given), the harness (installed CLIs weighted by
646
+ best-account headroom), and the account (the strategy above). Zero
647
+ healthy accounts on any harness exits nonzero with the earliest reset.
602
648
 
603
649
  Account picker: append @ with no version (agents run claude@) to choose one
604
650
  installed account for this run. Rows show identity, login state, plan,
@@ -682,6 +728,32 @@ export function registerRunCommand(program) {
682
728
  process.exit(1);
683
729
  }
684
730
  }
731
+ // `agents run auto`: the reserved harness keyword — full-auto dispatch
732
+ // (host affinity → cross-harness balance → account balance, RUSH-2132).
733
+ if (normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD && normalizedAgentSpec !== RUN_AUTO_KEYWORD) {
734
+ console.error(chalk.red(`agents run auto picks the harness itself — a @version pin does not apply. ` +
735
+ `Pin a concrete harness instead: agents run <harness>@<version>.`));
736
+ process.exit(1);
737
+ }
738
+ const autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
739
+ if (autoHarnessRequested) {
740
+ // `auto` is reserved. If a future harness registers that id, the
741
+ // keyword collides — fail loud rather than silently shadow the harness.
742
+ if (RUN_AUTO_KEYWORD in AGENTS) {
743
+ console.error(chalk.red(`'${RUN_AUTO_KEYWORD}' is now a registered harness and collides with the reserved 'run auto' keyword. ` +
744
+ `Run the harness by name instead.`));
745
+ process.exit(1);
746
+ }
747
+ if (accountPickerRequested) {
748
+ console.error(chalk.red(`agents run auto picks the harness and account itself — the trailing-@ account picker needs a concrete harness (agents run <harness>@).`));
749
+ process.exit(1);
750
+ }
751
+ // Host layer: with no explicit --host/--device, default to the
752
+ // affinity pick. Skipped on a host-dispatched run — its dispatcher
753
+ // already resolved this layer (see runAutoDefaultsToAffinity).
754
+ if (runAutoDefaultsToAffinity(options))
755
+ options.device = 'auto';
756
+ }
685
757
  // --device auto / --host auto (and deprecated --smart): affinity-pick host.
686
758
  // Harness is always the agent the user typed — never auto-picked.
687
759
  // Affinity failure degrades to local (does not kill the run).
@@ -1062,6 +1134,10 @@ export function registerRunCommand(program) {
1062
1134
  process.exit(1);
1063
1135
  }
1064
1136
  const hostName = hostGiven[0];
1137
+ // Note: a `run auto` dispatch needs no marker forwarded from here — the
1138
+ // dispatch layer (hosts/dispatch.ts remoteRunShellPrelude) exports the
1139
+ // chain-hop guard into the remote shell for BOTH interactive and
1140
+ // headless paths, keyed off the agent name being `auto`.
1065
1141
  const { resolveHostRunTarget, resolveHostSessionId, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
1066
1142
  const { runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
1067
1143
  const { registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
@@ -1242,11 +1318,16 @@ export function registerRunCommand(program) {
1242
1318
  // the stream we resolve the id by one ssh read of the remote hook
1243
1319
  // record — the same launch-id join used locally (RUSH-2034). Not
1244
1320
  // needed for Claude (id forced) or resume (id already known).
1245
- const correlationLaunchId = !hostSessionId && !resumeId && isSessionTrackedAgent(runAgent) ? randomUUID() : undefined;
1321
+ // `run auto` ALWAYS joins: the remote picks the harness, so an
1322
+ // explicit --session-id is only adopted by a claude pick.
1323
+ const correlationLaunchId = hostInteractiveNeedsCorrelationId(runAgent, hostSessionId, resumeId) ? randomUUID() : undefined;
1246
1324
  const hostEnv = correlationLaunchId
1247
1325
  ? [...options.env, `AGENT_LAUNCH_ID=${correlationLaunchId}`]
1248
1326
  : options.env;
1249
- if (hostSessionId) {
1327
+ // `run auto` never pre-registers: the explicit id is only real when
1328
+ // the remote pick lands on claude. The launch-id join below records
1329
+ // the id the remote ACTUALLY used, whatever the pick.
1330
+ if (hostSessionId && runAgent !== RUN_AUTO_KEYWORD) {
1250
1331
  registerInteractiveHostSession({
1251
1332
  cwd: process.cwd(),
1252
1333
  host: host.name,
@@ -1316,7 +1397,11 @@ export function registerRunCommand(program) {
1316
1397
  // re-attach the live pane automatically instead of exiting — the user
1317
1398
  // never has to notice the drop and `agents sessions focus` by hand.
1318
1399
  // `raw` runs aren't tmux wrapped, so there is nothing to reconnect to.
1319
- const reconnectId = hostSessionId ?? resolvedRemoteId ?? resumeId;
1400
+ // For `run auto` prefer the join-resolved id (the harness the remote
1401
+ // ACTUALLY picked) over the explicit --session-id only claude adopts.
1402
+ const reconnectId = (runAgent === RUN_AUTO_KEYWORD
1403
+ ? resolvedRemoteId ?? hostSessionId
1404
+ : hostSessionId ?? resolvedRemoteId) ?? resumeId;
1320
1405
  if (reconnectId && !isRaw) {
1321
1406
  const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
1322
1407
  if (exitCode === SSH_CONN_FAILURE) {
@@ -1482,7 +1567,7 @@ export function registerRunCommand(program) {
1482
1567
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
1483
1568
  process.exit(resumeExit);
1484
1569
  }
1485
- 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, isHeadlessSecretsContext }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
1570
+ 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, isHeadlessSecretsContext }, { 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 }, { shareRuntimeEnv },] = await Promise.all([
1486
1571
  import('../lib/exec.js'),
1487
1572
  import('../lib/agents.js'),
1488
1573
  import('../lib/profiles.js'),
@@ -1537,7 +1622,28 @@ export function registerRunCommand(program) {
1537
1622
  process.exit(1);
1538
1623
  }
1539
1624
  }
1540
- if (isValidAgent(rawAgent)) {
1625
+ if (autoHarnessRequested) {
1626
+ // Harness layer (RUSH-2132): weighted pick across installed harnesses
1627
+ // by best-account headroom. Zero healthy accounts anywhere fails loud
1628
+ // — launching a default "because it's there" is how a rotate loop
1629
+ // hammers an exhausted account.
1630
+ const byHarness = await collectHarnessCandidates();
1631
+ const harnessPick = pickHarnessWeighted(byHarness);
1632
+ if (!harnessPick) {
1633
+ console.error(chalk.red(formatNoHealthyHarnessError(classifyHarnessCandidates(byHarness))));
1634
+ process.exit(1);
1635
+ }
1636
+ agent = harnessPick.picked.agent;
1637
+ if (!options.quiet) {
1638
+ process.stderr.write(chalk.gray(formatHarnessPickBanner(harnessPick) + '\n'));
1639
+ }
1640
+ // --session-id keeps its claude-only semantics: honored when auto
1641
+ // picks claude, ignored (loudly) otherwise.
1642
+ if (options.sessionId && agent !== 'claude' && !options.quiet) {
1643
+ process.stderr.write(chalk.yellow(`[agents] --session-id ignored: auto picked ${agent} (only claude accepts a forced session id)\n`));
1644
+ }
1645
+ }
1646
+ else if (isValidAgent(rawAgent)) {
1541
1647
  agent = rawAgent;
1542
1648
  }
1543
1649
  else if (profileExists(rawAgent)) {
@@ -1929,6 +2035,15 @@ export function registerRunCommand(program) {
1929
2035
  else {
1930
2036
  try {
1931
2037
  const resolved = await resolveRunVersion(agent, strategy, cwd);
2038
+ if (resolved.exhausted) {
2039
+ // Fail loud (RUSH-2132): the old behavior warned "found no
2040
+ // usable version; falling back to defaults" and launched the
2041
+ // pinned default anyway — the exact move that loops a rotate
2042
+ // into an exhausted account. The message text is a contract
2043
+ // the Factory watchdog tail-detects; do not reword it.
2044
+ console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
2045
+ process.exit(1);
2046
+ }
1932
2047
  if (resolved.version) {
1933
2048
  version = resolved.version;
1934
2049
  rotationResult = resolved.rotation;
@@ -1938,6 +2053,8 @@ export function registerRunCommand(program) {
1938
2053
  }
1939
2054
  }
1940
2055
  else if (!options.quiet) {
2056
+ // No installed version at all (not "accounts exhausted" — that
2057
+ // fails loud above): keep the pre-existing default resolution.
1941
2058
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
1942
2059
  }
1943
2060
  }