@phnx-labs/agents-cli 1.22.38 → 1.22.39
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 +8 -0
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +2 -2
- package/dist/commands/config.js +31 -1
- package/dist/commands/exec.js +1 -1
- package/dist/commands/ssh.js +156 -8
- package/dist/lib/config-keys.d.ts +11 -3
- package/dist/lib/config-keys.js +22 -3
- package/dist/lib/config-machine-keys.js +1 -0
- package/dist/lib/device-config.d.ts +34 -0
- package/dist/lib/device-config.js +96 -0
- package/dist/lib/devices/pool.d.ts +56 -0
- package/dist/lib/devices/pool.js +85 -0
- package/dist/lib/exec.d.ts +4 -1
- package/dist/lib/exec.js +8 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +36 -0
- package/dist/lib/migrate.js +107 -0
- 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/smart-launch.d.ts +33 -3
- package/dist/lib/smart-launch.js +61 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.39
|
|
4
|
+
|
|
5
|
+
- **The auto-detected `default` browser profile no longer sits in the fleet-shared `agents.yaml`, which was wedging `agents repos pull user` fleet-wide (RUSH-2161).** `browser` is a central key because named profiles a user creates are real fleet config, but the one `default` entry inside it is machine-local: its `binary` is an OS-specific path and its endpoint is a locally chosen free port. `createProfile`/`updateProfile` have routed that entry to the per-device file since 1.22.38 (`isMachineLocalProfile`), but nothing removed the copy older versions had already written into the shared file, and `serializeCentral` could not — it deletes whole device-scoped KEYS, and `browser` is not one. So the stale entry stayed committed and every box rewrote it with its own browser. Measured across three boxes all running 1.22.38: zion `chrome` + `/Applications/Google Chrome.app/...`, yosemite-s1 `brave` + `/opt/brave.com/brave/brave`, mark-1 the same shape, each with an empty `deviceBrowser`. Because the pull refuses when an incoming change touches a locally modified path, a permanently dirty `agents.yaml` blocked fleet config sync outright — those boxes sat 5, 8 and 79 commits behind, so merged config (a `notify.owner` block, device roles) reached zero machines. A new migration moves the entry into `devices/<machine>/agents.yaml` and deletes it from central, writing the device file first so a crash cannot lose the profile and keeping an existing device entry when one is already there. Central is edited through a YAML `Document`, so the hand-written comments in the committed file survive rather than being flattened by a re-stringify — which would have re-created the same churn. Named profiles are untouched. Source: `apps/cli/src/lib/migrate.ts`, `apps/cli/src/bootstrap.ts`.
|
|
6
|
+
|
|
7
|
+
- **`agents config set devices.<name>.tmux off` turns the interactive tmux wrap off for one machine (RUSH-2620).** Interactive `agents run` wraps the harness in the shared-socket tmux session so every agent gets an addressable `%pane` — that is what lets `agents sessions --active` tell co-located agents apart and `agents focus` re-attach without forking. Until now the only ways out were per-run (`--raw` / `--no-tmux` / `--disable-tmux`) or the `AGENTS_NO_TMUX=1` env var, so a box whose tmux is broken needed the flag retyped on every launch. The new key is machine-local by design (a broken tmux is a property of one machine): it never enters the fleet-shared `agents.yaml` and is refused for a peer. Unset still means wrap. `agents devices config <name> tmux.enabled off` sets the same key. Source: `apps/cli/src/lib/device-config.ts` (`isTmuxEnabled`), `apps/cli/src/lib/exec.ts` (`shouldWrapInTmux`).
|
|
8
|
+
|
|
9
|
+
- **`agents devices role <name> worker` marks which boxes agents run on, and `--device auto` follows it.** Roles are fleet-wide (`worker` / `personal`), stored in the shared `fleet.devices.<name>.config.role` block of `~/.agents/agents.yaml`, so a mark set on any box travels with `agents repo push/pull` — the per-device files under `~/.agents/devices/` are written only by the machine they name, so they could never carry a fleet-wide statement about a different box. Marking ANY device `worker` turns automatic placement into an allowlist: `agents run --device auto`, `agents teams add --device auto`, and the AGI EXT launch commands then pick only from the marked workers. A device marked `personal` (a machine you sit at) is never picked automatically, under any mode, and a paired cockpit stays excluded through its existing registry `control` role. When roles leave the pool empty, `--device auto` fails loud naming the fix instead of quietly running on the local machine — through `agents ssh auto` and the `--host auto` passthrough too, not just `agents run`. Nothing marked = today's behavior, every online device. Widen it back with `agents config set auto.pool all`. `agents devices role` with no arguments prints who is marked what and exactly which devices `--device auto` would consider; `agents devices list` tags marked rows and `--json` carries `role` plus an `autoPool` boolean per device. Source: `apps/cli/src/lib/devices/pool.ts`, `apps/cli/src/lib/device-config.ts`, `apps/cli/src/lib/smart-launch.ts`.
|
|
10
|
+
|
|
3
11
|
## 1.22.38
|
|
4
12
|
|
|
5
13
|
- Webhook `stateTo` triggers and handlers now fire only on the delivery that actually moved a Linear issue INTO that state, not on every later update while it still sits there. Previously a `stateTo: Plan` handler re-matched on any subsequent `Issue/update` — a label edit, an assignee change, a description touch — because it checked the issue's *current* state instead of the transition, which accumulated 11 duplicate plan comments on one issue. It now additionally requires the delivery's `updatedFrom` to record a state change. (RUSH-2539)
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/bootstrap.js
CHANGED
|
@@ -1095,7 +1095,7 @@ const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help', 'uninstall']);
|
|
|
1095
1095
|
//
|
|
1096
1096
|
// Skipped for --help/--version (RUSH-2454): pure documentation paths must not
|
|
1097
1097
|
// load any migration graph. Loaded from migrate-fold.js (leaf: fs + createLink),
|
|
1098
|
-
// not migrate.js, so a real command pays only the fold hop unless the
|
|
1098
|
+
// not migrate.js, so a real command pays only the fold hop unless the v20
|
|
1099
1099
|
// sentinel is missing and runMigration() is required below.
|
|
1100
1100
|
if (process.env.AGENTS_SKIP_MIGRATION !== '1' && !helpOrVersionRequested) {
|
|
1101
1101
|
try {
|
|
@@ -1130,7 +1130,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1' && !helpOrVersionRequested) {
|
|
|
1130
1130
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
1131
1131
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
1132
1132
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
1133
|
-
const sentinelValue = '
|
|
1133
|
+
const sentinelValue = 'v20';
|
|
1134
1134
|
let needRun = true;
|
|
1135
1135
|
try {
|
|
1136
1136
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
package/dist/commands/config.js
CHANGED
|
@@ -41,12 +41,17 @@ function parseValue(key, parsed, raw) {
|
|
|
41
41
|
return raw.trim();
|
|
42
42
|
case 'usage':
|
|
43
43
|
return raw.trim();
|
|
44
|
+
case 'auto':
|
|
45
|
+
return raw.trim();
|
|
44
46
|
case 'browser':
|
|
45
47
|
return raw.trim();
|
|
46
48
|
case 'project':
|
|
47
49
|
return raw.trim();
|
|
48
50
|
case 'device': {
|
|
49
|
-
|
|
51
|
+
const property = parsed.property;
|
|
52
|
+
switch (property) {
|
|
53
|
+
case 'role':
|
|
54
|
+
return raw.trim();
|
|
50
55
|
case 'max-agents':
|
|
51
56
|
if (!/^\d+$/.test(raw.trim())) {
|
|
52
57
|
throw new Error(`Config key '${key}' expects an integer, got '${raw}'.`);
|
|
@@ -55,6 +60,7 @@ function parseValue(key, parsed, raw) {
|
|
|
55
60
|
case 'scheduler':
|
|
56
61
|
case 'daemon':
|
|
57
62
|
case 'watchdog':
|
|
63
|
+
case 'tmux':
|
|
58
64
|
case 'browser.remote-control':
|
|
59
65
|
return parseBool(raw, key);
|
|
60
66
|
case 'notes':
|
|
@@ -62,6 +68,13 @@ function parseValue(key, parsed, raw) {
|
|
|
62
68
|
case 'browser.profile':
|
|
63
69
|
return raw.trim();
|
|
64
70
|
}
|
|
71
|
+
// A device property with no arm above used to fall out of the switch and
|
|
72
|
+
// return `undefined`, so the write failed downstream with "expects a
|
|
73
|
+
// boolean, got undefined" instead of naming the real gap. The `never`
|
|
74
|
+
// binding makes adding a DeviceConfigProperty without a parse rule a
|
|
75
|
+
// compile error rather than a runtime mystery.
|
|
76
|
+
const unhandled = property;
|
|
77
|
+
throw new Error(`Config key '${key}' has no parse rule for device property '${String(unhandled)}'.`);
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
}
|
|
@@ -93,6 +106,10 @@ function setConfig(parsed, value) {
|
|
|
93
106
|
setConfigValue('usage.primary-host', value);
|
|
94
107
|
return;
|
|
95
108
|
}
|
|
109
|
+
case 'auto': {
|
|
110
|
+
setConfigValue('auto.pool', value);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
96
113
|
case 'browser': {
|
|
97
114
|
// Device-local default lives in the central fleet.devices.<name>.config
|
|
98
115
|
// block (same store `agents devices config` / getConfigValue use). Bare
|
|
@@ -137,6 +154,11 @@ function unsetConfig(parsed) {
|
|
|
137
154
|
unsetConfigValue('usage.primary-host');
|
|
138
155
|
return had;
|
|
139
156
|
}
|
|
157
|
+
case 'auto': {
|
|
158
|
+
const had = getConfigValue('auto.pool').value !== undefined;
|
|
159
|
+
unsetConfigValue('auto.pool');
|
|
160
|
+
return had;
|
|
161
|
+
}
|
|
140
162
|
case 'browser': {
|
|
141
163
|
const target = parsed.device ? { device: parsed.device } : undefined;
|
|
142
164
|
const had = getConfigValue('browser.profile', target).value !== undefined;
|
|
@@ -173,6 +195,8 @@ function getConfig(parsed) {
|
|
|
173
195
|
return getConfigValue('interactive.host').value;
|
|
174
196
|
case 'usage':
|
|
175
197
|
return getConfigValue('usage.primary-host').value;
|
|
198
|
+
case 'auto':
|
|
199
|
+
return getConfigValue('auto.pool').value;
|
|
176
200
|
case 'browser': {
|
|
177
201
|
return getConfigValue('browser.profile', parsed.device ? { device: parsed.device } : undefined).value;
|
|
178
202
|
}
|
|
@@ -231,6 +255,9 @@ function* listCentralConfigEntries() {
|
|
|
231
255
|
if (meta.config?.usagePrimaryHost !== undefined) {
|
|
232
256
|
yield { key: 'usage.primary-host', value: meta.config.usagePrimaryHost, hint: 'config.usagePrimaryHost' };
|
|
233
257
|
}
|
|
258
|
+
if (meta.config?.autoPool !== undefined) {
|
|
259
|
+
yield { key: 'auto.pool', value: meta.config.autoPool, hint: 'config.autoPool' };
|
|
260
|
+
}
|
|
234
261
|
if (meta.projectRoot !== undefined) {
|
|
235
262
|
yield { key: 'project.root', value: meta.projectRoot, hint: 'devices.<self>.projectRoot' };
|
|
236
263
|
}
|
|
@@ -262,6 +289,9 @@ function* listDeviceConfigEntries(device) {
|
|
|
262
289
|
case 'watchdog.enabled':
|
|
263
290
|
key = `${prefix}watchdog`;
|
|
264
291
|
break;
|
|
292
|
+
case 'tmux.enabled':
|
|
293
|
+
key = `${prefix}tmux`;
|
|
294
|
+
break;
|
|
265
295
|
case 'browser.remote-control':
|
|
266
296
|
key = `${prefix}browser.remote-control`;
|
|
267
297
|
break;
|
package/dist/commands/exec.js
CHANGED
|
@@ -557,7 +557,7 @@ export function registerRunCommand(program) {
|
|
|
557
557
|
.option('--terminal [backend]', "Open this run in a real terminal tab instead of here. Without a value the terminal is detected from your live sessions (`agents sessions --active` host), so it lands where you already work — Ghostty for a Ghostty user, iTerm for an iTerm user. Name one to force it: iterm | ghostty | terminal | tmux | vscodium-agent. This is how the menu bar's New Session opens.")
|
|
558
558
|
.option('--verbose', 'Show detailed execution logs')
|
|
559
559
|
.option('--raw', 'Interactive runs on macOS/Linux launch inside a shared tmux session (for %pane addressing + re-attach). Pass --raw to spawn the agent directly instead. Also disabled by AGENTS_NO_TMUX=1.')
|
|
560
|
-
.option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing
|
|
560
|
+
.option('--no-tmux', 'Spawn the agent directly instead of wrapping it in the shared tmux session. Same effect as --raw / AGENTS_NO_TMUX=1. Use this to see the agent\'s full startup output when a launch is failing; to turn the wrap off for every run on this machine, set `agents config set devices.<name>.tmux off`.')
|
|
561
561
|
.option('--disable-tmux', 'Alias for --no-tmux.')
|
|
562
562
|
.option('--timeout <duration>', 'Kill the agent after this duration (e.g., 30m, 1h, 2h30m)')
|
|
563
563
|
.option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,antigravity). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
|
package/dist/commands/ssh.js
CHANGED
|
@@ -56,7 +56,8 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
|
|
|
56
56
|
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
|
|
57
57
|
import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
58
58
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
59
|
-
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec } from '../lib/device-config.js';
|
|
59
|
+
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
|
|
60
|
+
import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
|
|
60
61
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
61
62
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
62
63
|
* command is running on so it stands out from the rest of the tailnet.
|
|
@@ -77,7 +78,20 @@ function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
|
|
|
77
78
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
78
79
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
79
80
|
const interactive = isInteractive ? chalk.yellow(' ★ interactive') : '';
|
|
80
|
-
|
|
81
|
+
const role = roleTag(d.name, listConfiguredDeviceRoles());
|
|
82
|
+
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}${interactive}${role}`;
|
|
83
|
+
}
|
|
84
|
+
/** The fleet-wide role mark, rendered for a device row. Empty when unmarked —
|
|
85
|
+
* an unmarked device is the common case and must not add a column of noise. */
|
|
86
|
+
function roleTag(name, roles) {
|
|
87
|
+
const role = roles[name];
|
|
88
|
+
if (!role)
|
|
89
|
+
return '';
|
|
90
|
+
if (role === 'worker')
|
|
91
|
+
return chalk.green(' worker');
|
|
92
|
+
if (role === 'personal')
|
|
93
|
+
return chalk.yellow(' personal');
|
|
94
|
+
return chalk.gray(' control');
|
|
81
95
|
}
|
|
82
96
|
const HEADROOM_BADGE = {
|
|
83
97
|
idle: chalk.green('○ idle'),
|
|
@@ -107,6 +121,7 @@ function pctCell(v, width) {
|
|
|
107
121
|
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
108
122
|
if (!statsMap)
|
|
109
123
|
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost));
|
|
124
|
+
const deviceRoles = listConfiguredDeviceRoles();
|
|
110
125
|
const lines = [];
|
|
111
126
|
const head = ' ' +
|
|
112
127
|
chalk.gray('device'.padEnd(16)) +
|
|
@@ -149,7 +164,7 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
149
164
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
150
165
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
151
166
|
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
152
|
-
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}`);
|
|
167
|
+
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}${roleTag(name, deviceRoles)}`);
|
|
153
168
|
}
|
|
154
169
|
// Fleet capacity summary — total cores + how much RAM is free right now.
|
|
155
170
|
const cap = fleetCapacity(statsMap.values());
|
|
@@ -1161,6 +1176,84 @@ function registerDevicesCommands(program) {
|
|
|
1161
1176
|
}
|
|
1162
1177
|
await runDevicesConfigMenu(name);
|
|
1163
1178
|
};
|
|
1179
|
+
/**
|
|
1180
|
+
* The `devices role` engine — read or write the fleet-wide role mark, and say
|
|
1181
|
+
* what it does to automatic placement.
|
|
1182
|
+
*
|
|
1183
|
+
* A role written here lands in the SHARED block (`fleet.devices.<name>.config.role`)
|
|
1184
|
+
* because every box has to agree on it. The vocabulary is deliberately
|
|
1185
|
+
* `worker | personal` only: a paired cockpit's `control` role lives in the
|
|
1186
|
+
* per-machine device registry, is written by `agents devices pair-ios`, and is
|
|
1187
|
+
* what the existing dial-exclusion filters (`isControlDevice`) read. Accepting
|
|
1188
|
+
* `control` here too would promise a fleet-wide dial exclusion this key cannot
|
|
1189
|
+
* deliver — those filters read each box's own registry, so the mark would only
|
|
1190
|
+
* hold on the machine that ran the command.
|
|
1191
|
+
*/
|
|
1192
|
+
const runDevicesRole = async (name, role, opts) => {
|
|
1193
|
+
if (!name) {
|
|
1194
|
+
if (role)
|
|
1195
|
+
throw new Error('Name a device: agents devices role <name> <worker|personal>');
|
|
1196
|
+
const roles = listConfiguredDeviceRoles();
|
|
1197
|
+
const mode = autoPoolMode();
|
|
1198
|
+
const reg = await loadDevices();
|
|
1199
|
+
const online = Object.entries(reg)
|
|
1200
|
+
.filter(([, d]) => d?.tailscale?.online !== false)
|
|
1201
|
+
.map(([n]) => n);
|
|
1202
|
+
const pool = filterAutoPool(online, { mode, roles });
|
|
1203
|
+
if (opts.json) {
|
|
1204
|
+
writeJson({ mode, roles, autoPool: pool });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const marked = Object.entries(roles);
|
|
1208
|
+
if (marked.length === 0) {
|
|
1209
|
+
console.log(chalk.gray('No device is marked. `--device auto` considers every online device.'));
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
for (const [device, r] of marked) {
|
|
1213
|
+
const tint = r === 'worker' ? chalk.green : r === 'personal' ? chalk.yellow : chalk.gray;
|
|
1214
|
+
console.log(` ${device.padEnd(20)} ${tint(r)}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
console.log();
|
|
1218
|
+
console.log(chalk.bold('--device auto picks from: ') + (pool.length > 0 ? pool.join(', ') : chalk.red('nothing — no eligible device')));
|
|
1219
|
+
if (mode === 'all')
|
|
1220
|
+
console.log(chalk.gray('auto.pool=all — worker marks are ignored (a personal device is still excluded).'));
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
await mustGetDevice(name);
|
|
1224
|
+
if (opts.clear || role === 'none') {
|
|
1225
|
+
setConfiguredDeviceRole(name, undefined);
|
|
1226
|
+
if (opts.json)
|
|
1227
|
+
writeJson({ device: name, role: null });
|
|
1228
|
+
else
|
|
1229
|
+
console.log(chalk.green(`Cleared the role on '${name}'.`));
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (!role) {
|
|
1233
|
+
const current = configuredDeviceRole(name);
|
|
1234
|
+
if (opts.json)
|
|
1235
|
+
writeJson({ device: name, role: current ?? null });
|
|
1236
|
+
else
|
|
1237
|
+
console.log(` ${name.padEnd(20)} ${current ? chalk.cyan(current) : chalk.gray('— (unmarked)')}`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
// configuredDeviceRole's key spec validates the value; a bad one throws with
|
|
1241
|
+
// the accepted list, which the command's catch turns into exit 1.
|
|
1242
|
+
setConfiguredDeviceRole(name, role);
|
|
1243
|
+
if (opts.json) {
|
|
1244
|
+
writeJson({ device: name, role, autoPoolWorkers: listWorkerDevices() });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
console.log(chalk.green(`Marked '${name}' role=${role}.`));
|
|
1248
|
+
const workers = listWorkerDevices();
|
|
1249
|
+
if (workers.length > 0) {
|
|
1250
|
+
console.log(chalk.gray(`\`--device auto\` now picks only from: ${workers.join(', ')}`));
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
console.log(chalk.gray('No device is marked worker, so `--device auto` still considers every online device.'));
|
|
1254
|
+
}
|
|
1255
|
+
console.log(chalk.gray('Sync it to the fleet with `agents repo push`.'));
|
|
1256
|
+
};
|
|
1164
1257
|
/** The interactive settings menu: pick a key, edit it, repeat. TTY-only. */
|
|
1165
1258
|
const runDevicesConfigMenu = async (name) => {
|
|
1166
1259
|
const { select, input, confirm } = await import('@inquirer/prompts');
|
|
@@ -1250,22 +1343,27 @@ function registerDevicesCommands(program) {
|
|
|
1250
1343
|
agents devices config win-mini ssh.auth password # password auth…
|
|
1251
1344
|
agents devices config win-mini ssh.bundle muqsit # …from this secrets bundle
|
|
1252
1345
|
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
1346
|
+
agents devices config mac-mini role worker # same as \`agents devices role mac-mini worker\`
|
|
1253
1347
|
agents devices config mac-mini auto-launch.enabled off # exclude from AGI EXT auto-launch
|
|
1254
1348
|
agents devices config mac-mini auto-launch.preferred on # boost in auto-launch ranking
|
|
1255
1349
|
agents devices config zion interactive.host zion # user scope: where agents show YOU artifacts
|
|
1256
1350
|
agents devices config mac-mini --json # machine-readable
|
|
1257
1351
|
`,
|
|
1258
1352
|
notes: `
|
|
1259
|
-
Keys:
|
|
1260
|
-
|
|
1261
|
-
|
|
1353
|
+
Keys: role (worker|personal), see 'agents devices role',
|
|
1354
|
+
agents.max-concurrent, scheduler.enabled, daemon.enabled,
|
|
1355
|
+
watchdog.enabled, tmux.enabled, browser.remote-control, browser.profile,
|
|
1356
|
+
notes, ssh.user, ssh.auth (key|password), ssh.bundle, ssh.bundle-key,
|
|
1262
1357
|
ssh.identity-file, platform (windows|linux|macos|unknown),
|
|
1263
1358
|
auto-launch.enabled, auto-launch.preferred — plus the user-scope
|
|
1264
1359
|
interactive.host (stored centrally; the device name is syntax only).
|
|
1265
1360
|
|
|
1266
1361
|
Booleans take on/off (or true/false). 'notes' appends one entry per
|
|
1267
|
-
invocation. Values land in ~/.agents/agents.yaml under
|
|
1268
|
-
fleet.devices.<name>.config and sync with 'agents repo push/pull'.
|
|
1362
|
+
invocation. Values a PEER reads land in ~/.agents/agents.yaml under
|
|
1363
|
+
fleet.devices.<name>.config and sync with 'agents repo push/pull'. The
|
|
1364
|
+
keys only the owning box reads — scheduler.enabled, daemon.enabled,
|
|
1365
|
+
tmux.enabled, browser.remote-control, browser.profile — stay in that
|
|
1366
|
+
machine's own doc, never sync, and can only be set on the device itself.
|
|
1269
1367
|
ssh.* / platform / user overlay the discovered registry profile at dial
|
|
1270
1368
|
time. scheduler.enabled / daemon.enabled take effect when the daemon
|
|
1271
1369
|
reloads or restarts on that device.
|
|
@@ -1274,6 +1372,49 @@ function registerDevicesCommands(program) {
|
|
|
1274
1372
|
set, set-interactive, enable, disable, prefer, unprefer.
|
|
1275
1373
|
`,
|
|
1276
1374
|
});
|
|
1375
|
+
const roleCmd = devicesCmd
|
|
1376
|
+
.command('role [name] [role]')
|
|
1377
|
+
.description('Show or set what a device is for: worker (agents run here) or personal (you sit here — never picked automatically). ' +
|
|
1378
|
+
'Marking any device worker makes `--device auto` an allowlist over the marked workers.')
|
|
1379
|
+
.option('--clear', 'remove the mark, returning the device to unmarked')
|
|
1380
|
+
.option('--json', 'output machine-readable JSON')
|
|
1381
|
+
.action(async (name, role, opts) => {
|
|
1382
|
+
try {
|
|
1383
|
+
await runDevicesRole(name, role, opts);
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
console.error(chalk.red(err.message));
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
setHelpSections(roleCmd, {
|
|
1391
|
+
examples: `
|
|
1392
|
+
agents devices role # who is marked what, and what --device auto would pick
|
|
1393
|
+
agents devices role yosemite-s0 worker # agents spin up here
|
|
1394
|
+
agents devices role yosemite-s1 worker # …and here; auto now rotates over these two only
|
|
1395
|
+
agents devices role zion personal # your laptop — keep automatic placement off it
|
|
1396
|
+
agents devices role yosemite-s0 --clear # unmark
|
|
1397
|
+
agents devices role --json # machine-readable
|
|
1398
|
+
`,
|
|
1399
|
+
notes: `
|
|
1400
|
+
Roles are stored fleet-wide in ~/.agents/agents.yaml under
|
|
1401
|
+
fleet.devices.<name>.config.role and travel with 'agents repo push/pull',
|
|
1402
|
+
so a mark set on one box is the whole fleet's answer.
|
|
1403
|
+
|
|
1404
|
+
Effect on '--device auto' (agents run, teams, agents ssh auto, and the AGI
|
|
1405
|
+
EXT launch commands, which all resolve placement through the CLI):
|
|
1406
|
+
no device marked -> every online device, as before
|
|
1407
|
+
any worker marked -> ONLY the marked workers
|
|
1408
|
+
personal -> never picked, under either state
|
|
1409
|
+
|
|
1410
|
+
Turn the allowlist off with 'agents config set auto.pool all'; a personal
|
|
1411
|
+
box stays excluded, since that is what the mark is for.
|
|
1412
|
+
|
|
1413
|
+
A paired iPhone/iPad cockpit is a separate role: 'agents devices pair-ios'
|
|
1414
|
+
marks it control in that box's device registry, and the fleet never dials
|
|
1415
|
+
it — including for placement. This command does not set that role.
|
|
1416
|
+
`,
|
|
1417
|
+
});
|
|
1277
1418
|
/** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
|
|
1278
1419
|
const configTombstoneNotice = (retired, replacement) => {
|
|
1279
1420
|
console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
|
|
@@ -1493,12 +1634,19 @@ function registerDevicesCommands(program) {
|
|
|
1493
1634
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
1494
1635
|
}
|
|
1495
1636
|
if (opts.json) {
|
|
1637
|
+
const jsonRoles = listConfiguredDeviceRoles();
|
|
1638
|
+
const autoPool = new Set(filterAutoPool(names, { roles: jsonRoles }));
|
|
1496
1639
|
process.stdout.write(JSON.stringify(names.map((name) => {
|
|
1497
1640
|
const config = deviceConfigJson(name);
|
|
1498
1641
|
const health = statsMap?.get(name);
|
|
1499
1642
|
return {
|
|
1500
1643
|
...resolveDeviceProfile(reg[name]),
|
|
1501
1644
|
interactive: name === interactiveHost,
|
|
1645
|
+
// Roles as machine-readable fields: `role` is what the operator
|
|
1646
|
+
// marked (absent when unmarked), `autoPool` is the answer that
|
|
1647
|
+
// matters to a caller — may `--device auto` pick this box.
|
|
1648
|
+
...(jsonRoles[name] ? { role: jsonRoles[name] } : {}),
|
|
1649
|
+
autoPool: autoPool.has(name),
|
|
1502
1650
|
...(config ? { config } : {}),
|
|
1503
1651
|
...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
|
|
1504
1652
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { AgentId } from './types.js';
|
|
10
10
|
import { type ModelTier } from './model-tiers.js';
|
|
11
11
|
/** The top-level scope of a unified config key. */
|
|
12
|
-
export type ConfigScope = 'run' | 'interactive' | 'usage' | 'browser' | 'project' | 'device';
|
|
12
|
+
export type ConfigScope = 'run' | 'interactive' | 'usage' | 'auto' | 'browser' | 'project' | 'device';
|
|
13
13
|
/** A run-time default key: model, mode, effort, or tier override. */
|
|
14
14
|
export interface ParsedRunConfigKey {
|
|
15
15
|
scope: 'run';
|
|
@@ -28,6 +28,11 @@ export interface ParsedUsageConfigKey {
|
|
|
28
28
|
scope: 'usage';
|
|
29
29
|
property: 'primary-host';
|
|
30
30
|
}
|
|
31
|
+
/** Which devices automatic placement (`--device auto`) may pick. */
|
|
32
|
+
export interface ParsedAutoConfigKey {
|
|
33
|
+
scope: 'auto';
|
|
34
|
+
property: 'pool';
|
|
35
|
+
}
|
|
31
36
|
/** The default browser profile (device-scope, self or peer). */
|
|
32
37
|
export interface ParsedBrowserConfigKey {
|
|
33
38
|
scope: 'browser';
|
|
@@ -44,8 +49,8 @@ export interface ParsedDeviceConfigKey {
|
|
|
44
49
|
device: string;
|
|
45
50
|
property: DeviceConfigProperty;
|
|
46
51
|
}
|
|
47
|
-
export type ParsedConfigKey = ParsedRunConfigKey | ParsedInteractiveConfigKey | ParsedUsageConfigKey | ParsedBrowserConfigKey | ParsedProjectConfigKey | ParsedDeviceConfigKey;
|
|
48
|
-
export type DeviceConfigProperty = 'max-agents' | 'scheduler' | 'daemon' | 'watchdog' | 'browser.remote-control' | 'notes' | 'browser.profile';
|
|
52
|
+
export type ParsedConfigKey = ParsedRunConfigKey | ParsedInteractiveConfigKey | ParsedUsageConfigKey | ParsedAutoConfigKey | ParsedBrowserConfigKey | ParsedProjectConfigKey | ParsedDeviceConfigKey;
|
|
53
|
+
export type DeviceConfigProperty = 'role' | 'max-agents' | 'scheduler' | 'daemon' | 'watchdog' | 'tmux' | 'browser.remote-control' | 'notes' | 'browser.profile';
|
|
49
54
|
/** Normalize agent@version to use `@` consistently. */
|
|
50
55
|
export declare function formatAgentVersion(agent: AgentId, version: string): string;
|
|
51
56
|
/**
|
|
@@ -58,12 +63,15 @@ export declare function formatAgentVersion(agent: AgentId, version: string): str
|
|
|
58
63
|
* run.<agent@version>.tier.<cheap|default|best|ultra>
|
|
59
64
|
* interactive.host
|
|
60
65
|
* usage.primary-host
|
|
66
|
+
* auto.pool
|
|
61
67
|
* browser.profile
|
|
62
68
|
* project.root
|
|
69
|
+
* devices.<name>.role
|
|
63
70
|
* devices.<name>.max-agents
|
|
64
71
|
* devices.<name>.scheduler
|
|
65
72
|
* devices.<name>.daemon
|
|
66
73
|
* devices.<name>.watchdog
|
|
74
|
+
* devices.<name>.tmux
|
|
67
75
|
* devices.<name>.browser.remote-control
|
|
68
76
|
* devices.<name>.notes
|
|
69
77
|
* devices.<name>.browser.profile
|
package/dist/lib/config-keys.js
CHANGED
|
@@ -10,10 +10,12 @@ import { AGENTS } from './agents.js';
|
|
|
10
10
|
import { MODEL_TIERS } from './model-tiers.js';
|
|
11
11
|
import { VERSION_RE } from './run-defaults.js';
|
|
12
12
|
const DEVICE_CONFIG_PROPERTIES = [
|
|
13
|
+
'role',
|
|
13
14
|
'max-agents',
|
|
14
15
|
'scheduler',
|
|
15
16
|
'daemon',
|
|
16
17
|
'watchdog',
|
|
18
|
+
'tmux',
|
|
17
19
|
'browser.remote-control',
|
|
18
20
|
'notes',
|
|
19
21
|
'browser.profile',
|
|
@@ -45,12 +47,15 @@ export function formatAgentVersion(agent, version) {
|
|
|
45
47
|
* run.<agent@version>.tier.<cheap|default|best|ultra>
|
|
46
48
|
* interactive.host
|
|
47
49
|
* usage.primary-host
|
|
50
|
+
* auto.pool
|
|
48
51
|
* browser.profile
|
|
49
52
|
* project.root
|
|
53
|
+
* devices.<name>.role
|
|
50
54
|
* devices.<name>.max-agents
|
|
51
55
|
* devices.<name>.scheduler
|
|
52
56
|
* devices.<name>.daemon
|
|
53
57
|
* devices.<name>.watchdog
|
|
58
|
+
* devices.<name>.tmux
|
|
54
59
|
* devices.<name>.browser.remote-control
|
|
55
60
|
* devices.<name>.notes
|
|
56
61
|
* devices.<name>.browser.profile
|
|
@@ -75,13 +80,16 @@ export function parseConfigKey(key) {
|
|
|
75
80
|
if (raw === 'usage.primary-host') {
|
|
76
81
|
return { scope: 'usage', property: 'primary-host' };
|
|
77
82
|
}
|
|
83
|
+
if (raw === 'auto.pool') {
|
|
84
|
+
return { scope: 'auto', property: 'pool' };
|
|
85
|
+
}
|
|
78
86
|
if (raw === 'browser.profile') {
|
|
79
87
|
return { scope: 'browser', property: 'profile' };
|
|
80
88
|
}
|
|
81
89
|
if (raw === 'project.root') {
|
|
82
90
|
return { scope: 'project', property: 'root' };
|
|
83
91
|
}
|
|
84
|
-
const deviceMatch = raw.match(/^devices\.(.+)\.(max-agents|scheduler|daemon|watchdog|notes|browser\.remote-control|browser\.profile)$/);
|
|
92
|
+
const deviceMatch = raw.match(/^devices\.(.+)\.(role|max-agents|scheduler|daemon|watchdog|tmux|notes|browser\.remote-control|browser\.profile)$/);
|
|
85
93
|
if (deviceMatch) {
|
|
86
94
|
return {
|
|
87
95
|
scope: 'device',
|
|
@@ -99,6 +107,9 @@ export function parseConfigKey(key) {
|
|
|
99
107
|
if (raw.startsWith('usage.')) {
|
|
100
108
|
throw new Error(`Invalid usage config key '${key}'. Use usage.primary-host.`);
|
|
101
109
|
}
|
|
110
|
+
if (raw.startsWith('auto.')) {
|
|
111
|
+
throw new Error(`Invalid auto config key '${key}'. Use auto.pool.`);
|
|
112
|
+
}
|
|
102
113
|
if (raw.startsWith('browser.')) {
|
|
103
114
|
throw new Error(`Invalid browser config key '${key}'. Use browser.profile.`);
|
|
104
115
|
}
|
|
@@ -108,7 +119,7 @@ export function parseConfigKey(key) {
|
|
|
108
119
|
if (raw.startsWith('devices.')) {
|
|
109
120
|
throw new Error(`Invalid device config key '${key}'. Expected devices.<name>.<${DEVICE_CONFIG_PROPERTIES.join('|')}>.`);
|
|
110
121
|
}
|
|
111
|
-
throw new Error(`Unknown config scope in '${key}'. Use one of: run, interactive, usage, browser, project, devices.`);
|
|
122
|
+
throw new Error(`Unknown config scope in '${key}'. Use one of: run, interactive, usage, auto, browser, project, devices.`);
|
|
112
123
|
}
|
|
113
124
|
/** Render a parsed key back to its canonical dotted string. */
|
|
114
125
|
export function formatConfigKey(parsed) {
|
|
@@ -122,6 +133,8 @@ export function formatConfigKey(parsed) {
|
|
|
122
133
|
return 'interactive.host';
|
|
123
134
|
case 'usage':
|
|
124
135
|
return 'usage.primary-host';
|
|
136
|
+
case 'auto':
|
|
137
|
+
return 'auto.pool';
|
|
125
138
|
case 'browser':
|
|
126
139
|
return parsed.device ? `devices.${parsed.device}.browser.profile` : 'browser.profile';
|
|
127
140
|
case 'project':
|
|
@@ -137,7 +150,7 @@ export function listKnownConfigKeys() {
|
|
|
137
150
|
for (const tier of MODEL_TIERS) {
|
|
138
151
|
keys.push(`run.<agent@version>.tier.${tier}`);
|
|
139
152
|
}
|
|
140
|
-
keys.push('interactive.host', 'usage.primary-host', 'browser.profile', 'project.root');
|
|
153
|
+
keys.push('interactive.host', 'usage.primary-host', 'auto.pool', 'browser.profile', 'project.root');
|
|
141
154
|
for (const prop of DEVICE_CONFIG_PROPERTIES) {
|
|
142
155
|
keys.push(`devices.<name>.${prop}`);
|
|
143
156
|
}
|
|
@@ -150,6 +163,8 @@ export function listKnownConfigKeys() {
|
|
|
150
163
|
*/
|
|
151
164
|
export function devicePropertyToConfigName(property) {
|
|
152
165
|
switch (property) {
|
|
166
|
+
case 'role':
|
|
167
|
+
return 'role';
|
|
153
168
|
case 'max-agents':
|
|
154
169
|
return 'agents.max-concurrent';
|
|
155
170
|
case 'scheduler':
|
|
@@ -158,6 +173,8 @@ export function devicePropertyToConfigName(property) {
|
|
|
158
173
|
return 'daemon.enabled';
|
|
159
174
|
case 'watchdog':
|
|
160
175
|
return 'watchdog.enabled';
|
|
176
|
+
case 'tmux':
|
|
177
|
+
return 'tmux.enabled';
|
|
161
178
|
case 'browser.remote-control':
|
|
162
179
|
return 'browser.remote-control';
|
|
163
180
|
case 'notes':
|
|
@@ -181,6 +198,8 @@ export function configKeyStorageHint(parsed) {
|
|
|
181
198
|
return 'config.interactiveHost';
|
|
182
199
|
case 'usage':
|
|
183
200
|
return 'config.usagePrimaryHost';
|
|
201
|
+
case 'auto':
|
|
202
|
+
return 'config.autoPool';
|
|
184
203
|
case 'browser':
|
|
185
204
|
return parsed.device
|
|
186
205
|
? `fleet.devices.${parsed.device}.config.defaultBrowserProfile`
|
|
@@ -14,4 +14,5 @@ export const MACHINE_LOCAL_YAML_KEYS = new Set([
|
|
|
14
14
|
'browserRemoteControl', // browser.remote-control — a consent flag; syncing it is a leak
|
|
15
15
|
'schedulerEnabled', // scheduler.enabled
|
|
16
16
|
'daemonEnabled', // daemon.enabled
|
|
17
|
+
'tmuxEnabled', // tmux.enabled — whether an interactive run is tmux-wrapped on THIS box
|
|
17
18
|
]);
|
|
@@ -91,6 +91,10 @@ export interface ConfigEntry {
|
|
|
91
91
|
export interface ConfigTarget {
|
|
92
92
|
device?: string;
|
|
93
93
|
}
|
|
94
|
+
/** Roles a device can be marked with — see the `role` key below. */
|
|
95
|
+
declare const DEVICE_ROLES: readonly ["worker", "personal"];
|
|
96
|
+
/** Which devices automatic placement may pick — see the `auto.pool` key below. */
|
|
97
|
+
declare const AUTO_POOL_MODES: readonly ["workers", "all"];
|
|
94
98
|
export declare const CONFIG_KEYS: readonly ConfigKeySpec[];
|
|
95
99
|
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
96
100
|
export declare function configKeySpec(name: string): ConfigKeySpec;
|
|
@@ -140,6 +144,27 @@ export declare function resolveUsagePrimaryHost(): string | null;
|
|
|
140
144
|
export declare function setConfigValue(name: string, value: unknown, opts?: ConfigTarget): void;
|
|
141
145
|
/** Unset a config key — restores default behavior. No-op when already unset. */
|
|
142
146
|
export declare function unsetConfigValue(name: string, opts?: ConfigTarget): void;
|
|
147
|
+
/** A role an operator marked a device with (`agents devices role <name> <role>`). */
|
|
148
|
+
export type ConfiguredDeviceRole = (typeof DEVICE_ROLES)[number];
|
|
149
|
+
/** Which devices automatic placement may pick (`auto.pool`). */
|
|
150
|
+
export type AutoPoolMode = (typeof AUTO_POOL_MODES)[number];
|
|
151
|
+
/**
|
|
152
|
+
* The role marked on one device, or undefined when the operator never marked it.
|
|
153
|
+
*
|
|
154
|
+
* Undefined is meaningful and is NOT the same as `worker`: an unmarked device is
|
|
155
|
+
* eligible for automatic placement only while no device anywhere carries an
|
|
156
|
+
* explicit `worker` mark (see {@link listConfiguredDeviceRoles}).
|
|
157
|
+
*/
|
|
158
|
+
export declare function configuredDeviceRole(name: string): ConfiguredDeviceRole | undefined;
|
|
159
|
+
/** Mark a device's role fleet-wide; `undefined` clears the mark. */
|
|
160
|
+
export declare function setConfiguredDeviceRole(name: string, role: ConfiguredDeviceRole | undefined): void;
|
|
161
|
+
/**
|
|
162
|
+
* Every device an operator has marked, keyed by device name. Devices with no
|
|
163
|
+
* mark are absent — that absence is what makes the worker allowlist opt-in.
|
|
164
|
+
*/
|
|
165
|
+
export declare function listConfiguredDeviceRoles(): Record<string, ConfiguredDeviceRole>;
|
|
166
|
+
/** The configured automatic-placement pool mode. Unset means `workers`. */
|
|
167
|
+
export declare function autoPoolMode(): AutoPoolMode;
|
|
143
168
|
/** A device's auto-launch flags, as read by the ext's launch ranking. */
|
|
144
169
|
export interface AutoLaunchPreference {
|
|
145
170
|
enabled?: boolean;
|
|
@@ -167,6 +192,15 @@ export declare function isSchedulerEnabled(): boolean;
|
|
|
167
192
|
* scheduler init) refuses with.
|
|
168
193
|
*/
|
|
169
194
|
export declare function assertSchedulerEnabled(): void;
|
|
195
|
+
/**
|
|
196
|
+
* True unless this machine's config turns off the managed tmux wrap for
|
|
197
|
+
* interactive `agents run` launches (`tmux.enabled=false`).
|
|
198
|
+
*
|
|
199
|
+
* Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
|
|
200
|
+
* per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
|
|
201
|
+
* broken or unwanted. Unset means today's behavior: wrap.
|
|
202
|
+
*/
|
|
203
|
+
export declare function isTmuxEnabled(): boolean;
|
|
170
204
|
/** True unless this machine's config disables the daemon outright (top-level kill switch). */
|
|
171
205
|
export declare function isDaemonEnabled(): boolean;
|
|
172
206
|
/**
|