@phnx-labs/agents-cli 1.22.115 → 1.22.117
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 +161 -14
- package/README.md +1 -1
- package/dist/commands/browser.js +11 -0
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +263 -152
- package/dist/commands/feed.js +65 -14
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +88 -7
- package/dist/commands/sessions.d.ts +21 -2
- package/dist/commands/sessions.js +157 -3
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-term.d.ts +25 -0
- package/dist/commands/setup-term.js +71 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +12 -4
- package/dist/commands/ssh.js +1 -69
- package/dist/lib/accounting/rotate.d.ts +63 -1
- package/dist/lib/accounting/rotate.js +56 -0
- package/dist/lib/accounts/add.js +2 -2
- package/dist/lib/accounts/slots.js +32 -2
- package/dist/lib/answer-router.d.ts +11 -2
- package/dist/lib/answer-router.js +26 -2
- package/dist/lib/auth-mint.d.ts +5 -4
- package/dist/lib/auth-mint.js +4 -3
- package/dist/lib/browser/drivers/arc.d.ts +1 -1
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +208 -36
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +1 -1
- package/dist/lib/config-keys.js +5 -0
- package/dist/lib/device-config.js +61 -0
- package/dist/lib/devices/doctor-findings.js +2 -6
- package/dist/lib/feed/answer.d.ts +153 -4
- package/dist/lib/feed/answer.js +716 -105
- package/dist/lib/feed/feed.d.ts +61 -1
- package/dist/lib/feed/feed.js +226 -14
- package/dist/lib/feed/hub-server.d.ts +58 -3
- package/dist/lib/feed/hub-server.js +306 -54
- package/dist/lib/feed/pr-status.d.ts +8 -0
- package/dist/lib/feed/pr-status.js +9 -1
- package/dist/lib/feed-outcome.d.ts +1 -1
- package/dist/lib/feed-outcome.js +9 -2
- package/dist/lib/feed-policy.js +9 -3
- package/dist/lib/fleet/auth-sync.d.ts +2 -55
- package/dist/lib/fleet/auth-sync.js +2 -89
- package/dist/lib/harness-auth-capabilities.js +7 -2
- package/dist/lib/hosts/dispatch.d.ts +20 -1
- package/dist/lib/hosts/dispatch.js +52 -30
- package/dist/lib/hosts/remote-cmd.d.ts +21 -0
- package/dist/lib/hosts/remote-cmd.js +27 -2
- package/dist/lib/mailbox.d.ts +12 -0
- package/dist/lib/mailbox.js +16 -2
- package/dist/lib/menubar/snapshot.d.ts +51 -0
- package/dist/lib/menubar/snapshot.js +42 -3
- package/dist/lib/open-url.js +2 -2
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/projects.d.ts +23 -0
- package/dist/lib/projects.js +78 -0
- package/dist/lib/secrets-cli.d.ts +3 -3
- package/dist/lib/secrets-cli.js +1 -1
- package/dist/lib/session/active.d.ts +1 -0
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/db.d.ts +67 -3
- package/dist/lib/session/db.js +381 -126
- package/dist/lib/session/prompt.d.ts +23 -7
- package/dist/lib/session/prompt.js +46 -8
- package/dist/lib/session/remote/remote-list.d.ts +20 -0
- package/dist/lib/session/remote/remote-list.js +22 -6
- package/dist/lib/session/remote/watch.d.ts +12 -0
- package/dist/lib/session/remote/watch.js +9 -0
- package/dist/lib/session/remote-preview-cache.d.ts +29 -0
- package/dist/lib/session/remote-preview-cache.js +373 -0
- package/dist/lib/session/tail.d.ts +50 -0
- package/dist/lib/session/tail.js +219 -0
- package/dist/lib/setup-tool-install.js +2 -1
- package/dist/lib/setup-tool-status.d.ts +1 -1
- package/dist/lib/setup-tool-status.js +7 -1
- package/dist/lib/signin-badge.d.ts +19 -4
- package/dist/lib/signin-badge.js +29 -11
- package/dist/lib/term-driver.d.ts +24 -0
- package/dist/lib/term-driver.js +36 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/index.js +1 -1
- package/dist/lib/terminal/inject.d.ts +38 -0
- package/dist/lib/terminal/inject.js +55 -9
- package/dist/lib/terminal/transport.d.ts +15 -5
- package/dist/lib/terminal/transport.js +61 -11
- package/package.json +1 -1
- package/dist/lib/fleet/remote-login.d.ts +0 -170
- package/dist/lib/fleet/remote-login.js +0 -568
package/dist/commands/exec.js
CHANGED
|
@@ -90,6 +90,24 @@ export function parseRunPickerMarkers(agentSpec) {
|
|
|
90
90
|
export function hostTargetGiven(options) {
|
|
91
91
|
return [options.host, options.device, options.on, options.computer].filter((v) => !!v);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Turn a host target that names THIS machine into an explicit local pin: the
|
|
95
|
+
* host-family flags are cleared and `local` is set, so no dispatch path
|
|
96
|
+
* self-SSHes and the bare-interactive default (`bareInteractiveRunDefaultsToDeviceAuto`)
|
|
97
|
+
* sees a decided placement. Returns true when the pin was applied. Pure over
|
|
98
|
+
* the `isSelf` predicate so the rule is testable without a device registry.
|
|
99
|
+
*/
|
|
100
|
+
export function pinLocalWhenTargetIsSelf(options, isSelf) {
|
|
101
|
+
const targets = hostTargetGiven(options);
|
|
102
|
+
if (targets.length === 0 || !targets.every(isSelf))
|
|
103
|
+
return false;
|
|
104
|
+
options.host = undefined;
|
|
105
|
+
options.device = undefined;
|
|
106
|
+
options.on = undefined;
|
|
107
|
+
options.computer = undefined;
|
|
108
|
+
options.local = true;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
93
111
|
/**
|
|
94
112
|
* Return every option whose selection semantics conflict with an account
|
|
95
113
|
* choice (`agent#`). Device routing is deliberately absent: the marker rides
|
|
@@ -118,6 +136,8 @@ export function runAccountPickerConflicts(options) {
|
|
|
118
136
|
*/
|
|
119
137
|
export function runDevicePickerConflicts(options) {
|
|
120
138
|
const conflicts = hostTargetGiven(options).map((h) => `--device ${h}`);
|
|
139
|
+
if (options.local)
|
|
140
|
+
conflicts.push('--local');
|
|
121
141
|
if (options.lease)
|
|
122
142
|
conflicts.push('--lease');
|
|
123
143
|
if (options.box)
|
|
@@ -145,6 +165,11 @@ export { RUN_AUTO_KEYWORD };
|
|
|
145
165
|
* fleet. Pure so the pinning matrix is unit-testable.
|
|
146
166
|
*/
|
|
147
167
|
export function runAutoDefaultsToAffinity(options, env = process.env) {
|
|
168
|
+
// An explicit local pin (--local, --where local, --device <this machine>)
|
|
169
|
+
// is a decided host layer with an empty host set; read it here so the
|
|
170
|
+
// `run auto` and bare-interactive defaults share one rule.
|
|
171
|
+
if (options.local)
|
|
172
|
+
return false;
|
|
148
173
|
if (hostTargetGiven(options).length > 0)
|
|
149
174
|
return false;
|
|
150
175
|
if (env.AGENTS_RUN_AUTO_HOST_RESOLVED === '1')
|
|
@@ -639,7 +664,8 @@ export function registerRunCommand(program) {
|
|
|
639
664
|
.option('--until <signal>', 'Loop stop condition. `signal` reads <runDir>/loop-signal.json {continue,reason} each iteration; absent or continue:false stops (fail-closed). Loop only.')
|
|
640
665
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
641
666
|
.option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --device/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
|
|
642
|
-
.option('
|
|
667
|
+
.option('--local', 'Run on this machine. A bare interactive run otherwise places itself like --device auto; --local pins it here. Same as --where local or --device <this machine>.')
|
|
668
|
+
.option('-D, --device <name>', 'Offload this run onto another machine over SSH — a registered device, or user@host. Pass "auto" to pick the least-loaded reachable device where the requested agent is installed and signed in, keeping the run local when no remote is better, or "interactive" for the machine pinned as interactive.host (the box a human is sitting at). Naming this machine runs locally, no SSH. Same as --where device:<name>. See `agents devices`.')
|
|
643
669
|
.option('--remote-cwd <dir>', "Explicit device working directory for --device runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
|
|
644
670
|
.option('--no-follow', 'With --device, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
645
671
|
.option('--any', 'With --device <cap> (a capability tag), pick any matching device instead of erroring when several match.')
|
|
@@ -680,9 +706,9 @@ export function registerRunCommand(program) {
|
|
|
680
706
|
agents run claude "fix lint errors in src/" --mode edit
|
|
681
707
|
|
|
682
708
|
# Interactive (TUI): a bare run places itself like --device auto (a fleet
|
|
683
|
-
# worker, TUI forwarded over SSH);
|
|
709
|
+
# worker, TUI forwarded over SSH); --local keeps it on this machine
|
|
684
710
|
agents run claude
|
|
685
|
-
agents run claude --
|
|
711
|
+
agents run claude --local # stay local (or pick this machine in claude@)
|
|
686
712
|
|
|
687
713
|
# Pick a signed-in account/version for only this run (# = account picker)
|
|
688
714
|
agents run claude#
|
|
@@ -790,10 +816,10 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
790
816
|
Interactive placement: a bare 'agents run <harness>' (no prompt, real TTY)
|
|
791
817
|
places itself like --device auto — a fleet worker runs it, with the TUI
|
|
792
818
|
forwarded over SSH. Headless runs (any prompt, --json, no TTY) are
|
|
793
|
-
unchanged: they run in place. To stay on this machine, pass
|
|
794
|
-
--device <this machine
|
|
795
|
-
'<harness>@' device picker. When placement finds no
|
|
796
|
-
run fails loud and names the local
|
|
819
|
+
unchanged: they run in place. To stay on this machine, pass --local
|
|
820
|
+
(--device <this machine> means the same), or pick this machine (listed
|
|
821
|
+
first) in the '<harness>@' device picker. When placement finds no
|
|
822
|
+
healthy device the run fails loud and names the local spelling.
|
|
797
823
|
|
|
798
824
|
Fallback: --fallback codex,antigravity retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
799
825
|
|
|
@@ -891,6 +917,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
891
917
|
const { placementFromRunFlags, expandPlacementToRunFlags, PlacementError } = await import('../lib/placement.js');
|
|
892
918
|
try {
|
|
893
919
|
const placement = placementFromRunFlags(options);
|
|
920
|
+
// An explicit local placement (--local, --where local) must outlive
|
|
921
|
+
// this block: expansion yields no host flag, and the bare-interactive
|
|
922
|
+
// default below reads an empty host set as "decide for me".
|
|
923
|
+
if (placement.kind === 'local' && placement.source !== 'default')
|
|
924
|
+
options.local = true;
|
|
894
925
|
if (options.where) {
|
|
895
926
|
const expanded = expandPlacementToRunFlags(placement);
|
|
896
927
|
if (expanded.host !== undefined)
|
|
@@ -918,6 +949,16 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
918
949
|
throw err;
|
|
919
950
|
}
|
|
920
951
|
}
|
|
952
|
+
// --device naming this machine (short id, MagicDNS name, loopback, or the
|
|
953
|
+
// `interactive` sentinel pinned here) is a local run, not a self-SSH: the
|
|
954
|
+
// hop probes its own login shell and times out under load. Pinned here,
|
|
955
|
+
// ahead of every placement gate, so the bare-interactive default sees an
|
|
956
|
+
// explicit local choice rather than an empty host set.
|
|
957
|
+
{
|
|
958
|
+
const { isSelfHost } = await import('../lib/devices/self-host.js');
|
|
959
|
+
const { isDeviceInteractive, resolveInteractiveDevice } = await import('../lib/devices/interactive-host.js');
|
|
960
|
+
pinLocalWhenTargetIsSelf(options, (name) => isSelfHost(isDeviceInteractive(name) ? resolveInteractiveDevice() ?? name : name));
|
|
961
|
+
}
|
|
921
962
|
// Cloud refinement flags without the placement are a typo, not a no-op.
|
|
922
963
|
if (!options.cloud) {
|
|
923
964
|
const { cloudFlagsWithoutCloud } = await import('./run-cloud.js');
|
|
@@ -1101,13 +1142,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1101
1142
|
const { sessionRecoveryDestinationMatches, sessionRecoveryPeer, } = await import('../lib/session/recovery.js');
|
|
1102
1143
|
const sourceMachine = resolvedResumeSource.machine;
|
|
1103
1144
|
const sourcePeer = sessionRecoveryPeer(resolvedResumeSource);
|
|
1104
|
-
const explicitPlacement = hostTargetGiven(options).length > 0;
|
|
1145
|
+
const explicitPlacement = hostTargetGiven(options).length > 0 || options.local === true;
|
|
1105
1146
|
if (sourcePeer && !explicitPlacement) {
|
|
1106
1147
|
options.host = sourcePeer;
|
|
1107
1148
|
}
|
|
1108
1149
|
else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
|
|
1109
1150
|
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its conversation state is stored; ` +
|
|
1110
|
-
`the requested device was ${hostTargetGiven(options).join(', ')}.`));
|
|
1151
|
+
`the requested device was ${hostTargetGiven(options).join(', ') || 'this machine'}.`));
|
|
1111
1152
|
process.exit(1);
|
|
1112
1153
|
}
|
|
1113
1154
|
// Recovery is resolved on the device that owns the transcript. A remote
|
|
@@ -1246,8 +1287,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1246
1287
|
if (!defaultPlacement)
|
|
1247
1288
|
throw err;
|
|
1248
1289
|
console.error(chalk.red(err.message));
|
|
1249
|
-
|
|
1250
|
-
console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
|
|
1290
|
+
console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --local`));
|
|
1251
1291
|
process.exit(1);
|
|
1252
1292
|
}
|
|
1253
1293
|
if (!options.quiet && result.banner) {
|
|
@@ -2045,7 +2085,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2045
2085
|
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, readProfile, resolveProfileForRun },
|
|
2046
2086
|
// Engine DATA ops resolve through the standalone `secrets` process client
|
|
2047
2087
|
// (PHNX-3989); the agents-owned `bundle@host` policy helpers stay in agents-cli.
|
|
2048
|
-
{ readAndResolveBundleEnv, describeBundle, remoteResolveEnv }, { assertRemoteBundleFlagsUnsupported, splitBundleRef, resolveSecretsContextForRun }, { resolveHostSshTarget }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError, formatNoVerifiedUsageError, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
2088
|
+
{ readAndResolveBundleEnv, describeBundle, remoteResolveEnv }, { assertRemoteBundleFlagsUnsupported, splitBundleRef, resolveSecretsContextForRun }, { resolveHostSshTarget }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, DEFAULT_ROTATION_FAILOVER_LIMIT, shouldArmRotationFailover, preflightFallbackHandoff, preflightHandoffEligible, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError, formatNoVerifiedUsageError, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
2049
2089
|
import('../lib/exec.js'),
|
|
2050
2090
|
import('../lib/agents.js'),
|
|
2051
2091
|
import('../lib/profiles.js'),
|
|
@@ -2590,6 +2630,21 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2590
2630
|
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
2591
2631
|
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
2592
2632
|
let rotationResult = null;
|
|
2633
|
+
/**
|
|
2634
|
+
* The PRIMARY harness's configured run-default mode/effort, carried across a
|
|
2635
|
+
* preflight alternate-harness handoff (PHNX-3999 F19).
|
|
2636
|
+
*
|
|
2637
|
+
* Run defaults are resolved after the harness is known, so without this a
|
|
2638
|
+
* handoff would silently adopt the ALTERNATE's configured defaults — a
|
|
2639
|
+
* primary configured `mode: plan` becoming the alternate's `mode: skip`
|
|
2640
|
+
* escalates a read-only intent into an unattended writable run. The
|
|
2641
|
+
* canonical mid-run cascade keeps the primary's resolved mode for the same
|
|
2642
|
+
* reason (`runWithFallback` forwards `options.mode` and only re-derives the
|
|
2643
|
+
* mode that was never configured at all). An IMPLICIT mode is deliberately
|
|
2644
|
+
* not carried: it is the harness's own safe default, so the alternate's
|
|
2645
|
+
* applies (identical to `modeWasImplicit ? implicitModeFor(agent)`).
|
|
2646
|
+
*/
|
|
2647
|
+
let handoffRunDefaults;
|
|
2593
2648
|
// Precomputed launchable-signed-in verdict for the ACTUAL launched
|
|
2594
2649
|
// candidate, fed to the pre-launch `run.launch` event so it need not
|
|
2595
2650
|
// re-probe. Sourced per resolution branch from the candidate that WON, not
|
|
@@ -2622,155 +2677,202 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2622
2677
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
2623
2678
|
// `pinned` still resolves so a logged-out default can yield to a signed-in
|
|
2624
2679
|
// sibling on this device (PHNX-2685); an explicit @version pin is unchanged.
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if (
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2680
|
+
// Bounded handoff loop: each iteration resolves an account for `agent`, and
|
|
2681
|
+
// the only `continue` is the preflight exhaustion handoff below, which
|
|
2682
|
+
// consumes one entry of the `--fallback` spec before retrying as that
|
|
2683
|
+
// alternate. The spec is finite, so this terminates (PHNX-3999 F19).
|
|
2684
|
+
preflight: for (;;) {
|
|
2685
|
+
if (!accountPickerRequested && !configuredAccount && (!version || strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
2686
|
+
if (version) {
|
|
2687
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
2688
|
+
}
|
|
2689
|
+
else if (fromProfile) {
|
|
2690
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: custom harness pins its own version/auth\n`));
|
|
2691
|
+
}
|
|
2692
|
+
else {
|
|
2693
|
+
try {
|
|
2694
|
+
// Account-centric candidate list: native version-home logins PLUS
|
|
2695
|
+
// provider accounts (setup-token / API-key) that can auth this agent,
|
|
2696
|
+
// so `--strategy balanced` spreads across ALL accounts, not just the
|
|
2697
|
+
// ones sitting in a version home (RUSH-3182). Run-path only — the
|
|
2698
|
+
// picker's other callers keep the native-only collector.
|
|
2699
|
+
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2700
|
+
bootMark('resolve-version:start');
|
|
2701
|
+
const routingModel = options.model ?? resolvedResumeSource?.model ?? workflowModel
|
|
2702
|
+
?? (options.fallback ? undefined : resolveRunDefaults(agent, resolveVersion(agent, cwd), cwd).model);
|
|
2703
|
+
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun, routingModel);
|
|
2704
|
+
bootMark('resolve-version:done');
|
|
2705
|
+
if (resolved.exhausted) {
|
|
2706
|
+
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2707
|
+
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
2708
|
+
//
|
|
2709
|
+
// - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
|
|
2710
|
+
// The old behavior warned "found no usable version; falling back to
|
|
2711
|
+
// defaults" and launched the pinned default anyway — the exact move
|
|
2712
|
+
// that loops a rotate into an exhausted account. Only a window reset
|
|
2713
|
+
// clears it. The message text is a contract the Factory watchdog
|
|
2714
|
+
// tail-detects; do not reword it.
|
|
2715
|
+
// - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
|
|
2716
|
+
// because the harness's own TUI is the login surface. So on a TTY we
|
|
2717
|
+
// carry the user into that login instead of erroring. Exiting here
|
|
2718
|
+
// made `agents run <agent>`, `agents run <agent>#`, and `agents use`
|
|
2719
|
+
// all dead-end with no reachable way to authenticate.
|
|
2720
|
+
const recoverable = signInRecoverableCandidates(resolved.exhausted);
|
|
2721
|
+
const { signInLaunchDecision } = await import('./run-account-picker.js');
|
|
2722
|
+
const decision = signInLaunchDecision({
|
|
2723
|
+
recoverable: recoverable.length,
|
|
2724
|
+
tty: isInteractiveTerminal(),
|
|
2725
|
+
json: options.json === true,
|
|
2726
|
+
});
|
|
2727
|
+
if (decision === 'launch') {
|
|
2728
|
+
const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
|
|
2729
|
+
const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
|
|
2730
|
+
// A cancelled prompt launches nothing — same contract as the
|
|
2731
|
+
// trailing-# account picker above.
|
|
2732
|
+
if (!signInVersion)
|
|
2733
|
+
return;
|
|
2734
|
+
version = signInVersion;
|
|
2735
|
+
// We just told the user this account is logged out and why we're
|
|
2736
|
+
// launching it, so suppress the downstream login preflight — it
|
|
2737
|
+
// would print a second, near-identical "looks logged out" warning.
|
|
2738
|
+
signInLaunch = true;
|
|
2739
|
+
}
|
|
2740
|
+
else {
|
|
2741
|
+
// Every account of this harness is throttled. A configured
|
|
2742
|
+
// alternate exists for exactly this case, so hand off to it here
|
|
2743
|
+
// rather than exiting — `runWithFallback` would only reach it
|
|
2744
|
+
// after a run that can no longer start (PHNX-3999 F19). The
|
|
2745
|
+
// alternate becomes the primary and is dropped from the spec, so
|
|
2746
|
+
// the chain that remains never lists the agent now running.
|
|
2747
|
+
//
|
|
2748
|
+
// ONLY for a run shape `--fallback` is valid on. The handoff
|
|
2749
|
+
// consumes the entry it uses, and the canonical --fallback
|
|
2750
|
+
// validation runs later (it rejects interactive/--acp/--loop/
|
|
2751
|
+
// --resume-checkpoint chains and requires a prompt) — so
|
|
2752
|
+
// switching first on an ineligible shape would empty the spec,
|
|
2753
|
+
// leave that guard nothing to reject, and start the alternate
|
|
2754
|
+
// harness on a run the CLI refuses today. An ineligible shape
|
|
2755
|
+
// never switches, so the spec survives and still fails there
|
|
2756
|
+
// with its own message. See preflightHandoffEligible.
|
|
2757
|
+
const handoff = preflightHandoffEligible({
|
|
2758
|
+
hasPrompt: prompt !== undefined,
|
|
2759
|
+
interactive: options.interactive === true,
|
|
2760
|
+
acp: options.acp === true,
|
|
2761
|
+
loop: options.loop === true,
|
|
2762
|
+
resumeCheckpoint: !!options.resumeCheckpoint,
|
|
2763
|
+
resume: !!options.resume,
|
|
2764
|
+
workflowScoped: !!workflowToolsRestrict || !!workflowMcpConfigPath,
|
|
2765
|
+
})
|
|
2766
|
+
? preflightFallbackHandoff(options.fallback, agent, resolved.exhausted)
|
|
2767
|
+
: null;
|
|
2768
|
+
if (handoff) {
|
|
2769
|
+
process.stderr.write(chalk.yellow(`[agents] every ${agent} account is exhausted — handing off to ${handoff.agent}${handoff.version ? `@${handoff.version}` : ''}\n`));
|
|
2770
|
+
// Snapshot the PRIMARY's configured defaults BEFORE the switch —
|
|
2771
|
+
// this is the operator's intent for this run, not the alternate's.
|
|
2772
|
+
handoffRunDefaults ??= fromProfile ? undefined : resolveRunDefaults(agent, version ?? resolveVersion(agent, cwd), cwd);
|
|
2773
|
+
agent = handoff.agent;
|
|
2774
|
+
// Alias-resolve against the NEW harness's installs, exactly as
|
|
2775
|
+
// the canonical --fallback parse does for its own entries.
|
|
2776
|
+
version = resolveVersionAlias(agent, handoff.version);
|
|
2777
|
+
options.fallback = handoff.remainingSpec;
|
|
2778
|
+
rotationResult = null;
|
|
2779
|
+
continue preflight;
|
|
2780
|
+
}
|
|
2781
|
+
console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
|
|
2782
|
+
if (recoverable.length > 0) {
|
|
2783
|
+
// Off a TTY nobody can complete a login, so we still exit — but
|
|
2784
|
+
// name the actual fix rather than only offering --strategy pinned,
|
|
2785
|
+
// which would just pin the same unauthenticated account.
|
|
2786
|
+
const { loginHint } = await import('../lib/signin-badge.js');
|
|
2787
|
+
console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
|
|
2788
|
+
}
|
|
2789
|
+
process.exit(1);
|
|
2790
|
+
}
|
|
2679
2791
|
}
|
|
2680
|
-
else {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2792
|
+
else if (resolved.noVerifiedUsage) {
|
|
2793
|
+
// Entirely stale usage (PHNX-2526): every eligible account carries
|
|
2794
|
+
// a stale-but-present usage number and none is verified, so the
|
|
2795
|
+
// route would be a guess. NEVER auto-pick it. Interactive: show the
|
|
2796
|
+
// account picker so a human chooses with the (stale) numbers in
|
|
2797
|
+
// view. Unattended: fail loud with NO_VERIFIED_USAGE. The stale
|
|
2798
|
+
// pool survives ONLY as `resolved.rotation.healthy` for bounded
|
|
2799
|
+
// post-rejection failover, never as the initial pick.
|
|
2800
|
+
const { noVerifiedUsageDecision, pickRunAccountCandidate } = await import('./run-account-picker.js');
|
|
2801
|
+
const decision = noVerifiedUsageDecision({
|
|
2802
|
+
tty: isInteractiveTerminal(),
|
|
2803
|
+
json: options.json === true,
|
|
2804
|
+
headless: options.headless === true,
|
|
2805
|
+
});
|
|
2806
|
+
if (decision === 'picker') {
|
|
2807
|
+
const selected = await pickRunAccountCandidate(agent);
|
|
2808
|
+
// A cancelled picker launches nothing — same contract as the
|
|
2809
|
+
// trailing-# account picker and the sign-in launch above.
|
|
2810
|
+
if (!selected)
|
|
2811
|
+
return;
|
|
2812
|
+
version = selected.version;
|
|
2813
|
+
// Source the run.launch verdict from the account the user ACTUALLY
|
|
2814
|
+
// picked — the picker may deliberately return a logged-out one
|
|
2815
|
+
// (RUSH-2334), so it can differ from rotationResult.picked.
|
|
2816
|
+
launchSignedIn = selected.signedIn;
|
|
2817
|
+
launchEmail = selected.email;
|
|
2818
|
+
// Keep the rotation so mid-run failover can still cascade across
|
|
2819
|
+
// the other (stale) healthy accounts after a real rejection.
|
|
2820
|
+
rotationResult = resolved.rotation;
|
|
2821
|
+
if (!options.quiet) {
|
|
2822
|
+
const identity = selected.accountLabel || 'signed-in account';
|
|
2823
|
+
process.stderr.write(chalk.gray(`[agents] no fresh usage for any ${agent} account — you picked ${identity} · ${agent}@${selected.version}\n`));
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
else {
|
|
2827
|
+
console.error(chalk.red(formatNoVerifiedUsageError(agent, strategy, resolved.rotation?.healthy ?? [])));
|
|
2828
|
+
process.exit(1);
|
|
2688
2829
|
}
|
|
2689
|
-
process.exit(1);
|
|
2690
2830
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
// Entirely stale usage (PHNX-2526): every eligible account carries
|
|
2694
|
-
// a stale-but-present usage number and none is verified, so the
|
|
2695
|
-
// route would be a guess. NEVER auto-pick it. Interactive: show the
|
|
2696
|
-
// account picker so a human chooses with the (stale) numbers in
|
|
2697
|
-
// view. Unattended: fail loud with NO_VERIFIED_USAGE. The stale
|
|
2698
|
-
// pool survives ONLY as `resolved.rotation.healthy` for bounded
|
|
2699
|
-
// post-rejection failover, never as the initial pick.
|
|
2700
|
-
const { noVerifiedUsageDecision, pickRunAccountCandidate } = await import('./run-account-picker.js');
|
|
2701
|
-
const decision = noVerifiedUsageDecision({
|
|
2702
|
-
tty: isInteractiveTerminal(),
|
|
2703
|
-
json: options.json === true,
|
|
2704
|
-
headless: options.headless === true,
|
|
2705
|
-
});
|
|
2706
|
-
if (decision === 'picker') {
|
|
2707
|
-
const selected = await pickRunAccountCandidate(agent);
|
|
2708
|
-
// A cancelled picker launches nothing — same contract as the
|
|
2709
|
-
// trailing-# account picker and the sign-in launch above.
|
|
2710
|
-
if (!selected)
|
|
2711
|
-
return;
|
|
2712
|
-
version = selected.version;
|
|
2713
|
-
// Source the run.launch verdict from the account the user ACTUALLY
|
|
2714
|
-
// picked — the picker may deliberately return a logged-out one
|
|
2715
|
-
// (RUSH-2334), so it can differ from rotationResult.picked.
|
|
2716
|
-
launchSignedIn = selected.signedIn;
|
|
2717
|
-
launchEmail = selected.email;
|
|
2718
|
-
// Keep the rotation so mid-run failover can still cascade across
|
|
2719
|
-
// the other (stale) healthy accounts after a real rejection.
|
|
2831
|
+
else if (resolved.version) {
|
|
2832
|
+
version = resolved.version;
|
|
2720
2833
|
rotationResult = resolved.rotation;
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2834
|
+
// The auto-pick already computed the launchable-signed-in verdict for
|
|
2835
|
+
// this exact version via the same gate — reuse it for run.launch.
|
|
2836
|
+
if (resolved.rotation) {
|
|
2837
|
+
launchSignedIn = resolved.rotation.picked.signedIn;
|
|
2838
|
+
launchEmail = resolved.rotation.picked.email;
|
|
2724
2839
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
// API-key) carries `providerAccount`. Resolve its env through the
|
|
2742
|
-
// same `resolveSpawnAccount` path an explicit `--account` uses, so
|
|
2743
|
-
// exec injects the credential; a native pick has no providerAccount
|
|
2744
|
-
// and runs from its own version home unchanged (RUSH-3182).
|
|
2745
|
-
const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
|
|
2746
|
-
if (pickedProviderAccount) {
|
|
2747
|
-
try {
|
|
2748
|
-
const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
|
|
2749
|
-
if (picked?.kind === 'provider')
|
|
2750
|
-
accountEnv = picked.env;
|
|
2840
|
+
// A balanced/available pick of a PROVIDER account (setup-token /
|
|
2841
|
+
// API-key) carries `providerAccount`. Resolve its env through the
|
|
2842
|
+
// same `resolveSpawnAccount` path an explicit `--account` uses, so
|
|
2843
|
+
// exec injects the credential; a native pick has no providerAccount
|
|
2844
|
+
// and runs from its own version home unchanged (RUSH-3182).
|
|
2845
|
+
const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
|
|
2846
|
+
if (pickedProviderAccount) {
|
|
2847
|
+
try {
|
|
2848
|
+
const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
|
|
2849
|
+
if (picked?.kind === 'provider')
|
|
2850
|
+
accountEnv = picked.env;
|
|
2851
|
+
}
|
|
2852
|
+
catch (err) {
|
|
2853
|
+
console.error(chalk.red(err.message));
|
|
2854
|
+
process.exit(1);
|
|
2855
|
+
}
|
|
2751
2856
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
process.
|
|
2857
|
+
if (resolved.rotation && !options.quiet) {
|
|
2858
|
+
const banner = formatRotationBanner(resolved.rotation, strategy);
|
|
2859
|
+
process.stderr.write(chalk.gray(banner + '\n'));
|
|
2755
2860
|
}
|
|
2756
2861
|
}
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2759
|
-
|
|
2862
|
+
else if (!options.quiet) {
|
|
2863
|
+
// No installed version at all (not "accounts exhausted" — that
|
|
2864
|
+
// fails loud above): keep the pre-existing default resolution.
|
|
2865
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
|
|
2760
2866
|
}
|
|
2761
2867
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
catch (err) {
|
|
2769
|
-
if (!options.quiet) {
|
|
2770
|
-
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
|
|
2868
|
+
catch (err) {
|
|
2869
|
+
if (!options.quiet) {
|
|
2870
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
|
|
2871
|
+
}
|
|
2771
2872
|
}
|
|
2772
2873
|
}
|
|
2773
2874
|
}
|
|
2875
|
+
break;
|
|
2774
2876
|
}
|
|
2775
2877
|
// Self-heal the launch target. A gutted install (JS wrapper present,
|
|
2776
2878
|
// native binary missing — a partial/raced npm extraction of the optional
|
|
@@ -2899,9 +3001,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2899
3001
|
// alias for 'skip' (rewritten downstream by normalizeMode in exec.ts).
|
|
2900
3002
|
let mode = options.mode;
|
|
2901
3003
|
const modeSource = runCmd.getOptionValueSource('mode');
|
|
2902
|
-
|
|
3004
|
+
// A configured mode carried across a preflight handoff wins over the
|
|
3005
|
+
// alternate harness's own configured default (see handoffRunDefaults).
|
|
3006
|
+
const configuredMode = handoffRunDefaults?.mode ?? runDefaults.mode;
|
|
3007
|
+
const modeFromRunDefault = modeSource === 'default' && !!configuredMode;
|
|
2903
3008
|
if (modeFromRunDefault) {
|
|
2904
|
-
mode =
|
|
3009
|
+
mode = configuredMode;
|
|
2905
3010
|
}
|
|
2906
3011
|
if (!['plan', 'edit', 'auto', 'skip', 'full'].includes(mode)) {
|
|
2907
3012
|
console.error(chalk.red(`Invalid mode: ${mode}. Use plan, edit, auto, or skip ('full' accepted as alias for skip).`));
|
|
@@ -2952,7 +3057,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2952
3057
|
process.exit(1);
|
|
2953
3058
|
}
|
|
2954
3059
|
const effortSource = runCmd.getOptionValueSource('effort');
|
|
2955
|
-
const
|
|
3060
|
+
const configuredEffort = handoffRunDefaults?.effort ?? runDefaults.effort;
|
|
3061
|
+
const effort = (effortSource === 'default' && configuredEffort ? configuredEffort : options.effort);
|
|
2956
3062
|
if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
|
|
2957
3063
|
console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
|
|
2958
3064
|
process.exit(1);
|
|
@@ -3190,7 +3296,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
3190
3296
|
loop: !!options.loop,
|
|
3191
3297
|
resumeCheckpoint: !!options.resumeCheckpoint,
|
|
3192
3298
|
})) {
|
|
3193
|
-
|
|
3299
|
+
// With an explicit alternate harness configured, "use the alternate once
|
|
3300
|
+
// the primary's accounts are exhausted" is the policy — so EVERY healthy
|
|
3301
|
+
// same-agent account has to be tried first, not the default cap of three
|
|
3302
|
+
// (PHNX-3999 F19). Without one, the cap stands: it bounds how long a
|
|
3303
|
+
// rate-limited run keeps retrying itself.
|
|
3304
|
+
const failover = rotationFailoverChain(rotationResult, version, fallback.length > 0 ? (rotationResult.healthy.length || DEFAULT_ROTATION_FAILOVER_LIMIT) : undefined);
|
|
3194
3305
|
if (failover.length > 0) {
|
|
3195
3306
|
fallback.unshift(...failover);
|
|
3196
3307
|
if (!options.quiet) {
|