@phnx-labs/agents-cli 1.22.37 → 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 +50 -0
- package/README.md +8 -8
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/artifacts-setup.d.ts +53 -0
- package/dist/commands/{setup-share.js → artifacts-setup.js} +59 -13
- package/dist/commands/artifacts.d.ts +18 -0
- package/dist/commands/artifacts.js +58 -0
- package/dist/commands/browser.js +2 -0
- package/dist/commands/config.js +31 -1
- package/dist/commands/exec.js +2 -2
- package/dist/commands/models.js +67 -0
- package/dist/commands/setup.js +5 -5
- package/dist/commands/share.d.ts +20 -7
- package/dist/commands/share.js +74 -75
- package/dist/commands/ssh.js +156 -8
- package/dist/lib/browser/hygiene.d.ts +90 -0
- package/dist/lib/browser/hygiene.js +146 -0
- package/dist/lib/browser/ipc.js +12 -0
- package/dist/lib/browser/service.d.ts +75 -1
- package/dist/lib/browser/service.js +201 -11
- package/dist/lib/browser/types.d.ts +44 -1
- 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/git.d.ts +1 -1
- package/dist/lib/git.js +1 -1
- 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/routines.d.ts +3 -1
- 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/share/analytics.js +1 -1
- package/dist/lib/share/capture.d.ts +1 -1
- package/dist/lib/share/capture.js +3 -3
- package/dist/lib/share/config.d.ts +2 -2
- package/dist/lib/share/config.js +5 -5
- package/dist/lib/share/delete.js +3 -3
- package/dist/lib/share/provision.js +3 -3
- package/dist/lib/share/publish.d.ts +1 -1
- package/dist/lib/share/publish.js +3 -3
- package/dist/lib/share/worker-template.d.ts +12 -1
- package/dist/lib/share/worker-template.js +13 -2
- package/dist/lib/smart-launch.d.ts +33 -3
- package/dist/lib/smart-launch.js +61 -6
- package/dist/lib/startup/command-registry.d.ts +10 -2
- package/dist/lib/startup/command-registry.js +16 -7
- package/dist/lib/tmux/orphan-reap.d.ts +15 -19
- package/dist/lib/tmux/orphan-reap.js +15 -21
- package/dist/lib/tmux/session.js +4 -3
- package/dist/lib/triggers/handlers.js +10 -0
- package/dist/lib/triggers/webhook.js +10 -0
- package/dist/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/commands/set.d.ts +0 -15
- package/dist/commands/set.js +0 -79
- package/dist/commands/setup-share.d.ts +0 -17
|
@@ -77,6 +77,20 @@ export interface Task {
|
|
|
77
77
|
tabs: Record<string, string>;
|
|
78
78
|
currentTabId?: string;
|
|
79
79
|
createdAt: number;
|
|
80
|
+
/**
|
|
81
|
+
* When this task last did anything. Stamped at creation (equal to
|
|
82
|
+
* `createdAt`) and refreshed by every task-scoped action, because every one
|
|
83
|
+
* of them resolves the task through `BrowserService.findTask`.
|
|
84
|
+
*
|
|
85
|
+
* Read by the idle half of the abandoned-task reaper (`hygiene.ts`): a task
|
|
86
|
+
* nobody has driven for `idleMs` is stopped so its tabs stop piling up in the
|
|
87
|
+
* profile window (RUSH-2622).
|
|
88
|
+
*
|
|
89
|
+
* Persisted in tasks.json. Tasks written before RUSH-2622 carry none;
|
|
90
|
+
* `loadTaskState` normalizes those to `createdAt` on read, so every task in
|
|
91
|
+
* memory has one and the reaper never has to guess.
|
|
92
|
+
*/
|
|
93
|
+
lastActionAt: number;
|
|
80
94
|
pid: number;
|
|
81
95
|
/**
|
|
82
96
|
* Resolved actor id (`resolveActor().id`) stamped at task start — WHO launched
|
|
@@ -151,7 +165,24 @@ export interface HistoricalTask {
|
|
|
151
165
|
/** Actor id who launched the task (RUSH-2020); absent for pre-RUSH-2020 history. */
|
|
152
166
|
owner?: string;
|
|
153
167
|
}
|
|
154
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Why the reaper stopped a task. `session-dead` — the agent session (or run)
|
|
170
|
+
* that started it is provably gone; `idle` — nothing has driven it for the
|
|
171
|
+
* idle window. See `hygiene.ts`.
|
|
172
|
+
*/
|
|
173
|
+
export type ReapReason = 'session-dead' | 'idle';
|
|
174
|
+
/** One task the reaper stopped (or, under `dryRun`, would have stopped). */
|
|
175
|
+
export interface ReapedTask {
|
|
176
|
+
task: string;
|
|
177
|
+
profile: string;
|
|
178
|
+
reason: ReapReason;
|
|
179
|
+
}
|
|
180
|
+
export interface ReapResult {
|
|
181
|
+
closed: ReapedTask[];
|
|
182
|
+
/** Tasks left alone this pass: still live, still inside the idle window, or recording. */
|
|
183
|
+
skipped: number;
|
|
184
|
+
}
|
|
185
|
+
export type IPCAction = 'start' | 'gc' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'pdf' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
|
|
155
186
|
export interface IPCRequest {
|
|
156
187
|
action: IPCAction;
|
|
157
188
|
task?: string;
|
|
@@ -201,6 +232,16 @@ export interface IPCRequest {
|
|
|
201
232
|
until?: string;
|
|
202
233
|
appLevel?: string;
|
|
203
234
|
skipDomainSkill?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Browser start: always open a new tab. Without it, `start --url` reclaims a
|
|
237
|
+
* tab that an ABANDONED task is still holding on this exact URL rather than
|
|
238
|
+
* opening a duplicate (RUSH-2622). A tab held by a live task, or one nobody's
|
|
239
|
+
* task owns (the user's own), is never taken either way — set this when the
|
|
240
|
+
* caller wants its own tab regardless.
|
|
241
|
+
*/
|
|
242
|
+
fresh?: boolean;
|
|
243
|
+
idleMinutes?: number;
|
|
244
|
+
dryRun?: boolean;
|
|
204
245
|
actor?: string;
|
|
205
246
|
launchId?: string;
|
|
206
247
|
/**
|
|
@@ -236,6 +277,8 @@ export interface IPCResponse {
|
|
|
236
277
|
tabs?: TabInfo[];
|
|
237
278
|
profiles?: ProfileStatus[];
|
|
238
279
|
history?: HistoricalTask[];
|
|
280
|
+
/** `gc`: what the abandoned-task reaper closed (or would close under dryRun). */
|
|
281
|
+
reaped?: ReapResult;
|
|
239
282
|
result?: unknown;
|
|
240
283
|
path?: string;
|
|
241
284
|
bytes?: number;
|
|
@@ -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
|
/**
|
|
@@ -38,6 +38,10 @@ import { assertValidDeviceName } from './devices/registry.js';
|
|
|
38
38
|
import { fleetDevicesMapForWrite, migrateDeviceConfigToCentral } from './devices/config-migration.js';
|
|
39
39
|
const DEVICE_PLATFORMS = ['windows', 'linux', 'macos', 'unknown'];
|
|
40
40
|
const SSH_AUTH_METHODS = ['key', 'password'];
|
|
41
|
+
/** Roles a device can be marked with — see the `role` key below. */
|
|
42
|
+
const DEVICE_ROLES = ['worker', 'personal'];
|
|
43
|
+
/** Which devices automatic placement may pick — see the `auto.pool` key below. */
|
|
44
|
+
const AUTO_POOL_MODES = ['workers', 'all'];
|
|
41
45
|
export const CONFIG_KEYS = [
|
|
42
46
|
{
|
|
43
47
|
name: 'interactive.host',
|
|
@@ -71,6 +75,19 @@ export const CONFIG_KEYS = [
|
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
},
|
|
78
|
+
{
|
|
79
|
+
name: 'auto.pool',
|
|
80
|
+
yamlKey: 'autoPool',
|
|
81
|
+
scope: 'user',
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: "Which devices automatic placement (`--device auto`) may pick: 'workers' (default — only devices marked role=worker, " +
|
|
84
|
+
"once at least one is marked) or 'all' (every online device, ignoring worker marks). A device marked personal is " +
|
|
85
|
+
'never picked automatically under either mode.',
|
|
86
|
+
defaultValue: 'workers',
|
|
87
|
+
validate: (v) => AUTO_POOL_MODES.includes(v)
|
|
88
|
+
? null
|
|
89
|
+
: `auto.pool must be one of ${AUTO_POOL_MODES.join(' | ')}.`,
|
|
90
|
+
},
|
|
74
91
|
{
|
|
75
92
|
name: 'browser.profile',
|
|
76
93
|
yamlKey: 'defaultBrowserProfile',
|
|
@@ -119,6 +136,18 @@ export const CONFIG_KEYS = [
|
|
|
119
136
|
defaultValue: false,
|
|
120
137
|
description: 'Whether the daemon runs the watchdog pass on this device.',
|
|
121
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'tmux.enabled',
|
|
141
|
+
yamlKey: 'tmuxEnabled',
|
|
142
|
+
scope: 'device',
|
|
143
|
+
visibility: 'machine',
|
|
144
|
+
type: 'bool',
|
|
145
|
+
defaultValue: true,
|
|
146
|
+
description: 'Whether an interactive `agents run` on this device is wrapped in the shared-socket tmux session. ' +
|
|
147
|
+
'On gives every agent an addressable pane (`agents sessions --active` tells co-located agents apart, ' +
|
|
148
|
+
'`agents focus` re-attaches without forking). Off spawns the agent directly on this box — the durable ' +
|
|
149
|
+
'form of `--no-tmux`, for a machine whose tmux is broken or unwanted.',
|
|
150
|
+
},
|
|
122
151
|
{
|
|
123
152
|
name: 'browser.remote-control',
|
|
124
153
|
yamlKey: 'browserRemoteControl',
|
|
@@ -191,6 +220,20 @@ export const CONFIG_KEYS = [
|
|
|
191
220
|
? null
|
|
192
221
|
: `platform must be one of ${DEVICE_PLATFORMS.join(' | ')}.`,
|
|
193
222
|
},
|
|
223
|
+
{
|
|
224
|
+
name: 'role',
|
|
225
|
+
yamlKey: 'role',
|
|
226
|
+
scope: 'device',
|
|
227
|
+
visibility: 'shared',
|
|
228
|
+
type: 'string',
|
|
229
|
+
description: "What this device is for, fleet-wide: 'worker' (a box agents run on) or 'personal' (a machine you sit at — never " +
|
|
230
|
+
'picked automatically). Marking ANY device worker turns automatic placement into an allowlist: `--device auto` then ' +
|
|
231
|
+
'picks only from the marked workers. (A paired iPhone/iPad cockpit is marked control by `agents devices pair-ios` ' +
|
|
232
|
+
'and is excluded from placement by that role, not this key.)',
|
|
233
|
+
validate: (v) => DEVICE_ROLES.includes(v)
|
|
234
|
+
? null
|
|
235
|
+
: `role must be one of ${DEVICE_ROLES.join(' | ')}.`,
|
|
236
|
+
},
|
|
194
237
|
{
|
|
195
238
|
name: 'auto-launch.enabled',
|
|
196
239
|
yamlKey: 'autoLaunchEnabled',
|
|
@@ -442,6 +485,48 @@ export function unsetConfigValue(name, opts) {
|
|
|
442
485
|
}
|
|
443
486
|
unsetInCentralBlock(device, spec);
|
|
444
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* The role marked on one device, or undefined when the operator never marked it.
|
|
490
|
+
*
|
|
491
|
+
* Undefined is meaningful and is NOT the same as `worker`: an unmarked device is
|
|
492
|
+
* eligible for automatic placement only while no device anywhere carries an
|
|
493
|
+
* explicit `worker` mark (see {@link listConfiguredDeviceRoles}).
|
|
494
|
+
*/
|
|
495
|
+
export function configuredDeviceRole(name) {
|
|
496
|
+
assertValidDeviceName(name);
|
|
497
|
+
return getConfigValue('role', { device: name }).value;
|
|
498
|
+
}
|
|
499
|
+
/** Mark a device's role fleet-wide; `undefined` clears the mark. */
|
|
500
|
+
export function setConfiguredDeviceRole(name, role) {
|
|
501
|
+
assertValidDeviceName(name);
|
|
502
|
+
if (role === undefined)
|
|
503
|
+
unsetConfigValue('role', { device: name });
|
|
504
|
+
else
|
|
505
|
+
setConfigValue('role', role, { device: name });
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Every device an operator has marked, keyed by device name. Devices with no
|
|
509
|
+
* mark are absent — that absence is what makes the worker allowlist opt-in.
|
|
510
|
+
*/
|
|
511
|
+
export function listConfiguredDeviceRoles() {
|
|
512
|
+
ensureDeviceConfigMigrated();
|
|
513
|
+
const devices = readMeta().fleet?.devices;
|
|
514
|
+
const out = {};
|
|
515
|
+
if (!devices || devices === 'all')
|
|
516
|
+
return out;
|
|
517
|
+
for (const [name, override] of Object.entries(devices)) {
|
|
518
|
+
const role = override?.config?.role;
|
|
519
|
+
if (typeof role === 'string' && DEVICE_ROLES.includes(role)) {
|
|
520
|
+
out[name] = role;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
/** The configured automatic-placement pool mode. Unset means `workers`. */
|
|
526
|
+
export function autoPoolMode() {
|
|
527
|
+
const value = getConfigValue('auto.pool').value;
|
|
528
|
+
return value === 'all' ? 'all' : 'workers';
|
|
529
|
+
}
|
|
445
530
|
/** True if the device is enabled for auto-launch. Unset defaults to true. */
|
|
446
531
|
export function isAutoLaunchEnabled(name) {
|
|
447
532
|
assertValidDeviceName(name);
|
|
@@ -509,6 +594,17 @@ export function assertSchedulerEnabled() {
|
|
|
509
594
|
throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/agents.yaml fleet.devices.${machineId()}.config). ` +
|
|
510
595
|
`Re-enable with: agents devices config ${machineId()} scheduler.enabled on`);
|
|
511
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* True unless this machine's config turns off the managed tmux wrap for
|
|
599
|
+
* interactive `agents run` launches (`tmux.enabled=false`).
|
|
600
|
+
*
|
|
601
|
+
* Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
|
|
602
|
+
* per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
|
|
603
|
+
* broken or unwanted. Unset means today's behavior: wrap.
|
|
604
|
+
*/
|
|
605
|
+
export function isTmuxEnabled() {
|
|
606
|
+
return getConfigValue('tmux.enabled').value !== false;
|
|
607
|
+
}
|
|
512
608
|
/** True unless this machine's config disables the daemon outright (top-level kill switch). */
|
|
513
609
|
export function isDaemonEnabled() {
|
|
514
610
|
return getConfigValue('daemon.enabled').value !== false;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The automatic-placement pool — which devices `--device auto` may pick from.
|
|
3
|
+
*
|
|
4
|
+
* One rule, in one place, so every automatic-placement path agrees: `agents run
|
|
5
|
+
* --device auto`, `agents teams add --device auto`, the generic host resolver,
|
|
6
|
+
* and the AGI EXT launch commands (which emit `--device auto` rather than
|
|
7
|
+
* scoring devices themselves) all draw from {@link filterAutoPool}.
|
|
8
|
+
*
|
|
9
|
+
* The pool is an ALLOWLIST the moment the operator marks a worker. Roles are
|
|
10
|
+
* stored in the fleet-shared `fleet.devices.<name>.config.role` block of
|
|
11
|
+
* `~/.agents/agents.yaml` (see `lib/device-config.ts`), which is the only
|
|
12
|
+
* device store that syncs — the device registry under `~/.agents/devices/` is
|
|
13
|
+
* gitignored and per-machine, so a role kept there could never be a fleet-wide
|
|
14
|
+
* statement.
|
|
15
|
+
*
|
|
16
|
+
* | Fleet state | `--device auto` picks from |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | no device marked | every online device (unchanged behavior) |
|
|
19
|
+
* | some marked `worker` | ONLY those workers |
|
|
20
|
+
* | marked `personal` | never, under either state |
|
|
21
|
+
*
|
|
22
|
+
* `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
|
|
23
|
+
* machine the user sits at is marked precisely so agents stay off it.
|
|
24
|
+
*
|
|
25
|
+
* Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
|
|
26
|
+
* by `agents devices pair-ios`) are excluded by the CALLER that reads the
|
|
27
|
+
* registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
|
|
28
|
+
* role is machine-local by nature and already has a home; duplicating it in the
|
|
29
|
+
* shared config would be a second store for one concept.
|
|
30
|
+
*/
|
|
31
|
+
import { type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
|
|
32
|
+
export interface AutoPoolOptions {
|
|
33
|
+
/** Pool mode; defaults to the configured `auto.pool`. */
|
|
34
|
+
mode?: AutoPoolMode;
|
|
35
|
+
/** Configured roles by device name; defaults to the fleet-shared block. */
|
|
36
|
+
roles?: Record<string, ConfiguredDeviceRole>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Narrow a candidate host list to the devices automatic placement may pick.
|
|
40
|
+
*
|
|
41
|
+
* Returns the input order, minus the excluded devices. An empty result is a
|
|
42
|
+
* real answer — "you marked workers and none of them is a candidate right now"
|
|
43
|
+
* — and callers surface it as their own no-healthy-device error rather than
|
|
44
|
+
* quietly widening back to the full fleet.
|
|
45
|
+
*/
|
|
46
|
+
export declare function filterAutoPool(pool: string[], opts?: AutoPoolOptions): string[];
|
|
47
|
+
/** True when this host is one automatic placement may pick. */
|
|
48
|
+
export declare function isAutoPoolMember(host: string, opts?: AutoPoolOptions): boolean;
|
|
49
|
+
/** Device names explicitly marked `worker`, in registry order. */
|
|
50
|
+
export declare function listWorkerDevices(opts?: Pick<AutoPoolOptions, 'roles'>): string[];
|
|
51
|
+
/**
|
|
52
|
+
* One line naming why the pool is what it is, for the `--device auto` banner and
|
|
53
|
+
* the no-healthy-device error. Empty string when no role narrows anything, so
|
|
54
|
+
* callers can append it unconditionally.
|
|
55
|
+
*/
|
|
56
|
+
export declare function describeAutoPool(opts?: AutoPoolOptions): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The automatic-placement pool — which devices `--device auto` may pick from.
|
|
3
|
+
*
|
|
4
|
+
* One rule, in one place, so every automatic-placement path agrees: `agents run
|
|
5
|
+
* --device auto`, `agents teams add --device auto`, the generic host resolver,
|
|
6
|
+
* and the AGI EXT launch commands (which emit `--device auto` rather than
|
|
7
|
+
* scoring devices themselves) all draw from {@link filterAutoPool}.
|
|
8
|
+
*
|
|
9
|
+
* The pool is an ALLOWLIST the moment the operator marks a worker. Roles are
|
|
10
|
+
* stored in the fleet-shared `fleet.devices.<name>.config.role` block of
|
|
11
|
+
* `~/.agents/agents.yaml` (see `lib/device-config.ts`), which is the only
|
|
12
|
+
* device store that syncs — the device registry under `~/.agents/devices/` is
|
|
13
|
+
* gitignored and per-machine, so a role kept there could never be a fleet-wide
|
|
14
|
+
* statement.
|
|
15
|
+
*
|
|
16
|
+
* | Fleet state | `--device auto` picks from |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | no device marked | every online device (unchanged behavior) |
|
|
19
|
+
* | some marked `worker` | ONLY those workers |
|
|
20
|
+
* | marked `personal` | never, under either state |
|
|
21
|
+
*
|
|
22
|
+
* `auto.pool all` turns the allowlist off; `personal` stays excluded, because a
|
|
23
|
+
* machine the user sits at is marked precisely so agents stay off it.
|
|
24
|
+
*
|
|
25
|
+
* Paired cockpits (an iPhone/iPad, `role: control` in the device registry, set
|
|
26
|
+
* by `agents devices pair-ios`) are excluded by the CALLER that reads the
|
|
27
|
+
* registry — `listOnlineDeviceNames` in `lib/smart-launch.ts` — not here. That
|
|
28
|
+
* role is machine-local by nature and already has a home; duplicating it in the
|
|
29
|
+
* shared config would be a second store for one concept.
|
|
30
|
+
*/
|
|
31
|
+
import { autoPoolMode, listConfiguredDeviceRoles } from '../device-config.js';
|
|
32
|
+
import { normalizeHost } from '../machine-id.js';
|
|
33
|
+
/** Roles that automatic placement never picks, whatever the pool mode. */
|
|
34
|
+
const NEVER_AUTO = new Set(['personal']);
|
|
35
|
+
/**
|
|
36
|
+
* Narrow a candidate host list to the devices automatic placement may pick.
|
|
37
|
+
*
|
|
38
|
+
* Returns the input order, minus the excluded devices. An empty result is a
|
|
39
|
+
* real answer — "you marked workers and none of them is a candidate right now"
|
|
40
|
+
* — and callers surface it as their own no-healthy-device error rather than
|
|
41
|
+
* quietly widening back to the full fleet.
|
|
42
|
+
*/
|
|
43
|
+
export function filterAutoPool(pool, opts = {}) {
|
|
44
|
+
const roles = opts.roles ?? listConfiguredDeviceRoles();
|
|
45
|
+
const byHost = new Map(Object.entries(roles).map(([name, role]) => [normalizeHost(name), role]));
|
|
46
|
+
const roleOf = (host) => byHost.get(normalizeHost(host));
|
|
47
|
+
const eligible = pool.filter((host) => {
|
|
48
|
+
const role = roleOf(host);
|
|
49
|
+
return role === undefined || !NEVER_AUTO.has(role);
|
|
50
|
+
});
|
|
51
|
+
const mode = opts.mode ?? autoPoolMode();
|
|
52
|
+
if (mode === 'all')
|
|
53
|
+
return eligible;
|
|
54
|
+
const anyWorkerMarked = [...byHost.values()].some((role) => role === 'worker');
|
|
55
|
+
if (!anyWorkerMarked)
|
|
56
|
+
return eligible;
|
|
57
|
+
return eligible.filter((host) => roleOf(host) === 'worker');
|
|
58
|
+
}
|
|
59
|
+
/** True when this host is one automatic placement may pick. */
|
|
60
|
+
export function isAutoPoolMember(host, opts = {}) {
|
|
61
|
+
return filterAutoPool([host], opts).length > 0;
|
|
62
|
+
}
|
|
63
|
+
/** Device names explicitly marked `worker`, in registry order. */
|
|
64
|
+
export function listWorkerDevices(opts = {}) {
|
|
65
|
+
const roles = opts.roles ?? listConfiguredDeviceRoles();
|
|
66
|
+
return Object.entries(roles)
|
|
67
|
+
.filter(([, role]) => role === 'worker')
|
|
68
|
+
.map(([name]) => name);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* One line naming why the pool is what it is, for the `--device auto` banner and
|
|
72
|
+
* the no-healthy-device error. Empty string when no role narrows anything, so
|
|
73
|
+
* callers can append it unconditionally.
|
|
74
|
+
*/
|
|
75
|
+
export function describeAutoPool(opts = {}) {
|
|
76
|
+
const roles = opts.roles ?? listConfiguredDeviceRoles();
|
|
77
|
+
const mode = opts.mode ?? autoPoolMode();
|
|
78
|
+
const workers = listWorkerDevices({ roles });
|
|
79
|
+
if (mode === 'all') {
|
|
80
|
+
return workers.length > 0 ? 'auto.pool=all (worker marks ignored)' : '';
|
|
81
|
+
}
|
|
82
|
+
if (workers.length === 0)
|
|
83
|
+
return '';
|
|
84
|
+
return `workers: ${workers.join(', ')}`;
|
|
85
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -398,6 +398,8 @@ export interface TmuxWrapContext {
|
|
|
398
398
|
raw: boolean;
|
|
399
399
|
/** The AGENTS_NO_TMUX=1 escape hatch. */
|
|
400
400
|
noTmuxEnv: boolean;
|
|
401
|
+
/** This device's `tmux.enabled` config — false turns the wrap off for every launch on this box. */
|
|
402
|
+
configEnabled: boolean;
|
|
401
403
|
/** Whether a tmux binary is on PATH. */
|
|
402
404
|
tmuxAvailable: boolean;
|
|
403
405
|
}
|
|
@@ -410,12 +412,13 @@ export interface TmuxWrapContext {
|
|
|
410
412
|
* focus` re-attach a live session without forking it. Pure so the gate is unit-
|
|
411
413
|
* tested independently of the (side-effecting) spawn.
|
|
412
414
|
*
|
|
413
|
-
* All
|
|
415
|
+
* All seven guards must pass:
|
|
414
416
|
* - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
|
|
415
417
|
* - not Windows — no tmux path on win32.
|
|
416
418
|
* - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
|
|
417
419
|
* - not --raw — explicit opt-out.
|
|
418
420
|
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
421
|
+
* - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
|
|
419
422
|
* - tmux installed — otherwise there is nothing to wrap with.
|
|
420
423
|
*/
|
|
421
424
|
export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
package/dist/lib/exec.js
CHANGED
|
@@ -29,6 +29,8 @@ import { recordRunName } from './session/run-names.js';
|
|
|
29
29
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
30
30
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
31
31
|
import { isTmuxInstalled } from './tmux/binary.js';
|
|
32
|
+
import { isTmuxEnabled } from './device-config.js';
|
|
33
|
+
import { machineId } from './machine-id.js';
|
|
32
34
|
import { shellQuote } from './ssh-exec.js';
|
|
33
35
|
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
34
36
|
import { codexEditWritableRoots, codexPolicyArgs } from './codex-policy.js';
|
|
@@ -1327,12 +1329,13 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
|
|
|
1327
1329
|
* focus` re-attach a live session without forking it. Pure so the gate is unit-
|
|
1328
1330
|
* tested independently of the (side-effecting) spawn.
|
|
1329
1331
|
*
|
|
1330
|
-
* All
|
|
1332
|
+
* All seven guards must pass:
|
|
1331
1333
|
* - interactive — a headless `-p` run has no TTY to attach; keep bare spawn.
|
|
1332
1334
|
* - not Windows — no tmux path on win32.
|
|
1333
1335
|
* - not already in tmux — nesting tmux-in-tmux is pointless and confusing.
|
|
1334
1336
|
* - not --raw — explicit opt-out.
|
|
1335
1337
|
* - not AGENTS_NO_TMUX=1 — env opt-out (CI, scripts, the shim passthrough path).
|
|
1338
|
+
* - tmux.enabled — this device's durable opt-out, for a box whose tmux is broken.
|
|
1336
1339
|
* - tmux installed — otherwise there is nothing to wrap with.
|
|
1337
1340
|
*/
|
|
1338
1341
|
export function shouldWrapInTmux(ctx) {
|
|
@@ -1346,6 +1349,8 @@ export function shouldWrapInTmux(ctx) {
|
|
|
1346
1349
|
return false;
|
|
1347
1350
|
if (ctx.noTmuxEnv)
|
|
1348
1351
|
return false;
|
|
1352
|
+
if (!ctx.configEnabled)
|
|
1353
|
+
return false;
|
|
1349
1354
|
if (!ctx.tmuxAvailable)
|
|
1350
1355
|
return false;
|
|
1351
1356
|
return true;
|
|
@@ -1459,7 +1464,7 @@ async function runInTmux(options, executable, args) {
|
|
|
1459
1464
|
const idSeed = (options.sessionId ?? randomUUID()).slice(0, 8);
|
|
1460
1465
|
const name = slugifyName(`ag-${options.agent}-${idSeed}`);
|
|
1461
1466
|
const RED = '\x1b[31m', GRAY = '\x1b[90m', OFF = '\x1b[0m';
|
|
1462
|
-
const NO_TMUX_TIP = `${GRAY} Tip: re-run with --no-tmux to launch the agent directly and see its full output.${OFF}\n\n`;
|
|
1467
|
+
const NO_TMUX_TIP = `${GRAY} Tip: re-run with --no-tmux to launch the agent directly and see its full output.\n If tmux is broken on this machine, turn the wrap off for good: agents config set devices.${machineId()}.tmux off${OFF}\n\n`;
|
|
1463
1468
|
// Recap a dead pane's tail into THIS shell's stderr. The pane-died hook
|
|
1464
1469
|
// detaches the client the instant the agent exits, so a fast failure (a
|
|
1465
1470
|
// gutted install that dies with ENOENT, a bad flag, a crash on startup) would
|
|
@@ -1766,6 +1771,7 @@ async function spawnAgent(options) {
|
|
|
1766
1771
|
inTmux: !!process.env.TMUX,
|
|
1767
1772
|
raw: options.raw === true,
|
|
1768
1773
|
noTmuxEnv: process.env.AGENTS_NO_TMUX === '1',
|
|
1774
|
+
configEnabled: isTmuxEnabled(),
|
|
1769
1775
|
tmuxAvailable: isTmuxInstalled(),
|
|
1770
1776
|
})) {
|
|
1771
1777
|
timer.mark('startup');
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -123,7 +123,7 @@ export declare function _resetSnapshotShaCacheForTest(): void;
|
|
|
123
123
|
*/
|
|
124
124
|
export declare function getGitHubUsername(): Promise<string | null>;
|
|
125
125
|
/**
|
|
126
|
-
* Resolve the GitHub username synchronously for `agents share`. Order:
|
|
126
|
+
* Resolve the GitHub username synchronously for `agents artifacts share`. Order:
|
|
127
127
|
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
128
128
|
* 2. `git config --global github.user`
|
|
129
129
|
* Falls back to null so callers can decide whether to require auth or proceed
|
package/dist/lib/git.js
CHANGED
|
@@ -437,7 +437,7 @@ function readGitConfigUser() {
|
|
|
437
437
|
}
|
|
438
438
|
}
|
|
439
439
|
/**
|
|
440
|
-
* Resolve the GitHub username synchronously for `agents share`. Order:
|
|
440
|
+
* Resolve the GitHub username synchronously for `agents artifacts share`. Order:
|
|
441
441
|
* 1. `AGENTS_SHARE_GITHUB_USER` env override
|
|
442
442
|
* 2. `git config --global github.user`
|
|
443
443
|
* Falls back to null so callers can decide whether to require auth or proceed
|