@phnx-labs/agents-cli 1.22.106 → 1.22.109
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 +78 -0
- package/README.md +9 -5
- package/dist/commands/daemon.js +45 -40
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/exec.d.ts +78 -8
- package/dist/commands/exec.js +276 -53
- package/dist/commands/monitors.js +15 -2
- package/dist/commands/routines.js +46 -6
- package/dist/commands/run-account-picker.d.ts +11 -0
- package/dist/commands/run-account-picker.js +11 -1
- package/dist/commands/sessions-backup-setup.d.ts +12 -0
- package/dist/commands/sessions-backup-setup.js +65 -0
- package/dist/commands/sessions-resume.d.ts +14 -1
- package/dist/commands/sessions-resume.js +56 -9
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/share.js +15 -2
- package/dist/commands/sync.js +30 -1
- package/dist/lib/accounts/slots.js +7 -0
- package/dist/lib/agent-spec/agents.d.ts +60 -8
- package/dist/lib/agent-spec/agents.js +118 -45
- package/dist/lib/daemon/daemon.d.ts +34 -1
- package/dist/lib/daemon/daemon.js +63 -9
- package/dist/lib/daemon/leaked-daemons.d.ts +60 -0
- package/dist/lib/daemon/leaked-daemons.js +180 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +32 -1
- package/dist/lib/hooks/install.js +70 -63
- package/dist/lib/hosts/dispatch.d.ts +1 -1
- package/dist/lib/hosts/dispatch.js +1 -1
- package/dist/lib/models.js +76 -5
- package/dist/lib/session/cloud.js +3 -1
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +136 -2
- package/dist/lib/session/recovery.d.ts +9 -1
- package/dist/lib/session/recovery.js +14 -4
- package/dist/lib/session/tool-calls.d.ts +1 -1
- package/dist/lib/session/tool-calls.js +40 -5
- package/dist/lib/share/backend.d.ts +23 -0
- package/dist/lib/share/backend.js +24 -0
- package/dist/lib/share/provision.d.ts +12 -0
- package/dist/lib/share/provision.js +30 -0
- package/dist/lib/share/worker-template.js +273 -0
- package/dist/lib/terminal/engine.js +13 -1
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -11,6 +11,7 @@ import { isTierToken } from '../lib/model-tiers.js';
|
|
|
11
11
|
import { RUN_AUTO_KEYWORD } from '../lib/types.js';
|
|
12
12
|
import { setHelpSections } from '../lib/help.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
14
|
+
import { isHumanFacingRun } from './run-account-picker.js';
|
|
14
15
|
import { getUserAgentsDir, readMeta } from '../lib/state.js';
|
|
15
16
|
import { parseLoopInterval } from '../lib/loop.js';
|
|
16
17
|
import { AGENTS, resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
@@ -36,15 +37,47 @@ export function parseExplicitSessionId(value) {
|
|
|
36
37
|
}
|
|
37
38
|
return value;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Parse the trailing picker markers on an `agents run` agent spec. A terminal
|
|
42
|
+
* run of `#`/`@` characters requests the account picker (`#`) and/or the
|
|
43
|
+
* device picker (`@`) — each at most once, in either order (`claude#@`,
|
|
44
|
+
* `claude@#`). Stripping them yields the normalized spec (`claude#work@` →
|
|
45
|
+
* `claude#work`, version pins and `#label` account pins intact). A picker
|
|
46
|
+
* cannot combine with an explicit pin of what it picks: `claude@2.1.218#`
|
|
47
|
+
* (the account picker already chooses the version — the old `claude@2.1.218@`
|
|
48
|
+
* rule), `claude#work#`, `claude@@`.
|
|
49
|
+
*/
|
|
50
|
+
export function parseRunPickerMarkers(agentSpec) {
|
|
51
|
+
let rest = agentSpec;
|
|
52
|
+
let accountPicker = false;
|
|
53
|
+
let devicePicker = false;
|
|
54
|
+
let reason;
|
|
55
|
+
while (rest.endsWith('#') || rest.endsWith('@')) {
|
|
56
|
+
const marker = rest.endsWith('#') ? '#' : '@';
|
|
57
|
+
if (marker === '#') {
|
|
58
|
+
if (accountPicker && reason === undefined)
|
|
59
|
+
reason = `the # picker marker may appear at most once in '${agentSpec}'`;
|
|
60
|
+
accountPicker = true;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (devicePicker && reason === undefined)
|
|
64
|
+
reason = `the @ picker marker may appear at most once in '${agentSpec}'`;
|
|
65
|
+
devicePicker = true;
|
|
66
|
+
}
|
|
67
|
+
rest = rest.slice(0, -1);
|
|
68
|
+
}
|
|
69
|
+
if (reason === undefined) {
|
|
70
|
+
if (!rest) {
|
|
71
|
+
reason = `'${agentSpec}' names no agent before the picker markers`;
|
|
72
|
+
}
|
|
73
|
+
else if (accountPicker && (rest.includes('@') || rest.includes('#'))) {
|
|
74
|
+
reason = `an explicit pin in '${rest}' already selects what the # account picker chooses`;
|
|
75
|
+
}
|
|
76
|
+
else if (devicePicker && rest.includes('@')) {
|
|
77
|
+
reason = `an explicit pin in '${rest}' cannot combine with the @ device picker`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { accountPicker, devicePicker, normalizedAgentSpec: rest, valid: reason === undefined, reason };
|
|
48
81
|
}
|
|
49
82
|
/**
|
|
50
83
|
* The `--device` alias family — the flags that mean "dispatch this run to another
|
|
@@ -57,7 +90,11 @@ export function parseRunAccountPickerRequest(agentSpec) {
|
|
|
57
90
|
export function hostTargetGiven(options) {
|
|
58
91
|
return [options.host, options.device, options.on, options.computer].filter((v) => !!v);
|
|
59
92
|
}
|
|
60
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Return every option whose selection semantics conflict with an account
|
|
95
|
+
* choice (`agent#`). Device routing is deliberately absent: the marker rides
|
|
96
|
+
* the hop and the peer picks from ITS slots.
|
|
97
|
+
*/
|
|
61
98
|
export function runAccountPickerConflicts(options) {
|
|
62
99
|
const conflicts = [];
|
|
63
100
|
if (options.resume !== undefined)
|
|
@@ -66,6 +103,21 @@ export function runAccountPickerConflicts(options) {
|
|
|
66
103
|
conflicts.push('--strategy');
|
|
67
104
|
if (options.balanced)
|
|
68
105
|
conflicts.push('--balanced');
|
|
106
|
+
if (options.lease)
|
|
107
|
+
conflicts.push('--lease');
|
|
108
|
+
if (options.box)
|
|
109
|
+
conflicts.push('--box');
|
|
110
|
+
if (options.account)
|
|
111
|
+
conflicts.push(`--account ${options.account}`);
|
|
112
|
+
return conflicts;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Return every option that already decides where the run lands, so a device
|
|
116
|
+
* choice (`agent@`) would be silently ignored: any explicit host flag, and the
|
|
117
|
+
* lease/box paths, which own placement outright.
|
|
118
|
+
*/
|
|
119
|
+
export function runDevicePickerConflicts(options) {
|
|
120
|
+
const conflicts = hostTargetGiven(options).map((h) => `--device ${h}`);
|
|
69
121
|
if (options.lease)
|
|
70
122
|
conflicts.push('--lease');
|
|
71
123
|
if (options.box)
|
|
@@ -82,16 +134,54 @@ export { RUN_AUTO_KEYWORD };
|
|
|
82
134
|
/**
|
|
83
135
|
* Whether `run auto` should default its host layer to the affinity pick (the
|
|
84
136
|
* same machinery as `--device auto`). False when the caller pinned any host
|
|
85
|
-
* flag, and false when this process was itself dispatched by a host run —
|
|
86
|
-
* dispatcher exports AGENTS_RUN_AUTO_HOST_RESOLVED=1 into the remote SHELL
|
|
137
|
+
* flag, and false when this process was itself dispatched by a host run —
|
|
138
|
+
* the dispatcher exports AGENTS_RUN_AUTO_HOST_RESOLVED=1 into the remote SHELL
|
|
87
139
|
* (hosts/dispatch.ts remoteRunShellPrelude) because it already resolved the
|
|
88
140
|
* host layer, and re-picking here would chain-hop the run across the fleet.
|
|
89
|
-
*
|
|
141
|
+
* An INTERACTIVE dispatch of a named harness also reaches the remote as a bare
|
|
142
|
+
* `agents run <harness>` (its argv forwards without the routing flag), so the
|
|
143
|
+
* interactive prelude's AGENTS_REMOTE_INTERACTIVE=1 counts as "already placed"
|
|
144
|
+
* too — without it the remote would re-place the run and ping-pong across the
|
|
145
|
+
* fleet. Pure so the pinning matrix is unit-testable.
|
|
90
146
|
*/
|
|
91
147
|
export function runAutoDefaultsToAffinity(options, env = process.env) {
|
|
92
148
|
if (hostTargetGiven(options).length > 0)
|
|
93
149
|
return false;
|
|
94
|
-
|
|
150
|
+
if (env.AGENTS_RUN_AUTO_HOST_RESOLVED === '1')
|
|
151
|
+
return false;
|
|
152
|
+
return env.AGENTS_REMOTE_INTERACTIVE !== '1';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether a bare human-facing `agents run <harness>` — no prompt, so an
|
|
156
|
+
* interactive TUI run — defaults its placement to `--device auto` (PHNX-4083).
|
|
157
|
+
* A marker left off is decided for you: no `#` means balanced rotation, and no
|
|
158
|
+
* `@` (and no other placement flag) now means automatic device placement — the
|
|
159
|
+
* same engine as `--device auto`, whose pool never contains a box marked
|
|
160
|
+
* `personal`. ALL of these must hold:
|
|
161
|
+
*
|
|
162
|
+
* - no prompt (headless runs — teams, routines, hooks, `run <agent> "…"` —
|
|
163
|
+
* keep running in place, unchanged);
|
|
164
|
+
* - a human-facing surface: a real TTY and no `--json`. This is the same
|
|
165
|
+
* two-condition gate `signInLaunchDecision` uses in run-account-picker.ts —
|
|
166
|
+
* reused here through isHumanFacingRun, not re-derived;
|
|
167
|
+
* - no device-picker marker (`agent@` already chose the device; picking this
|
|
168
|
+
* machine there is a plain local run);
|
|
169
|
+
* - no `--resume` / `--lease` / `--box` / `--cloud` (those own placement);
|
|
170
|
+
* - the host layer is unpinned and this process is not itself a dispatched
|
|
171
|
+
* hop — delegated to runAutoDefaultsToAffinity, which encodes both.
|
|
172
|
+
*
|
|
173
|
+
* Pure so the default-placement matrix is unit-testable.
|
|
174
|
+
*/
|
|
175
|
+
export function bareInteractiveRunDefaultsToDeviceAuto(options, run, surface, env = process.env) {
|
|
176
|
+
if (run.prompt !== undefined)
|
|
177
|
+
return false;
|
|
178
|
+
if (!isHumanFacingRun({ tty: surface.tty, json: surface.json === true }))
|
|
179
|
+
return false;
|
|
180
|
+
if (run.devicePickerRequested)
|
|
181
|
+
return false;
|
|
182
|
+
if (options.resume !== undefined || options.lease || options.box || options.cloud)
|
|
183
|
+
return false;
|
|
184
|
+
return runAutoDefaultsToAffinity(options, env);
|
|
95
185
|
}
|
|
96
186
|
/**
|
|
97
187
|
* Whether an interactive host dispatch must mint a correlation launch id and
|
|
@@ -426,7 +516,7 @@ async function handleTerminalHandoff(agentSpec, options, prompt) {
|
|
|
426
516
|
// isValidAgent / profileExists / resolveWorkflowRef chain below), so this must
|
|
427
517
|
// accept all three. Gating on the agent table alone rejected every profile —
|
|
428
518
|
// the whole Kimi/DeepSeek/Qwen/GLM path — for `--terminal` runs only.
|
|
429
|
-
const rawTarget =
|
|
519
|
+
const rawTarget = parseRunPickerMarkers(agentSpec).normalizedAgentSpec.split('#')[0].split('@')[0];
|
|
430
520
|
const knownAgent = resolveAgentName(rawTarget);
|
|
431
521
|
const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
|
|
432
522
|
import('../lib/profiles.js'),
|
|
@@ -587,12 +677,22 @@ export function registerRunCommand(program) {
|
|
|
587
677
|
# Headless, can edit: have the agent make changes
|
|
588
678
|
agents run claude "fix lint errors in src/" --mode edit
|
|
589
679
|
|
|
590
|
-
# Interactive (TUI)
|
|
680
|
+
# Interactive (TUI): a bare run places itself like --device auto (a fleet
|
|
681
|
+
# worker, TUI forwarded over SSH); pin the device to stay on this machine
|
|
591
682
|
agents run claude
|
|
683
|
+
agents run claude --device zion # stay local (or pick this machine in claude@)
|
|
592
684
|
|
|
593
|
-
# Pick a signed-in account/version for only this run
|
|
685
|
+
# Pick a signed-in account/version for only this run (# = account picker)
|
|
686
|
+
agents run claude#
|
|
687
|
+
|
|
688
|
+
# Pick the device this run lands on (@ = device picker); this machine
|
|
689
|
+
# first, offline rows disabled, fleet state aged in the prompt
|
|
594
690
|
agents run claude@
|
|
595
691
|
|
|
692
|
+
# Ask both, account first, then device — the run dispatches with the
|
|
693
|
+
# picked account to the picked device
|
|
694
|
+
agents run claude#@
|
|
695
|
+
|
|
596
696
|
# Full-auto: affinity-pick the host, then the harness with the most
|
|
597
697
|
# account headroom, then a balanced account on it
|
|
598
698
|
agents run auto "fix the flaky test" --mode edit
|
|
@@ -677,9 +777,21 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
677
777
|
best-account headroom), and the account (the strategy above). Zero
|
|
678
778
|
healthy accounts on any harness exits nonzero with the earliest reset.
|
|
679
779
|
|
|
680
|
-
|
|
681
|
-
installed account for this run
|
|
682
|
-
and available limits; unsafe accounts stay visible but
|
|
780
|
+
Pickers: a trailing # opens the account picker (agents run claude#) to
|
|
781
|
+
choose one installed account for this run — rows show identity, login
|
|
782
|
+
state, plan, and available limits; unsafe accounts stay visible but
|
|
783
|
+
disabled. A trailing @ opens the device picker (agents run claude@);
|
|
784
|
+
#@ asks both, account first, then device. The pickers cannot combine
|
|
785
|
+
with an explicit pin of the same thing (--account, --device/--on/
|
|
786
|
+
--computer/--host) or with --strategy/--balanced/--resume/--lease/--box.
|
|
787
|
+
|
|
788
|
+
Interactive placement: a bare 'agents run <harness>' (no prompt, real TTY)
|
|
789
|
+
places itself like --device auto — a fleet worker runs it, with the TUI
|
|
790
|
+
forwarded over SSH. Headless runs (any prompt, --json, no TTY) are
|
|
791
|
+
unchanged: they run in place. To stay on this machine, pass
|
|
792
|
+
--device <this machine>, or pick this machine (listed first) in the
|
|
793
|
+
'<harness>@' device picker. When placement finds no healthy device the
|
|
794
|
+
run fails loud and names the local spellings.
|
|
683
795
|
|
|
684
796
|
Fallback: --fallback codex,antigravity retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
685
797
|
|
|
@@ -834,19 +946,25 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
834
946
|
host: options.host,
|
|
835
947
|
});
|
|
836
948
|
}
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
949
|
+
// Trailing picker markers request an interactive choice: `#` picks the
|
|
950
|
+
// account (and the version it signs in with), `@` picks the device, and
|
|
951
|
+
// `#@`/`@#` ask both. Strip only those terminal markers; concrete
|
|
952
|
+
// agent@version pins and `#label` account pins retain their meaning in
|
|
953
|
+
// every dispatch path below.
|
|
954
|
+
const pickerMarkers = parseRunPickerMarkers(agentSpec);
|
|
955
|
+
const accountPickerRequested = pickerMarkers.accountPicker;
|
|
956
|
+
const devicePickerRequested = pickerMarkers.devicePicker;
|
|
957
|
+
let normalizedAgentSpec = pickerMarkers.normalizedAgentSpec;
|
|
958
|
+
if (!pickerMarkers.valid) {
|
|
959
|
+
console.error(chalk.red(`Invalid run picker target: ${agentSpec}. ` +
|
|
960
|
+
`${pickerMarkers.reason ?? 'unrecognized picker markers'}. ` +
|
|
961
|
+
'Use agents run <agent># to pick an account, <agent>@ to pick a device, <agent>#@ for both.'));
|
|
845
962
|
process.exit(1);
|
|
846
963
|
}
|
|
847
964
|
// Peel `#name` off before --device dispatch so the selector rides the hop
|
|
848
965
|
// unchanged and the peer resolves ITS slot (PHNX-3940 T5). The later local
|
|
849
|
-
// parse is idempotent when the values match.
|
|
966
|
+
// parse is idempotent when the values match. A bare trailing `#` was the
|
|
967
|
+
// picker marker above, so it can never land here as an empty label.
|
|
850
968
|
{
|
|
851
969
|
const labelParts = normalizedAgentSpec.split('#');
|
|
852
970
|
if (labelParts.length > 2 || labelParts[1] === '') {
|
|
@@ -868,6 +986,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
868
986
|
throw new Error('--resume cannot be combined with --session-id, --loop, --fallback, --resume-checkpoint, or --lease.');
|
|
869
987
|
}
|
|
870
988
|
const { sessionsResumeAction } = await import('./sessions-resume.js');
|
|
989
|
+
const { toRemotePortable } = await import('../lib/project-root.js');
|
|
990
|
+
const device = options.host || options.device || options.on || options.computer;
|
|
871
991
|
await sessionsResumeAction(undefined, prompt, {
|
|
872
992
|
agent: normalizedAgentSpec.split('#')[0] === RUN_AUTO_KEYWORD ? undefined : normalizedAgentSpec.split('#')[0],
|
|
873
993
|
account: options.account,
|
|
@@ -875,9 +995,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
875
995
|
mode: command.getOptionValueSource('mode') === 'default' ? undefined : options.mode,
|
|
876
996
|
interactive: options.interactive,
|
|
877
997
|
headless: options.headless,
|
|
878
|
-
cwd: options.cwd,
|
|
998
|
+
cwd: device ? options.remoteCwd ?? (options.cwd ? toRemotePortable(options.cwd) : undefined) : options.cwd,
|
|
879
999
|
quiet: options.quiet,
|
|
880
|
-
device
|
|
1000
|
+
device,
|
|
881
1001
|
runArgs: rawArgs.slice(2),
|
|
882
1002
|
});
|
|
883
1003
|
return;
|
|
@@ -890,14 +1010,22 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
890
1010
|
console.error(chalk.red(hardDeprecationError(runBaseAgentId)));
|
|
891
1011
|
process.exit(1);
|
|
892
1012
|
}
|
|
893
|
-
//
|
|
894
|
-
// so an
|
|
895
|
-
// "cannot be combined with --
|
|
1013
|
+
// Picker conflict checks run BEFORE --resume/--device auto may place the
|
|
1014
|
+
// run implicitly, so an implied placement never surfaces as a fake
|
|
1015
|
+
// "cannot be combined with --device" when the user only typed a marker.
|
|
896
1016
|
if (accountPickerRequested) {
|
|
897
1017
|
const conflicts = runAccountPickerConflicts(options);
|
|
898
1018
|
if (conflicts.length > 0) {
|
|
899
1019
|
console.error(chalk.red(`Account selection with ${agentSpec} cannot be combined with ${conflicts.join(', ')}. ` +
|
|
900
|
-
'Remove the conflicting selector, or
|
|
1020
|
+
'Remove the conflicting selector, or pin the target explicitly (agent@version or agent#label).'));
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (devicePickerRequested) {
|
|
1025
|
+
const conflicts = runDevicePickerConflicts(options);
|
|
1026
|
+
if (conflicts.length > 0) {
|
|
1027
|
+
console.error(chalk.red(`Device selection with ${agentSpec} cannot be combined with ${conflicts.join(', ')}. ` +
|
|
1028
|
+
'Remove one — the picker already chooses where the run lands.'));
|
|
901
1029
|
process.exit(1);
|
|
902
1030
|
}
|
|
903
1031
|
}
|
|
@@ -1013,7 +1141,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1013
1141
|
process.exit(1);
|
|
1014
1142
|
}
|
|
1015
1143
|
if (accountPickerRequested) {
|
|
1016
|
-
console.error(chalk.red(`agents run auto picks the harness and account itself — the trailing
|
|
1144
|
+
console.error(chalk.red(`agents run auto picks the harness and account itself — the trailing-# account picker needs a concrete harness (agents run <harness>#).`));
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
if (devicePickerRequested) {
|
|
1148
|
+
console.error(chalk.red(`agents run auto picks the harness and its placement itself — the trailing-@ device picker needs a concrete harness (agents run <harness>@).`));
|
|
1017
1149
|
process.exit(1);
|
|
1018
1150
|
}
|
|
1019
1151
|
// Host layer: with no explicit --device, default to the
|
|
@@ -1022,19 +1154,100 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1022
1154
|
if (!resolvedResumeSource && runAutoDefaultsToAffinity(options))
|
|
1023
1155
|
options.device = 'auto';
|
|
1024
1156
|
}
|
|
1157
|
+
// The picker menus run HERE — after every conflict check, before
|
|
1158
|
+
// placement resolves — so the picked account labels the device rows and
|
|
1159
|
+
// the picked device is concrete for every host branch below.
|
|
1160
|
+
// `#@`/`@#`: account first, against THIS machine's slots; the picked
|
|
1161
|
+
// account becomes options.account and the device menu shows its
|
|
1162
|
+
// ✓/– per-device mark. A remote device pick dispatches with
|
|
1163
|
+
// `<agent>#<label>` so the peer resolves its own slot.
|
|
1164
|
+
// `@`: just the device menu; picking this machine is a plain local run.
|
|
1165
|
+
// Either menu cancelled (Esc/Ctrl-C) launches nothing and exits 0.
|
|
1166
|
+
let upFrontAccountPick = null;
|
|
1167
|
+
if (accountPickerRequested && devicePickerRequested) {
|
|
1168
|
+
const baseName = normalizedAgentSpec.split('#')[0].split('@')[0];
|
|
1169
|
+
const baseAgentId = resolveAgentName(baseName);
|
|
1170
|
+
const { profileExists: baseProfileExists } = await import('../lib/profiles.js');
|
|
1171
|
+
if (!baseAgentId || baseProfileExists(baseName)) {
|
|
1172
|
+
console.error(chalk.red(baseProfileExists(baseName)
|
|
1173
|
+
? `Account selection is not available for custom harness '${baseName}'. Run its concrete host agent with # instead.`
|
|
1174
|
+
: `Account selection is not available for '${baseName}'. Run a concrete agent with # instead.`));
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
const { supportsAccountInspection: baseSupportsInspection, agentLabel: baseAgentLabel, ACCOUNT_INSPECTION_AGENT_IDS: inspectionAgentIds } = await import('../lib/agents.js');
|
|
1178
|
+
if (!baseSupportsInspection(baseAgentId)) {
|
|
1179
|
+
console.error(chalk.red(`${baseAgentLabel(baseAgentId)} does not expose local account state, so agents-cli cannot safely select an account.`));
|
|
1180
|
+
console.error(chalk.gray(`Supported account pickers: ${inspectionAgentIds.join(', ')}`));
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
const { pickRunAccountCandidate } = await import('./run-account-picker.js');
|
|
1184
|
+
const selected = await pickRunAccountCandidate(baseAgentId);
|
|
1185
|
+
if (!selected)
|
|
1186
|
+
return; // Esc/Ctrl-C: launch nothing.
|
|
1187
|
+
if (selected.nativeAccount)
|
|
1188
|
+
options.account = selected.nativeAccount;
|
|
1189
|
+
upFrontAccountPick = selected;
|
|
1190
|
+
if (!options.quiet) {
|
|
1191
|
+
const identity = selected.accountLabel || 'signed-in account';
|
|
1192
|
+
process.stderr.write(chalk.gray(`[agents] selected ${identity} · ${baseAgentId}@${selected.version} for this run\n`));
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (devicePickerRequested) {
|
|
1196
|
+
// --resume above may have pinned the host implicitly (recovery to the
|
|
1197
|
+
// source peer); a device menu against it would be a silent no-op.
|
|
1198
|
+
const impliedHost = hostTargetGiven(options);
|
|
1199
|
+
if (impliedHost.length > 0) {
|
|
1200
|
+
console.error(chalk.red(`Device selection with ${agentSpec} cannot be combined with the placement --resume already chose (${impliedHost.join(', ')}). ` +
|
|
1201
|
+
'Remove one — both pick where the run lands.'));
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
const { pickRunDevice } = await import('./run-device-picker.js');
|
|
1205
|
+
const baseName = normalizedAgentSpec.split('#')[0].split('@')[0];
|
|
1206
|
+
const pickedDevice = await pickRunDevice({
|
|
1207
|
+
agent: (resolveAgentName(baseName) ?? baseName),
|
|
1208
|
+
accountLabel: options.account,
|
|
1209
|
+
});
|
|
1210
|
+
if (pickedDevice === null)
|
|
1211
|
+
return; // Esc/Ctrl-C: launch nothing.
|
|
1212
|
+
const { isSelfHost } = await import('../lib/devices/self-host.js');
|
|
1213
|
+
// Picking this machine is a plain local run — leave options.device unset.
|
|
1214
|
+
if (!isSelfHost(pickedDevice))
|
|
1215
|
+
options.device = pickedDevice;
|
|
1216
|
+
}
|
|
1217
|
+
// Default placement (PHNX-4083): a bare human-facing `agents run
|
|
1218
|
+
// <harness>` (no prompt, real TTY, no placement flags, not a dispatched
|
|
1219
|
+
// hop) places itself like `--device auto` — a marker left off is decided
|
|
1220
|
+
// for you. Headless runs (a prompt, --json, teams/routines/hooks) keep
|
|
1221
|
+
// running in place, unchanged.
|
|
1222
|
+
const defaultPlacement = bareInteractiveRunDefaultsToDeviceAuto(options, { prompt, devicePickerRequested }, { tty: isInteractiveTerminal(), json: options.json });
|
|
1223
|
+
if (defaultPlacement)
|
|
1224
|
+
options.device = 'auto';
|
|
1025
1225
|
// --device auto (and deprecated --smart): live fleet pick.
|
|
1026
1226
|
// Harness is always the agent the user typed — never auto-picked.
|
|
1027
1227
|
// Placement failure propagates; an automatic request never becomes local.
|
|
1028
1228
|
{
|
|
1029
1229
|
const { applyDeviceAutoToOptions } = await import('../lib/smart-launch.js');
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
:
|
|
1037
|
-
|
|
1230
|
+
let result;
|
|
1231
|
+
try {
|
|
1232
|
+
result = await applyDeviceAutoToOptions(options, {
|
|
1233
|
+
accountPickerRequested,
|
|
1234
|
+
// `run auto` selects its harness after placement, so do not filter
|
|
1235
|
+
// candidates against an arbitrary proxy harness at this stage.
|
|
1236
|
+
agent: normalizedAgentSpec.split('#')[0].split('@')[0] === RUN_AUTO_KEYWORD
|
|
1237
|
+
? undefined
|
|
1238
|
+
: (resolveAgentName(normalizedAgentSpec.split('#')[0].split('@')[0]) ?? undefined),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
catch (err) {
|
|
1242
|
+
// Placement the DEFAULT chose must fail loud AND name the local
|
|
1243
|
+
// escape hatch — never silently fall back to a local launch.
|
|
1244
|
+
if (!defaultPlacement)
|
|
1245
|
+
throw err;
|
|
1246
|
+
console.error(chalk.red(err.message));
|
|
1247
|
+
const { machineId } = await import('../lib/machine-id.js');
|
|
1248
|
+
console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1038
1251
|
if (!options.quiet && result.deprecationSmart) {
|
|
1039
1252
|
process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
|
|
1040
1253
|
}
|
|
@@ -1512,9 +1725,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1512
1725
|
process.exit(1);
|
|
1513
1726
|
}
|
|
1514
1727
|
const interactiveHost = options.interactive === true || (prompt === undefined && options.headless !== true);
|
|
1515
|
-
|
|
1728
|
+
// When the account was already picked up front (`#@`), the dispatch
|
|
1729
|
+
// forwards `#label` and the peer needs no interactive picker.
|
|
1730
|
+
if (accountPickerRequested && !upFrontAccountPick && !interactiveHost) {
|
|
1516
1731
|
console.error(chalk.red(`Account selection with ${agentSpec} requires an interactive host run. ` +
|
|
1517
|
-
`Use agents run ${runAgent}
|
|
1732
|
+
`Use agents run ${runAgent}# --device ${host.name} --interactive.`));
|
|
1518
1733
|
process.exit(1);
|
|
1519
1734
|
}
|
|
1520
1735
|
if (interactiveHost) {
|
|
@@ -1576,7 +1791,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1576
1791
|
const exitCode = await runInteractiveOnHost(host, {
|
|
1577
1792
|
agent: runAgent,
|
|
1578
1793
|
version: resumeId ? undefined : runVersion,
|
|
1579
|
-
accountPicker: accountPickerRequested,
|
|
1794
|
+
accountPicker: accountPickerRequested && !upFrontAccountPick,
|
|
1580
1795
|
strategy: resumeId ? undefined : runStrategy,
|
|
1581
1796
|
account: options.account,
|
|
1582
1797
|
fallback: options.fallback,
|
|
@@ -1908,12 +2123,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1908
2123
|
let workflowHasSubagents = false;
|
|
1909
2124
|
const cwd = options.cwd ?? process.cwd();
|
|
1910
2125
|
if (accountPickerRequested && profileExists(rawAgent)) {
|
|
1911
|
-
console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with
|
|
2126
|
+
console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with # instead.`));
|
|
1912
2127
|
process.exit(1);
|
|
1913
2128
|
}
|
|
1914
2129
|
if (accountPickerRequested && !isValidAgent(rawAgent)) {
|
|
1915
2130
|
if (resolveWorkflowRef(rawAgent, cwd)) {
|
|
1916
|
-
console.error(chalk.red(`Account selection is not available for workflow '${rawAgent}'. Run a concrete agent with
|
|
2131
|
+
console.error(chalk.red(`Account selection is not available for workflow '${rawAgent}'. Run a concrete agent with # instead.`));
|
|
1917
2132
|
process.exit(1);
|
|
1918
2133
|
}
|
|
1919
2134
|
}
|
|
@@ -2194,7 +2409,10 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2194
2409
|
process.exit(1);
|
|
2195
2410
|
}
|
|
2196
2411
|
}
|
|
2197
|
-
|
|
2412
|
+
// `#@` already asked the account up front (before the device menu) — a
|
|
2413
|
+
// remote device pick never reaches here, so apply the local pick's
|
|
2414
|
+
// version/home and skip the on-the-spot menu.
|
|
2415
|
+
if (accountPickerRequested && !upFrontAccountPick) {
|
|
2198
2416
|
if (!supportsAccountInspection(agent)) {
|
|
2199
2417
|
console.error(chalk.red(`${agentLabel(agent)} does not expose local account state, so agents-cli cannot safely select an account.`));
|
|
2200
2418
|
console.error(chalk.gray(`Supported account pickers: ${ACCOUNT_INSPECTION_AGENT_IDS.join(', ')}`));
|
|
@@ -2220,6 +2438,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2220
2438
|
process.exit(1);
|
|
2221
2439
|
}
|
|
2222
2440
|
}
|
|
2441
|
+
if (upFrontAccountPick) {
|
|
2442
|
+
version = upFrontAccountPick.version;
|
|
2443
|
+
if (upFrontAccountPick.slotDir)
|
|
2444
|
+
execHome = upFrontAccountPick.slotDir;
|
|
2445
|
+
}
|
|
2223
2446
|
version = resolveVersionAlias(agent, version);
|
|
2224
2447
|
const { resolveSpawnAccount } = await import('../lib/account-registry.js');
|
|
2225
2448
|
let spawnAccount = null;
|
|
@@ -2437,7 +2660,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2437
2660
|
// - NEEDS A SIGN-IN (signed_out / revoked) -> launching IS the fix,
|
|
2438
2661
|
// because the harness's own TUI is the login surface. So on a TTY we
|
|
2439
2662
|
// carry the user into that login instead of erroring. Exiting here
|
|
2440
|
-
// made `agents run <agent>`, `agents run <agent
|
|
2663
|
+
// made `agents run <agent>`, `agents run <agent>#`, and `agents use`
|
|
2441
2664
|
// all dead-end with no reachable way to authenticate.
|
|
2442
2665
|
const recoverable = signInRecoverableCandidates(resolved.exhausted);
|
|
2443
2666
|
const { signInLaunchDecision } = await import('./run-account-picker.js');
|
|
@@ -2450,7 +2673,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2450
2673
|
const { pickSignInLaunchVersion } = await import('./run-account-picker.js');
|
|
2451
2674
|
const signInVersion = await pickSignInLaunchVersion(agent, recoverable, !!options.quiet);
|
|
2452
2675
|
// A cancelled prompt launches nothing — same contract as the
|
|
2453
|
-
// trailing
|
|
2676
|
+
// trailing-# account picker above.
|
|
2454
2677
|
if (!signInVersion)
|
|
2455
2678
|
return;
|
|
2456
2679
|
version = signInVersion;
|
|
@@ -2488,7 +2711,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2488
2711
|
if (decision === 'picker') {
|
|
2489
2712
|
const selected = await pickRunAccountCandidate(agent);
|
|
2490
2713
|
// A cancelled picker launches nothing — same contract as the
|
|
2491
|
-
// trailing
|
|
2714
|
+
// trailing-# account picker and the sign-in launch above.
|
|
2492
2715
|
if (!selected)
|
|
2493
2716
|
return;
|
|
2494
2717
|
version = selected.version;
|
|
@@ -12,7 +12,7 @@ import { withAliases } from '../lib/verbs.js';
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
|
-
import { isDaemonRunning, signalDaemonReload, startDaemon, } from '../lib/daemon/daemon.js';
|
|
15
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, RedirectedHomeDaemonError, } from '../lib/daemon/daemon.js';
|
|
16
16
|
import { findDuplicateMonitor, monitorFingerprint } from '../lib/monitors/fingerprint.js';
|
|
17
17
|
import { gatherFleetMonitors, NO_MONITOR_FANOUT_ENV } from '../lib/monitors/remote.js';
|
|
18
18
|
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, requiresSingleOwner, monitorSharedInputOwner, parseInterval, } from '../lib/monitors/config.js';
|
|
@@ -246,7 +246,20 @@ function ensureDaemonRunning() {
|
|
|
246
246
|
stderrLine(chalk.gray('Daemon reloaded'));
|
|
247
247
|
return true;
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = startDaemon();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
// The redirected-HOME refusal (W4) is an auto-start policy, not a failure
|
|
255
|
+
// of the monitor just added — state it and leave the foreground command
|
|
256
|
+
// green. Anything else (a genuinely unspawnable binary) still fails loud.
|
|
257
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
258
|
+
stderrLine(chalk.yellow(err.message));
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
250
263
|
if (result.pid) {
|
|
251
264
|
console.log(chalk.green(`Daemon started (PID: ${result.pid}). It will watch monitors in the background.`));
|
|
252
265
|
console.log(chalk.gray('Disable only monitor polling with: agents daemon services disable monitors'));
|
|
@@ -10,7 +10,7 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
|
-
import { isDaemonRunning, signalDaemonReload, startDaemon, readDaemonLog, getDaemonStatus, getDaemonLogPath, } from '../lib/daemon/daemon.js';
|
|
13
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, readDaemonLog, getDaemonStatus, getDaemonLogPath, RedirectedHomeDaemonError, } from '../lib/daemon/daemon.js';
|
|
14
14
|
import { isDaemonServiceEnabled, setDaemonServiceEnabled } from '../lib/daemon-services.js';
|
|
15
15
|
import { assertSchedulerEnabled, assertDaemonEnabled, isDaemonEnabled, isSchedulerEnabled, } from '../lib/device-config.js';
|
|
16
16
|
import { parseAgentVersionSpec, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
@@ -327,7 +327,22 @@ function ensureSchedulerRunning(opts = {}) {
|
|
|
327
327
|
}
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
|
-
|
|
330
|
+
let result;
|
|
331
|
+
try {
|
|
332
|
+
result = startDaemon();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
// The redirected-HOME refusal (W4) is an auto-start policy, not a failure
|
|
336
|
+
// of the routine just added — state it and leave the foreground command
|
|
337
|
+
// green, the same tier split as the auto-start circuit breaker. Anything
|
|
338
|
+
// else (a genuinely unspawnable binary) still fails loud.
|
|
339
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
340
|
+
if (!opts.quiet)
|
|
341
|
+
log(chalk.yellow(err.message));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
331
346
|
if (opts.quiet)
|
|
332
347
|
return;
|
|
333
348
|
if (result.pid) {
|
|
@@ -1851,9 +1866,22 @@ export function registerRoutinesCommands(program) {
|
|
|
1851
1866
|
console.log(chalk.yellow(`Daemon is disabled (daemon.enabled=false) — run(s) below fire but are not monitored. Re-enable with: agents daemon enable`));
|
|
1852
1867
|
}
|
|
1853
1868
|
else {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1869
|
+
try {
|
|
1870
|
+
const started = startDaemon();
|
|
1871
|
+
if (started.pid) {
|
|
1872
|
+
console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
catch (err) {
|
|
1876
|
+
// Same contract as the daemon.enabled=false branch above: a refused
|
|
1877
|
+
// auto-start (W4's redirected-HOME guard) never cancels the fire —
|
|
1878
|
+
// the runs below fire but are not monitored. Anything else rethrows.
|
|
1879
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
1880
|
+
console.log(chalk.yellow(err.message));
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
throw err;
|
|
1884
|
+
}
|
|
1857
1885
|
}
|
|
1858
1886
|
}
|
|
1859
1887
|
}
|
|
@@ -2185,7 +2213,19 @@ export function registerRoutinesCommands(program) {
|
|
|
2185
2213
|
process.exit(1);
|
|
2186
2214
|
}
|
|
2187
2215
|
setDaemonServiceEnabled('scheduler', true);
|
|
2188
|
-
|
|
2216
|
+
let result;
|
|
2217
|
+
try {
|
|
2218
|
+
result = startDaemon();
|
|
2219
|
+
}
|
|
2220
|
+
catch (err) {
|
|
2221
|
+
// The redirected-HOME refusal (W4) is user-actionable — print it
|
|
2222
|
+
// without a stack, same as the asserts above.
|
|
2223
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
2224
|
+
console.error(chalk.red(err.message));
|
|
2225
|
+
process.exit(1);
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2189
2229
|
if (result.method === 'already-running') {
|
|
2190
2230
|
// Signal a reload even here: if the daemon booted while this device had
|
|
2191
2231
|
// scheduler.enabled=false, the reload re-evaluates the gate and boots
|
|
@@ -35,6 +35,17 @@ export declare function buildSwitchAccountChoices(rows: SwitchAccountRow[]): Run
|
|
|
35
35
|
* A cancelled picker writes nothing.
|
|
36
36
|
*/
|
|
37
37
|
export declare function pickSwitchAccount(agent: AgentId, rows: SwitchAccountRow[]): Promise<string | null>;
|
|
38
|
+
/**
|
|
39
|
+
* The two-condition "human-facing" gate behind signInLaunchDecision and
|
|
40
|
+
* noVerifiedUsageDecision: a real TTY and no `--json`. Off a TTY nobody can
|
|
41
|
+
* answer a prompt, and `--json` marks a MACHINE consumer, which must never be
|
|
42
|
+
* handed a picker or dropped into a login TUI. Mirrors the canonical
|
|
43
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isHumanFacingRun(input: {
|
|
46
|
+
tty: boolean;
|
|
47
|
+
json: boolean;
|
|
48
|
+
}): boolean;
|
|
38
49
|
/**
|
|
39
50
|
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
40
51
|
* failing loud. Three inputs, all of which have to hold:
|