@phnx-labs/agents-cli 1.22.49 → 1.22.50
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 +102 -0
- package/README.md +10 -0
- package/dist/commands/accounts.js +1 -13
- package/dist/commands/daemon-test-harness.d.ts +42 -0
- package/dist/commands/daemon-test-harness.js +90 -0
- package/dist/commands/device-config-test-harness.d.ts +11 -0
- package/dist/commands/device-config-test-harness.js +59 -0
- package/dist/commands/exec.js +24 -1
- package/dist/commands/share.js +9 -4
- package/dist/commands/teams.d.ts +12 -0
- package/dist/commands/teams.js +44 -67
- package/dist/commands/traces.js +4 -0
- package/dist/lib/account-provider-registry.d.ts +9 -0
- package/dist/lib/account-provider-registry.js +20 -0
- package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
- package/dist/lib/accounting/account-pool-collect.js +71 -0
- package/dist/lib/accounting/account-pool.d.ts +46 -0
- package/dist/lib/accounting/account-pool.js +33 -0
- package/dist/lib/accounting/capacity.d.ts +24 -0
- package/dist/lib/accounting/capacity.js +30 -0
- package/dist/lib/accounting/rotate.d.ts +11 -18
- package/dist/lib/accounting/rotate.js +6 -24
- package/dist/lib/installations/store.js +6 -3
- package/dist/lib/installations/versions.js +48 -46
- package/dist/lib/share/publish.d.ts +8 -3
- package/dist/lib/share/publish.js +6 -3
- package/dist/lib/share/worker-template.js +239 -22
- package/dist/lib/teams/agents.d.ts +14 -12
- package/dist/lib/teams/agents.js +188 -73
- package/dist/lib/teams/api.d.ts +6 -1
- package/dist/lib/teams/api.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/teams/scheduler.js +26 -8
- package/dist/lib/traces/sync.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,107 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.50
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Teams now persist observed teammate failure evidence and keep independent DAG branches moving.** Placement, local/remote launch, cloud dispatch, dependency, and process-exit failures carry a stable code, sanitized message, exit code, retryability, and observation time in `teams status` text/JSON. A runnable node with a durable placement failure fails independently; a node blocked only by pool capacity or load stays pending with retryable evidence so a later wave can launch it. Descendants name failed or missing `--after` blockers instead of spinning until `--max-waves`. `teams start` (no `--watch`) reports teammates that failed during the wave in a `Failed this wave` section (JSON: `failed[]` with evidence) and exits non-zero when a wave produced only failures. Because a failed launch now keeps its record (evidence) instead of deleting it, re-running the identical `teams add --name <name>` after a failure requires `teams remove <team> <name>` first.
|
|
8
|
+
Successfully resuming a failed teammate clears the prior attempt's evidence as
|
|
9
|
+
the replacement enters `running`; a replacement that fails to launch restores
|
|
10
|
+
the original terminal state and evidence intact.
|
|
11
|
+
|
|
12
|
+
- **Register the Phoenix session with Prix after sync (PHNX-3257).** A successful
|
|
13
|
+
(non-dry-run) `agents traces sync` now fire-and-forget POSTs the Phoenix bearer
|
|
14
|
+
to `api.prix.dev/api/v1/traces/link`, so the Prix web console can mint a token
|
|
15
|
+
and serve live trajectories instead of fixtures. Scoped to the managed Phoenix
|
|
16
|
+
backend only — a BYO/self-hosted `AGENTS_TRACES_WRITE_TOKEN` is never sent to
|
|
17
|
+
Prix. Source: `cli/src/lib/traces/sync.ts`.
|
|
18
|
+
|
|
19
|
+
- **Ship `traces-daily-sync` routine (PHNX-3258).** Daily `agents traces sync` at 02:00 UTC
|
|
20
|
+
to accumulate `bucketHistory` for drift signals; requires a device pin
|
|
21
|
+
(`agents routines devices traces-daily-sync --set <name>`) before it fires.
|
|
22
|
+
Source: `cli/routines/traces-daily-sync.yml`.
|
|
23
|
+
|
|
24
|
+
- **`--strategy balanced` now spreads across your provider accounts, not just
|
|
25
|
+
native logins (RUSH-3182).** A setup-token or API-key account added with
|
|
26
|
+
`agents accounts add` is now a first-class balancing candidate for every harness
|
|
27
|
+
its provider can authenticate — claude, codex, grok, cursor, opencode (a harness
|
|
28
|
+
with only a native login and no provider adapter, like kimi, keeps balancing its
|
|
29
|
+
native logins). Before, balanced only rotated across accounts that sat in a version
|
|
30
|
+
home, so a worker's shared setup-tokens never participated and `--account`
|
|
31
|
+
couldn't select them. The run path folds those provider accounts into the
|
|
32
|
+
candidate list and injects the picked one through the existing `--account` path
|
|
33
|
+
(a setup-token still authenticates via `CLAUDE_CODE_OAUTH_TOKEN`); the other
|
|
34
|
+
candidate consumers — the watchdog, session recovery, teams placement — keep the
|
|
35
|
+
native-only list, so nothing else changes. On a box dominated by verified native
|
|
36
|
+
logins a usage-less provider account is deprioritized until the daemon fetches
|
|
37
|
+
its usage; on a worker (setup-token majority) it participates immediately.
|
|
38
|
+
Source: `cli/src/lib/accounting/account-pool.ts`,
|
|
39
|
+
`cli/src/lib/accounting/account-pool-collect.ts`,
|
|
40
|
+
`cli/src/lib/accounting/rotate.ts`, `cli/src/commands/exec.ts`.
|
|
41
|
+
|
|
42
|
+
- **The attestation producer skips the helper manifest by default too (RUSH-3216).**
|
|
43
|
+
`release.sh` gained `--with-helpers` so an ordinary release does no helper work, but
|
|
44
|
+
`release-attestation-produce.sh` kept the same unconditional
|
|
45
|
+
`release-manifest.sh` verification — so the coupling survived one step upstream. Found
|
|
46
|
+
live: a one-line **comment** fix in `native/computer-mac/scripts/build.sh` (an
|
|
47
|
+
`apps/cli/` → `cli/` path in prose) changed that helper's input digest and aborted an
|
|
48
|
+
otherwise-clean 1.22.49 attestation, for a helper the tarball no longer ships and the
|
|
49
|
+
CLI resolves from its own tag. The producer now takes the same `--with-helpers` flag,
|
|
50
|
+
default off. Source: `cli/scripts/release-attestation-produce.sh`.
|
|
51
|
+
|
|
52
|
+
- **`scripts/publish-computer-win.sh` — a publish path for the Windows helper (RUSH-3228).**
|
|
53
|
+
Its release now triggers on `computer-win/v<x.y.z>` rather than the CLI's `v*` tag, but
|
|
54
|
+
nothing in the repo cut such a tag, so the trigger would have been dead — trading a
|
|
55
|
+
wasteful 165 MB rebuild on every CLI release for no rebuild at all. The tag *is* the
|
|
56
|
+
publish action (`release-exe` builds, smokes on a real windows-latest runner, and
|
|
57
|
+
uploads the exe + sha256), so a mis-shaped tag is a silent no-op: the script refuses a
|
|
58
|
+
`v`-prefixed or non-semver version, refuses an existing tag because the upload uses
|
|
59
|
+
`--clobber` and an installed CLI may already pin it, and is dry-run by default.
|
|
60
|
+
Symmetric with `publish-computer-helper-mac.sh`. Source: `cli/scripts/publish-computer-win.sh`.
|
|
61
|
+
|
|
62
|
+
- **`scripts/test.sh --shard <n>` — fan the suite across n fleet workers (RUSH-3230).**
|
|
63
|
+
Uses vitest's own `--shard=i/n`, drawing workers from the same auto pool a single
|
|
64
|
+
`--device auto` run uses, so `role=worker`/`role=personal` marks govern the fan-out too.
|
|
65
|
+
This is the change that moves release time, and the reason is arithmetic: a measured full
|
|
66
|
+
run is 3,079s of CPU at 11.5× parallelism on one box, so wall equals CPU/workers (269s) —
|
|
67
|
+
the suite is **throughput-bound**, not bound by any single slow file. Adding boxes divides
|
|
68
|
+
the CPU: 3 ≈ 93s, 6 ≈ 47s, 9 ≈ 31s. Shards run concurrently and every one is waited on
|
|
69
|
+
before reporting, so a failure in one does not hide the others. Requires `agents` ≥ 1.22.49
|
|
70
|
+
for `devices pick --json`; an older CLI fails naming the version and the fix rather than
|
|
71
|
+
passing through a commander error. Source: `cli/scripts/test.sh`.
|
|
72
|
+
- **`scripts/test.sh --devices a,b,c` — name the shard workers explicitly (RUSH-3230).**
|
|
73
|
+
Pins the fan-out to known-idle boxes instead of auto-picking, and skips the
|
|
74
|
+
`devices pick --json` dependency, so sharding also works from a machine whose installed
|
|
75
|
+
CLI predates 1.22.49. Source: `cli/scripts/test.sh`.
|
|
76
|
+
- **`--shard` fails loud on a count below 2 and on a conflicting target flag (RUSH-3230).**
|
|
77
|
+
`--shard 0` previously passed the numeric check, ran zero shards, and still printed
|
|
78
|
+
`All 0 shards passed.` with exit 0 — a false green that reported success having run no
|
|
79
|
+
tests. It now requires at least 2 (a single worker is `--device auto`). Separately, `MODE`
|
|
80
|
+
was last-write-wins with no cross-flag validation, so `--shard 6 --device box` silently
|
|
81
|
+
dropped one of the two purely on argument order; conflicting target flags now die naming
|
|
82
|
+
both. Source: `cli/scripts/test.sh`.
|
|
83
|
+
- **`--devices` obeys the same 2-worker floor, and is validated before prerequisites (RUSH-3230).**
|
|
84
|
+
`--devices onebox` derived the shard count from the list length and skipped the floor
|
|
85
|
+
entirely, running a one-shard fan-out. The list is now resolved and checked immediately
|
|
86
|
+
after argument parsing rather than inside the dispatch branch, so a bad invocation reports
|
|
87
|
+
its own problem instead of dying on a missing `rsync` first. Source: `cli/scripts/test.sh`.
|
|
88
|
+
|
|
89
|
+
- **Split `ssh.device-config.test.ts`, the second-slowest file (RUSH-3230).** 18 subprocess
|
|
90
|
+
tests in one file at ~44s locally (151s on a loaded worker) — 8.4s *per test*. Now three
|
|
91
|
+
files along its existing `describe` boundaries (per-device config / fleet-wide defaults +
|
|
92
|
+
role + describe / retired-subcommand tombstones) over a shared
|
|
93
|
+
`device-config-test-harness.ts`: **45s → 19s wall, all 18 tests still passing**, none
|
|
94
|
+
changed, skipped, or dropped. Source: `cli/src/commands/device-config-test-harness.ts`.
|
|
95
|
+
|
|
96
|
+
- **Split `daemon.test.ts`, the suite's floor (RUSH-3230).** It was 35 subprocess tests in
|
|
97
|
+
one file at ~53s locally (159s on a loaded worker) — the slowest file in the repo, and
|
|
98
|
+
therefore the whole suite's floor, because vitest parallelises across *files* and runs
|
|
99
|
+
one file's tests sequentially in a single worker. Now three files by theme (command
|
|
100
|
+
surface / services + webhooks / doctor + logs) over a shared
|
|
101
|
+
`daemon-test-harness.ts`, so they run concurrently: **53.5s → 26s wall, all 35 tests
|
|
102
|
+
still passing**. No test was changed, skipped, or dropped. Source:
|
|
103
|
+
`cli/src/commands/daemon-test-harness.ts`.
|
|
104
|
+
|
|
3
105
|
## 1.22.49
|
|
4
106
|
|
|
5
107
|
- **Check CLI benchmark numbers into `docs/benchmarks.md` (RUSH-2385).** Commander-bootstrap and audit-hook means from the yosemite-s1 vitest bench (PR #2349) plus pointers to OPT-01/OPT-02 live in git. Linear is not the ledger. Source: `cli/docs/benchmarks.md`.
|
package/README.md
CHANGED
|
@@ -741,6 +741,16 @@ Teammates run detached -- close your terminal, they keep working. Check in with
|
|
|
741
741
|
|
|
742
742
|
Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/observability.md](cli/docs/observability.md).
|
|
743
743
|
|
|
744
|
+
Placement, spawn, dependency, and non-zero-exit failures are persisted as sanitized
|
|
745
|
+
evidence on the teammate record and shown by `teams status`. A failed root does not stop
|
|
746
|
+
independent DAG branches; descendants name the failed or missing `--after` dependency
|
|
747
|
+
that blocked them instead of remaining pending indefinitely. Capacity- or load-blocked
|
|
748
|
+
placement stays pending with retryable evidence and is reconsidered on the next wave.
|
|
749
|
+
`teams start` reports
|
|
750
|
+
teammates that failed during the wave (`Failed this wave` / JSON `failed[]`) and exits
|
|
751
|
+
non-zero when a wave produced only failures. A failed teammate keeps its record as
|
|
752
|
+
evidence, so re-adding the same name needs `teams remove <team> <name>` first.
|
|
753
|
+
|
|
744
754
|
---
|
|
745
755
|
|
|
746
756
|
## Cloud
|
|
@@ -18,7 +18,7 @@ import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
|
18
18
|
import { runDevicesAccounts } from './ssh.js';
|
|
19
19
|
import { discoverNativeAccounts } from '../lib/account-catalog.js';
|
|
20
20
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
21
|
-
import { getAccountProvider, listAccountProviders } from '../lib/account-provider-registry.js';
|
|
21
|
+
import { getAccountProvider, listAccountProviders, providerAuthenticatesHarness } from '../lib/account-provider-registry.js';
|
|
22
22
|
import { accountBindings, addAccount, addNativeAccount, bindAccount, findAccount, findUnifiedAccount, inspectAccount, labelNativeAccount, listNativeAccounts, readAccountRegistry, removeAccount, renameAccount, setAccountSecret, unbindAccount } from '../lib/account-registry.js';
|
|
23
23
|
function cleanCommandError(command, err) {
|
|
24
24
|
command.error(err instanceof Error ? err.message : String(err), { exitCode: 1, code: 'accounts.error' });
|
|
@@ -288,18 +288,6 @@ async function pickLabelIdentity(agent, identities) {
|
|
|
288
288
|
throw err;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
-
function providerAuthenticatesHarness(provider, auth, agent) {
|
|
292
|
-
try {
|
|
293
|
-
getAccountProvider(provider).envFor(agent, auth);
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
298
|
-
if (message.includes('cannot authenticate'))
|
|
299
|
-
return false;
|
|
300
|
-
throw err;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
291
|
/** Every account (native + provider) registered/usable for this harness, oldest-first by name. */
|
|
304
292
|
function accountsForHarness(agent) {
|
|
305
293
|
const meta = readMeta();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared harness for the `agents daemon` CLI subprocess suites (RUSH-2354).
|
|
3
|
+
*
|
|
4
|
+
* Every test spawns the real CLI against an isolated mkdtemp HOME with no
|
|
5
|
+
* daemon running — no mocks. Modeled on routines.test.ts.
|
|
6
|
+
*
|
|
7
|
+
* EXTRACTED so the suite can live in several files. `daemon.test.ts` was 35
|
|
8
|
+
* tests in ONE file at 159s — the slowest file in the repo and therefore the
|
|
9
|
+
* SUITE'S FLOOR, because vitest parallelises across files and runs the tests
|
|
10
|
+
* inside one file sequentially in a single worker. Splitting the tests across
|
|
11
|
+
* files lets them run concurrently; sharing the harness is what makes that
|
|
12
|
+
* possible without duplicating the spawn plumbing.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync, type ChildProcess } from 'child_process';
|
|
15
|
+
export declare const REPO_ROOT: string;
|
|
16
|
+
export declare const TSX_IMPORT: string;
|
|
17
|
+
export declare const CLI_ENTRYPOINT: string;
|
|
18
|
+
/**
|
|
19
|
+
* win32: subprocess CLI + process-group signals / path spawn assumptions
|
|
20
|
+
* (RUSH-2215). Exported as a PREDICATE, not as a pre-bound `describe.skip` —
|
|
21
|
+
* vitest's suite type is not nameable across a module boundary (TS4023), so
|
|
22
|
+
* each suite builds its own `describe` from this.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DAEMON_TESTS_SUPPORTED: boolean;
|
|
25
|
+
/** Provision an isolated HOME with just enough scaffolding for the CLI to boot. */
|
|
26
|
+
export declare function makeHome(): string;
|
|
27
|
+
/** Run `agents daemon <args>` against an isolated HOME — no daemon process ever started. */
|
|
28
|
+
export declare function run(home: string, args: string[]): ReturnType<typeof spawnSync>;
|
|
29
|
+
/**
|
|
30
|
+
* Spawn a real, long-lived process whose command line ends in `__daemon-run`
|
|
31
|
+
* (so `isDaemonRunProcess`'s `ps` check accepts it — see
|
|
32
|
+
* `lib/daemon.test.ts`'s "reaps a live __daemon-run registrant" test, same
|
|
33
|
+
* technique) and register it in `home`'s OWN instance registry, exactly the
|
|
34
|
+
* marker `registerDaemonInstance` would write. A real live process, not a
|
|
35
|
+
* mock — `agents daemon status` reads it through the actual registry +
|
|
36
|
+
* `ps`-liveness path, the same one the reaper and `stopDaemon`'s postcondition
|
|
37
|
+
* use.
|
|
38
|
+
*/
|
|
39
|
+
export declare function spawnFakeRegisteredDaemon(home: string): Promise<ChildProcess>;
|
|
40
|
+
/** Register a pid in `home`'s instance registry — the scope stale/duplicate reporting uses. */
|
|
41
|
+
export declare function registerInstance(home: string, pid: number): void;
|
|
42
|
+
export declare function killFakeDaemon(child: ChildProcess): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared harness for the `agents daemon` CLI subprocess suites (RUSH-2354).
|
|
3
|
+
*
|
|
4
|
+
* Every test spawns the real CLI against an isolated mkdtemp HOME with no
|
|
5
|
+
* daemon running — no mocks. Modeled on routines.test.ts.
|
|
6
|
+
*
|
|
7
|
+
* EXTRACTED so the suite can live in several files. `daemon.test.ts` was 35
|
|
8
|
+
* tests in ONE file at 159s — the slowest file in the repo and therefore the
|
|
9
|
+
* SUITE'S FLOOR, because vitest parallelises across files and runs the tests
|
|
10
|
+
* inside one file sequentially in a single worker. Splitting the tests across
|
|
11
|
+
* files lets them run concurrently; sharing the harness is what makes that
|
|
12
|
+
* possible without duplicating the spawn plumbing.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync, spawn } from 'child_process';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
export const TSX_IMPORT = pathToFileURL(require.resolve('tsx')).href;
|
|
23
|
+
export const CLI_ENTRYPOINT = path.join(REPO_ROOT, 'src', 'index.ts');
|
|
24
|
+
/**
|
|
25
|
+
* win32: subprocess CLI + process-group signals / path spawn assumptions
|
|
26
|
+
* (RUSH-2215). Exported as a PREDICATE, not as a pre-bound `describe.skip` —
|
|
27
|
+
* vitest's suite type is not nameable across a module boundary (TS4023), so
|
|
28
|
+
* each suite builds its own `describe` from this.
|
|
29
|
+
*/
|
|
30
|
+
export const DAEMON_TESTS_SUPPORTED = process.platform !== 'win32';
|
|
31
|
+
/** Provision an isolated HOME with just enough scaffolding for the CLI to boot. */
|
|
32
|
+
export function makeHome() {
|
|
33
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-daemon-test-'));
|
|
34
|
+
fs.mkdirSync(path.join(home, '.agents', '.system', '.git'), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(home, '.agents', 'agents.yaml'), 'agents: {}\n');
|
|
36
|
+
return home;
|
|
37
|
+
}
|
|
38
|
+
/** Run `agents daemon <args>` against an isolated HOME — no daemon process ever started. */
|
|
39
|
+
export function run(home, args) {
|
|
40
|
+
return spawnSync('node', ['--import', TSX_IMPORT, CLI_ENTRYPOINT, 'daemon', ...args], {
|
|
41
|
+
cwd: REPO_ROOT,
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
HOME: home,
|
|
45
|
+
USERPROFILE: home,
|
|
46
|
+
AGENTS_SKIP_MIGRATION: '1',
|
|
47
|
+
AGENTS_NO_AUTOPULL: '1',
|
|
48
|
+
AGENTS_CLI_DISABLE_AUTO_UPDATE: '1',
|
|
49
|
+
AGENTS_DAEMON_DIR: path.join(home, '.agents', '.cache', 'helpers', 'daemon'),
|
|
50
|
+
},
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
timeout: 30_000,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Spawn a real, long-lived process whose command line ends in `__daemon-run`
|
|
57
|
+
* (so `isDaemonRunProcess`'s `ps` check accepts it — see
|
|
58
|
+
* `lib/daemon.test.ts`'s "reaps a live __daemon-run registrant" test, same
|
|
59
|
+
* technique) and register it in `home`'s OWN instance registry, exactly the
|
|
60
|
+
* marker `registerDaemonInstance` would write. A real live process, not a
|
|
61
|
+
* mock — `agents daemon status` reads it through the actual registry +
|
|
62
|
+
* `ps`-liveness path, the same one the reaper and `stopDaemon`'s postcondition
|
|
63
|
+
* use.
|
|
64
|
+
*/
|
|
65
|
+
export async function spawnFakeRegisteredDaemon(home) {
|
|
66
|
+
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1e9)', '__daemon-run'], {
|
|
67
|
+
stdio: 'ignore',
|
|
68
|
+
});
|
|
69
|
+
// Give the exec a moment to land before `ps` (read by the status command's
|
|
70
|
+
// isDaemonRunProcess check) is asked to see its real argv — mirrors
|
|
71
|
+
// lib/daemon.test.ts's identical fake-daemon technique.
|
|
72
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
73
|
+
const instancesDir = path.join(home, '.agents', '.cache', 'helpers', 'daemon', 'instances');
|
|
74
|
+
fs.mkdirSync(instancesDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(path.join(instancesDir, String(child.pid)), '__daemon-run', 'utf-8');
|
|
76
|
+
return child;
|
|
77
|
+
}
|
|
78
|
+
/** Register a pid in `home`'s instance registry — the scope stale/duplicate reporting uses. */
|
|
79
|
+
export function registerInstance(home, pid) {
|
|
80
|
+
const dir = path.join(home, '.agents', '.cache', 'helpers', 'daemon', 'instances');
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
fs.writeFileSync(path.join(dir, String(pid)), '__daemon-run', 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
export function killFakeDaemon(child) {
|
|
85
|
+
try {
|
|
86
|
+
if (child.pid)
|
|
87
|
+
process.kill(child.pid, 'SIGKILL');
|
|
88
|
+
}
|
|
89
|
+
catch { /* already gone */ }
|
|
90
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const REPO_ROOT: string;
|
|
2
|
+
export declare const INDEX: string;
|
|
3
|
+
export declare function guardedHome(): void;
|
|
4
|
+
export declare function run(args: string[], extraEnv?: Record<string, string>): {
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
status: number | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function centralDoc(): string;
|
|
10
|
+
export declare function deviceDoc(name: string): string;
|
|
11
|
+
export declare function addDevice(name: string, target?: string): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { afterEach, expect } from 'vitest';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
// End-to-end tests for the unified `agents devices config` surface and the
|
|
8
|
+
// retired-subcommand tombstones (configure / note / set / set-interactive /
|
|
9
|
+
// enable / disable / prefer / unprefer). Spawns the REAL CLI against a
|
|
10
|
+
// throwaway HOME (same pattern as ssh.test.ts) — no mocking; the assertions
|
|
11
|
+
// read the actual files the commands wrote: per-device docs under
|
|
12
|
+
// devices/<name>/agents.yaml (device layer) and central agents.yaml
|
|
13
|
+
// fleet.defaults.config (fleet layer).
|
|
14
|
+
export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
15
|
+
export const INDEX = path.join(REPO_ROOT, 'src', 'index.ts');
|
|
16
|
+
let testHome = '';
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (testHome)
|
|
19
|
+
fs.rmSync(testHome, { recursive: true, force: true });
|
|
20
|
+
testHome = '';
|
|
21
|
+
});
|
|
22
|
+
export function guardedHome() {
|
|
23
|
+
testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-devices-config-home-'));
|
|
24
|
+
const systemDir = path.join(testHome, '.agents', '.system');
|
|
25
|
+
fs.mkdirSync(path.join(systemDir, '.git'), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(systemDir, '.update-check'), JSON.stringify({ lastCheck: 4102444800000, latestVersion: '0.0.0' }));
|
|
27
|
+
}
|
|
28
|
+
export function run(args, extraEnv = {}) {
|
|
29
|
+
const r = spawnSync('bun', [INDEX, ...args], {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
env: {
|
|
32
|
+
...process.env,
|
|
33
|
+
HOME: testHome,
|
|
34
|
+
// os.homedir() reads USERPROFILE on Windows, so HOME alone leaves the
|
|
35
|
+
// spawned CLI resolving the real profile ('agents-cli is not set up').
|
|
36
|
+
USERPROFILE: testHome,
|
|
37
|
+
AGENTS_NO_UPDATE_CHECK: '1',
|
|
38
|
+
AGENTS_NO_USAGE_TRACK: '1',
|
|
39
|
+
// Default identity is mac-mini so machine-local keys (scheduler, tmux,
|
|
40
|
+
// browser consent) can be set in these tests. Override per-call to act
|
|
41
|
+
// as a different box.
|
|
42
|
+
AGENTS_SYNC_MACHINE_ID: 'mac-mini',
|
|
43
|
+
...extraEnv,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', status: r.status };
|
|
47
|
+
}
|
|
48
|
+
export function centralDoc() {
|
|
49
|
+
const p = path.join(testHome, '.agents', 'agents.yaml');
|
|
50
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
51
|
+
}
|
|
52
|
+
export function deviceDoc(name) {
|
|
53
|
+
const p = path.join(testHome, '.agents', 'devices', name, 'agents.yaml');
|
|
54
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8') : '';
|
|
55
|
+
}
|
|
56
|
+
export function addDevice(name, target = 'muqsit@192.0.2.1') {
|
|
57
|
+
const r = run(['devices', 'add', name, target]);
|
|
58
|
+
expect(r.status, r.stderr).toBe(0);
|
|
59
|
+
}
|
package/dist/commands/exec.js
CHANGED
|
@@ -2377,7 +2377,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2377
2377
|
}
|
|
2378
2378
|
else {
|
|
2379
2379
|
try {
|
|
2380
|
-
|
|
2380
|
+
// Account-centric candidate list: native version-home logins PLUS
|
|
2381
|
+
// provider accounts (setup-token / API-key) that can auth this agent,
|
|
2382
|
+
// so `--strategy balanced` spreads across ALL accounts, not just the
|
|
2383
|
+
// ones sitting in a version home (RUSH-3182). Run-path only — the
|
|
2384
|
+
// picker's other callers keep the native-only collector.
|
|
2385
|
+
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2386
|
+
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun);
|
|
2381
2387
|
if (resolved.exhausted) {
|
|
2382
2388
|
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2383
2389
|
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
@@ -2428,6 +2434,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2428
2434
|
else if (resolved.version) {
|
|
2429
2435
|
version = resolved.version;
|
|
2430
2436
|
rotationResult = resolved.rotation;
|
|
2437
|
+
// A balanced/available pick of a PROVIDER account (setup-token /
|
|
2438
|
+
// API-key) carries `providerAccount`. Resolve its env through the
|
|
2439
|
+
// same `resolveSpawnAccount` path an explicit `--account` uses, so
|
|
2440
|
+
// exec injects the credential; a native pick has no providerAccount
|
|
2441
|
+
// and runs from its own version home unchanged (RUSH-3182).
|
|
2442
|
+
const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
|
|
2443
|
+
if (pickedProviderAccount) {
|
|
2444
|
+
try {
|
|
2445
|
+
const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
|
|
2446
|
+
if (picked?.kind === 'provider')
|
|
2447
|
+
accountEnv = picked.env;
|
|
2448
|
+
}
|
|
2449
|
+
catch (err) {
|
|
2450
|
+
console.error(chalk.red(err.message));
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2431
2454
|
if (resolved.rotation && !options.quiet) {
|
|
2432
2455
|
const banner = formatRotationBanner(resolved.rotation, strategy);
|
|
2433
2456
|
process.stderr.write(chalk.gray(banner + '\n'));
|
package/dist/commands/share.js
CHANGED
|
@@ -36,6 +36,9 @@ export function formatSharePublishResult(result, json = false) {
|
|
|
36
36
|
if (visibility === 'unlisted') {
|
|
37
37
|
lines.push(chalk.dim(' visibility: unlisted (noindex, hidden from gallery)'));
|
|
38
38
|
}
|
|
39
|
+
else if (visibility === 'me' || visibility === 'org') {
|
|
40
|
+
lines.push(chalk.dim(` visibility: ${visibility} (login required, hidden from gallery)`));
|
|
41
|
+
}
|
|
39
42
|
else {
|
|
40
43
|
lines.push(chalk.dim(` visibility: ${visibility}`));
|
|
41
44
|
}
|
|
@@ -436,8 +439,8 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
436
439
|
.option('--slug <slug>', 'custom URL slug under your namespace (default: <feature>-<16hex>)')
|
|
437
440
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
|
|
438
441
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
439
|
-
.addOption(new Option('--visibility <level>', 'public | unlisted (default public). unlisted is a capability URL
|
|
440
|
-
.choices(['public', 'unlisted'])
|
|
442
|
+
.addOption(new Option('--visibility <level>', 'public | unlisted | me | org (default public). unlisted is a capability URL; me/org require a Phoenix session and are hidden from the gallery')
|
|
443
|
+
.choices(['public', 'unlisted', 'me', 'org'])
|
|
441
444
|
.default('public'))
|
|
442
445
|
.addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted').hideHelp())
|
|
443
446
|
.addOption(new Option('--private', 'hidden alias of --visibility unlisted').hideHelp())
|
|
@@ -525,8 +528,10 @@ ${SHARE_DELETE_EXAMPLES}
|
|
|
525
528
|
a permanent link. --visibility unlisted (hidden aliases: --unlisted / --private)
|
|
526
529
|
hides the page from the public gallery and agents artifacts share list; the
|
|
527
530
|
direct URL is still world-readable (unlisted, not secret) and GET sends
|
|
528
|
-
X-Robots-Tag: noindex.
|
|
529
|
-
|
|
531
|
+
X-Robots-Tag: noindex. --visibility me requires a Phoenix session and is only
|
|
532
|
+
visible to the signed-in owner; --visibility org requires the same and is
|
|
533
|
+
visible to members of the same Phoenix organization. A pre-publish scan
|
|
534
|
+
refuses emails and credential-shaped strings unless --force is passed.
|
|
530
535
|
|
|
531
536
|
Every publish carries provenance auto-captured from the exec env/git/clock
|
|
532
537
|
(agent, session, host, repo, date) — never invented, only sent when present —
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Command } from 'commander';
|
|
9
9
|
import { AgentManager, AgentStatus, type TaskType } from '../lib/teams/agents.js';
|
|
10
|
+
import type { DispatchOptions } from '../lib/cloud/types.js';
|
|
10
11
|
import { type TaskInfo } from '../lib/teams/api.js';
|
|
11
12
|
import { type TeamRow } from './teams-picker.js';
|
|
12
13
|
import { type TeamSpawner } from '../lib/session/db.js';
|
|
14
|
+
import type { AgentProcess } from '../lib/teams/agents.js';
|
|
13
15
|
type TeamRegistry = Record<string, {
|
|
14
16
|
created_at: string;
|
|
15
17
|
description?: string;
|
|
@@ -106,6 +108,16 @@ export declare function staleRepoError(params: {
|
|
|
106
108
|
* the teammate itself so we don't need the original --cloud CLI args.
|
|
107
109
|
*/
|
|
108
110
|
export declare function wireCloudDispatcher(mgr: AgentManager): void;
|
|
111
|
+
export declare function cloudDispatchOptions(agent: Pick<AgentProcess, 'prompt' | 'agentType' | 'cloudRepo' | 'cloudBranch' | 'model'>): DispatchOptions;
|
|
112
|
+
/**
|
|
113
|
+
* Single-wave start used by `teams start` without --watch. startReady()
|
|
114
|
+
* terminalizes a placement/spawn/cloud/dependency failure to FAILED with
|
|
115
|
+
* evidence rather than leaving it PENDING, so a teammate that failed this wave
|
|
116
|
+
* appears in neither `launched` nor `still_pending` — it must be diffed against
|
|
117
|
+
* the pre-wave roster and reported explicitly, or the wave reads as a success.
|
|
118
|
+
* A wave that only produced failures sets a non-zero exit code.
|
|
119
|
+
*/
|
|
120
|
+
export declare function runOneWave(mgr: AgentManager, team: string, json: boolean): Promise<void>;
|
|
109
121
|
export declare function buildTeamRowsFromSnapshots(registry: TeamRegistry, agents: TeamListAgentSnapshot[],
|
|
110
122
|
/** team name -> the session that spawned it (see teamSpawners). */
|
|
111
123
|
spawners?: Map<string, TeamSpawner>): {
|
package/dist/commands/teams.js
CHANGED
|
@@ -364,17 +364,21 @@ export function wireCloudDispatcher(mgr) {
|
|
|
364
364
|
throw new Error(`Teammate ${a.agentId} has no cloud provider set`);
|
|
365
365
|
}
|
|
366
366
|
const prov = resolveProvider(a.cloudProvider);
|
|
367
|
-
const dispatchOpts =
|
|
368
|
-
prompt: a.prompt,
|
|
369
|
-
agent: a.agentType,
|
|
370
|
-
repo: a.cloudRepo ?? undefined,
|
|
371
|
-
branch: a.cloudBranch ?? undefined,
|
|
372
|
-
model: a.model ?? undefined,
|
|
373
|
-
};
|
|
367
|
+
const dispatchOpts = cloudDispatchOptions(a);
|
|
374
368
|
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
375
369
|
return { cloudSessionId: cloudTask.id };
|
|
376
370
|
});
|
|
377
371
|
}
|
|
372
|
+
export function cloudDispatchOptions(agent) {
|
|
373
|
+
return {
|
|
374
|
+
prompt: agent.prompt,
|
|
375
|
+
agent: agent.agentType,
|
|
376
|
+
repo: agent.cloudRepo ?? undefined,
|
|
377
|
+
branch: agent.cloudBranch ?? undefined,
|
|
378
|
+
model: agent.model ?? undefined,
|
|
379
|
+
env: shareRuntimeEnv(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
378
382
|
/**
|
|
379
383
|
* Advisory: warn once per agent type of any still-pending (staged `--after`)
|
|
380
384
|
* teammate whose CLI may not be signed in. Warn-only — never blocks `start`.
|
|
@@ -446,20 +450,32 @@ async function warnUnsignedTeammates(mgr, team) {
|
|
|
446
450
|
}
|
|
447
451
|
}
|
|
448
452
|
}
|
|
449
|
-
/**
|
|
450
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Single-wave start used by `teams start` without --watch. startReady()
|
|
455
|
+
* terminalizes a placement/spawn/cloud/dependency failure to FAILED with
|
|
456
|
+
* evidence rather than leaving it PENDING, so a teammate that failed this wave
|
|
457
|
+
* appears in neither `launched` nor `still_pending` — it must be diffed against
|
|
458
|
+
* the pre-wave roster and reported explicitly, or the wave reads as a success.
|
|
459
|
+
* A wave that only produced failures sets a non-zero exit code.
|
|
460
|
+
*/
|
|
461
|
+
export async function runOneWave(mgr, team, json) {
|
|
462
|
+
const preWavePending = new Set((await mgr.listByTask(team)).filter((a) => a.status === 'pending').map((a) => a.agentId));
|
|
451
463
|
const launched = await mgr.startReady(team);
|
|
452
464
|
const all = await mgr.listByTask(team);
|
|
453
465
|
const stillPending = all.filter((a) => a.status === 'pending');
|
|
466
|
+
const failed = all.filter((a) => a.status === 'failed' && preWavePending.has(a.agentId));
|
|
467
|
+
if (failed.length > 0 && launched.length === 0)
|
|
468
|
+
process.exitCode = 1;
|
|
454
469
|
if (json) {
|
|
455
470
|
console.log(JSON.stringify({
|
|
456
471
|
team,
|
|
457
472
|
launched: launched.map((a) => ({ agent_id: a.agentId, name: a.name, after: a.after })),
|
|
458
473
|
still_pending: stillPending.map((a) => ({ agent_id: a.agentId, name: a.name, after: a.after })),
|
|
474
|
+
failed: failed.map((a) => ({ agent_id: a.agentId, name: a.name, after: a.after, failure: a.failure })),
|
|
459
475
|
}, null, 2));
|
|
460
476
|
return;
|
|
461
477
|
}
|
|
462
|
-
if (launched.length === 0 && stillPending.length === 0) {
|
|
478
|
+
if (launched.length === 0 && stillPending.length === 0 && failed.length === 0) {
|
|
463
479
|
console.log(chalk.gray(`No pending teammates in team ${team}.`));
|
|
464
480
|
return;
|
|
465
481
|
}
|
|
@@ -471,6 +487,16 @@ async function runOneWave(mgr, team, json) {
|
|
|
471
487
|
console.log(` ${chalk.cyan(h)} ${who}`);
|
|
472
488
|
}
|
|
473
489
|
}
|
|
490
|
+
if (failed.length > 0) {
|
|
491
|
+
if (launched.length > 0)
|
|
492
|
+
console.log();
|
|
493
|
+
console.log(chalk.red(`Failed this wave (${failed.length}):`));
|
|
494
|
+
for (const a of failed) {
|
|
495
|
+
const h = a.name || shortId(a.agentId);
|
|
496
|
+
const why = a.failure ? `${a.failure.code}: ${a.failure.message}` : 'no failure evidence recorded';
|
|
497
|
+
console.log(` ${chalk.red(h)} ${why}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
474
500
|
if (stillPending.length > 0) {
|
|
475
501
|
console.log();
|
|
476
502
|
console.log(chalk.gray(`Still pending (${stillPending.length}):`));
|
|
@@ -710,6 +736,9 @@ function printAgentDetail(a, session) {
|
|
|
710
736
|
if (a.host) {
|
|
711
737
|
console.log(` ${chalk.gray('host ')} ${chalk.cyan(a.host)}`);
|
|
712
738
|
}
|
|
739
|
+
if (a.failure) {
|
|
740
|
+
console.log(` ${chalk.red('failure ')} ${a.failure.code}: ${a.failure.message}`);
|
|
741
|
+
}
|
|
713
742
|
// If the agent's internal session id differs from ours (non-Claude), show
|
|
714
743
|
// it as a hint for `agents sessions <id>`.
|
|
715
744
|
if (a.remote_session_id && a.remote_session_id !== a.agent_id) {
|
|
@@ -798,6 +827,9 @@ function printAgentSummary(s) {
|
|
|
798
827
|
const tools = chalk.gray(` · ${s.tool_count} tools`);
|
|
799
828
|
const hostBadge = s.host ? chalk.gray(' · on ') + chalk.cyan(s.host) : '';
|
|
800
829
|
console.log(` ${chalk.cyan(handle.padEnd(14))} ${ident.padEnd(11)} ${label}${duration}${tools}${hostBadge}${errBadge}`);
|
|
830
|
+
if (s.failure) {
|
|
831
|
+
console.log(` ${chalk.red('failure ')} ${s.failure.code}: ${s.failure.message}`);
|
|
832
|
+
}
|
|
801
833
|
// Files: counts + basenames. Read is count only.
|
|
802
834
|
const fileLines = [];
|
|
803
835
|
const renderCat = (label, cat) => {
|
|
@@ -1856,36 +1888,8 @@ export function registerTeamsCommands(program) {
|
|
|
1856
1888
|
return { cloudSessionId: cloudTask.id };
|
|
1857
1889
|
});
|
|
1858
1890
|
}
|
|
1859
|
-
|
|
1891
|
+
const cloudSessionId = null;
|
|
1860
1892
|
const isStaged = after.length > 0;
|
|
1861
|
-
if (cloudProviderId && !isStaged) {
|
|
1862
|
-
// Ready to run now: dispatch to the cloud provider before registering
|
|
1863
|
-
// the teammate so we have the remote session id up front.
|
|
1864
|
-
//
|
|
1865
|
-
// resolveProvider() and shareRuntimeEnv() are INSIDE the try on purpose:
|
|
1866
|
-
// both read config (the cloud registry; `agents.yaml`'s share block) and
|
|
1867
|
-
// both throw on a malformed one, and by this point the worktree already
|
|
1868
|
-
// exists — leaving them outside stranded the branch with no catch at all.
|
|
1869
|
-
try {
|
|
1870
|
-
const prov = resolveProvider(cloudProviderId);
|
|
1871
|
-
const dispatchOpts = {
|
|
1872
|
-
prompt: effectiveTask,
|
|
1873
|
-
agent,
|
|
1874
|
-
repo: opts.repo,
|
|
1875
|
-
branch: opts.branch,
|
|
1876
|
-
model: opts.model,
|
|
1877
|
-
env: shareRuntimeEnv(),
|
|
1878
|
-
};
|
|
1879
|
-
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
1880
|
-
cloudSessionId = cloudTask.id;
|
|
1881
|
-
}
|
|
1882
|
-
catch (err) {
|
|
1883
|
-
// Same orphan class as a failed spawn: this add already created its
|
|
1884
|
-
// worktree, so a dispatch failure must not strand the branch either.
|
|
1885
|
-
await tearDownCreatedWorktree();
|
|
1886
|
-
dieFriction('teams', 'cloud-dispatch-failed', `Cloud dispatch failed: ${err.message}`);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
1893
|
try {
|
|
1890
1894
|
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName, hostName, hostTarget, hostRepoPath, teamMeta?.project ?? null);
|
|
1891
1895
|
emit('teams.add', { module: 'teams', team, agent, name: result.name, agent_id: result.agent_id, status: result.status });
|
|
@@ -2057,7 +2061,7 @@ export function registerTeamsCommands(program) {
|
|
|
2057
2061
|
});
|
|
2058
2062
|
// start — fire any staged teammates whose --after deps have all completed
|
|
2059
2063
|
addHostOption(teams.command('start [team]'))
|
|
2060
|
-
.description('Launch
|
|
2064
|
+
.description('Launch ready teammates independently. Failed launch/placement and blocked --after dependencies persist in status; --watch keeps draining viable DAG branches.')
|
|
2061
2065
|
.option('--json', 'Output machine-readable JSON')
|
|
2062
2066
|
.option('--watch', 'Keep running: poll every --interval seconds, fire new waves, exit when the DAG drains.')
|
|
2063
2067
|
.option('--interval <seconds>', 'Seconds between waves in --watch mode (default 8)', '8')
|
|
@@ -2094,33 +2098,6 @@ export function registerTeamsCommands(program) {
|
|
|
2094
2098
|
await warnUnsignedTeammates(mgr, team);
|
|
2095
2099
|
await warnThrottledTeammates(mgr, team);
|
|
2096
2100
|
}
|
|
2097
|
-
// Health-/harness-aware pre-flight (RUSH-2002): if the team declares a
|
|
2098
|
-
// device pool and no device in it can run a pending teammate's agent, fail
|
|
2099
|
-
// loud here rather than launching a wave that strands teammates pending.
|
|
2100
|
-
// --force downgrades it to a warning so the user can still try.
|
|
2101
|
-
const placementError = await mgr.preflightPlacement(team);
|
|
2102
|
-
if (placementError) {
|
|
2103
|
-
if (isJsonMode(opts)) {
|
|
2104
|
-
console.log(JSON.stringify({
|
|
2105
|
-
team,
|
|
2106
|
-
error: 'no-viable-device',
|
|
2107
|
-
message: placementError.message,
|
|
2108
|
-
excluded: placementError.excluded,
|
|
2109
|
-
}));
|
|
2110
|
-
if (!opts.force) {
|
|
2111
|
-
process.exitCode = 1;
|
|
2112
|
-
return;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
else if (opts.force) {
|
|
2116
|
-
console.error(chalk.yellow(`⚠ ${placementError.message}\n Continuing anyway (--force).`));
|
|
2117
|
-
}
|
|
2118
|
-
else {
|
|
2119
|
-
console.error(chalk.red(placementError.message));
|
|
2120
|
-
process.exitCode = 1;
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
2101
|
emit('teams.start', { module: 'teams', team, watch: Boolean(opts.watch) });
|
|
2125
2102
|
if (!opts.watch) {
|
|
2126
2103
|
await runOneWave(mgr, team, Boolean(opts.json));
|