@phnx-labs/agents-cli 1.20.87 → 1.20.89
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 +323 -0
- package/README.md +12 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/commands.js +7 -7
- 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/factory.js +26 -2
- package/dist/commands/funnel.js +16 -1
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/menubar.js +117 -34
- package/dist/commands/routines.js +25 -3
- package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
- package/dist/commands/secrets-rotate-passphrase.js +96 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/commands/sessions.d.ts +7 -1
- package/dist/commands/sessions.js +40 -12
- package/dist/commands/ssh.js +3 -3
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/webhook.js +7 -2
- 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 +28 -2
- package/dist/lib/daemon.d.ts +29 -0
- package/dist/lib/daemon.js +75 -6
- 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/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +41 -8
- package/dist/lib/factory/snapshot.d.ts +78 -0
- package/dist/lib/factory/snapshot.js +209 -0
- package/dist/lib/fs-atomic.d.ts +14 -1
- package/dist/lib/fs-atomic.js +35 -3
- package/dist/lib/funnel.d.ts +1 -0
- package/dist/lib/funnel.js +8 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/menubar/install-menubar.d.ts +53 -2
- package/dist/lib/menubar/install-menubar.js +183 -28
- package/dist/lib/platform/process.d.ts +2 -0
- package/dist/lib/platform/process.js +5 -3
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/resources.d.ts +8 -0
- package/dist/lib/resources.js +34 -1
- package/dist/lib/routines-placement.d.ts +2 -1
- package/dist/lib/routines-placement.js +8 -4
- package/dist/lib/routines.d.ts +57 -1
- package/dist/lib/routines.js +74 -1
- package/dist/lib/runner.d.ts +16 -1
- package/dist/lib/runner.js +58 -16
- 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/bundles.js +9 -34
- package/dist/lib/secrets/filestore.d.ts +152 -34
- package/dist/lib/secrets/filestore.js +676 -123
- 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/remote-active.d.ts +4 -1
- package/dist/lib/session/remote-active.js +8 -2
- 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/session/viewing-in.d.ts +31 -0
- package/dist/lib/session/viewing-in.js +47 -0
- 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/state.d.ts +17 -0
- package/dist/lib/state.js +30 -2
- package/dist/lib/triggers/handlers.d.ts +95 -0
- package/dist/lib/triggers/handlers.js +384 -0
- package/dist/lib/triggers/webhook.d.ts +10 -2
- package/dist/lib/triggers/webhook.js +65 -11
- 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/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.unlocked' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
export declare function levelFor(event: EventType): EventLevel;
|
|
18
18
|
export interface EventMeta {
|
|
19
19
|
ts: string;
|
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) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ActiveSession } from '../session/active.js';
|
|
2
|
+
import { serializeActiveSessionsForJson } from '../../commands/sessions.js';
|
|
3
|
+
import { readAuthHealthCache, type AuthVerdict } from '../auth-health.js';
|
|
4
|
+
import { readStatsCache } from '../devices/stats-cache.js';
|
|
5
|
+
export declare const FACTORY_PROJECTS: readonly [{
|
|
6
|
+
readonly name: "Prix";
|
|
7
|
+
readonly repo: "phnx-labs/prix";
|
|
8
|
+
}, {
|
|
9
|
+
readonly name: "Rush App";
|
|
10
|
+
readonly repo: "phnx-labs/rush";
|
|
11
|
+
}, {
|
|
12
|
+
readonly name: "Rush CLI";
|
|
13
|
+
readonly repo: "phnx-labs/rush-cli";
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "Agents CLI";
|
|
16
|
+
readonly repo: "phnx-labs/agents-cli";
|
|
17
|
+
}, {
|
|
18
|
+
readonly name: "Linear CLI";
|
|
19
|
+
readonly repo: "phnx-labs/linear-cli";
|
|
20
|
+
}];
|
|
21
|
+
export interface FactoryConfig {
|
|
22
|
+
source: 'default' | 'file';
|
|
23
|
+
ceiling: number;
|
|
24
|
+
max_dispatch_per_tick: number;
|
|
25
|
+
per_project: Record<string, {
|
|
26
|
+
weight: number;
|
|
27
|
+
cap: number;
|
|
28
|
+
}>;
|
|
29
|
+
idle_boxes: string[];
|
|
30
|
+
digest: {
|
|
31
|
+
times: string[];
|
|
32
|
+
tz: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface FactorySnapshot {
|
|
36
|
+
generatedAt: string;
|
|
37
|
+
sessions: ReturnType<typeof serializeActiveSessionsForJson>;
|
|
38
|
+
queues: Record<string, {
|
|
39
|
+
todo: number;
|
|
40
|
+
inProgress: number;
|
|
41
|
+
blocked: number;
|
|
42
|
+
}>;
|
|
43
|
+
prs: Array<{
|
|
44
|
+
repo: string;
|
|
45
|
+
number: number;
|
|
46
|
+
ci: string;
|
|
47
|
+
review: string;
|
|
48
|
+
mergeable: string;
|
|
49
|
+
}>;
|
|
50
|
+
devices: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
load: number | null;
|
|
53
|
+
idle: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
recentRuns: Array<{
|
|
56
|
+
routine: string;
|
|
57
|
+
status: string;
|
|
58
|
+
durationMs: number | null;
|
|
59
|
+
}>;
|
|
60
|
+
auth: {
|
|
61
|
+
claude: AuthVerdict | null;
|
|
62
|
+
};
|
|
63
|
+
config: FactoryConfig;
|
|
64
|
+
}
|
|
65
|
+
export interface SnapshotDependencies {
|
|
66
|
+
home: string;
|
|
67
|
+
now: () => Date;
|
|
68
|
+
activeSessions: () => Promise<ActiveSession[]>;
|
|
69
|
+
run: (file: string, args: string[]) => Promise<string>;
|
|
70
|
+
readAuth: () => ReturnType<typeof readAuthHealthCache>;
|
|
71
|
+
readDeviceStats: () => ReturnType<typeof readStatsCache>;
|
|
72
|
+
}
|
|
73
|
+
export declare function readFactoryConfig(home: string): FactoryConfig;
|
|
74
|
+
export declare function queueCounts(todoPayload: unknown, openPayload: unknown): FactorySnapshot['queues'][string];
|
|
75
|
+
export declare function parsePullRequests(repo: string, payload: unknown): FactorySnapshot['prs'];
|
|
76
|
+
export declare function parseDevices(payload: unknown, cached?: ReturnType<typeof readStatsCache>): FactorySnapshot['devices'];
|
|
77
|
+
export declare function readRecentRuns(home: string, limit?: number): FactorySnapshot['recentRuns'];
|
|
78
|
+
export declare function buildFactorySnapshot(overrides?: Partial<SnapshotDependencies>): Promise<FactorySnapshot>;
|