@phnx-labs/agents-cli 1.22.27 → 1.22.28
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.
- package/CHANGELOG.md +49 -0
- package/README.md +62 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +54 -10
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions.d.ts +26 -6
- package/dist/commands/sessions.js +46 -12
- package/dist/commands/teams.js +19 -8
- package/dist/lib/activity.js +45 -99
- package/dist/lib/claude-account-token.js +38 -11
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.js +4 -1
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +78 -22
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +5 -0
- package/dist/lib/hosts/passthrough.js +5 -2
- package/dist/lib/hosts/progress.d.ts +10 -0
- package/dist/lib/hosts/progress.js +37 -5
- package/dist/lib/hosts/ready.d.ts +41 -2
- package/dist/lib/hosts/ready.js +100 -12
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +13 -5
- package/dist/lib/hosts/reconnect.js +5 -3
- package/dist/lib/hosts/registry.js +1 -0
- package/dist/lib/hosts/tasks.d.ts +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +18 -11
- package/dist/lib/models.js +19 -15
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +58 -20
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +32 -15
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
- package/dist/lib/exec.bench.d.ts +0 -1
- package/dist/lib/exec.bench.js +0 -186
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.28
|
|
4
|
+
|
|
5
|
+
- **`agents run` / `agents teams` `--device`/`--host`: fail loud when a pinned harness version is not installed on the target (RUSH-2313).** A concrete pin like `codex@0.145.0` is checked against the remote `agents view --json` listing during `ensureHostReady` *before* the run is marked dispatched. Missing pins exit non-zero naming the box, the pin, what is installed there, and `agents ssh <box> -- agents add <agent>@<ver>` — so detached fleet drains no longer print `Dispatched` and then die only in the remote log. Aliases (`@latest` / …) still resolve on the remote; a bare agent name still only warns. Source: `apps/cli/src/lib/hosts/ready.ts`, `dispatch.ts`, `teams/agents.ts`.
|
|
6
|
+
|
|
7
|
+
- **fix: Windows-host e2e suites can resolve `win-mini` again under the hermetic device registry.** `tests/setup.ts` (RUSH-2042 / #1572) redirects `AGENTS_DEVICES_DIR` to an empty fork-private directory so unit fixtures never leak into the real fleet registry. The live `ssh-tunnel.e2e` / `browser/drivers/ssh.e2e` suites still need a real `DeviceProfile` for `AGENTS_TEST_WIN_HOST`, so after #1572 every `tests-windows-host-e2e.yml` run failed immediately with `Unknown device 'win-mini'` — even on a tailnet-joined runner that could ssh to the box. The setup file now seeds the private registry from the real fleet entry (or synthesizes one from `ssh -G <host>`) when `AGENTS_TEST_WIN_HOST` is set; the real registry is never written. Source: `apps/cli/tests/seed-e2e-win-host.ts`, `apps/cli/tests/setup.ts`.
|
|
8
|
+
|
|
9
|
+
- **OpenCode timeout-sample spool dir is Windows-safe (#1869).** The generated
|
|
10
|
+
`agents-cli-hooks.ts` plugin used `PERF_SPOOL.slice(0, PERF_SPOOL.lastIndexOf("/"))`
|
|
11
|
+
to mkdir the perf spool parent. On Windows `getPerfDir()` is backslash-separated,
|
|
12
|
+
so `lastIndexOf("/")` returned -1 and the slice dropped one character — the
|
|
13
|
+
sample write was fail-silent and never landed. The plugin now imports
|
|
14
|
+
`node:path` and uses `path.dirname(PERF_SPOOL)`. Source: `apps/cli/src/lib/hooks.ts`.
|
|
15
|
+
|
|
16
|
+
- **Bash-command classifier: single-source tool registry + broader unwrap (#1889).** The embedded Python activity-log hook (`ACTIVITY_LOG_HOOK_SCRIPT` in `activity.ts`) hand-duplicated `bash-command.ts`'s `TOOL_REGISTRY` and `VALUE_FLAGS` with a "keep them in sync" comment — the two had already drifted (Python was missing `rmdir`, and `agents`/`linear` two-level tools). Both tables are now generated from the TypeScript source (`pythonToolRegistryLiteral` / `pythonValueFlagsLiteral`) at module load. Separately, `unwrapCommand` (and the matching Python `_unwrap_command`) now peels `export VAR=…`, `set -euo pipefail`, `for`/`until` loop bodies, `if`/`then` branches, and `(command)` subshell prefixes that previously classified as `other`. Source: `apps/cli/src/lib/session/bash-command.ts`, `apps/cli/src/lib/activity.ts`.
|
|
17
|
+
|
|
18
|
+
- **Active-session polls skip re-parsing quiet transcripts (#2047).** `computeLiveSignals` (the per-session tail/parse behind `agents sessions --active` and the menu-bar badge tick) now memoizes by transcript path + mtime + `pidAlive` inside the process, so a 30s poll no longer re-tails every live session whose file has not changed. Positive Claude transcript-path resolutions are also memoized while the file still exists, avoiding a full walk of every Claude version-home `projects/` tree per pid per tick. Source: `apps/cli/src/lib/session/active.ts`.
|
|
19
|
+
|
|
20
|
+
- perf(sessions): throttle headless `ps`/`lsof` scan + memoize process table across an active-session poll (#2047)
|
|
21
|
+
|
|
22
|
+
- **`agents routines devices --set` / `--clear` no longer abort when a fleet peer is offline (#2118).** Pinning a routine used to fan out pause/resume to every registered device and throw on the first unreachable one — often *after* the pin had already succeeded on the target — so a single asleep laptop made fleet pins unusable and the error looked like the pin failed. Offline peers are now skipped with a warning; the command exits non-zero only when a *selected* target device cannot be reached. Source: `apps/cli/src/commands/routines.ts`.
|
|
23
|
+
|
|
24
|
+
- **`agents models claude` no longer lists a per-cloud bare-minor id as a plain catalog entry (#2233).** The id-scan fallback (`scanClaudeCatalogIds`, used when the structured alias/perCloud maps yield fewer than two models) used to surface short forms like `claude-opus-4-1` that only appear as a `foundry:` / cloud-scoped field next to a real firstParty id (`claude-opus-4-1-20250805`). `dropBareLegacyIds` now drops **any** id that is a dash-boundary prefix of a more-specific sibling also present in the scan — covering both the bare-major `.includes("claude-opus-4")` artifacts (#1892) and these bare-minor cloud-metadata values — while keeping genuine bare currents with no sibling (`claude-sonnet-5`) and not collapsing `claude-opus-4-1` into `claude-opus-4-10`. Source: `apps/cli/src/lib/models.ts`.
|
|
25
|
+
|
|
26
|
+
- **Menu-bar dropdown density toggle removed.** The `Density: Auto/Rich/Compact` footer item and the whole rich/compact rendering fork are gone; the dropdown now always renders the rich rows (session/work titles, expanded Routines and Recent sections). Removes the `menubarDensity` UserDefaults key and `MENUBAR_DENSITY` env override, and updates the menu-bar docs to match. Source: `apps/cli/menubar/Sources/MenubarHelper/StatusItemController.swift`, `apps/cli/docs/menubar.md`.
|
|
27
|
+
|
|
28
|
+
- **The macOS secrets broker can now hold an explicit key subset under its own lease id and expiry (RUSH-2255).** `agents secrets lease <bundle> --keys K1,K2 --for 8h` authenticates once, stores only those resolved values in broker memory and the restart session, and reports lease metadata without exposing values. Unknown, missing, and expired keys fail closed. Source: `apps/cli/src/{commands/secrets.ts,lib/secrets/{agent,lease,session-store}.ts}`.
|
|
29
|
+
|
|
30
|
+
- **Scoped leases can now be listed and revoked by id (RUSH-2256).** `agents secrets leases` shows bundle, keys, and remaining lifetime; `agents secrets revoke <lease-id>` wipes exactly that broker grant and its restart session. Source: `apps/cli/src/{commands/secrets.ts,lib/secrets/{agent,session-store}.ts}`.
|
|
31
|
+
|
|
32
|
+
- **Fleet devices accept an explicit SSH private-key path (RUSH-2265).** `agents devices set <name> --auth key --identity-file <path>` stores the path on that device and every shared SSH invocation passes it to OpenSSH with `-i`, so devices no longer depend on whichever key the ambient ssh agent happens to offer. Switching a device from password to key auth also removes stale password-bundle metadata from the registry.
|
|
33
|
+
|
|
34
|
+
- **Detached agent dispatch now runs on Windows OpenSSH hosts (RUSH-2267).** Headless `agents run … --host <windows>` launches through a hidden PowerShell process, preserves actor/session/env context, and uses durable Windows-native log, follow, reconcile, stop, and cleanup operations.
|
|
35
|
+
|
|
36
|
+
- **`agents repo pull` fast-forwards a clean behind-only checkout instead of failing with "Cannot rebase onto multiple branches" (RUSH-2282).** After fetch, a 1-behind tree integrates via `merge --ff-only` against the tracking ref; only genuinely diverged histories enter rebase. Avoids re-running `git pull --rebase` on a multi-entry `FETCH_HEAD` (common after bare fetch when the remote has several branches, or under concurrent fleet fetch). Source: `apps/cli/src/lib/git.ts` `pullRepo`.
|
|
37
|
+
|
|
38
|
+
- **`agents perf` splits intentional deny (exit 2) from crashes (exit 1) — hook health is readable again (RUSH-2294).** Deny-by-design guards (`ask-user-question-guard`, `plan-html-reminder`, `git-guard`, …) exit 2 on purpose; the warehouse previously counted any nonzero exit as `errorCount`, so a working guard read as a "92%-error" hook. Exit 0 = allow, exit 2 = `blockCount`/`blockRate`, exit 1 / other = `errorCount`/`errorRate`. The hooks table column is now `ERR/BLOCK/TO` (`err:…% block:…% to:…%`). JSON rows gain `blockCount`/`blockRate`. Source: `apps/cli/src/lib/perf/db.ts`, `apps/cli/src/lib/hooks/profile.ts`, `apps/cli/src/commands/perf.ts`.
|
|
39
|
+
|
|
40
|
+
- **Claude launches reuse an unchanged file-backed setup-token instead of decrypting it on every exec (RUSH-2317).** The process-lifetime cache is isolated by version home and invalidates from the encrypted credential file's identity, ctime, mtime, and size; token rotation is picked up on the next launch, missing per-account tokens are negatively cached, and plaintext tokens remain memory-only. Source: `apps/cli/src/lib/claude-account-token.ts`, `apps/cli/src/lib/secrets/filestore.ts`.
|
|
41
|
+
|
|
42
|
+
- **Repeated full session listings reuse stable transcript membership (RUSH-2318).** The process briefly caches settled transcript directories by mtime+size, rereads recently changed directories, and expires every entry within the filesystem timestamp-precision window, avoiding burst-time repeat reads without hiding filesystem-only creates or deletes. Source: `apps/cli/src/lib/session/db.ts`.
|
|
43
|
+
|
|
44
|
+
- **`agents sync --yes` drops ~1s/agent of dead work and reuses still-fresh fingerprints (RUSH-2320).** Measured on a real install: `getActuallySyncedResources` was ~1055 ms/agent on the unattended path that never reads it; `buildManifest` re-hashed every file (~716 ms) after a no-op force sync; the guard-hit path spent ~12 ms building inventories it then discarded. Unattended sync now skips the interactive inventory, the skills detector is stat-first on size before any content read, `buildManifest` carries still-fresh fingerprints from the previous manifest, and `syncResourcesToVersion` runs its no-change guard before pattern expansion (accepting a caller-supplied `available` inventory so multi-version fan-out does not re-scan). Source: `apps/cli/src/lib/refresh.ts`, `apps/cli/src/lib/versions.ts`, `apps/cli/src/lib/staleness/`.
|
|
45
|
+
|
|
46
|
+
- **fix: `agents run <agent>` no longer dead-ends when every account is logged out — it launches so you can sign in.** A harness with one installed, signed-out version had no reachable login path at all: `agents run cursor` exited with `no healthy cursor account under strategy 'balanced' — excluded: 2026.07.23 (signed_out)`, the `agents run cursor@` account picker marked the logged-out row `disabled` and offered only `No usable accounts — cancel`, and `agents use cursor` only set a default. The zero-healthy guard (RUSH-2132) treated a missing login like an exhausted account, but they are opposites — a throttled account must not be launched, while a signed-out one is fixed precisely BY launching, since the harness's own TUI is the login surface. On a human-facing terminal run (a real TTY and no `--json`), a single sign-in-recoverable account now launches directly (naming the version and the login command) and several open the account picker with auth-blocked rows selectable and labelled `launch to sign in`; `rate_limited` / `out_of_credits` still fail loud, and off a TTY — or under `--json` — both classes keep the exact watchdog-parsed error, now with the harness's login command alongside `--strategy pinned`. Source: `apps/cli/src/lib/rotate.ts` (`isSignInRecoverable`, `signInRecoverableCandidates`), `apps/cli/src/commands/run-account-picker.ts` (`pickSignInLaunchVersion`), `apps/cli/src/commands/exec.ts`. (RUSH-2334)
|
|
47
|
+
|
|
48
|
+
- **`sessions --active` no longer shows retained dead/queued rows, and every process row now carries its PID (RUSH-2336).** Bare `--active` (CLI table/JSON, the interactive browser, `focus`, and the menu bar) previously kept a row alive as long as its pid wasn't *known* dead — so a queued-but-not-started row, or a process of genuinely unverified liveness, could still show up. The canonical selector (`isRunningLiveSession`) now excludes `queued`/`closed`/`crashed` outright (still reachable via `--queued`/`--closed`/`--crashed`) and requires a real process row to positively verify its machine, a positive pid, and `pidAlive === true`; a cloud row stays active on its provider + task id alone. Every process-backed `--active --json` row now guarantees `machine`/`pid`/`pidAlive: true`, and the human CLI row and the menu bar's session detail both show the matching `machine:pid` (or `provider · taskId` for cloud) locator. Source: `apps/cli/src/commands/sessions.ts`, `apps/cli/src/lib/menubar/snapshot.ts`, `apps/cli/menubar/Sources/MenubarHelper/`.
|
|
49
|
+
|
|
50
|
+
- **`agents projects status` card scans faster: grouped warnings, a fleet health summary, and a truthful `dead` label (RUSH-2337).** On a busy fleet the card printed each host's git drift twice — once in the inline `fleet` table, then again as one 2-line block per host in the warnings footer (18 lines for 3 facts) — and read `dead 41 finished or lost (41 crashed)`, which contradicts itself when every dead session is a crash. Now: (1) the warnings footer **groups by root cause** — all behind hosts collapse to one warning listing each with its count (`4 hosts behind origin/main — mac-mini ↓172, yosemite-m2 ↓217, …`) under one shared remediation, dirty/missing the same, a lone host keeps its full sentence, grouped per probed path so two repos never merge (mirrors doctor's `emitGroup`); (2) a one-line **fleet health summary** (`6/13 clean · 4 behind · 4 dirty · 1 missing`) sits above the per-host table, which keeps its branch/drift detail; (3) the `dead` row names the status directly when singular (`dead 41 crashed`); (4) the fleet-wide rollup carries a `fleet snapshot · as of HH:MM` stamp; (5) `linear` shows a completion percent (`468/547 done (86%)`). Source: apps/cli/src/lib/project-probe.ts, apps/cli/src/lib/project-status.ts, apps/cli/src/commands/projects.ts, apps/cli/docs/11-projects.md.
|
|
51
|
+
|
|
3
52
|
## 1.22.27
|
|
4
53
|
|
|
5
54
|
- **`agents sessions --teams` groups sessions by team (RUSH-1997).** Instead of one flat list with a `[team/handle]` tag, `--teams` now prints a report grouped by team: each team names its spawner (the orchestrator session that created it) and spawn time, and every teammate row shows its mode + handle. Team-flagged spawns that carry no teammate `meta.json` — headless `agents run` sessions, or teammates whose team record aged out — sink into a trailing `(no team)` bucket, so a real `agents teams` teammate and a bare SDK spawn are never shown as the same thing. `--teams --flat`/`--tree` keep the plain inline table, and a search query keeps the interactive picker. `TeamOrigin` (also on `--teams --json`) now carries the teammate's `startedAt` spawn time and a `source` (`meta` = teammate, `entrypoint` = bare spawn). Source: `apps/cli/src/lib/session/team-filter.ts`, `apps/cli/src/commands/sessions.ts`.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<a href="https://github.com/phnx-labs/agents-cli"><img src="https://img.shields.io/badge/github-phnx--labs%2Fagents--cli-blue?style=flat-square" alt="github" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
-
**
|
|
14
|
+
**A framework for running a distributed agent factory.** Dispatch Claude, Codex, Antigravity, Grok, and more across your own machines, in parallel, on your existing subscriptions. Measure every run with `agents perf` / `agents insights`, fold what you learn back into `AGENTS.md` and skills, then put the loop on a schedule with routines and monitors. Spawn parallel teams in isolated terminals or dispatch to the cloud for a PR. Watch live state across the fleet, nudge stalled runs, and message agents mid-flight. Store secrets behind Touch ID, drive real browsers and Electron apps, and steer the whole fleet from a menu bar — all from one CLI.
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<a href="https://github.com/anthropics/claude-code" title="Claude Code"><img src="assets/harnesses/anthropic.svg" height="32" alt="Claude Code" /></a>
|
|
@@ -60,20 +60,25 @@ Source: [github.com/phnx-labs/agents-cli](https://github.com/phnx-labs/agents-cl
|
|
|
60
60
|
|
|
61
61
|
Also available as `ag` -- all commands work with both `agents` and `ag`.
|
|
62
62
|
|
|
63
|
-
- [
|
|
63
|
+
- [Factory loop](#factory-loop)
|
|
64
64
|
- [One config, every agent](#one-config-every-agent)
|
|
65
65
|
- [Run any agent](#run-any-agent)
|
|
66
66
|
- [Sessions across agents](#sessions-across-agents)
|
|
67
67
|
- [Control the fleet](#control-the-fleet)
|
|
68
68
|
- [Sync the fleet](#sync-the-fleet)
|
|
69
|
-
- [
|
|
69
|
+
- [Pin versions per project](#pin-versions-per-project)
|
|
70
|
+
- [Run open models through Claude Code](#run-open-models-through-claude-code-experimental)
|
|
71
|
+
- [Run on your own machines](#run-on-your-own-machines)
|
|
70
72
|
- [Teams](#teams)
|
|
71
73
|
- [Cloud](#cloud)
|
|
72
74
|
- [Workflows](#workflows)
|
|
75
|
+
- [Plugins](#plugins)
|
|
76
|
+
- [Make it yours](#make-it-yours)
|
|
73
77
|
- [Browser](#browser)
|
|
74
78
|
- [Secrets](#secrets)
|
|
75
79
|
- [Routines](#routines)
|
|
76
80
|
- [Monitors](#monitors)
|
|
81
|
+
- [Share](#share)
|
|
77
82
|
- [PTY](#pty)
|
|
78
83
|
- [Portable setup](#portable-setup)
|
|
79
84
|
- [Menu bar](#menu-bar)
|
|
@@ -84,34 +89,35 @@ Also available as `ag` -- all commands work with both `agents` and `ag`.
|
|
|
84
89
|
|
|
85
90
|
---
|
|
86
91
|
|
|
87
|
-
##
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
# This project needs claude@2.0.65 -- newer versions changed tool calling.
|
|
91
|
-
agents use claude@2.0.65 -p
|
|
92
|
-
|
|
93
|
-
# The monorepo uses codex@0.116.0 across the team.
|
|
94
|
-
agents use codex@0.116.0 -p
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
This creates an `agents.yaml` at the project root:
|
|
98
|
-
|
|
99
|
-
```yaml
|
|
100
|
-
# agents.yaml (commit this to your repo)
|
|
101
|
-
agents:
|
|
102
|
-
claude: "2.0.65"
|
|
103
|
-
codex: "0.116.0"
|
|
104
|
-
```
|
|
92
|
+
## Factory loop
|
|
105
93
|
|
|
106
|
-
|
|
94
|
+
The same loop whether it's one agent on your laptop or a fleet across a dozen machines: dispatch work, measure what happened, fold the lesson back into the harness, then put it on a schedule.
|
|
107
95
|
|
|
108
96
|
```bash
|
|
109
|
-
|
|
110
|
-
agents
|
|
111
|
-
agents add
|
|
112
|
-
agents
|
|
97
|
+
# Dispatch a team across the fleet -- each teammate in its own worktree
|
|
98
|
+
agents teams create checkout --devices yosemite-s0,yosemite-s1
|
|
99
|
+
agents teams add checkout claude "Owns: app/payments/*" --name payments
|
|
100
|
+
agents teams add checkout codex "Write tests for the new code" --name qa --after payments
|
|
101
|
+
agents teams start checkout --watch
|
|
102
|
+
|
|
103
|
+
# Measure what happened -- latency, friction, dead-weight skills
|
|
104
|
+
agents perf commands --days 7 # slowest CLI entrypoints
|
|
105
|
+
agents insights --since 30d # friction, harness comparison, ranked actions
|
|
106
|
+
|
|
107
|
+
# Fold the lesson back into the harness -- every agent picks it up next run
|
|
108
|
+
agents rules add ~/lessons/payments-review.md --agents claude
|
|
109
|
+
agents skills add ~/skills/payments-review --agents claude
|
|
110
|
+
|
|
111
|
+
# Put it on a schedule so it runs itself
|
|
112
|
+
agents routines add nightly-payments-audit \
|
|
113
|
+
--schedule "0 2 * * *" --agent claude --prompt "Audit payments/* for regressions"
|
|
114
|
+
|
|
115
|
+
# Steer the fleet this loop runs on from the menu bar
|
|
116
|
+
agents menubar setup
|
|
113
117
|
```
|
|
114
118
|
|
|
119
|
+
`agents perf` reads a disposable warehouse at `~/.agents/.cache/perf/perf.db` -- hook, command, and run timing rollups, deletable any time. `agents insights` (alias `agents sessions insights`) is deterministic and offline: it caches per-session facets, compares harnesses, and ranks actions by evidence count -- no model call unless you pass `--narrative`. Routines put any of this on a cron ([Routines](#routines)); monitors fire it on a change instead of a clock ([Monitors](#monitors)); the menu bar is the always-on control surface for the fleet these commands drive ([Menu bar](#menu-bar)).
|
|
120
|
+
|
|
115
121
|
---
|
|
116
122
|
|
|
117
123
|
## One config, every agent
|
|
@@ -464,6 +470,36 @@ See [docs/fleet.md](apps/cli/docs/fleet.md) for the manifest schema and reconcil
|
|
|
464
470
|
|
|
465
471
|
---
|
|
466
472
|
|
|
473
|
+
## Pin versions per project
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# This project needs claude@2.0.65 -- newer versions changed tool calling.
|
|
477
|
+
agents use claude@2.0.65 -p
|
|
478
|
+
|
|
479
|
+
# The monorepo uses codex@0.116.0 across the team.
|
|
480
|
+
agents use codex@0.116.0 -p
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
This creates an `agents.yaml` at the project root:
|
|
484
|
+
|
|
485
|
+
```yaml
|
|
486
|
+
# agents.yaml (commit this to your repo)
|
|
487
|
+
agents:
|
|
488
|
+
claude: "2.0.65"
|
|
489
|
+
codex: "0.116.0"
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Think `requirements.txt` for CLI coding agents, on steroids. A shim reads `agents.yaml` from the project root and routes `claude` / `codex` / `antigravity` / `grok` (and others) to the right version automatically. Each version gets its own isolated home -- switching backs up config and re-syncs resources.
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
agents add claude@2.0.65 # Install a specific version
|
|
496
|
+
agents add codex@latest # Install latest
|
|
497
|
+
agents add codex@oldest # Install the oldest published version
|
|
498
|
+
agents view # See everything installed
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
467
503
|
## Run open models through Claude Code (experimental)
|
|
468
504
|
|
|
469
505
|
> **Note:** Profiles are experimental, but available by default — no enable step needed.
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/exec.js
CHANGED
|
@@ -1795,7 +1795,7 @@ export function registerRunCommand(program) {
|
|
|
1795
1795
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
1796
1796
|
process.exit(resumeExit);
|
|
1797
1797
|
}
|
|
1798
|
-
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([
|
|
1798
|
+
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, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
1799
1799
|
import('../lib/exec.js'),
|
|
1800
1800
|
import('../lib/agents.js'),
|
|
1801
1801
|
import('../lib/profiles.js'),
|
|
@@ -2302,6 +2302,9 @@ export function registerRunCommand(program) {
|
|
|
2302
2302
|
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
2303
2303
|
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
2304
2304
|
let rotationResult = null;
|
|
2305
|
+
// Set when the zero-healthy path already announced a deliberate
|
|
2306
|
+
// launch-to-sign-in, so the login preflight below does not repeat it.
|
|
2307
|
+
let signInLaunch = false;
|
|
2305
2308
|
if (options.strategy && !explicitStrategy) {
|
|
2306
2309
|
console.error(chalk.red(`Invalid strategy: ${options.strategy}. Use ${RUN_STRATEGIES.join(', ')}.`));
|
|
2307
2310
|
process.exit(1);
|
|
@@ -2328,15 +2331,53 @@ export function registerRunCommand(program) {
|
|
|
2328
2331
|
try {
|
|
2329
2332
|
const resolved = await resolveRunVersion(agent, strategy, cwd);
|
|
2330
2333
|
if (resolved.exhausted) {
|
|
2331
|
-
//
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
2334
|
-
//
|
|
2335
|
-
//
|
|
2336
|
-
|
|
2337
|
-
|
|
2334
|
+
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2335
|
+
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
2336
|
+
//
|
|
2337
|
+
// - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
|
|
2338
|
+
// The old behavior warned "found no usable version; falling back to
|
|
2339
|
+
// defaults" and launched the pinned default anyway — the exact move
|
|
2340
|
+
// that loops a rotate into an exhausted account. Only a window reset
|
|
2341
|
+
// clears it. The message text is a contract the Factory watchdog
|
|
2342
|
+
// tail-detects; do not reword it.
|
|
2343
|
+
// - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
|
|
2344
|
+
// because the harness's own TUI is the login surface. So on a TTY we
|
|
2345
|
+
// carry the user into that login instead of erroring. Exiting here
|
|
2346
|
+
// made `agents run <agent>`, `agents run <agent>@`, and `agents use`
|
|
2347
|
+
// all dead-end with no reachable way to authenticate.
|
|
2348
|
+
const recoverable = signInRecoverableCandidates(resolved.exhausted);
|
|
2349
|
+
const { signInLaunchDecision } = await import('./run-account-picker.js');
|
|
2350
|
+
const decision = signInLaunchDecision({
|
|
2351
|
+
recoverable: recoverable.length,
|
|
2352
|
+
tty: isInteractiveTerminal(),
|
|
2353
|
+
json: options.json === true,
|
|
2354
|
+
});
|
|
2355
|
+
if (decision === 'launch') {
|
|
2356
|
+
const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
|
|
2357
|
+
const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
|
|
2358
|
+
// A cancelled prompt launches nothing — same contract as the
|
|
2359
|
+
// trailing-@ account picker above.
|
|
2360
|
+
if (!signInVersion)
|
|
2361
|
+
return;
|
|
2362
|
+
version = signInVersion;
|
|
2363
|
+
// We just told the user this account is logged out and why we're
|
|
2364
|
+
// launching it, so suppress the downstream login preflight — it
|
|
2365
|
+
// would print a second, near-identical "looks logged out" warning.
|
|
2366
|
+
signInLaunch = true;
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
|
|
2370
|
+
if (recoverable.length > 0) {
|
|
2371
|
+
// Off a TTY nobody can complete a login, so we still exit — but
|
|
2372
|
+
// name the actual fix rather than only offering --strategy pinned,
|
|
2373
|
+
// which would just pin the same unauthenticated account.
|
|
2374
|
+
const { loginHint } = await import('../lib/signin-badge.js');
|
|
2375
|
+
console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
|
|
2376
|
+
}
|
|
2377
|
+
process.exit(1);
|
|
2378
|
+
}
|
|
2338
2379
|
}
|
|
2339
|
-
if (resolved.version) {
|
|
2380
|
+
else if (resolved.version) {
|
|
2340
2381
|
version = resolved.version;
|
|
2341
2382
|
rotationResult = resolved.rotation;
|
|
2342
2383
|
if (resolved.rotation && !options.quiet) {
|
|
@@ -2428,7 +2469,10 @@ export function registerRunCommand(program) {
|
|
|
2428
2469
|
json: options.json,
|
|
2429
2470
|
quiet: options.quiet,
|
|
2430
2471
|
authCheckDisabled: options.authCheck === false || process.env.AGENTS_NO_AUTH_CHECK === '1',
|
|
2431
|
-
|
|
2472
|
+
// `signInLaunch` means the zero-healthy path already reported this exact
|
|
2473
|
+
// account as logged out and named the login command, so re-probing here
|
|
2474
|
+
// only prints a second, near-identical warning.
|
|
2475
|
+
rotated: !!rotationResult || accountPickerRequested || signInLaunch,
|
|
2432
2476
|
});
|
|
2433
2477
|
if (preflight) {
|
|
2434
2478
|
try {
|
package/dist/commands/hosts.js
CHANGED
|
@@ -13,7 +13,7 @@ import { isInteractiveTerminal } from './utils.js';
|
|
|
13
13
|
import { assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
14
14
|
import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
|
|
15
15
|
import { getDevice } from '../lib/devices/registry.js';
|
|
16
|
-
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
16
|
+
import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
|
|
17
17
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
18
18
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
19
19
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
@@ -190,14 +190,15 @@ async function doCheck(name) {
|
|
|
190
190
|
const target = sshTargetFor(host);
|
|
191
191
|
const os = host.os ?? resolveRemoteOsSync(name);
|
|
192
192
|
process.stdout.write(`Probing ${chalk.cyan(name)} (${target})… `);
|
|
193
|
-
const
|
|
193
|
+
const identityArgs = hostIdentityArgs(host);
|
|
194
|
+
const probe = probeHost(target, os, identityArgs);
|
|
194
195
|
if (!probe.reachable) {
|
|
195
196
|
console.log(chalk.red('unreachable'));
|
|
196
197
|
process.exitCode = 1;
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
200
|
console.log(chalk.green('reachable') + chalk.gray(probe.os ? ` · ${probe.os}` : ''));
|
|
200
|
-
const ver = remoteAgentsVersion(target, probe.os ?? os);
|
|
201
|
+
const ver = remoteAgentsVersion(target, probe.os ?? os, identityArgs);
|
|
201
202
|
console.log(` agents-cli: ${ver ? chalk.green(ver) : chalk.yellow('not installed')}`);
|
|
202
203
|
}
|
|
203
204
|
async function doRemove(name) {
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -23,6 +23,16 @@ interface PerfGlobalOpts {
|
|
|
23
23
|
}
|
|
24
24
|
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
25
25
|
export declare function asHookRows(rows: PerfAggregateRow[]): HookProfileRow[];
|
|
26
|
+
/**
|
|
27
|
+
* `err:12% block:40% to:4%` when any rate is present, else ''.
|
|
28
|
+
* Exit 2 (intentional deny) is `block:`, not `err:` — so a deny-by-design
|
|
29
|
+
* guard no longer reads as a crashing hook in the table.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatRateColumn(r: {
|
|
32
|
+
errorRate?: number;
|
|
33
|
+
blockRate?: number;
|
|
34
|
+
timeoutRate?: number;
|
|
35
|
+
}): string;
|
|
26
36
|
export declare function renderHookTable(rows: HookProfileRow[], warnMs: number): void;
|
|
27
37
|
/**
|
|
28
38
|
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
package/dist/commands/perf.js
CHANGED
|
@@ -43,6 +43,8 @@ export function asHookRows(rows) {
|
|
|
43
43
|
cacheMissPct: r.cacheMissPct ?? 0,
|
|
44
44
|
errorCount: r.errorCount ?? 0,
|
|
45
45
|
errorRate: r.errorRate,
|
|
46
|
+
blockCount: r.blockCount ?? 0,
|
|
47
|
+
blockRate: r.blockRate,
|
|
46
48
|
timeoutRate: r.timeoutRate,
|
|
47
49
|
project: r.project,
|
|
48
50
|
}));
|
|
@@ -57,11 +59,17 @@ function printTable(headers, widths, lines, slowFlags) {
|
|
|
57
59
|
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
+
/**
|
|
63
|
+
* `err:12% block:40% to:4%` when any rate is present, else ''.
|
|
64
|
+
* Exit 2 (intentional deny) is `block:`, not `err:` — so a deny-by-design
|
|
65
|
+
* guard no longer reads as a crashing hook in the table.
|
|
66
|
+
*/
|
|
67
|
+
export function formatRateColumn(r) {
|
|
62
68
|
const parts = [];
|
|
63
69
|
if (r.errorRate)
|
|
64
70
|
parts.push(`err:${Math.round(r.errorRate * 100)}%`);
|
|
71
|
+
if (r.blockRate)
|
|
72
|
+
parts.push(`block:${Math.round(r.blockRate * 100)}%`);
|
|
65
73
|
if (r.timeoutRate)
|
|
66
74
|
parts.push(`to:${Math.round(r.timeoutRate * 100)}%`);
|
|
67
75
|
return parts.join(' ');
|
|
@@ -73,7 +81,7 @@ export function renderHookTable(rows, warnMs) {
|
|
|
73
81
|
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
|
-
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate:
|
|
84
|
+
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate: 22 };
|
|
77
85
|
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
78
86
|
const header = [
|
|
79
87
|
pad('HOOK', widths.hook),
|
|
@@ -84,7 +92,7 @@ export function renderHookTable(rows, warnMs) {
|
|
|
84
92
|
pad('MEAN', widths.mean),
|
|
85
93
|
pad('MAX', widths.max),
|
|
86
94
|
pad('CACHE', widths.cache),
|
|
87
|
-
pad('ERR/
|
|
95
|
+
pad('ERR/BLOCK/TO', widths.rate),
|
|
88
96
|
].join(' ');
|
|
89
97
|
console.log(chalk.bold(header));
|
|
90
98
|
console.log(chalk.gray('─'.repeat(header.length)));
|
|
@@ -112,8 +120,8 @@ function renderLabelTable(title, rows, warnMs, limit) {
|
|
|
112
120
|
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
115
|
-
const widths = [40, 5, 7, 7, 7, 7, 7,
|
|
116
|
-
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/
|
|
123
|
+
const widths = [40, 5, 7, 7, 7, 7, 7, 22];
|
|
124
|
+
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/BLOCK/TO'], widths, sliced.map((r) => [
|
|
117
125
|
r.label,
|
|
118
126
|
String(r.n),
|
|
119
127
|
formatMs(r.p50Ms),
|
|
@@ -20,9 +20,9 @@ import { machineId } from '../lib/machine-id.js';
|
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
22
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
23
|
-
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
23
|
+
import { formatFleetSummary, formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
24
24
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, validateProjectDef, } from '../lib/projects.js';
|
|
25
|
-
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
25
|
+
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, formatDeadSummary, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
26
26
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
27
27
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
28
28
|
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
@@ -302,8 +302,7 @@ warnWorkspaces = []) {
|
|
|
302
302
|
if (split.dead > 0) {
|
|
303
303
|
// Wreckage is worth a number of its own — 19 crashed sessions is a thing to
|
|
304
304
|
// go fix, not a throughput signal to fold into the headline.
|
|
305
|
-
|
|
306
|
-
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${deadDetail})`)}`);
|
|
305
|
+
console.log(` ${chalk.dim('dead')} ${formatDeadSummary(split)}`);
|
|
307
306
|
}
|
|
308
307
|
// Live only, grouped by host when machine stamps exist so "who is on which
|
|
309
308
|
// box" is visible. Flat collapse hid that when harness×status matched across hosts.
|
|
@@ -327,7 +326,8 @@ warnWorkspaces = []) {
|
|
|
327
326
|
if (ships.length)
|
|
328
327
|
console.log(` ${chalk.dim('ships')} ${ships.join(' · ')}`);
|
|
329
328
|
if (linear) {
|
|
330
|
-
|
|
329
|
+
const pct = linear.total > 0 ? ` ${chalk.dim(`(${Math.round((linear.done / linear.total) * 100)}%)`)}` : '';
|
|
330
|
+
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done${pct} · ${linear.inProgress} in progress`);
|
|
331
331
|
}
|
|
332
332
|
for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
|
|
333
333
|
console.log(line);
|
|
@@ -346,13 +346,18 @@ warnWorkspaces = []) {
|
|
|
346
346
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
347
347
|
}
|
|
348
348
|
if (fleet) {
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
349
|
+
const table = formatFleetWorkspaces(fleet);
|
|
350
|
+
if (table.length === 0) {
|
|
351
351
|
console.log(` ${chalk.dim('fleet')} ${chalk.gray('no workspace paths (set root or repos[].path)')}`);
|
|
352
352
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
else {
|
|
354
|
+
// A compact health line first (scan without reading every host), then the
|
|
355
|
+
// full per-host table under it. The grouped footer carries the actionable
|
|
356
|
+
// subset; this line + the table carry the whole picture.
|
|
357
|
+
[formatFleetSummary(fleet), ...table].forEach((line, i) => {
|
|
358
|
+
console.log(` ${chalk.dim((i === 0 ? 'fleet' : '').padEnd(5))} ${line}`);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
356
361
|
}
|
|
357
362
|
if (remote?.artifacts) {
|
|
358
363
|
const last = remote.lastArtifact ? ` ${chalk.dim(`· last: ${remote.lastArtifact}`)}` : '';
|
|
@@ -639,6 +644,15 @@ export function registerProjectsCommands(program) {
|
|
|
639
644
|
}
|
|
640
645
|
// Compact rollup shows the next milestone; `view` shows every declared one.
|
|
641
646
|
const milestoneLimit = detail ? Number.POSITIVE_INFINITY : 1;
|
|
647
|
+
// Stamp the fleet-wide rollup with when it was taken, so a scrollback isn't
|
|
648
|
+
// mistaken for a live snapshot. A single named `view` skips it (one card, and
|
|
649
|
+
// its own detail makes the freshness obvious).
|
|
650
|
+
if (!detail) {
|
|
651
|
+
const t = new Date(nowMs);
|
|
652
|
+
const hhmm = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
|
|
653
|
+
console.log(chalk.dim(`fleet snapshot · as of ${hhmm}`));
|
|
654
|
+
console.log('');
|
|
655
|
+
}
|
|
642
656
|
for (const d of defs) {
|
|
643
657
|
renderCard(d, roll.get(d.name), remote.get(d.name), fleetFor(d), linear.get(d.name), nowMs, milestoneLimit, focus.get(d.name) ?? [], detail,
|
|
644
658
|
// Always feed workspace rows into the warnings footer so behind/dirty
|
|
@@ -1560,8 +1560,17 @@ export function registerRoutinesCommands(program) {
|
|
|
1560
1560
|
const unknown = [...selectedSet].filter((device) => !all.includes(device));
|
|
1561
1561
|
if (unknown.length > 0)
|
|
1562
1562
|
throw new Error(`Unknown device${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
1563
|
+
// --set/--clear fan out to every registered device so peers outside the
|
|
1564
|
+
// new set get paused. An asleep/offline peer must not abort the whole
|
|
1565
|
+
// pin: the target may already be applied, and an unreachable box cannot
|
|
1566
|
+
// be running the routine (it picks up the enabled set on next sync).
|
|
1567
|
+
// Exit non-zero only when a *selected* device could not be enabled
|
|
1568
|
+
// (github.com/phnx-labs/agents-cli#2118).
|
|
1569
|
+
const skipped = [];
|
|
1570
|
+
const failedTargets = [];
|
|
1563
1571
|
for (const device of all) {
|
|
1564
1572
|
const action = selectedSet.has(device) ? 'resume' : 'pause';
|
|
1573
|
+
const isTarget = selectedSet.has(device);
|
|
1565
1574
|
if (device === normalizeHost(machineId())) {
|
|
1566
1575
|
setJobEnabled(name, action === 'resume');
|
|
1567
1576
|
if (isDaemonRunning())
|
|
@@ -1570,12 +1579,30 @@ export function registerRoutinesCommands(program) {
|
|
|
1570
1579
|
}
|
|
1571
1580
|
const launch = getCliLaunch(['routines', action, name, '--host', device]);
|
|
1572
1581
|
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
1573
|
-
if ((result.status ?? 1) !== 0)
|
|
1574
|
-
|
|
1582
|
+
if ((result.status ?? 1) !== 0) {
|
|
1583
|
+
if (isTarget) {
|
|
1584
|
+
failedTargets.push(device);
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
skipped.push({ device, action });
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1575
1590
|
}
|
|
1591
|
+
for (const s of skipped) {
|
|
1592
|
+
console.log(chalk.yellow(`Skipped ${s.action} of '${name}' on ${s.device} (unreachable; will sync when online)`));
|
|
1593
|
+
}
|
|
1594
|
+
if (failedTargets.length > 0) {
|
|
1595
|
+
const offlineNote = skipped.length > 0
|
|
1596
|
+
? ` (also skipped offline: ${skipped.map((s) => s.device).join(', ')})`
|
|
1597
|
+
: '';
|
|
1598
|
+
throw new Error(`Could not enable '${name}' on: ${failedTargets.join(', ')}${offlineNote}`);
|
|
1599
|
+
}
|
|
1600
|
+
const offlineNote = skipped.length > 0
|
|
1601
|
+
? ` (${skipped.length} offline device${skipped.length === 1 ? '' : 's'} skipped)`
|
|
1602
|
+
: '';
|
|
1576
1603
|
console.log(chalk.green(selected.length === 0
|
|
1577
|
-
? `Routine '${name}' disabled on every registered device`
|
|
1578
|
-
: `Routine '${name}' enabled on: ${selected.join(', ')}`));
|
|
1604
|
+
? `Routine '${name}' disabled on every registered device${offlineNote}`
|
|
1605
|
+
: `Routine '${name}' enabled on: ${selected.join(', ')}${offlineNote}`));
|
|
1579
1606
|
};
|
|
1580
1607
|
if (options.clear) {
|
|
1581
1608
|
await applyDevices([]);
|
|
@@ -4,11 +4,48 @@ export interface RunAccountChoice {
|
|
|
4
4
|
name: string;
|
|
5
5
|
value: string;
|
|
6
6
|
disabled?: string;
|
|
7
|
+
/** Can serve a run right now: signed in, authenticated, and under quota. */
|
|
7
8
|
ready: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Selectable, but picking it launches the harness so you can authenticate
|
|
11
|
+
* first (RUSH-2334). Mutually exclusive with `ready`; never `disabled`.
|
|
12
|
+
*/
|
|
13
|
+
signInRequired: boolean;
|
|
8
14
|
}
|
|
9
15
|
/** Human-readable remaining capacity for every window the provider exposes. */
|
|
10
16
|
export declare function formatAccountLimits(candidate: RotateCandidate): string;
|
|
11
17
|
/** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
|
|
12
18
|
export declare function buildRunAccountChoices(candidates: RotateCandidate[], globalDefault: string | null): RunAccountChoice[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
21
|
+
* failing loud. Three inputs, all of which have to hold:
|
|
22
|
+
*
|
|
23
|
+
* - `recoverable` — at least one excluded account is only auth-blocked. An
|
|
24
|
+
* all-throttled set is never launched (RUSH-2132): only a window reset clears it.
|
|
25
|
+
* - `tty` — a login needs a human present; off a TTY nobody can complete one.
|
|
26
|
+
* - `json` — `--json` marks a MACHINE consumer, which must never be handed a
|
|
27
|
+
* picker or dropped into a login TUI. This mirrors the canonical
|
|
28
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`; a `--json` caller
|
|
29
|
+
* gets the parseable fail-loud error instead.
|
|
30
|
+
*/
|
|
31
|
+
export declare function signInLaunchDecision(input: {
|
|
32
|
+
recoverable: number;
|
|
33
|
+
tty: boolean;
|
|
34
|
+
json: boolean;
|
|
35
|
+
}): 'launch' | 'fail-loud';
|
|
36
|
+
/**
|
|
37
|
+
* Choose which installed version to launch so the user can authenticate, when a
|
|
38
|
+
* strategy found zero healthy accounts but at least one is merely signed out
|
|
39
|
+
* (RUSH-2334). Returns the version to launch, or null if the user cancelled.
|
|
40
|
+
*
|
|
41
|
+
* A single candidate does NOT prompt — a one-item picker is pure noise, and the
|
|
42
|
+
* only thing to decide has one answer. Several candidates fall through to the
|
|
43
|
+
* normal account picker, which shows every account with its state so the choice
|
|
44
|
+
* is informed (throttled rows stay disabled there).
|
|
45
|
+
*
|
|
46
|
+
* Callers MUST have already confirmed an interactive terminal: off a TTY there
|
|
47
|
+
* is nobody to complete the login, and the run should fail loud instead.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pickSignInLaunchVersion(agent: AgentId, recoverable: RotateCandidate[], quiet?: boolean): Promise<string | null>;
|
|
13
50
|
/** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
|
|
14
51
|
export declare function pickRunAccountCandidate(agent: AgentId): Promise<RotateCandidate | null>;
|