@phnx-labs/agents-cli 1.22.49 → 1.22.51
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 +229 -0
- package/README.md +10 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/accounts.js +1 -13
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +77 -2
- package/dist/commands/config.js +20 -0
- 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/feedback.js +1 -1
- package/dist/commands/menubar.js +6 -4
- 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 +36 -4
- 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/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/installations/store.js +6 -3
- package/dist/lib/installations/versions.js +48 -46
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- 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/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- 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.d.ts +30 -0
- package/dist/lib/traces/sync.js +103 -10
- package/package.json +3 -3
|
@@ -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'));
|
|
@@ -7,7 +7,7 @@ import { arch, platform, release } from 'node:os';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { showUrl } from '../lib/open-url.js';
|
|
10
|
-
const REPO = 'phnx-labs/
|
|
10
|
+
const REPO = 'phnx-labs/agi-cli';
|
|
11
11
|
const DISCUSSION_BASE = `https://github.com/${REPO}/discussions/new`;
|
|
12
12
|
const ISSUE_BASE = `https://github.com/${REPO}/issues/new`;
|
|
13
13
|
function readCliVersion() {
|
package/dist/commands/menubar.js
CHANGED
|
@@ -27,8 +27,9 @@ function printStatus(s, opts = {}) {
|
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
console.log(` app installed ${s.installedApp ? chalk.gray(s.installedApp) : chalk.gray('no')}`);
|
|
30
|
-
console.log(` installed
|
|
31
|
-
console.log(`
|
|
30
|
+
console.log(` helper installed ${s.installedVersion ? chalk.gray(s.installedVersion) : chalk.gray('unknown')}`);
|
|
31
|
+
console.log(` helper available ${chalk.gray(s.currentVersion)}`);
|
|
32
|
+
console.log(` CLI version ${chalk.gray(s.cliVersion)}`);
|
|
32
33
|
console.log(` bundle source ${s.source ? chalk.gray(s.source) : chalk.red('missing (cannot enable)')}`);
|
|
33
34
|
console.log(` disabled by user ${yn(s.disabledByUser)}`);
|
|
34
35
|
// Two copies of the INSTALLED bundle is the duplicate the user sees as two
|
|
@@ -80,8 +81,9 @@ function printSetupResult(r) {
|
|
|
80
81
|
function printDoctorReport(r) {
|
|
81
82
|
console.log(chalk.bold('AGI Menu doctor\n'));
|
|
82
83
|
console.log(` install path ${r.installPath ? chalk.gray(r.installPath) : chalk.red('not installed')}`);
|
|
83
|
-
console.log(` installed
|
|
84
|
-
console.log(`
|
|
84
|
+
console.log(` helper installed ${r.installedVersion ? chalk.gray(r.installedVersion) : chalk.gray('unknown')}`);
|
|
85
|
+
console.log(` helper available ${chalk.gray(r.currentVersion)}${r.versionMatches ? '' : chalk.yellow(' (newer helper available — `agents menubar setup` installs it)')}`);
|
|
86
|
+
console.log(` CLI version ${chalk.gray(r.cliVersion)}`);
|
|
85
87
|
const identity = r.signingIdentity === 'developer-id'
|
|
86
88
|
? chalk.green('Developer ID (update-stable)')
|
|
87
89
|
: r.signingIdentity === 'ad-hoc'
|
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));
|
package/dist/commands/traces.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { setHelpSections } from '../lib/help.js';
|
|
3
|
-
import { syncTraces } from '../lib/traces/sync.js';
|
|
4
|
-
import { readSyncLedger } from '../lib/traces/sync.js';
|
|
3
|
+
import { readSyncLedger, syncTraces } from '../lib/traces/sync.js';
|
|
5
4
|
import { managedTracesBaseUrl, resolveTracesBackend } from '../lib/traces/backend.js';
|
|
6
5
|
import { showUrl } from '../lib/open-url.js';
|
|
7
6
|
import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
|
|
@@ -32,6 +31,10 @@ const SYNC_NOTES = `
|
|
|
32
31
|
An incremental gate (traces-sync.json ledger) means re-runs only upload
|
|
33
32
|
sessions whose file_mtime_ms changed since the last sync.
|
|
34
33
|
|
|
34
|
+
On a real (non-dry-run) sync with the managed Phoenix backend, also
|
|
35
|
+
fire-and-forget registers this session with Prix (api.prix.dev) so the
|
|
36
|
+
console can serve live trajectories — a link failure never blocks sync.
|
|
37
|
+
|
|
35
38
|
Sign in first with: agents auth login
|
|
36
39
|
`.trimStart();
|
|
37
40
|
const STATUS_EXAMPLES = `
|
|
@@ -89,7 +92,18 @@ async function handleSync(opts) {
|
|
|
89
92
|
parts.push(chalk.dim(`${result.skipped} skipped`));
|
|
90
93
|
}
|
|
91
94
|
if (result.errors > 0) {
|
|
92
|
-
|
|
95
|
+
// Distinguish expected history (transcripts cleaned off disk) from genuine
|
|
96
|
+
// failures that will be retried, so the operator knows which need attention.
|
|
97
|
+
const detail = [];
|
|
98
|
+
if (result.transcriptUnavailable > 0) {
|
|
99
|
+
detail.push(`${result.transcriptUnavailable} transcripts no longer on disk`);
|
|
100
|
+
}
|
|
101
|
+
const retryable = result.parseFailed + result.uploadFailed;
|
|
102
|
+
if (retryable > 0) {
|
|
103
|
+
detail.push(`${retryable} parse/upload failures — will retry`);
|
|
104
|
+
}
|
|
105
|
+
parts.push(chalk.yellow(`${result.errors} errors`) +
|
|
106
|
+
(detail.length ? chalk.dim(` (${detail.join(' · ')})`) : ''));
|
|
93
107
|
}
|
|
94
108
|
if (parts.length === 0) {
|
|
95
109
|
parts.push(chalk.dim('nothing new'));
|
|
@@ -104,7 +118,25 @@ async function handleStatus() {
|
|
|
104
118
|
}
|
|
105
119
|
const when = new Date(ledger.lastSyncMtime).toLocaleString();
|
|
106
120
|
console.log(`Last sync: ${chalk.bold(when)}`);
|
|
107
|
-
|
|
121
|
+
const failures = ledger.failures ?? [];
|
|
122
|
+
if (failures.length > 0) {
|
|
123
|
+
const byKind = new Map();
|
|
124
|
+
for (const f of failures) {
|
|
125
|
+
const list = byKind.get(f.kind) ?? [];
|
|
126
|
+
list.push(f);
|
|
127
|
+
byKind.set(f.kind, list);
|
|
128
|
+
}
|
|
129
|
+
console.log(chalk.yellow(`\n${failures.length} unresolved failure${failures.length === 1 ? '' : 's'}:`));
|
|
130
|
+
for (const [kind, list] of byKind) {
|
|
131
|
+
const retry = kind === 'transcript-unavailable' ? chalk.dim(' (not retried)') : chalk.dim(' (retried each sync)');
|
|
132
|
+
console.log(` ${chalk.bold(list.length)} ${kind}${retry}`);
|
|
133
|
+
const example = list[0];
|
|
134
|
+
if (example?.detail) {
|
|
135
|
+
console.log(chalk.dim(` e.g. ${example.id.slice(0, 8)} · ${example.detail}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log(chalk.dim('\nRun `agents traces sync` to push new sessions.'));
|
|
108
140
|
}
|
|
109
141
|
async function handleOpen() {
|
|
110
142
|
// M2 console — for now open the base URL which will route to the console once deployed.
|
|
@@ -15,4 +15,13 @@ export interface AccountProviderAdapter {
|
|
|
15
15
|
validate(kind: AccountAuthKind, value: string): void;
|
|
16
16
|
}
|
|
17
17
|
export declare function getAccountProvider(provider: string): AccountProviderAdapter;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a provider account of kind `auth` can authenticate `agent` — i.e. the
|
|
20
|
+
* adapter has an `envFor(agent, auth)` mapping. Used both by the account UI and by
|
|
21
|
+
* the run-candidate pool so a Cursor key never enters Claude's pool and a harness
|
|
22
|
+
* with no provider adapter at all (e.g. kimi, native-login only) yields nothing.
|
|
23
|
+
* A "cannot authenticate" is a clean false; any other error (unknown provider,
|
|
24
|
+
* unsupported kind) is a real bug and re-throws rather than being swallowed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function providerAuthenticatesHarness(provider: string, auth: AccountAuthKind, agent: AgentId): boolean;
|
|
18
27
|
export declare function listAccountProviders(): string[];
|
|
@@ -54,6 +54,26 @@ export function getAccountProvider(provider) {
|
|
|
54
54
|
throw new Error(`Unknown account provider '${provider}'. Supported: ${listAccountProviders().join(', ')}.`);
|
|
55
55
|
return adapter;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a provider account of kind `auth` can authenticate `agent` — i.e. the
|
|
59
|
+
* adapter has an `envFor(agent, auth)` mapping. Used both by the account UI and by
|
|
60
|
+
* the run-candidate pool so a Cursor key never enters Claude's pool and a harness
|
|
61
|
+
* with no provider adapter at all (e.g. kimi, native-login only) yields nothing.
|
|
62
|
+
* A "cannot authenticate" is a clean false; any other error (unknown provider,
|
|
63
|
+
* unsupported kind) is a real bug and re-throws rather than being swallowed.
|
|
64
|
+
*/
|
|
65
|
+
export function providerAuthenticatesHarness(provider, auth, agent) {
|
|
66
|
+
try {
|
|
67
|
+
getAccountProvider(provider).envFor(agent, auth);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
if (message.includes('cannot authenticate'))
|
|
73
|
+
return false;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
57
77
|
export function listAccountProviders() {
|
|
58
78
|
return [...ADAPTERS.keys()].sort();
|
|
59
79
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import { type RotateCandidate } from './rotate.js';
|
|
3
|
+
import { type RegistryAccountRecord } from './account-pool.js';
|
|
4
|
+
/** Inputs to {@link foldRegistryCandidates} — injectable so the fold is unit-tested. */
|
|
5
|
+
export interface RunCandidateInputs {
|
|
6
|
+
native: RotateCandidate[];
|
|
7
|
+
records: RegistryAccountRecord[];
|
|
8
|
+
runVersion: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* PURE: fold provider-account candidates into the native version-home list.
|
|
12
|
+
*
|
|
13
|
+
* A registry account runs in `runVersion` (the agent's default installed home);
|
|
14
|
+
* the token/key overrides that home's own login at exec, so any home works. It
|
|
15
|
+
* carries `providerAccount` so the run path routes it through the existing
|
|
16
|
+
* `--account` injection (`resolveSpawnAccount` → `accountEnv`). An account already
|
|
17
|
+
* covered by a native login is skipped by accountKey. With no installed version
|
|
18
|
+
* there is nowhere to execute a registry account, so the native list passes
|
|
19
|
+
* through unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export declare function foldRegistryCandidates(agent: AgentId, inputs: RunCandidateInputs): RotateCandidate[];
|
|
22
|
+
/**
|
|
23
|
+
* Candidate list for the `agents run` strategy pick, account-centric: the usual
|
|
24
|
+
* native version-home candidates PLUS one candidate per provider account whose
|
|
25
|
+
* provider can authenticate `agent` and whose credential is present locally
|
|
26
|
+
* (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
|
|
27
|
+
*
|
|
28
|
+
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
|
|
29
|
+
* ONLY — the other consumers of {@link collectRunCandidates} (watchdog, session
|
|
30
|
+
* recovery, teams placement) keep calling it directly and are unaffected. The
|
|
31
|
+
* existing {@link pickBalancedCandidate} selects over the combined list, so the
|
|
32
|
+
* revoked / rate-limit / exhausted logic is unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare function collectRunCandidatesForRun(agent: AgentId): Promise<RotateCandidate[]>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readAccountRegistry } from '../account-registry.js';
|
|
2
|
+
import { hasKeychainToken } from '../secrets/index.js';
|
|
3
|
+
import { getGlobalDefault, listInstalledVersions } from '../installations/versions.js';
|
|
4
|
+
import { collectRunCandidates } from './rotate.js';
|
|
5
|
+
import { registryPoolCandidates } from './account-pool.js';
|
|
6
|
+
/**
|
|
7
|
+
* Provider accounts whose credential is present on THIS device, as capability
|
|
8
|
+
* records. A registry entry with no local secret is skipped: including it would
|
|
9
|
+
* let `--strategy balanced` pick an account that then fails at spawn and exits the
|
|
10
|
+
* whole run (`resolveSpawnAccount` throws on a missing credential) instead of
|
|
11
|
+
* rotating to a healthy one.
|
|
12
|
+
*/
|
|
13
|
+
function localRegistryRecords() {
|
|
14
|
+
return Object.values(readAccountRegistry().accounts)
|
|
15
|
+
.filter((a) => hasKeychainToken(a.secretRef))
|
|
16
|
+
.map((a) => ({ name: a.name, provider: a.provider, auth: a.auth }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* PURE: fold provider-account candidates into the native version-home list.
|
|
20
|
+
*
|
|
21
|
+
* A registry account runs in `runVersion` (the agent's default installed home);
|
|
22
|
+
* the token/key overrides that home's own login at exec, so any home works. It
|
|
23
|
+
* carries `providerAccount` so the run path routes it through the existing
|
|
24
|
+
* `--account` injection (`resolveSpawnAccount` → `accountEnv`). An account already
|
|
25
|
+
* covered by a native login is skipped by accountKey. With no installed version
|
|
26
|
+
* there is nowhere to execute a registry account, so the native list passes
|
|
27
|
+
* through unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export function foldRegistryCandidates(agent, inputs) {
|
|
30
|
+
const { native, records, runVersion } = inputs;
|
|
31
|
+
if (!runVersion)
|
|
32
|
+
return native;
|
|
33
|
+
const seen = new Set(native.filter((c) => c.accountKey).map((c) => c.accountKey));
|
|
34
|
+
const extra = registryPoolCandidates(records, agent)
|
|
35
|
+
.filter((r) => !seen.has(r.accountKey))
|
|
36
|
+
.map((r) => ({
|
|
37
|
+
agent,
|
|
38
|
+
version: runVersion,
|
|
39
|
+
accountKey: r.accountKey,
|
|
40
|
+
accountLabel: r.name,
|
|
41
|
+
email: null,
|
|
42
|
+
usageKey: null,
|
|
43
|
+
usageStatus: null,
|
|
44
|
+
usageSnapshot: null,
|
|
45
|
+
usageError: null,
|
|
46
|
+
usageMinutesToLimit: null,
|
|
47
|
+
plan: null,
|
|
48
|
+
signedIn: true,
|
|
49
|
+
authVerdict: null,
|
|
50
|
+
lastActive: null,
|
|
51
|
+
providerAccount: r.name,
|
|
52
|
+
}));
|
|
53
|
+
return [...native, ...extra];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Candidate list for the `agents run` strategy pick, account-centric: the usual
|
|
57
|
+
* native version-home candidates PLUS one candidate per provider account whose
|
|
58
|
+
* provider can authenticate `agent` and whose credential is present locally
|
|
59
|
+
* (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
|
|
60
|
+
*
|
|
61
|
+
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
|
|
62
|
+
* ONLY — the other consumers of {@link collectRunCandidates} (watchdog, session
|
|
63
|
+
* recovery, teams placement) keep calling it directly and are unaffected. The
|
|
64
|
+
* existing {@link pickBalancedCandidate} selects over the combined list, so the
|
|
65
|
+
* revoked / rate-limit / exhausted logic is unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export async function collectRunCandidatesForRun(agent) {
|
|
68
|
+
const native = await collectRunCandidates(agent);
|
|
69
|
+
const runVersion = getGlobalDefault(agent) ?? listInstalledVersions(agent)[0];
|
|
70
|
+
return foldRegistryCandidates(agent, { native, records: localRegistryRecords(), runVersion });
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { AccountAuthKind } from '../account-provider-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The provider-account side of the run-candidate pool (RUSH-3182).
|
|
5
|
+
*
|
|
6
|
+
* `--strategy balanced` used to enumerate only version-home native logins, so a
|
|
7
|
+
* setup-token / API-key account added via `agents accounts add` never balanced.
|
|
8
|
+
* This module turns the account registry into the extra candidates the run path
|
|
9
|
+
* folds in — see `collectRunCandidatesForRun` in `account-pool-collect.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + dependency-light so the harness-capability filter is unit-tested in
|
|
12
|
+
* isolation with fixtures.
|
|
13
|
+
*/
|
|
14
|
+
/** A provider account record as stored in the registry (identity captured separately). */
|
|
15
|
+
export interface RegistryAccountRecord {
|
|
16
|
+
name: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
auth: AccountAuthKind;
|
|
19
|
+
}
|
|
20
|
+
/** A registry account eligible to run one harness, ready to map to a candidate. */
|
|
21
|
+
export interface RegistryAccountInput {
|
|
22
|
+
/** Agent-scoped key so `(claude, X)` and `(codex, X)` stay distinct. */
|
|
23
|
+
accountKey: string;
|
|
24
|
+
email: string | null;
|
|
25
|
+
name: string;
|
|
26
|
+
provider: string;
|
|
27
|
+
auth: AccountAuthKind;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Which provider accounts can authenticate `agent`. An account is a candidate for
|
|
31
|
+
* harness H only when its provider adapter has an `envFor(H, kind)` mapping
|
|
32
|
+
* (`providerAuthenticatesHarness`, the same check the account UI uses) — so a
|
|
33
|
+
* Cursor key never enters Claude's pool and a Claude setup-token never enters
|
|
34
|
+
* Codex's. This is registry-driven, no per-harness `else if`: a harness gets a
|
|
35
|
+
* provider-account pool exactly when the adapter table maps a provider to it —
|
|
36
|
+
* today claude, codex, grok, cursor, opencode (plus gemini/antigravity). A harness
|
|
37
|
+
* with only a native OAuth login and no provider adapter — kimi — returns `[]`
|
|
38
|
+
* here and keeps balancing its native logins only.
|
|
39
|
+
*
|
|
40
|
+
* `accountKey` is synthetic here (`${agent}:name=${name}`) — a stable per-account
|
|
41
|
+
* key so balancing can include it immediately; the real agent-scoped identity
|
|
42
|
+
* (email / org uuid) is backfilled by identity capture and replaces it. `email`
|
|
43
|
+
* is null until then, which routes the account as usage-unverified but still a
|
|
44
|
+
* candidate, never excluded.
|
|
45
|
+
*/
|
|
46
|
+
export declare function registryPoolCandidates(records: RegistryAccountRecord[], agent: AgentId): RegistryAccountInput[];
|