@phnx-labs/agents-cli 1.22.115 → 1.22.116
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 +149 -14
- package/README.md +1 -1
- package/dist/commands/browser.js +11 -0
- package/dist/commands/exec.js +212 -141
- 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 +24 -0
- package/dist/commands/setup-term.js +70 -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 +26 -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/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 +6 -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
|
@@ -2045,7 +2045,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2045
2045
|
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
2046
|
// Engine DATA ops resolve through the standalone `secrets` process client
|
|
2047
2047
|
// (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([
|
|
2048
|
+
{ 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
2049
|
import('../lib/exec.js'),
|
|
2050
2050
|
import('../lib/agents.js'),
|
|
2051
2051
|
import('../lib/profiles.js'),
|
|
@@ -2590,6 +2590,21 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2590
2590
|
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
2591
2591
|
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
2592
2592
|
let rotationResult = null;
|
|
2593
|
+
/**
|
|
2594
|
+
* The PRIMARY harness's configured run-default mode/effort, carried across a
|
|
2595
|
+
* preflight alternate-harness handoff (PHNX-3999 F19).
|
|
2596
|
+
*
|
|
2597
|
+
* Run defaults are resolved after the harness is known, so without this a
|
|
2598
|
+
* handoff would silently adopt the ALTERNATE's configured defaults — a
|
|
2599
|
+
* primary configured `mode: plan` becoming the alternate's `mode: skip`
|
|
2600
|
+
* escalates a read-only intent into an unattended writable run. The
|
|
2601
|
+
* canonical mid-run cascade keeps the primary's resolved mode for the same
|
|
2602
|
+
* reason (`runWithFallback` forwards `options.mode` and only re-derives the
|
|
2603
|
+
* mode that was never configured at all). An IMPLICIT mode is deliberately
|
|
2604
|
+
* not carried: it is the harness's own safe default, so the alternate's
|
|
2605
|
+
* applies (identical to `modeWasImplicit ? implicitModeFor(agent)`).
|
|
2606
|
+
*/
|
|
2607
|
+
let handoffRunDefaults;
|
|
2593
2608
|
// Precomputed launchable-signed-in verdict for the ACTUAL launched
|
|
2594
2609
|
// candidate, fed to the pre-launch `run.launch` event so it need not
|
|
2595
2610
|
// re-probe. Sourced per resolution branch from the candidate that WON, not
|
|
@@ -2622,155 +2637,202 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2622
2637
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
2623
2638
|
// `pinned` still resolves so a logged-out default can yield to a signed-in
|
|
2624
2639
|
// 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
|
-
|
|
2640
|
+
// Bounded handoff loop: each iteration resolves an account for `agent`, and
|
|
2641
|
+
// the only `continue` is the preflight exhaustion handoff below, which
|
|
2642
|
+
// consumes one entry of the `--fallback` spec before retrying as that
|
|
2643
|
+
// alternate. The spec is finite, so this terminates (PHNX-3999 F19).
|
|
2644
|
+
preflight: for (;;) {
|
|
2645
|
+
if (!accountPickerRequested && !configuredAccount && (!version || strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
2646
|
+
if (version) {
|
|
2647
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
2648
|
+
}
|
|
2649
|
+
else if (fromProfile) {
|
|
2650
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: custom harness pins its own version/auth\n`));
|
|
2651
|
+
}
|
|
2652
|
+
else {
|
|
2653
|
+
try {
|
|
2654
|
+
// Account-centric candidate list: native version-home logins PLUS
|
|
2655
|
+
// provider accounts (setup-token / API-key) that can auth this agent,
|
|
2656
|
+
// so `--strategy balanced` spreads across ALL accounts, not just the
|
|
2657
|
+
// ones sitting in a version home (RUSH-3182). Run-path only — the
|
|
2658
|
+
// picker's other callers keep the native-only collector.
|
|
2659
|
+
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2660
|
+
bootMark('resolve-version:start');
|
|
2661
|
+
const routingModel = options.model ?? resolvedResumeSource?.model ?? workflowModel
|
|
2662
|
+
?? (options.fallback ? undefined : resolveRunDefaults(agent, resolveVersion(agent, cwd), cwd).model);
|
|
2663
|
+
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun, routingModel);
|
|
2664
|
+
bootMark('resolve-version:done');
|
|
2665
|
+
if (resolved.exhausted) {
|
|
2666
|
+
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2667
|
+
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
2668
|
+
//
|
|
2669
|
+
// - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
|
|
2670
|
+
// The old behavior warned "found no usable version; falling back to
|
|
2671
|
+
// defaults" and launched the pinned default anyway — the exact move
|
|
2672
|
+
// that loops a rotate into an exhausted account. Only a window reset
|
|
2673
|
+
// clears it. The message text is a contract the Factory watchdog
|
|
2674
|
+
// tail-detects; do not reword it.
|
|
2675
|
+
// - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
|
|
2676
|
+
// because the harness's own TUI is the login surface. So on a TTY we
|
|
2677
|
+
// carry the user into that login instead of erroring. Exiting here
|
|
2678
|
+
// made `agents run <agent>`, `agents run <agent>#`, and `agents use`
|
|
2679
|
+
// all dead-end with no reachable way to authenticate.
|
|
2680
|
+
const recoverable = signInRecoverableCandidates(resolved.exhausted);
|
|
2681
|
+
const { signInLaunchDecision } = await import('./run-account-picker.js');
|
|
2682
|
+
const decision = signInLaunchDecision({
|
|
2683
|
+
recoverable: recoverable.length,
|
|
2684
|
+
tty: isInteractiveTerminal(),
|
|
2685
|
+
json: options.json === true,
|
|
2686
|
+
});
|
|
2687
|
+
if (decision === 'launch') {
|
|
2688
|
+
const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
|
|
2689
|
+
const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
|
|
2690
|
+
// A cancelled prompt launches nothing — same contract as the
|
|
2691
|
+
// trailing-# account picker above.
|
|
2692
|
+
if (!signInVersion)
|
|
2693
|
+
return;
|
|
2694
|
+
version = signInVersion;
|
|
2695
|
+
// We just told the user this account is logged out and why we're
|
|
2696
|
+
// launching it, so suppress the downstream login preflight — it
|
|
2697
|
+
// would print a second, near-identical "looks logged out" warning.
|
|
2698
|
+
signInLaunch = true;
|
|
2699
|
+
}
|
|
2700
|
+
else {
|
|
2701
|
+
// Every account of this harness is throttled. A configured
|
|
2702
|
+
// alternate exists for exactly this case, so hand off to it here
|
|
2703
|
+
// rather than exiting — `runWithFallback` would only reach it
|
|
2704
|
+
// after a run that can no longer start (PHNX-3999 F19). The
|
|
2705
|
+
// alternate becomes the primary and is dropped from the spec, so
|
|
2706
|
+
// the chain that remains never lists the agent now running.
|
|
2707
|
+
//
|
|
2708
|
+
// ONLY for a run shape `--fallback` is valid on. The handoff
|
|
2709
|
+
// consumes the entry it uses, and the canonical --fallback
|
|
2710
|
+
// validation runs later (it rejects interactive/--acp/--loop/
|
|
2711
|
+
// --resume-checkpoint chains and requires a prompt) — so
|
|
2712
|
+
// switching first on an ineligible shape would empty the spec,
|
|
2713
|
+
// leave that guard nothing to reject, and start the alternate
|
|
2714
|
+
// harness on a run the CLI refuses today. An ineligible shape
|
|
2715
|
+
// never switches, so the spec survives and still fails there
|
|
2716
|
+
// with its own message. See preflightHandoffEligible.
|
|
2717
|
+
const handoff = preflightHandoffEligible({
|
|
2718
|
+
hasPrompt: prompt !== undefined,
|
|
2719
|
+
interactive: options.interactive === true,
|
|
2720
|
+
acp: options.acp === true,
|
|
2721
|
+
loop: options.loop === true,
|
|
2722
|
+
resumeCheckpoint: !!options.resumeCheckpoint,
|
|
2723
|
+
resume: !!options.resume,
|
|
2724
|
+
workflowScoped: !!workflowToolsRestrict || !!workflowMcpConfigPath,
|
|
2725
|
+
})
|
|
2726
|
+
? preflightFallbackHandoff(options.fallback, agent, resolved.exhausted)
|
|
2727
|
+
: null;
|
|
2728
|
+
if (handoff) {
|
|
2729
|
+
process.stderr.write(chalk.yellow(`[agents] every ${agent} account is exhausted — handing off to ${handoff.agent}${handoff.version ? `@${handoff.version}` : ''}\n`));
|
|
2730
|
+
// Snapshot the PRIMARY's configured defaults BEFORE the switch —
|
|
2731
|
+
// this is the operator's intent for this run, not the alternate's.
|
|
2732
|
+
handoffRunDefaults ??= fromProfile ? undefined : resolveRunDefaults(agent, version ?? resolveVersion(agent, cwd), cwd);
|
|
2733
|
+
agent = handoff.agent;
|
|
2734
|
+
// Alias-resolve against the NEW harness's installs, exactly as
|
|
2735
|
+
// the canonical --fallback parse does for its own entries.
|
|
2736
|
+
version = resolveVersionAlias(agent, handoff.version);
|
|
2737
|
+
options.fallback = handoff.remainingSpec;
|
|
2738
|
+
rotationResult = null;
|
|
2739
|
+
continue preflight;
|
|
2740
|
+
}
|
|
2741
|
+
console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
|
|
2742
|
+
if (recoverable.length > 0) {
|
|
2743
|
+
// Off a TTY nobody can complete a login, so we still exit — but
|
|
2744
|
+
// name the actual fix rather than only offering --strategy pinned,
|
|
2745
|
+
// which would just pin the same unauthenticated account.
|
|
2746
|
+
const { loginHint } = await import('../lib/signin-badge.js');
|
|
2747
|
+
console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
|
|
2748
|
+
}
|
|
2749
|
+
process.exit(1);
|
|
2750
|
+
}
|
|
2679
2751
|
}
|
|
2680
|
-
else {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2752
|
+
else if (resolved.noVerifiedUsage) {
|
|
2753
|
+
// Entirely stale usage (PHNX-2526): every eligible account carries
|
|
2754
|
+
// a stale-but-present usage number and none is verified, so the
|
|
2755
|
+
// route would be a guess. NEVER auto-pick it. Interactive: show the
|
|
2756
|
+
// account picker so a human chooses with the (stale) numbers in
|
|
2757
|
+
// view. Unattended: fail loud with NO_VERIFIED_USAGE. The stale
|
|
2758
|
+
// pool survives ONLY as `resolved.rotation.healthy` for bounded
|
|
2759
|
+
// post-rejection failover, never as the initial pick.
|
|
2760
|
+
const { noVerifiedUsageDecision, pickRunAccountCandidate } = await import('./run-account-picker.js');
|
|
2761
|
+
const decision = noVerifiedUsageDecision({
|
|
2762
|
+
tty: isInteractiveTerminal(),
|
|
2763
|
+
json: options.json === true,
|
|
2764
|
+
headless: options.headless === true,
|
|
2765
|
+
});
|
|
2766
|
+
if (decision === 'picker') {
|
|
2767
|
+
const selected = await pickRunAccountCandidate(agent);
|
|
2768
|
+
// A cancelled picker launches nothing — same contract as the
|
|
2769
|
+
// trailing-# account picker and the sign-in launch above.
|
|
2770
|
+
if (!selected)
|
|
2771
|
+
return;
|
|
2772
|
+
version = selected.version;
|
|
2773
|
+
// Source the run.launch verdict from the account the user ACTUALLY
|
|
2774
|
+
// picked — the picker may deliberately return a logged-out one
|
|
2775
|
+
// (RUSH-2334), so it can differ from rotationResult.picked.
|
|
2776
|
+
launchSignedIn = selected.signedIn;
|
|
2777
|
+
launchEmail = selected.email;
|
|
2778
|
+
// Keep the rotation so mid-run failover can still cascade across
|
|
2779
|
+
// the other (stale) healthy accounts after a real rejection.
|
|
2780
|
+
rotationResult = resolved.rotation;
|
|
2781
|
+
if (!options.quiet) {
|
|
2782
|
+
const identity = selected.accountLabel || 'signed-in account';
|
|
2783
|
+
process.stderr.write(chalk.gray(`[agents] no fresh usage for any ${agent} account — you picked ${identity} · ${agent}@${selected.version}\n`));
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
else {
|
|
2787
|
+
console.error(chalk.red(formatNoVerifiedUsageError(agent, strategy, resolved.rotation?.healthy ?? [])));
|
|
2788
|
+
process.exit(1);
|
|
2688
2789
|
}
|
|
2689
|
-
process.exit(1);
|
|
2690
2790
|
}
|
|
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.
|
|
2791
|
+
else if (resolved.version) {
|
|
2792
|
+
version = resolved.version;
|
|
2720
2793
|
rotationResult = resolved.rotation;
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2794
|
+
// The auto-pick already computed the launchable-signed-in verdict for
|
|
2795
|
+
// this exact version via the same gate — reuse it for run.launch.
|
|
2796
|
+
if (resolved.rotation) {
|
|
2797
|
+
launchSignedIn = resolved.rotation.picked.signedIn;
|
|
2798
|
+
launchEmail = resolved.rotation.picked.email;
|
|
2724
2799
|
}
|
|
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;
|
|
2800
|
+
// A balanced/available pick of a PROVIDER account (setup-token /
|
|
2801
|
+
// API-key) carries `providerAccount`. Resolve its env through the
|
|
2802
|
+
// same `resolveSpawnAccount` path an explicit `--account` uses, so
|
|
2803
|
+
// exec injects the credential; a native pick has no providerAccount
|
|
2804
|
+
// and runs from its own version home unchanged (RUSH-3182).
|
|
2805
|
+
const pickedProviderAccount = resolved.rotation?.picked.providerAccount;
|
|
2806
|
+
if (pickedProviderAccount) {
|
|
2807
|
+
try {
|
|
2808
|
+
const picked = resolveSpawnAccount(pickedProviderAccount, agent, resolved.version, readMeta(), { useDefault: false });
|
|
2809
|
+
if (picked?.kind === 'provider')
|
|
2810
|
+
accountEnv = picked.env;
|
|
2811
|
+
}
|
|
2812
|
+
catch (err) {
|
|
2813
|
+
console.error(chalk.red(err.message));
|
|
2814
|
+
process.exit(1);
|
|
2815
|
+
}
|
|
2751
2816
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
process.
|
|
2817
|
+
if (resolved.rotation && !options.quiet) {
|
|
2818
|
+
const banner = formatRotationBanner(resolved.rotation, strategy);
|
|
2819
|
+
process.stderr.write(chalk.gray(banner + '\n'));
|
|
2755
2820
|
}
|
|
2756
2821
|
}
|
|
2757
|
-
if (
|
|
2758
|
-
|
|
2759
|
-
|
|
2822
|
+
else if (!options.quiet) {
|
|
2823
|
+
// No installed version at all (not "accounts exhausted" — that
|
|
2824
|
+
// fails loud above): keep the pre-existing default resolution.
|
|
2825
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} found no usable ${agent} version; falling back to defaults\n`));
|
|
2760
2826
|
}
|
|
2761
2827
|
}
|
|
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`));
|
|
2828
|
+
catch (err) {
|
|
2829
|
+
if (!options.quiet) {
|
|
2830
|
+
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} skipped: ${err.message}\n`));
|
|
2831
|
+
}
|
|
2771
2832
|
}
|
|
2772
2833
|
}
|
|
2773
2834
|
}
|
|
2835
|
+
break;
|
|
2774
2836
|
}
|
|
2775
2837
|
// Self-heal the launch target. A gutted install (JS wrapper present,
|
|
2776
2838
|
// native binary missing — a partial/raced npm extraction of the optional
|
|
@@ -2899,9 +2961,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2899
2961
|
// alias for 'skip' (rewritten downstream by normalizeMode in exec.ts).
|
|
2900
2962
|
let mode = options.mode;
|
|
2901
2963
|
const modeSource = runCmd.getOptionValueSource('mode');
|
|
2902
|
-
|
|
2964
|
+
// A configured mode carried across a preflight handoff wins over the
|
|
2965
|
+
// alternate harness's own configured default (see handoffRunDefaults).
|
|
2966
|
+
const configuredMode = handoffRunDefaults?.mode ?? runDefaults.mode;
|
|
2967
|
+
const modeFromRunDefault = modeSource === 'default' && !!configuredMode;
|
|
2903
2968
|
if (modeFromRunDefault) {
|
|
2904
|
-
mode =
|
|
2969
|
+
mode = configuredMode;
|
|
2905
2970
|
}
|
|
2906
2971
|
if (!['plan', 'edit', 'auto', 'skip', 'full'].includes(mode)) {
|
|
2907
2972
|
console.error(chalk.red(`Invalid mode: ${mode}. Use plan, edit, auto, or skip ('full' accepted as alias for skip).`));
|
|
@@ -2952,7 +3017,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2952
3017
|
process.exit(1);
|
|
2953
3018
|
}
|
|
2954
3019
|
const effortSource = runCmd.getOptionValueSource('effort');
|
|
2955
|
-
const
|
|
3020
|
+
const configuredEffort = handoffRunDefaults?.effort ?? runDefaults.effort;
|
|
3021
|
+
const effort = (effortSource === 'default' && configuredEffort ? configuredEffort : options.effort);
|
|
2956
3022
|
if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
|
|
2957
3023
|
console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
|
|
2958
3024
|
process.exit(1);
|
|
@@ -3190,7 +3256,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
3190
3256
|
loop: !!options.loop,
|
|
3191
3257
|
resumeCheckpoint: !!options.resumeCheckpoint,
|
|
3192
3258
|
})) {
|
|
3193
|
-
|
|
3259
|
+
// With an explicit alternate harness configured, "use the alternate once
|
|
3260
|
+
// the primary's accounts are exhausted" is the policy — so EVERY healthy
|
|
3261
|
+
// same-agent account has to be tried first, not the default cap of three
|
|
3262
|
+
// (PHNX-3999 F19). Without one, the cap stands: it bounds how long a
|
|
3263
|
+
// rate-limited run keeps retrying itself.
|
|
3264
|
+
const failover = rotationFailoverChain(rotationResult, version, fallback.length > 0 ? (rotationResult.healthy.length || DEFAULT_ROTATION_FAILOVER_LIMIT) : undefined);
|
|
3194
3265
|
if (failover.length > 0) {
|
|
3195
3266
|
fallback.unshift(...failover);
|
|
3196
3267
|
if (!options.quiet) {
|
package/dist/commands/feed.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, publishBlock, } from '../lib/feed/feed.js';
|
|
2
|
+
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified, buildDeclaredBlock, deriveBlockState, publishBlock, } from '../lib/feed/feed.js';
|
|
3
3
|
import { ensureActivityLogHook, readRecentActivity, formatActivityLine, formatProgressUpdate, mergeActivityEvents, parseActivityPayload, } from '../lib/feed/activity.js';
|
|
4
4
|
import { projectKeyFromCwd } from '../lib/project-key.js';
|
|
5
5
|
import { postFeedStatus } from '../lib/feed-post.js';
|
|
@@ -16,7 +16,7 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
16
16
|
import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
|
|
17
17
|
import { notifyUrgentBlock } from '../lib/notify.js';
|
|
18
18
|
import { registerFeedWatchCommand } from './feed-watch.js';
|
|
19
|
-
import { claimAndRouteAttentionAnswer, forwardFeedAnswer } from '../lib/feed/answer.js';
|
|
19
|
+
import { AnswerError, answerOwnerIsLocal, checkAnswerDelivery, claimAndRouteAttentionAnswer, forwardFeedAnswer, parseAttentionKey, } from '../lib/feed/answer.js';
|
|
20
20
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
21
21
|
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
22
22
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
@@ -171,7 +171,10 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
171
171
|
const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
|
|
172
172
|
const rank = b.delayRank ? chalk.gray(`rank:${Math.round(b.delayRank.score)}`) : '';
|
|
173
173
|
// Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
|
|
174
|
-
|
|
174
|
+
// A PENDING claim carries an answer record while the block is still open, so
|
|
175
|
+
// the delivered glyph is driven by the canonical state, not by `b.answer`
|
|
176
|
+
// being set — a claimed-but-undelivered ask must keep reading as needs-you.
|
|
177
|
+
const marker = deriveBlockState(b) !== 'open'
|
|
175
178
|
? chalk.green(GLYPH.delivered)
|
|
176
179
|
: b.kind === 'control'
|
|
177
180
|
? chalk.red('!')
|
|
@@ -197,7 +200,9 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
197
200
|
if (b.answer) {
|
|
198
201
|
const verified = b.answer.verified ? chalk.green(GLYPH.delivered) : chalk.yellow('?');
|
|
199
202
|
const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
|
|
200
|
-
|
|
203
|
+
const pending = deriveBlockState(b) === 'open';
|
|
204
|
+
const label = pending ? chalk.yellow('claimed (delivery unconfirmed)') : chalk.green('answered');
|
|
205
|
+
console.log(`${indent} ${label} by ${who} ${verified}`);
|
|
201
206
|
}
|
|
202
207
|
if (b.parkedAt) {
|
|
203
208
|
console.log(`${indent} ${chalk.red('hard-parked')} ${relTime(b.parkedAt)}`);
|
|
@@ -314,6 +319,23 @@ export async function loadSessionMetasForFeedEnrichment(load) {
|
|
|
314
319
|
throw err;
|
|
315
320
|
}
|
|
316
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* One human line per answer outcome. The three states the operator has to tell
|
|
324
|
+
* apart are a real receipt, a hand-off no rail can confirm, and a refusal —
|
|
325
|
+
* never collapsed into a single "Delivered" (PHNX-3999).
|
|
326
|
+
*/
|
|
327
|
+
function renderAnswerResult(result) {
|
|
328
|
+
switch (result.status) {
|
|
329
|
+
case 'failed':
|
|
330
|
+
return `Not delivered: ${result.reason ?? 'unknown failure'} — answering again is safe.`;
|
|
331
|
+
case 'unknown':
|
|
332
|
+
return `Delivery unconfirmed: ${result.reason ?? 'no rail reported a receipt.'} Run \`agents feed answer ${result.attentionKey} --check\` rather than resending.`;
|
|
333
|
+
case 'already_answered':
|
|
334
|
+
return `Already answered — ${result.receipt?.status ?? 'claimed'}${result.receipt ? ` as ${result.receipt.msgId}` : ''} at ${result.receipt?.at ?? result.attempt}.`;
|
|
335
|
+
default:
|
|
336
|
+
return `Delivered ${result.receipt?.msgId} (${result.receipt?.status}).`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
317
339
|
export function registerFeedCommand(program) {
|
|
318
340
|
const feed = program
|
|
319
341
|
.command('feed')
|
|
@@ -334,23 +356,52 @@ export function registerFeedCommand(program) {
|
|
|
334
356
|
.option('--choice <choice-id>', 'Stable choice id from the attention item')
|
|
335
357
|
.option('--text <answer>', 'Free-text answer')
|
|
336
358
|
.option('--as <operator>', 'Verified operator id for high-consequence answers')
|
|
359
|
+
.option('--check', 'Read-only: report this item\'s delivery state without claiming, routing or resending')
|
|
360
|
+
.option('--attempt <at>', 'With --check: the attempt timestamp being checked, so a newer one is reported as such')
|
|
337
361
|
.option('--json', 'Emit the delivery result as JSON')
|
|
338
362
|
.action(async (attentionKey, opts, invoked) => {
|
|
363
|
+
const wantsJson = Boolean(opts.json || invoked.parent?.opts()?.json);
|
|
339
364
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (opts.
|
|
365
|
+
// The key's own host decides the rail; `answerOwnerIsLocal` normalizes
|
|
366
|
+
// both sides so a `.local` suffix is not read as a different machine.
|
|
367
|
+
const { host: ownerHost } = parseAttentionKey(attentionKey);
|
|
368
|
+
const local = answerOwnerIsLocal(ownerHost);
|
|
369
|
+
if (opts.check && (opts.choice != null || opts.text != null)) {
|
|
370
|
+
throw new AnswerError('--check is read-only; it takes no --choice or --text.', 'empty_answer');
|
|
371
|
+
}
|
|
372
|
+
if (opts.attempt != null && !opts.check) {
|
|
373
|
+
throw new AnswerError('--attempt only applies to --check.', 'empty_answer');
|
|
374
|
+
}
|
|
375
|
+
const result = opts.check
|
|
376
|
+
? (local
|
|
377
|
+
? checkAnswerDelivery(attentionKey, undefined, opts.attempt)
|
|
378
|
+
: await forwardFeedAnswer({ host: ownerHost, attentionKey, check: true, attempt: opts.attempt, operatorId: opts.as }))
|
|
379
|
+
: local
|
|
380
|
+
? await claimAndRouteAttentionAnswer({
|
|
381
|
+
attentionKey, choiceId: opts.choice, text: opts.text,
|
|
382
|
+
operator: { id: opts.as, verified: Boolean(opts.as), label: opts.as },
|
|
383
|
+
})
|
|
384
|
+
: await forwardFeedAnswer({ host: ownerHost, attentionKey, choiceId: opts.choice, text: opts.text, operatorId: opts.as });
|
|
385
|
+
if (wantsJson)
|
|
348
386
|
console.log(JSON.stringify(result));
|
|
349
387
|
else
|
|
350
|
-
console.log(
|
|
388
|
+
console.log(renderAnswerResult(result));
|
|
351
389
|
}
|
|
352
390
|
catch (error) {
|
|
353
|
-
|
|
391
|
+
// A failure is a first-class outcome for the operator UI, not a parse
|
|
392
|
+
// error: `--json` still gets a result object it can branch on, with a
|
|
393
|
+
// non-zero exit so a scripted caller sees the failure too.
|
|
394
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
395
|
+
if (wantsJson) {
|
|
396
|
+
const failure = {
|
|
397
|
+
status: 'failed', delivery: 'failed', resolved: false, reason: message,
|
|
398
|
+
code: error instanceof AnswerError ? error.code : 'rail_failed', attentionKey,
|
|
399
|
+
};
|
|
400
|
+
console.log(JSON.stringify(failure));
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
invoked.error(`error: ${message}`);
|
|
354
405
|
}
|
|
355
406
|
});
|
|
356
407
|
feed
|
|
@@ -153,6 +153,17 @@ export interface SessionPreviewDigest {
|
|
|
153
153
|
firstError?: string;
|
|
154
154
|
toolHistogram: ReturnType<typeof toolHistogram>;
|
|
155
155
|
test: ReturnType<typeof detectTestResult>;
|
|
156
|
+
/**
|
|
157
|
+
* True when this digest was built WITHOUT parsing the transcript (PHNX-3999):
|
|
158
|
+
* the file exceeded {@link PREVIEW_DIGEST_MAX_PARSE_BYTES} and no cached
|
|
159
|
+
* digest existed yet. `firstUser`/`lastAssistant` fall back to the already-
|
|
160
|
+
* indexed `SessionMeta.firstUserMessage`/`lastUserMessage` (cheap, no parse)
|
|
161
|
+
* rather than being silently empty; every event-derived field (toolCalls,
|
|
162
|
+
* changedFiles, artifacts, etc.) stays at its zero-value default because it
|
|
163
|
+
* genuinely was not computed, not because nothing happened.
|
|
164
|
+
*/
|
|
165
|
+
partial?: boolean;
|
|
166
|
+
partialReason?: string;
|
|
156
167
|
}
|
|
157
168
|
/** Fold a harness-normalized event stream into the stable preview data model. */
|
|
158
169
|
export declare function buildSessionPreviewDigest(events: SessionEvent[], session: SessionMeta): SessionPreviewDigest;
|