@phnx-labs/agents-cli 1.22.26 → 1.22.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -12,7 +12,8 @@ import { buildLocalFindings, fleetDivergenceToFindings, signInToFindings, render
|
|
|
12
12
|
import { getCliVersion } from '../lib/version.js';
|
|
13
13
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
14
14
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
15
|
-
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
15
|
+
import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
|
|
16
|
+
import { deviceIdentityArgs } from '../lib/devices/connect.js';
|
|
16
17
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
17
18
|
import { findAmbiguousDevicePins } from '../lib/routines.js';
|
|
18
19
|
import chalk from 'chalk';
|
|
@@ -33,6 +34,7 @@ import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
|
|
|
33
34
|
import { setHelpSections } from '../lib/help.js';
|
|
34
35
|
import { heal, healChangedAnything } from '../lib/heal.js';
|
|
35
36
|
import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
|
|
37
|
+
import { auditWindowsSshEnrollment, diagnoseWindowsSshFailure } from '../lib/devices/windows-ssh-enrollment.js';
|
|
36
38
|
import { scanUserRcFiles, masterPassphraseInEnv } from '../lib/secrets/rc-hygiene.js';
|
|
37
39
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
38
40
|
import { readRepoBehindMarkers } from '../lib/auto-pull.js';
|
|
@@ -83,14 +85,14 @@ function printWrappedLine(prefix, text) {
|
|
|
83
85
|
console.log(chalk.gray(line));
|
|
84
86
|
}
|
|
85
87
|
/**
|
|
86
|
-
* Narrow a remote `findings` array to
|
|
88
|
+
* Narrow a remote `findings` array to rows only that device can observe.
|
|
87
89
|
*
|
|
88
90
|
* Deliberately NOT "forward every remote finding". The aggregator already
|
|
89
91
|
* rebuilds a remote's sign-in rows from its inventory and its divergence rows
|
|
90
92
|
* from the comparator, so forwarding wholesale would double them; and pulling a
|
|
91
93
|
* remote's orphan/drift rows into a fleet readout is a much larger UX change
|
|
92
|
-
* than this fix.
|
|
93
|
-
*
|
|
94
|
+
* than this fix. These kinds are unrecomputable centrally: shell/process secret
|
|
95
|
+
* hygiene and the Windows host's effective OpenSSH key path/content/ACL.
|
|
94
96
|
*
|
|
95
97
|
* **The remote contributes exactly one thing: the KIND.** Severity, message and
|
|
96
98
|
* remediation are all generated HERE. That is not defensiveness for its own
|
|
@@ -111,13 +113,15 @@ function printWrappedLine(prefix, text) {
|
|
|
111
113
|
* file and line. Run `agents doctor` on that box for the specifics — the
|
|
112
114
|
* message says so.
|
|
113
115
|
*/
|
|
114
|
-
const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export'];
|
|
116
|
+
const REMOTE_FORWARDED_KINDS = ['rc-secret-export', 'env-secret-export', 'ssh-key-enrollment'];
|
|
115
117
|
/** Canonical, locally-authored text for a forwarded kind. Never the remote's. */
|
|
116
118
|
const REMOTE_SECRET_MESSAGE = {
|
|
117
119
|
'rc-secret-export': 'a credential-shaped export was found in this box\'s shell rc files'
|
|
118
120
|
+ ' — run `agents doctor` there for the file and line',
|
|
119
121
|
'env-secret-export': 'AGENTS_SECRETS_PASSPHRASE is set in this box\'s process environment'
|
|
120
122
|
+ ' — run `agents doctor` there for detail',
|
|
123
|
+
'ssh-key-enrollment': 'Windows OpenSSH key enrollment is invalid'
|
|
124
|
+
+ ' — run `agents doctor` on this box for the effective path or ACL failure',
|
|
121
125
|
};
|
|
122
126
|
export function asRemoteSecretFindings(raw, device) {
|
|
123
127
|
if (!Array.isArray(raw))
|
|
@@ -158,11 +162,12 @@ async function resolveFleetTargets(opts) {
|
|
|
158
162
|
name: deviceProfile.name,
|
|
159
163
|
sshTarget: deviceProfile.name,
|
|
160
164
|
os: deviceProfile.platform !== 'unknown' ? deviceProfile.platform : undefined,
|
|
165
|
+
extraSshArgs: deviceIdentityArgs(deviceProfile),
|
|
161
166
|
}];
|
|
162
167
|
}
|
|
163
168
|
const host = await resolveHost(singleName);
|
|
164
169
|
if (host) {
|
|
165
|
-
return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os }];
|
|
170
|
+
return [{ name: singleName, sshTarget: sshTargetFor(host), os: host.os, extraSshArgs: hostIdentityArgs(host) }];
|
|
166
171
|
}
|
|
167
172
|
console.error(chalk.red(`Unknown host or device '${singleName}'.`));
|
|
168
173
|
process.exit(1);
|
|
@@ -180,6 +185,7 @@ async function resolveFleetTargets(opts) {
|
|
|
180
185
|
name: d.name,
|
|
181
186
|
sshTarget: d.name,
|
|
182
187
|
os: d.platform !== 'unknown' ? d.platform : undefined,
|
|
188
|
+
extraSshArgs: deviceIdentityArgs(d),
|
|
183
189
|
}));
|
|
184
190
|
}
|
|
185
191
|
async function probeFleetTarget(target) {
|
|
@@ -190,12 +196,14 @@ async function probeFleetTarget(target) {
|
|
|
190
196
|
// has it via the install profile, and our single-quote escaping would
|
|
191
197
|
// prevent $HOME expansion there, so skip the bootstrap on Windows.
|
|
192
198
|
isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
193
|
-
const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
199
|
+
const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
194
200
|
if (res.code !== 0) {
|
|
195
201
|
return {
|
|
196
202
|
name: target.name,
|
|
197
203
|
online: false,
|
|
198
|
-
error:
|
|
204
|
+
error: isWin
|
|
205
|
+
? diagnoseWindowsSshFailure(res.stderr, res.timedOut)
|
|
206
|
+
: res.timedOut ? 'timed out' : (res.stderr || `exit ${res.code ?? 'unknown'}`),
|
|
199
207
|
agents: {},
|
|
200
208
|
};
|
|
201
209
|
}
|
|
@@ -238,7 +246,11 @@ export const FLEET_INVENTORY_TIMEOUT_MS = 180_000;
|
|
|
238
246
|
async function probeFleetInventory(target) {
|
|
239
247
|
const isWin = /^win/i.test((target.os ?? '').trim());
|
|
240
248
|
const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
241
|
-
const res = await sshExecAsync(target.sshTarget, remoteCmd, {
|
|
249
|
+
const res = await sshExecAsync(target.sshTarget, remoteCmd, {
|
|
250
|
+
timeoutMs: FLEET_INVENTORY_TIMEOUT_MS,
|
|
251
|
+
multiplex: true,
|
|
252
|
+
extraSshArgs: target.extraSshArgs,
|
|
253
|
+
});
|
|
242
254
|
if (res.code !== 0)
|
|
243
255
|
return null;
|
|
244
256
|
try {
|
|
@@ -404,6 +416,7 @@ async function runDevicesDoctor(opts) {
|
|
|
404
416
|
execPolicy: process.platform === 'win32'
|
|
405
417
|
? { platform: process.platform, policy: getEffectiveExecutionPolicy() }
|
|
406
418
|
: undefined,
|
|
419
|
+
windowsSshEnrollment: auditWindowsSshEnrollment(),
|
|
407
420
|
isolatedVersions: localReports
|
|
408
421
|
.filter((rep) => isVersionIsolated(rep.agent, rep.version))
|
|
409
422
|
.map((rep) => `${rep.agent}@${rep.version}`),
|
|
@@ -1182,7 +1195,7 @@ function checkPayload(device, drift) {
|
|
|
1182
1195
|
async function probeDeviceCheck(target) {
|
|
1183
1196
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
1184
1197
|
const remoteCmd = buildRemoteAgentsInvocation(['doctor', '--check', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
1185
|
-
const res = await sshExecAsync(target.dialTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
1198
|
+
const res = await sshExecAsync(target.dialTarget, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
1186
1199
|
if (res.code !== 0 && !res.stdout.trim()) {
|
|
1187
1200
|
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
1188
1201
|
}
|
|
@@ -1211,6 +1224,7 @@ async function runDevicesCheck(opts, cwd) {
|
|
|
1211
1224
|
platform: t.device.platform,
|
|
1212
1225
|
skip: t.skip,
|
|
1213
1226
|
dialTarget: fleetDialTarget(t.device),
|
|
1227
|
+
extraSshArgs: deviceIdentityArgs(t.device),
|
|
1214
1228
|
}));
|
|
1215
1229
|
const remote = await fanOutDevices(remoteTargets, probeDeviceCheck);
|
|
1216
1230
|
const devices = [local];
|
package/dist/commands/exec.js
CHANGED
|
@@ -554,7 +554,7 @@ export function registerRunCommand(program) {
|
|
|
554
554
|
.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.')
|
|
555
555
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
556
556
|
.option('--where <spec>', 'Where this run\'s body executes (one placement door): local | device:<name> | auto | lease[:backend] | cloud[:provider]. Expands to --host/--lease/--cloud. Do not combine with those flags. See docs/00-concepts.md#placement.')
|
|
557
|
-
.option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, or user@host. Pass "auto" to pick
|
|
557
|
+
.option('--host <name>', 'Offload this run onto another machine over SSH — a device name, registered host, 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. Same as --where device:<name>. See `agents devices`.')
|
|
558
558
|
.option('--device <name>', 'Alias of --host. Pass "auto" for affinity-based device pick (same as --where auto).')
|
|
559
559
|
.option('--remote-cwd <dir>', "Explicit host working directory for --host 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).")
|
|
560
560
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
@@ -947,13 +947,18 @@ export function registerRunCommand(program) {
|
|
|
947
947
|
if (!resolvedResumeSource && runAutoDefaultsToAffinity(options))
|
|
948
948
|
options.device = 'auto';
|
|
949
949
|
}
|
|
950
|
-
// --device auto / --host auto (and deprecated --smart):
|
|
950
|
+
// --device auto / --host auto (and deprecated --smart): live fleet pick.
|
|
951
951
|
// Harness is always the agent the user typed — never auto-picked.
|
|
952
952
|
// Affinity failure degrades to local (does not kill the run).
|
|
953
953
|
{
|
|
954
954
|
const { applyDeviceAutoToOptions } = await import('../lib/smart-launch.js');
|
|
955
|
-
const result = applyDeviceAutoToOptions(options, {
|
|
955
|
+
const result = await applyDeviceAutoToOptions(options, {
|
|
956
956
|
accountPickerRequested,
|
|
957
|
+
// `run auto` selects its harness after placement, so do not filter
|
|
958
|
+
// candidates against an arbitrary proxy harness at this stage.
|
|
959
|
+
agent: normalizedAgentSpec.split('@')[0] === RUN_AUTO_KEYWORD
|
|
960
|
+
? undefined
|
|
961
|
+
: (resolveAgentName(normalizedAgentSpec.split('@')[0]) ?? undefined),
|
|
957
962
|
});
|
|
958
963
|
if (!options.quiet && result.deprecationSmart) {
|
|
959
964
|
process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
|
|
@@ -964,7 +969,7 @@ export function registerRunCommand(program) {
|
|
|
964
969
|
if (!options.quiet && result.banner) {
|
|
965
970
|
const { hostLabel, deviceHint, acctNote } = result.banner;
|
|
966
971
|
process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
|
|
967
|
-
(deviceHint ? ` (
|
|
972
|
+
(deviceHint ? ` (load ${deviceHint})` : '') +
|
|
968
973
|
` · ${acctNote}\n`));
|
|
969
974
|
}
|
|
970
975
|
}
|
|
@@ -1790,7 +1795,7 @@ export function registerRunCommand(program) {
|
|
|
1790
1795
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
1791
1796
|
process.exit(resumeExit);
|
|
1792
1797
|
}
|
|
1793
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
1798
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError, signInRecoverableCandidates }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
1794
1799
|
import('../lib/exec.js'),
|
|
1795
1800
|
import('../lib/agents.js'),
|
|
1796
1801
|
import('../lib/profiles.js'),
|
|
@@ -2268,22 +2273,18 @@ export function registerRunCommand(program) {
|
|
|
2268
2273
|
process.exit(1);
|
|
2269
2274
|
}
|
|
2270
2275
|
version = resolvedRecoveryTarget.version;
|
|
2271
|
-
|
|
2272
|
-
if (canResumeNatively) {
|
|
2276
|
+
if (resolvedRecoveryTarget.mode === 'native') {
|
|
2273
2277
|
version = session.version;
|
|
2274
2278
|
resumeNative = true;
|
|
2275
2279
|
resumeSessionId = session.id;
|
|
2276
|
-
//
|
|
2277
|
-
//
|
|
2278
|
-
//
|
|
2279
|
-
//
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
// the caller passed one (they're overriding on purpose).
|
|
2283
|
-
if (!options.cwd && session.cwd)
|
|
2284
|
-
options.cwd = session.cwd;
|
|
2280
|
+
// The centralized recovery decision proves the transcript belongs to
|
|
2281
|
+
// this exact isolated home and resolves any harness-specific launch cwd.
|
|
2282
|
+
// Claude's indexed `session.cwd` is the first user-turn cwd, which may
|
|
2283
|
+
// differ from the earlier cwd that selected projects/<cwd-key>.
|
|
2284
|
+
if (!options.cwd && resolvedRecoveryTarget.cwd)
|
|
2285
|
+
options.cwd = resolvedRecoveryTarget.cwd;
|
|
2285
2286
|
if (!options.quiet)
|
|
2286
|
-
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''}
|
|
2287
|
+
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
|
|
2287
2288
|
}
|
|
2288
2289
|
else {
|
|
2289
2290
|
// Tier-2: launch fresh with a /continue <id> first message; the agent
|
|
@@ -2301,6 +2302,9 @@ export function registerRunCommand(program) {
|
|
|
2301
2302
|
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
2302
2303
|
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
2303
2304
|
let rotationResult = null;
|
|
2305
|
+
// Set when the zero-healthy path already announced a deliberate
|
|
2306
|
+
// launch-to-sign-in, so the login preflight below does not repeat it.
|
|
2307
|
+
let signInLaunch = false;
|
|
2304
2308
|
if (options.strategy && !explicitStrategy) {
|
|
2305
2309
|
console.error(chalk.red(`Invalid strategy: ${options.strategy}. Use ${RUN_STRATEGIES.join(', ')}.`));
|
|
2306
2310
|
process.exit(1);
|
|
@@ -2327,15 +2331,53 @@ export function registerRunCommand(program) {
|
|
|
2327
2331
|
try {
|
|
2328
2332
|
const resolved = await resolveRunVersion(agent, strategy, cwd);
|
|
2329
2333
|
if (resolved.exhausted) {
|
|
2330
|
-
//
|
|
2331
|
-
//
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
2334
|
-
//
|
|
2335
|
-
|
|
2336
|
-
|
|
2334
|
+
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2335
|
+
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
2336
|
+
//
|
|
2337
|
+
// - THROTTLED (rate_limited / out_of_credits) -> fail loud (RUSH-2132).
|
|
2338
|
+
// The old behavior warned "found no usable version; falling back to
|
|
2339
|
+
// defaults" and launched the pinned default anyway — the exact move
|
|
2340
|
+
// that loops a rotate into an exhausted account. Only a window reset
|
|
2341
|
+
// clears it. The message text is a contract the Factory watchdog
|
|
2342
|
+
// tail-detects; do not reword it.
|
|
2343
|
+
// - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
|
|
2344
|
+
// because the harness's own TUI is the login surface. So on a TTY we
|
|
2345
|
+
// carry the user into that login instead of erroring. Exiting here
|
|
2346
|
+
// made `agents run <agent>`, `agents run <agent>@`, and `agents use`
|
|
2347
|
+
// all dead-end with no reachable way to authenticate.
|
|
2348
|
+
const recoverable = signInRecoverableCandidates(resolved.exhausted);
|
|
2349
|
+
const { signInLaunchDecision } = await import('./run-account-picker.js');
|
|
2350
|
+
const decision = signInLaunchDecision({
|
|
2351
|
+
recoverable: recoverable.length,
|
|
2352
|
+
tty: isInteractiveTerminal(),
|
|
2353
|
+
json: options.json === true,
|
|
2354
|
+
});
|
|
2355
|
+
if (decision === 'launch') {
|
|
2356
|
+
const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
|
|
2357
|
+
const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
|
|
2358
|
+
// A cancelled prompt launches nothing — same contract as the
|
|
2359
|
+
// trailing-@ account picker above.
|
|
2360
|
+
if (!signInVersion)
|
|
2361
|
+
return;
|
|
2362
|
+
version = signInVersion;
|
|
2363
|
+
// We just told the user this account is logged out and why we're
|
|
2364
|
+
// launching it, so suppress the downstream login preflight — it
|
|
2365
|
+
// would print a second, near-identical "looks logged out" warning.
|
|
2366
|
+
signInLaunch = true;
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
console.error(chalk.red(formatNoHealthyAccountError(agent, strategy, resolved.exhausted)));
|
|
2370
|
+
if (recoverable.length > 0) {
|
|
2371
|
+
// Off a TTY nobody can complete a login, so we still exit — but
|
|
2372
|
+
// name the actual fix rather than only offering --strategy pinned,
|
|
2373
|
+
// which would just pin the same unauthenticated account.
|
|
2374
|
+
const { loginHint } = await import('../lib/signin-badge.js');
|
|
2375
|
+
console.error(chalk.gray(`To sign in: ${loginHint(agent)} — or run \`agents run ${agent}\` from a terminal.`));
|
|
2376
|
+
}
|
|
2377
|
+
process.exit(1);
|
|
2378
|
+
}
|
|
2337
2379
|
}
|
|
2338
|
-
if (resolved.version) {
|
|
2380
|
+
else if (resolved.version) {
|
|
2339
2381
|
version = resolved.version;
|
|
2340
2382
|
rotationResult = resolved.rotation;
|
|
2341
2383
|
if (resolved.rotation && !options.quiet) {
|
|
@@ -2427,7 +2469,10 @@ export function registerRunCommand(program) {
|
|
|
2427
2469
|
json: options.json,
|
|
2428
2470
|
quiet: options.quiet,
|
|
2429
2471
|
authCheckDisabled: options.authCheck === false || process.env.AGENTS_NO_AUTH_CHECK === '1',
|
|
2430
|
-
|
|
2472
|
+
// `signInLaunch` means the zero-healthy path already reported this exact
|
|
2473
|
+
// account as logged out and named the login command, so re-probing here
|
|
2474
|
+
// only prints a second, near-identical warning.
|
|
2475
|
+
rotated: !!rotationResult || accountPickerRequested || signInLaunch,
|
|
2431
2476
|
});
|
|
2432
2477
|
if (preflight) {
|
|
2433
2478
|
try {
|
package/dist/commands/hosts.js
CHANGED
|
@@ -13,7 +13,7 @@ import { isInteractiveTerminal } from './utils.js';
|
|
|
13
13
|
import { assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
14
14
|
import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
|
|
15
15
|
import { getDevice } from '../lib/devices/registry.js';
|
|
16
|
-
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
16
|
+
import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
|
|
17
17
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
18
18
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
19
19
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
@@ -190,14 +190,15 @@ async function doCheck(name) {
|
|
|
190
190
|
const target = sshTargetFor(host);
|
|
191
191
|
const os = host.os ?? resolveRemoteOsSync(name);
|
|
192
192
|
process.stdout.write(`Probing ${chalk.cyan(name)} (${target})… `);
|
|
193
|
-
const
|
|
193
|
+
const identityArgs = hostIdentityArgs(host);
|
|
194
|
+
const probe = probeHost(target, os, identityArgs);
|
|
194
195
|
if (!probe.reachable) {
|
|
195
196
|
console.log(chalk.red('unreachable'));
|
|
196
197
|
process.exitCode = 1;
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
200
|
console.log(chalk.green('reachable') + chalk.gray(probe.os ? ` · ${probe.os}` : ''));
|
|
200
|
-
const ver = remoteAgentsVersion(target, probe.os ?? os);
|
|
201
|
+
const ver = remoteAgentsVersion(target, probe.os ?? os, identityArgs);
|
|
201
202
|
console.log(` agents-cli: ${ver ? chalk.green(ver) : chalk.yellow('not installed')}`);
|
|
202
203
|
}
|
|
203
204
|
async function doRemove(name) {
|
|
@@ -217,16 +217,27 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
219
|
// Per-group table — the headline, and the thing no sibling command produces.
|
|
220
|
+
// Includes silent-stall counts so harness/account laziness is visible without --json.
|
|
220
221
|
out.push('');
|
|
221
222
|
out.push(chalk.bold(`By ${dim}`));
|
|
222
|
-
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(
|
|
223
|
+
const labelW = Math.min(Math.max(...groups.map((g) => stringWidth(g.label)), 5), Math.max(16, terminalWidth() - 58));
|
|
223
224
|
const sessW = Math.max(...groups.map((g) => String(g.sessions).length), 3);
|
|
225
|
+
const stallOf = (g) => Object.entries(g.facets.frictionSignals)
|
|
226
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
227
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
228
|
+
const resumeOf = (g) => g.facets.correctionSignals['resume after silent stall'] ?? 0;
|
|
229
|
+
const stallW = Math.max(...groups.map((g) => String(stallOf(g)).length), 5);
|
|
230
|
+
out.push(chalk.gray(` ${padToWidth('', labelW)} ${''.padStart(sessW)} ` +
|
|
231
|
+
`${''.padStart(9)} ${''.padStart(8)} ${'stalls'.padStart(stallW)} resume`));
|
|
224
232
|
for (const g of groups) {
|
|
225
233
|
const cost = g.costUsd > 0 ? formatUsd(g.costUsd) : '—';
|
|
226
234
|
const dur = g.durationMs > 0 ? formatDuration(g.durationMs) : '—';
|
|
235
|
+
const stalls = stallOf(g);
|
|
236
|
+
const resumes = resumeOf(g);
|
|
227
237
|
out.push(` ${padToWidth(truncateToWidth(g.label, labelW), labelW)} ` +
|
|
228
238
|
`${chalk.gray(String(g.sessions).padStart(sessW))} ${chalk.gray('sess')} ` +
|
|
229
|
-
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(dur)}`
|
|
239
|
+
`${chalk.green(padToWidth(cost, 9))} ${chalk.gray(padToWidth(dur, 8))} ` +
|
|
240
|
+
`${chalk.cyan(String(stalls).padStart(stallW))} ${chalk.cyan(String(resumes))}`);
|
|
230
241
|
}
|
|
231
242
|
// Everything below is the whole scope folded together; per-group detail is in --json.
|
|
232
243
|
const all = newFacetAccumulator();
|
|
@@ -235,16 +246,32 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
235
246
|
renderCounts('Top tools', topEntries(all.toolCounts, 8), out);
|
|
236
247
|
renderCounts('Languages', topEntries(all.languages, 6), out);
|
|
237
248
|
renderCounts('Models', topEntries(all.models, 6), out);
|
|
249
|
+
renderCounts('Silent stalls by model', topEntries(all.silentStallsByModel ?? {}, 8), out);
|
|
238
250
|
// Friction — the section that earns the command.
|
|
239
251
|
const gaps = all.responseGaps;
|
|
252
|
+
const silentStalls = Object.entries(all.frictionSignals)
|
|
253
|
+
.filter(([k]) => k.startsWith('silent stall:'))
|
|
254
|
+
.reduce((n, [, c]) => n + c, 0);
|
|
255
|
+
const resumeNudges = all.correctionSignals['resume after silent stall'] ?? 0;
|
|
240
256
|
out.push('');
|
|
241
257
|
out.push(chalk.bold('Friction'));
|
|
242
258
|
out.push(` ${padToWidth('interruptions', 18)} ${chalk.cyan(String(all.interruptions))}` +
|
|
243
259
|
chalk.gray(' turns you cut short'));
|
|
244
260
|
out.push(` ${padToWidth('tool errors', 18)} ${chalk.cyan(String(all.errorCount))}`);
|
|
245
261
|
if (gaps.length > 0) {
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
// Same timestamps as silent stalls; this line is the distribution. Silent
|
|
263
|
+
// stalls (below) are the agent-attributed long gaps after the model stopped.
|
|
264
|
+
out.push(` ${padToWidth('gap until next msg', 18)} ` +
|
|
265
|
+
chalk.cyan(`p50 ${Math.round(percentile(gaps, 50))}s`) + chalk.gray(` · p90 ${Math.round(percentile(gaps, 90))}s`) +
|
|
266
|
+
chalk.gray(' after assistant last spoke'));
|
|
267
|
+
}
|
|
268
|
+
if (silentStalls > 0) {
|
|
269
|
+
out.push(` ${padToWidth('silent stalls', 18)} ${chalk.cyan(String(silentStalls))}` +
|
|
270
|
+
chalk.gray(' agent idle ≥5m until you resumed (also in By ' + dim + ' table)'));
|
|
271
|
+
}
|
|
272
|
+
if (resumeNudges > 0) {
|
|
273
|
+
out.push(` ${padToWidth('resume nudges', 18)} ${chalk.cyan(String(resumeNudges))}` +
|
|
274
|
+
chalk.gray(' "continue"/"keep going" after a silent stall'));
|
|
248
275
|
}
|
|
249
276
|
const errs = topEntries(all.errorCategories, 6);
|
|
250
277
|
if (errs.length > 0) {
|
|
@@ -321,7 +348,7 @@ function renderReport(groups, dim, meta, actions, harnesses) {
|
|
|
321
348
|
out.push(chalk.yellow(` ${meta.unreadable} transcripts could not be read; their behaviour is missing from these totals.`));
|
|
322
349
|
}
|
|
323
350
|
if (all.gapsOverCeiling > 0) {
|
|
324
|
-
out.push(chalk.gray(` ${all.gapsOverCeiling}
|
|
351
|
+
out.push(chalk.gray(` ${all.gapsOverCeiling} gaps over an hour excluded from p50/p90 (still counted as silent stall: 1h+ when the assistant last spoke).`));
|
|
325
352
|
}
|
|
326
353
|
out.push('');
|
|
327
354
|
out.push(chalk.gray(' `agents insights --by project` to see it per repo'));
|
|
@@ -341,6 +368,13 @@ async function renderNarrative(payload) {
|
|
|
341
368
|
'2. What is costing you — split into the assistant\'s fault vs your own workflow.',
|
|
342
369
|
'3. Quick wins — concrete, tied to a number in the data.',
|
|
343
370
|
'4. Worth trying — one more ambitious workflow change.',
|
|
371
|
+
'',
|
|
372
|
+
'Silent stalls (required): if frictionSignals contain "silent stall: …" or',
|
|
373
|
+
'correctionSignals contain "resume after silent stall" / "continue / keep going",',
|
|
374
|
+
'call that out explicitly in section 2 or 3 with the counts. Those mean the model',
|
|
375
|
+
'stopped mid-session and sat idle until the human pinged it (timestamps: last',
|
|
376
|
+
'assistant event → next user message ≥ 5 minutes). Do not reframe them as the',
|
|
377
|
+
'user being slow unless the data only shows short reply gaps.',
|
|
344
378
|
'Be specific and cite the numbers. No preamble, no flattery, no bullet padding.',
|
|
345
379
|
'',
|
|
346
380
|
JSON.stringify(payload),
|