@phnx-labs/agents-cli 1.20.87 → 1.20.88
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 +60 -0
- package/README.md +3 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/routines.js +2 -2
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +19 -1
- package/dist/lib/daemon.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/runner.d.ts +14 -1
- package/dist/lib/runner.js +37 -8
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -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/secrets/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +21 -27
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
|
@@ -40,6 +40,23 @@ export interface RepoState {
|
|
|
40
40
|
/** True when the working tree has uncommitted changes. */
|
|
41
41
|
dirty: boolean;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Per-version sign-in state a device self-reports, so the fleet doctor can show
|
|
45
|
+
* every installed version's account (and a provable logged-out) without a second
|
|
46
|
+
* SSH round-trip. `provable` is true only when the credential is absent from BOTH
|
|
47
|
+
* the version home and the active/global HOME (see `credentialPresence`); an
|
|
48
|
+
* unprovable absence (opaque/keychain agent) is a warning, not a critical.
|
|
49
|
+
*/
|
|
50
|
+
export interface FleetVersionSignIn {
|
|
51
|
+
version: string;
|
|
52
|
+
/** A usable local credential was found for this version (or shared globally). */
|
|
53
|
+
signedIn: boolean;
|
|
54
|
+
/** Human account label (email, org badge, or opaque id), when derivable. */
|
|
55
|
+
account: string | null;
|
|
56
|
+
/** True only when a logged-out state is PROVABLE (credential absent per-version
|
|
57
|
+
* AND globally) — the caller gates a critical on this. */
|
|
58
|
+
provable: boolean;
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* The self-reported harness inventory a single device emits in `doctor --json`.
|
|
45
62
|
* Comparable device-to-device with no further probing.
|
|
@@ -55,6 +72,11 @@ export interface FleetInventory {
|
|
|
55
72
|
agents: RepoState | null;
|
|
56
73
|
system: RepoState | null;
|
|
57
74
|
};
|
|
75
|
+
/** Per-version sign-in state per agent id, for the fleet doctor's accounts
|
|
76
|
+
* line and cross-fleet logged-out criticals. Optional — an older CLI that
|
|
77
|
+
* predates this field omits it, and the caller degrades to a warning
|
|
78
|
+
* ("older agents-cli — can't report per-version sign-in"). */
|
|
79
|
+
signIn?: Record<string, FleetVersionSignIn[]>;
|
|
58
80
|
}
|
|
59
81
|
/** A device's inventory paired with its name (and reachability). A device that
|
|
60
82
|
* was unreachable / failed to report carries `inventory: null` and is skipped
|
|
@@ -45,20 +45,25 @@ function repoLabel(repo) {
|
|
|
45
45
|
}
|
|
46
46
|
/** Describe how a remote repo state diverges from the local baseline, or null
|
|
47
47
|
* when they match. Compares HEAD first (the load-bearing difference), then
|
|
48
|
-
* branch, then a dirty tree on either side
|
|
48
|
+
* branch, then a dirty tree on either side.
|
|
49
|
+
*
|
|
50
|
+
* HEAD and branch differences are symmetric — by convention the remote is the
|
|
51
|
+
* one "diverged from the baseline" — but a dirty tree belongs to exactly one
|
|
52
|
+
* box, and blaming the wrong one sends the user to a clean machine. */
|
|
49
53
|
function describeRepoDrift(local, remote) {
|
|
50
54
|
if (local.head && remote.head && local.head !== remote.head) {
|
|
51
|
-
return `HEAD ${remote.head} != local ${local.head}
|
|
55
|
+
return { detail: `repo diverged: HEAD ${remote.head} != local ${local.head}`, blame: 'remote' };
|
|
52
56
|
}
|
|
53
57
|
if (local.branch !== remote.branch) {
|
|
54
|
-
return
|
|
58
|
+
return {
|
|
59
|
+
detail: `repo diverged: branch ${remote.branch ?? 'detached'} != local ${local.branch ?? 'detached'}`,
|
|
60
|
+
blame: 'remote',
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
|
-
// Flag a dirty tree on EITHER side (symmetric with HEAD/branch above), and name
|
|
57
|
-
// the side that has the uncommitted changes — the remote, or the local baseline.
|
|
58
63
|
if (remote.dirty !== local.dirty) {
|
|
59
64
|
return remote.dirty
|
|
60
|
-
? '
|
|
61
|
-
: '
|
|
65
|
+
? { detail: 'tree has uncommitted changes', blame: 'remote' }
|
|
66
|
+
: { detail: 'tree has uncommitted changes', blame: 'local' };
|
|
62
67
|
}
|
|
63
68
|
return null;
|
|
64
69
|
}
|
|
@@ -78,6 +83,9 @@ export function compareFleetInventories(devices, baselineName) {
|
|
|
78
83
|
const comparedDevices = [];
|
|
79
84
|
const skippedDevices = [];
|
|
80
85
|
const divergences = [];
|
|
86
|
+
/** Repos already reported as the BASELINE's problem — one dirty local tree is
|
|
87
|
+
* one finding, not one per remote compared against. */
|
|
88
|
+
const localBlamed = new Set();
|
|
81
89
|
if (!baseline) {
|
|
82
90
|
// No local baseline to compare against — record every remote as skipped so
|
|
83
91
|
// the caller can say so, but emit no divergences (we can't know the truth).
|
|
@@ -163,15 +171,31 @@ export function compareFleetInventories(devices, baselineName) {
|
|
|
163
171
|
if (!localRepo || !remoteRepo)
|
|
164
172
|
continue; // one side isn't a readable repo
|
|
165
173
|
const drift = describeRepoDrift(localRepo, remoteRepo);
|
|
166
|
-
if (drift)
|
|
174
|
+
if (!drift)
|
|
175
|
+
continue;
|
|
176
|
+
if (drift.blame === 'local') {
|
|
177
|
+
// The BASELINE owns this one. File it against the baseline, and only
|
|
178
|
+
// once: the local tree being dirty is a single fact about this machine,
|
|
179
|
+
// not one problem per remote we happened to compare against.
|
|
180
|
+
if (localBlamed.has(repo))
|
|
181
|
+
continue;
|
|
182
|
+
localBlamed.add(repo);
|
|
167
183
|
divergences.push({
|
|
168
184
|
kind: 'repo-drift',
|
|
169
|
-
device:
|
|
185
|
+
device: baselineName,
|
|
170
186
|
category: repo,
|
|
171
187
|
name: repoLabel(repo),
|
|
172
|
-
message: `${
|
|
188
|
+
message: `${baselineName} ${repoLabel(repo)} ${drift.detail}`,
|
|
173
189
|
});
|
|
190
|
+
continue;
|
|
174
191
|
}
|
|
192
|
+
divergences.push({
|
|
193
|
+
kind: 'repo-drift',
|
|
194
|
+
device: remote.name,
|
|
195
|
+
category: repo,
|
|
196
|
+
name: repoLabel(repo),
|
|
197
|
+
message: `${remote.name} ${repoLabel(repo)} ${drift.detail}`,
|
|
198
|
+
});
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
divergences.sort((a, b) => a.device.localeCompare(b.device) ||
|
|
@@ -8,12 +8,23 @@
|
|
|
8
8
|
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
9
|
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
10
|
*/
|
|
11
|
-
import { type FleetInventory } from './fleet-divergence.js';
|
|
11
|
+
import { type FleetInventory, type FleetVersionSignIn } from './fleet-divergence.js';
|
|
12
|
+
/**
|
|
13
|
+
* Probe every installed version's sign-in state, per agent. For each version we
|
|
14
|
+
* read its own home's account (via the shim-set config dir) and, when logged
|
|
15
|
+
* out, decide whether that is PROVABLE: an agent that can't be inspected
|
|
16
|
+
* (`!supportsAccountInspection`) never claims logged out, and a version that
|
|
17
|
+
* merely lacks its own credential but shares the global login (`active`) is
|
|
18
|
+
* signed in, not out. Pure reads (file-presence + cheap account parse), no
|
|
19
|
+
* network, no keychain prompt. Only agents with at least one installed version
|
|
20
|
+
* appear, so the map lines up with {@link FleetInventory.agentVersions}.
|
|
21
|
+
*/
|
|
22
|
+
export declare function collectLocalFleetSignIn(): Promise<Record<string, FleetVersionSignIn[]>>;
|
|
12
23
|
/**
|
|
13
24
|
* Collect this machine's harness inventory: installed resources per kind,
|
|
14
|
-
* installed version ids per agent,
|
|
15
|
-
* reads — never mutates the install. `promptcuts` (a
|
|
16
|
-
* in {@link getAvailableResources}) is surfaced as a
|
|
17
|
-
* compares like any other named resource.
|
|
25
|
+
* installed version ids per agent, `.agents`/`.system` repo state, and
|
|
26
|
+
* per-version sign-in. Pure reads — never mutates the install. `promptcuts` (a
|
|
27
|
+
* single present/absent bit in {@link getAvailableResources}) is surfaced as a
|
|
28
|
+
* one-element list so it compares like any other named resource.
|
|
18
29
|
*/
|
|
19
|
-
export declare function collectLocalFleetInventory(cwd?: string): FleetInventory
|
|
30
|
+
export declare function collectLocalFleetInventory(cwd?: string): Promise<FleetInventory>;
|
|
@@ -8,24 +8,71 @@
|
|
|
8
8
|
* FleetInventory} that both the local baseline and every remote box serialize
|
|
9
9
|
* into their doctor payload; the comparator then diffs those payloads.
|
|
10
10
|
*/
|
|
11
|
-
import { getAvailableResources, listInstalledVersions } from '../versions.js';
|
|
11
|
+
import { getAvailableResources, getVersionHomePath, listInstalledVersions } from '../versions.js';
|
|
12
12
|
import { getUserAgentsDir, getSystemAgentsDir } from '../state.js';
|
|
13
13
|
import { readRepoState } from '../git.js';
|
|
14
|
-
import { ALL_AGENT_IDS } from '../agents.js';
|
|
15
|
-
import { FLEET_RESOURCE_KINDS } from './fleet-divergence.js';
|
|
14
|
+
import { ALL_AGENT_IDS, accountDisplayLabel, credentialPresence, getAccountInfo, supportsAccountInspection, } from '../agents.js';
|
|
15
|
+
import { FLEET_RESOURCE_KINDS, } from './fleet-divergence.js';
|
|
16
16
|
function toRepoState(snap) {
|
|
17
17
|
if (!snap)
|
|
18
18
|
return null;
|
|
19
19
|
return { branch: snap.branch, head: snap.head, dirty: snap.dirty };
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Probe every installed version's sign-in state, per agent. For each version we
|
|
23
|
+
* read its own home's account (via the shim-set config dir) and, when logged
|
|
24
|
+
* out, decide whether that is PROVABLE: an agent that can't be inspected
|
|
25
|
+
* (`!supportsAccountInspection`) never claims logged out, and a version that
|
|
26
|
+
* merely lacks its own credential but shares the global login (`active`) is
|
|
27
|
+
* signed in, not out. Pure reads (file-presence + cheap account parse), no
|
|
28
|
+
* network, no keychain prompt. Only agents with at least one installed version
|
|
29
|
+
* appear, so the map lines up with {@link FleetInventory.agentVersions}.
|
|
30
|
+
*/
|
|
31
|
+
export async function collectLocalFleetSignIn() {
|
|
32
|
+
const out = {};
|
|
33
|
+
await Promise.all(ALL_AGENT_IDS.map(async (agent) => {
|
|
34
|
+
const versions = listInstalledVersions(agent);
|
|
35
|
+
if (versions.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const rows = await Promise.all(versions.map(async (version) => {
|
|
38
|
+
const home = getVersionHomePath(agent, version);
|
|
39
|
+
let signedIn = false;
|
|
40
|
+
let account = null;
|
|
41
|
+
try {
|
|
42
|
+
const info = await getAccountInfo(agent, home);
|
|
43
|
+
signedIn = info.signedIn;
|
|
44
|
+
account = accountDisplayLabel(info) || null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* advisory only — treat as logged out, provability decided below */
|
|
48
|
+
}
|
|
49
|
+
// Provable logout: the agent is inspectable AND the credential is absent
|
|
50
|
+
// from BOTH the version home and the active/global HOME. An opaque or
|
|
51
|
+
// keychain-only agent, or one sharing the global login, is never a
|
|
52
|
+
// provable logout.
|
|
53
|
+
let provable = false;
|
|
54
|
+
if (!signedIn && supportsAccountInspection(agent)) {
|
|
55
|
+
const presence = credentialPresence(agent, home);
|
|
56
|
+
// `knownLocation` is load-bearing: an agent can sit in the inspection
|
|
57
|
+
// set with no credential path (cursor does), and then both probes are
|
|
58
|
+
// false only because there is nothing to look for. Treating that as a
|
|
59
|
+
// provable logout prints a CRITICAL for a version that is signed in.
|
|
60
|
+
provable = presence.knownLocation && !presence.perVersion && !presence.active;
|
|
61
|
+
}
|
|
62
|
+
return { version, signedIn, account, provable };
|
|
63
|
+
}));
|
|
64
|
+
out[agent] = rows;
|
|
65
|
+
}));
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
21
68
|
/**
|
|
22
69
|
* Collect this machine's harness inventory: installed resources per kind,
|
|
23
|
-
* installed version ids per agent,
|
|
24
|
-
* reads — never mutates the install. `promptcuts` (a
|
|
25
|
-
* in {@link getAvailableResources}) is surfaced as a
|
|
26
|
-
* compares like any other named resource.
|
|
70
|
+
* installed version ids per agent, `.agents`/`.system` repo state, and
|
|
71
|
+
* per-version sign-in. Pure reads — never mutates the install. `promptcuts` (a
|
|
72
|
+
* single present/absent bit in {@link getAvailableResources}) is surfaced as a
|
|
73
|
+
* one-element list so it compares like any other named resource.
|
|
27
74
|
*/
|
|
28
|
-
export function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
75
|
+
export async function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
29
76
|
const available = getAvailableResources(cwd);
|
|
30
77
|
const resources = {};
|
|
31
78
|
for (const kind of FLEET_RESOURCE_KINDS) {
|
|
@@ -53,5 +100,6 @@ export function collectLocalFleetInventory(cwd = process.cwd()) {
|
|
|
53
100
|
agents: toRepoState(readRepoState(getUserAgentsDir())),
|
|
54
101
|
system: toRepoState(readRepoState(getSystemAgentsDir())),
|
|
55
102
|
},
|
|
103
|
+
signIn: await collectLocalFleetSignIn(),
|
|
56
104
|
};
|
|
57
105
|
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -66,7 +66,14 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
|
66
66
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
67
67
|
* (agents run / teams) and the routine runner.
|
|
68
68
|
*/
|
|
69
|
-
export declare function resolveHeadlessMode(agent: AgentId, requested: Mode, interactive: boolean): Mode;
|
|
69
|
+
export declare function resolveHeadlessMode(agent: AgentId, requested: Mode, interactive: boolean, warningContext?: string, warningState?: ModeWarningState): Mode;
|
|
70
|
+
export interface ModeWarningState {
|
|
71
|
+
/** Agents already warned about, so one run warns once per agent. A fallback
|
|
72
|
+
* chain degrades each agent independently and the agent that actually ran is
|
|
73
|
+
* usually not the first, so this cannot be a single boolean. */
|
|
74
|
+
emitted?: Set<AgentId>;
|
|
75
|
+
quiet?: boolean;
|
|
76
|
+
}
|
|
70
77
|
/**
|
|
71
78
|
* The mode an agent should run in when the caller has no preference.
|
|
72
79
|
*
|
|
@@ -94,6 +101,10 @@ export interface ExecOptions {
|
|
|
94
101
|
cwd?: string;
|
|
95
102
|
/** Force headless mode even when no prompt is provided (e.g. piping via stdin). */
|
|
96
103
|
headless?: boolean;
|
|
104
|
+
/** Prefix for mode-degradation warnings emitted by shared headless paths. */
|
|
105
|
+
modeWarningContext?: string;
|
|
106
|
+
/** Shared across command previews/spawns/loop iterations so degradation warns once. */
|
|
107
|
+
modeWarningState?: ModeWarningState;
|
|
97
108
|
json?: boolean;
|
|
98
109
|
model?: string;
|
|
99
110
|
addDirs?: string[];
|
|
@@ -400,8 +411,8 @@ export declare function detectAuthFailure(text: string): boolean;
|
|
|
400
411
|
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
401
412
|
* and the `result`+`is_error` text are the reliable signals.
|
|
402
413
|
*
|
|
403
|
-
* Gated on the Claude stream-json shape
|
|
404
|
-
*
|
|
414
|
+
* Gated on the Claude-compatible stream-json shape emitted by Claude and Cursor;
|
|
415
|
+
* callers pass their agent so unrelated stream formats cannot match by accident.
|
|
405
416
|
*/
|
|
406
417
|
export declare function detectAuthFailureEvent(logText: string, agent: AgentId): boolean;
|
|
407
418
|
/**
|
package/dist/lib/exec.js
CHANGED
|
@@ -124,10 +124,35 @@ export function resolveMode(agent, requested) {
|
|
|
124
124
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
125
125
|
* (agents run / teams) and the routine runner.
|
|
126
126
|
*/
|
|
127
|
-
export function resolveHeadlessMode(agent, requested, interactive) {
|
|
127
|
+
export function resolveHeadlessMode(agent, requested, interactive, warningContext, warningState) {
|
|
128
128
|
const mode = resolveMode(agent, requested);
|
|
129
|
+
const warn = (message) => {
|
|
130
|
+
if (warningState?.quiet)
|
|
131
|
+
return;
|
|
132
|
+
if (warningState) {
|
|
133
|
+
warningState.emitted ??= new Set();
|
|
134
|
+
if (warningState.emitted.has(agent))
|
|
135
|
+
return;
|
|
136
|
+
warningState.emitted.add(agent);
|
|
137
|
+
}
|
|
138
|
+
process.stderr.write(message);
|
|
139
|
+
};
|
|
140
|
+
if (mode !== requested) {
|
|
141
|
+
const subject = warningContext ? `${warningContext}: ` : '';
|
|
142
|
+
if (requested === 'plan') {
|
|
143
|
+
const limitation = agent === 'cursor'
|
|
144
|
+
? "cursor's read-only plan mode is not enabled in this build"
|
|
145
|
+
: `${agent} has no read-only 'plan' mode`;
|
|
146
|
+
warn(`[agents] ${subject}${limitation}; ` +
|
|
147
|
+
`running '${mode}' (writable) instead${agent === 'cursor' ? ' (RUSH-2101)' : ''}. ` +
|
|
148
|
+
`Pass --mode ${mode} to silence this.\n`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
warn(`[agents] ${subject}${agent} has no '${requested}' mode; using '${mode}'.\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
129
154
|
if (!interactive && mode === 'plan' && AGENTS[agent].capabilities.headlessPlan === false) {
|
|
130
|
-
|
|
155
|
+
warn(`warning: ${agent} has no headless plan mode; running --mode auto instead\n`);
|
|
131
156
|
return resolveMode(agent, 'auto');
|
|
132
157
|
}
|
|
133
158
|
return mode;
|
|
@@ -425,7 +450,6 @@ export const AGENT_COMMANDS = {
|
|
|
425
450
|
base: ['cursor-agent'],
|
|
426
451
|
promptFlag: '-p',
|
|
427
452
|
modeFlags: {
|
|
428
|
-
// cursor-agent has no read-only flag; we only expose edit + skip.
|
|
429
453
|
edit: [],
|
|
430
454
|
skip: ['-f'],
|
|
431
455
|
},
|
|
@@ -669,13 +693,18 @@ export function buildExecCommand(options) {
|
|
|
669
693
|
// degrades to `auto` with a stderr warning (see resolveHeadlessMode)
|
|
670
694
|
// - `skip` on an unsupported agent → throws a clear error
|
|
671
695
|
// After resolution, the chosen mode is guaranteed to be in template.modeFlags.
|
|
672
|
-
const resolvedMode = resolveHeadlessMode(options.agent, normalizeMode(options.mode), interactive);
|
|
696
|
+
const resolvedMode = resolveHeadlessMode(options.agent, normalizeMode(options.mode), interactive, options.modeWarningContext, options.modeWarningState);
|
|
673
697
|
const modeFlags = template.modeFlags[resolvedMode];
|
|
674
698
|
if (!modeFlags) {
|
|
675
699
|
// Defense in depth: would only fire if AGENTS.capabilities.modes and
|
|
676
700
|
// AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
|
|
677
701
|
throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
|
|
678
702
|
}
|
|
703
|
+
if (options.agent === 'cursor' && resolvedMode === 'edit' && !interactive) {
|
|
704
|
+
// A configured headless run is the workspace trust decision. Keep this
|
|
705
|
+
// narrower than --yolo/-f, which also bypasses permission checks.
|
|
706
|
+
cmd.push('--trust');
|
|
707
|
+
}
|
|
679
708
|
// Codex's workspace-write sandbox blocks $HOME (verified against the live CLI
|
|
680
709
|
// and OpenAI's sandbox docs: writable roots extend scope "without removing the
|
|
681
710
|
// sandbox entirely"). But the model routinely shells out to `agents ...`, whose
|
|
@@ -1269,7 +1298,10 @@ async function spawnAgent(options) {
|
|
|
1269
1298
|
agent: options.agent,
|
|
1270
1299
|
version: options.version,
|
|
1271
1300
|
cwd: options.cwd || process.cwd(),
|
|
1272
|
-
mode
|
|
1301
|
+
// The mode that ran, not the one requested — `agents run` passes the
|
|
1302
|
+
// requested mode so the resolver can warn, but telemetry must agree with
|
|
1303
|
+
// the audit log. See RUSH-2106 for removing that ambiguity at the source.
|
|
1304
|
+
mode: resolveMode(options.agent, normalizeMode(options.mode)),
|
|
1273
1305
|
model: options.model,
|
|
1274
1306
|
interactive,
|
|
1275
1307
|
sessionId: options.sessionId,
|
|
@@ -1549,6 +1581,7 @@ export function detectRateLimit(text) {
|
|
|
1549
1581
|
export const AUTH_FAILURE_PATTERNS = [
|
|
1550
1582
|
/OAuth (?:access token has been revoked|session expired)/i,
|
|
1551
1583
|
/(?:Please run|run) \/login/i,
|
|
1584
|
+
/Please run 'agent login' first/i,
|
|
1552
1585
|
/\bNot logged in\b/i,
|
|
1553
1586
|
/Invalid authentication credentials/i,
|
|
1554
1587
|
/Failed to authenticate/i,
|
|
@@ -1573,11 +1606,11 @@ export function detectAuthFailure(text) {
|
|
|
1573
1606
|
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
1574
1607
|
* and the `result`+`is_error` text are the reliable signals.
|
|
1575
1608
|
*
|
|
1576
|
-
* Gated on the Claude stream-json shape
|
|
1577
|
-
*
|
|
1609
|
+
* Gated on the Claude-compatible stream-json shape emitted by Claude and Cursor;
|
|
1610
|
+
* callers pass their agent so unrelated stream formats cannot match by accident.
|
|
1578
1611
|
*/
|
|
1579
1612
|
export function detectAuthFailureEvent(logText, agent) {
|
|
1580
|
-
if (agent !== 'claude')
|
|
1613
|
+
if (agent !== 'claude' && agent !== 'cursor')
|
|
1581
1614
|
return false;
|
|
1582
1615
|
const lines = logText.split('\n');
|
|
1583
1616
|
for (const line of lines) {
|
|
Binary file
|
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
|
|
4
4
|
import { supports } from './capabilities.js';
|
|
5
|
-
import { buildCommandSkillContent, commandSkillName, readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
5
|
+
import { buildCommandSkillContent, commandSkillName, readSkillSourceCommandMarker, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
6
6
|
import { commandAppliesTo, parseCommandMetadata } from './commands.js';
|
|
7
7
|
import { markdownToToml } from './convert.js';
|
|
8
8
|
import { safeJoin } from './paths.js';
|
|
@@ -134,6 +134,7 @@ function skip(dest, projectRoot, result) {
|
|
|
134
134
|
function syncProjectCommands(agent, version, projectAgentsDir, agentRoot, result, manifestPaths) {
|
|
135
135
|
const cfg = AGENTS[agent];
|
|
136
136
|
const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
|
|
137
|
+
const commandsAlsoAsSkills = shouldAlsoInstallCommandAsSkill(agent, version);
|
|
137
138
|
const supportsCommands = supports(agent, 'commands', version).ok;
|
|
138
139
|
if (!commandsAsSkills && !supportsCommands)
|
|
139
140
|
return;
|
|
@@ -146,37 +147,50 @@ function syncProjectCommands(agent, version, projectAgentsDir, agentRoot, result
|
|
|
146
147
|
const metadata = parseCommandMetadata(srcFile);
|
|
147
148
|
if (!commandAppliesTo(agent, version, metadata).ok)
|
|
148
149
|
continue;
|
|
149
|
-
|
|
150
|
+
const written = [];
|
|
151
|
+
if (commandsAsSkills || commandsAlsoAsSkills) {
|
|
150
152
|
const sourceMarker = readSkillSourceCommandMarker(name, [path.join(projectAgentsDir, 'skills')]);
|
|
151
|
-
if (pathExists(path.join(projectAgentsDir, 'skills', name)) && sourceMarker !== name)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
if (pathExists(path.join(projectAgentsDir, 'skills', name)) && sourceMarker !== name) {
|
|
154
|
+
if (commandsAsSkills)
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const skillName = commandSkillName(name);
|
|
159
|
+
const rel = path.join('skills', skillName);
|
|
160
|
+
const destDir = path.join(agentRoot, rel);
|
|
161
|
+
if (pathExists(destDir)) {
|
|
162
|
+
skip(destDir, projectRoot, result);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(destDir, 'SKILL.md'), buildCommandSkillContent(name, srcFile), 'utf-8');
|
|
167
|
+
written.push(rel);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (commandsAsSkills) {
|
|
171
|
+
if (written.length > 0)
|
|
172
|
+
record('commands', name, written, result, manifestPaths);
|
|
158
173
|
continue;
|
|
159
174
|
}
|
|
160
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
161
|
-
fs.writeFileSync(path.join(destDir, 'SKILL.md'), buildCommandSkillContent(name, srcFile), 'utf-8');
|
|
162
|
-
record('commands', name, [rel], result, manifestPaths);
|
|
163
|
-
continue;
|
|
164
175
|
}
|
|
165
176
|
const ext = cfg.format === 'toml' ? '.toml' : '.md';
|
|
166
177
|
const rel = path.join(cfg.commandsSubdir, `${name}${ext}`);
|
|
167
178
|
const destFile = path.join(agentRoot, rel);
|
|
168
179
|
if (pathExists(destFile)) {
|
|
169
180
|
skip(destFile, projectRoot, result);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
173
|
-
if (cfg.format === 'toml') {
|
|
174
|
-
fs.writeFileSync(destFile, markdownToToml(name, fs.readFileSync(srcFile, 'utf-8')), 'utf-8');
|
|
175
181
|
}
|
|
176
182
|
else {
|
|
177
|
-
fs.
|
|
183
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
184
|
+
if (cfg.format === 'toml') {
|
|
185
|
+
fs.writeFileSync(destFile, markdownToToml(name, fs.readFileSync(srcFile, 'utf-8')), 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
fs.copyFileSync(srcFile, destFile);
|
|
189
|
+
}
|
|
190
|
+
written.push(rel);
|
|
178
191
|
}
|
|
179
|
-
|
|
192
|
+
if (written.length > 0)
|
|
193
|
+
record('commands', name, written, result, manifestPaths);
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
196
|
function syncProjectSkills(agent, version, projectAgentsDir, agentRoot, result, manifestPaths) {
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface RunResult {
|
|
|
23
23
|
meta: RunMeta;
|
|
24
24
|
reportPath: string | null;
|
|
25
25
|
}
|
|
26
|
+
/** Agents the daemon can actually run, derived from the command table above
|
|
27
|
+
* so the `--agent` help and any validation can never drift from it. */
|
|
28
|
+
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
26
29
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
27
30
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
|
|
28
31
|
export declare function archiveRoutineTranscripts(meta: Pick<RunMeta, 'jobName' | 'runId' | 'agent'>, runDir: string, overlayHome?: string): void;
|
|
@@ -63,7 +66,7 @@ export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow
|
|
|
63
66
|
* (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
|
|
64
67
|
* with `agents run`.
|
|
65
68
|
*/
|
|
66
|
-
export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string): Record<string, string>;
|
|
69
|
+
export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, agent: AgentId, version: string | undefined, timezone?: string, overlayHome?: string): Record<string, string>;
|
|
67
70
|
export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
|
|
68
71
|
/**
|
|
69
72
|
* Optional lifecycle callbacks for a detached routine run. The daemon passes an
|
|
@@ -80,5 +83,15 @@ export interface RoutineHooks {
|
|
|
80
83
|
export declare function executeJobDetached(config: JobConfig, hooks?: RoutineHooks): Promise<RunMeta>;
|
|
81
84
|
/** Extract the final assistant message from a stream-JSON log file as a markdown report. */
|
|
82
85
|
export declare function extractReport(stdoutPath: string, agentType: AgentId): string | null;
|
|
86
|
+
/** Derive the final status of a detached run by reading the agent's stream-json
|
|
87
|
+
* tail. Detached children fire-and-forget, so we never see their exit code
|
|
88
|
+
* directly — but Claude's stream-json terminates with a `type: result` line
|
|
89
|
+
* that carries `is_error`. If we find it, the run completed cleanly (modulo
|
|
90
|
+
* agent-reported error). If not, the process likely died mid-stream and the
|
|
91
|
+
* caller should treat the run as failed. */
|
|
92
|
+
export declare function inferFinalStatusFromLog(stdoutPath: string, agent: AgentId): {
|
|
93
|
+
status: 'completed' | 'failed';
|
|
94
|
+
exitCode: number;
|
|
95
|
+
} | null;
|
|
83
96
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
84
97
|
export declare function monitorRunningJobs(): void;
|
package/dist/lib/runner.js
CHANGED
|
@@ -32,17 +32,23 @@ import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js
|
|
|
32
32
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, } from './rotate.js';
|
|
33
33
|
import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
34
34
|
import { machineId } from './machine-id.js';
|
|
35
|
+
import { isSelfUpdatingAgent } from './agents.js';
|
|
35
36
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
36
37
|
const AGENT_COMMANDS = {
|
|
37
38
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
38
39
|
codex: ['codex', 'exec', '--sandbox', 'workspace-write', '{prompt}', '--json'],
|
|
39
40
|
gemini: ['gemini', '{prompt}', '--output-format', 'stream-json'],
|
|
41
|
+
cursor: ['cursor-agent', '-p', '{prompt}', '--output-format', 'stream-json'],
|
|
40
42
|
kimi: ['kimi', '--prompt', '{prompt}', '--output-format', 'stream-json'],
|
|
41
43
|
droid: ['droid', 'exec', '{prompt}', '-o', 'stream-json'],
|
|
42
44
|
};
|
|
45
|
+
/** Agents the daemon can actually run, derived from the command table above
|
|
46
|
+
* so the `--agent` help and any validation can never drift from it. */
|
|
47
|
+
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
43
48
|
const ROUTINE_TRANSCRIPT_SPECS = {
|
|
44
49
|
claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
|
|
45
50
|
codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
|
|
51
|
+
cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
|
|
46
52
|
};
|
|
47
53
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
48
54
|
export function buildJobCommand(config, resolvedPrompt) {
|
|
@@ -133,6 +139,20 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
133
139
|
}
|
|
134
140
|
appendModelAndReasoning(cmd, config);
|
|
135
141
|
}
|
|
142
|
+
if (config.agent === 'cursor') {
|
|
143
|
+
// cursor-agent supports --plan, but the capability registry has not been
|
|
144
|
+
// upgraded yet. RUSH-2101 tracks adding that read-only mode after PR #1721.
|
|
145
|
+
const cursorMode = resolveHeadlessMode('cursor', mode, false, `routine ${config.name}`);
|
|
146
|
+
if (cursorMode === 'skip') {
|
|
147
|
+
cmd.push('-f');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// The configured cwd is the user's workspace trust decision. --trust is
|
|
151
|
+
// narrower than --yolo/-f because it does not bypass tool permissions.
|
|
152
|
+
cmd.push('--trust');
|
|
153
|
+
}
|
|
154
|
+
appendModelAndReasoning(cmd, config);
|
|
155
|
+
}
|
|
136
156
|
if (config.agent === 'kimi') {
|
|
137
157
|
// kimi daemon jobs always run headless via `--prompt`, which cannot be
|
|
138
158
|
// combined with any startup-mode flag (--plan/--auto/--yolo all abort with
|
|
@@ -385,7 +405,7 @@ export function dispatchesViaAgentsRun(config) {
|
|
|
385
405
|
* (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
|
|
386
406
|
* with `agents run`.
|
|
387
407
|
*/
|
|
388
|
-
export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
408
|
+
export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayHome) {
|
|
389
409
|
const execEnv = buildExecEnv({
|
|
390
410
|
agent,
|
|
391
411
|
version,
|
|
@@ -415,6 +435,11 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone) {
|
|
|
415
435
|
// provisioned box does. Drop it here so a routine always uses the login of the
|
|
416
436
|
// machine it runs on.
|
|
417
437
|
delete out.CLAUDE_CODE_OAUTH_TOKEN;
|
|
438
|
+
if (agent === 'cursor' && overlayHome) {
|
|
439
|
+
// prepareJobHome links this host's Cursor auth file here. Pin XDG_CONFIG_HOME
|
|
440
|
+
// to the overlay so an ambient value cannot bypass the routine sandbox.
|
|
441
|
+
out.XDG_CONFIG_HOME = path.join(overlayHome, '.config');
|
|
442
|
+
}
|
|
418
443
|
if (timezone)
|
|
419
444
|
out.TZ = timezone;
|
|
420
445
|
return out;
|
|
@@ -621,16 +646,20 @@ export async function executeJob(config, deps) {
|
|
|
621
646
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
622
647
|
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
623
648
|
if (config.loop) {
|
|
624
|
-
const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone);
|
|
649
|
+
const spawnEnv = buildRoutineSpawnEnv(baseEnv, effectiveAgent, primaryVersion, config.timezone, overlayHome);
|
|
625
650
|
const execOptions = {
|
|
626
651
|
agent: effectiveAgent,
|
|
627
|
-
|
|
652
|
+
// Routine-supported self-updating CLIs (Cursor/Droid) use one global
|
|
653
|
+
// binary; a versioned shim would point at a nonexistent isolated install.
|
|
654
|
+
version: isSelfUpdatingAgent(effectiveAgent) ? undefined : primaryVersion,
|
|
628
655
|
prompt: resolvedPrompt,
|
|
629
656
|
mode: normalizeMode(config.mode),
|
|
630
657
|
effort: config.effort,
|
|
631
658
|
env: spawnEnv,
|
|
632
659
|
json: true,
|
|
633
660
|
headless: true,
|
|
661
|
+
modeWarningContext: `routine ${config.name}`,
|
|
662
|
+
modeWarningState: {},
|
|
634
663
|
...(config.config?.model ? { model: config.config.model } : {}),
|
|
635
664
|
...(config.allow?.dirs ? {
|
|
636
665
|
addDirs: config.allow.dirs
|
|
@@ -681,7 +710,7 @@ export async function executeJob(config, deps) {
|
|
|
681
710
|
e.TZ = config.timezone;
|
|
682
711
|
return e;
|
|
683
712
|
})()
|
|
684
|
-
: buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone);
|
|
713
|
+
: buildRoutineSpawnEnv(baseEnv, attemptAgent, attemptVersion, config.timezone, overlayHome);
|
|
685
714
|
// Remaining timeout budget shared across failover attempts.
|
|
686
715
|
const elapsed = Date.now() - Date.parse(meta.startedAt);
|
|
687
716
|
const remaining = Math.max(1_000, timeoutMs - (Number.isFinite(elapsed) ? elapsed : 0));
|
|
@@ -1067,7 +1096,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1067
1096
|
return e;
|
|
1068
1097
|
})()
|
|
1069
1098
|
// Non-command path only: config.agent is always set here (command/workflow branch earlier).
|
|
1070
|
-
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone);
|
|
1099
|
+
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
|
|
1071
1100
|
const effectiveAgent = config.workflow ? 'claude' : config.agent;
|
|
1072
1101
|
const meta = {
|
|
1073
1102
|
jobName: config.name,
|
|
@@ -1262,7 +1291,7 @@ export function extractReport(stdoutPath, agentType) {
|
|
|
1262
1291
|
for (const line of lines) {
|
|
1263
1292
|
try {
|
|
1264
1293
|
const parsed = JSON.parse(line);
|
|
1265
|
-
if (agentType === 'claude') {
|
|
1294
|
+
if (agentType === 'claude' || agentType === 'cursor') {
|
|
1266
1295
|
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
1267
1296
|
for (const block of parsed.message.content) {
|
|
1268
1297
|
if (block.type === 'text' && block.text) {
|
|
@@ -1298,7 +1327,7 @@ export function extractReport(stdoutPath, agentType) {
|
|
|
1298
1327
|
* that carries `is_error`. If we find it, the run completed cleanly (modulo
|
|
1299
1328
|
* agent-reported error). If not, the process likely died mid-stream and the
|
|
1300
1329
|
* caller should treat the run as failed. */
|
|
1301
|
-
function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
1330
|
+
export function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
1302
1331
|
if (!fs.existsSync(stdoutPath))
|
|
1303
1332
|
return null;
|
|
1304
1333
|
try {
|
|
@@ -1309,7 +1338,7 @@ function inferFinalStatusFromLog(stdoutPath, agent) {
|
|
|
1309
1338
|
for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < 20; i--, scanned++) {
|
|
1310
1339
|
try {
|
|
1311
1340
|
const parsed = JSON.parse(lines[i]);
|
|
1312
|
-
if (agent === 'claude' && parsed.type === 'result') {
|
|
1341
|
+
if ((agent === 'claude' || agent === 'cursor') && parsed.type === 'result') {
|
|
1313
1342
|
return parsed.is_error
|
|
1314
1343
|
? { status: 'failed', exitCode: 1 }
|
|
1315
1344
|
: { status: 'completed', exitCode: 0 };
|
package/dist/lib/sandbox.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare function buildSpawnEnv(overlayHome: string, extraEnv?: Record<str
|
|
|
21
21
|
export declare function getJobHomePath(name: string): string;
|
|
22
22
|
/** Create a fresh overlay HOME for a job, including agent config and allowed-dir symlinks. */
|
|
23
23
|
export declare function prepareJobHome(config: JobConfig): string;
|
|
24
|
+
/** Link this host's Cursor login and CLI config into the disposable overlay. */
|
|
25
|
+
export declare function generateCursorConfig(overlayHome: string): void;
|
|
24
26
|
/** Remove a job's overlay HOME directory entirely. */
|
|
25
27
|
export declare function cleanJobHome(name: string): void;
|
|
26
28
|
/** Symlink allowed directories into the overlay HOME, skipping paths outside the real HOME. */
|