@phnx-labs/agents-cli 1.22.24 → 1.22.26
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 +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/mcp.js +5 -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/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -19
- 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 +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
5
|
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
-
* Acquisition is reuse-first against the warm
|
|
7
|
-
* the
|
|
8
|
-
* always leases a new, torn-down box).
|
|
9
|
-
*
|
|
6
|
+
* Acquisition is reuse-first against the shared warm pool (a ready box carrying
|
|
7
|
+
* the lease profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). Concurrent callers run from separate
|
|
9
|
+
* `~/workspaces/<repo>-<run>` directories on that box. The whole box-side
|
|
10
|
+
* sequence rides a single `--script-stdin` body so token contents never touch argv.
|
|
10
11
|
*
|
|
11
12
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
12
13
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -54,6 +55,8 @@ export interface LeaseRunOptions {
|
|
|
54
55
|
backend?: string;
|
|
55
56
|
boxClass?: string;
|
|
56
57
|
profile?: string;
|
|
58
|
+
/** Per-run directory under ~/workspaces; isolates concurrent runs on one box. */
|
|
59
|
+
workspaceId?: string;
|
|
57
60
|
/** Runtimes to install on the box. */
|
|
58
61
|
runtimes: AgentId[];
|
|
59
62
|
/** Runtime credentials to copy; defaults to `runtimes`. */
|
|
@@ -112,6 +115,10 @@ export interface LeaseRunResult {
|
|
|
112
115
|
exitCode: number | null;
|
|
113
116
|
toreDown: boolean;
|
|
114
117
|
}
|
|
118
|
+
/** Build a shell-safe, collision-resistant workspace id for one lease run. */
|
|
119
|
+
export declare function leaseWorkspaceId(repoRoot: string, startedAtMs?: number, pid?: number): string;
|
|
120
|
+
/** Isolated box-home path for one run, relative to the shared box user's home. */
|
|
121
|
+
export declare function leaseHomeDir(workspaceId: string): string;
|
|
115
122
|
/**
|
|
116
123
|
* Build the single bootstrap script run on the box: ensure agents-cli, install
|
|
117
124
|
* the picked runtime CLIs, write their credentials, run the agent, then shred
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
5
|
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
-
* Acquisition is reuse-first against the warm
|
|
7
|
-
* the
|
|
8
|
-
* always leases a new, torn-down box).
|
|
9
|
-
*
|
|
6
|
+
* Acquisition is reuse-first against the shared warm pool (a ready box carrying
|
|
7
|
+
* the lease profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). Concurrent callers run from separate
|
|
9
|
+
* `~/workspaces/<repo>-<run>` directories on that box. The whole box-side
|
|
10
|
+
* sequence rides a single `--script-stdin` body so token contents never touch argv.
|
|
10
11
|
*
|
|
11
12
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
12
13
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -38,6 +39,16 @@ import { DEFAULT_CRABBOX_PROFILE } from './config.js';
|
|
|
38
39
|
function q(s) {
|
|
39
40
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
40
41
|
}
|
|
42
|
+
/** Build a shell-safe, collision-resistant workspace id for one lease run. */
|
|
43
|
+
export function leaseWorkspaceId(repoRoot, startedAtMs = Date.now(), pid = process.pid) {
|
|
44
|
+
const repo = repoRoot.split(/[\\/]/).filter(Boolean).pop() ?? 'repo';
|
|
45
|
+
const safeRepo = repo.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'repo';
|
|
46
|
+
return `${safeRepo}-${startedAtMs.toString(36)}-${pid.toString(36)}`;
|
|
47
|
+
}
|
|
48
|
+
/** Isolated box-home path for one run, relative to the shared box user's home. */
|
|
49
|
+
export function leaseHomeDir(workspaceId) {
|
|
50
|
+
return `lease-homes/${workspaceId}`;
|
|
51
|
+
}
|
|
41
52
|
function profileRemotePath(name) {
|
|
42
53
|
return `.agents/profiles/${name}.yml`;
|
|
43
54
|
}
|
|
@@ -124,15 +135,35 @@ export function buildBootstrapScript(opts) {
|
|
|
124
135
|
// them as a structured step stream — they never appear as setup noise.
|
|
125
136
|
const step = (name) => `echo ${q(leasePhaseSentinel(name))}`;
|
|
126
137
|
const copySetup = opts.copySetup !== false; // default TRUE
|
|
138
|
+
const workspace = opts.workspaceId
|
|
139
|
+
? [
|
|
140
|
+
'BOX_HOME="$HOME"',
|
|
141
|
+
'REPO_DIR="$(pwd)"',
|
|
142
|
+
`WORKSPACE_DIR="$BOX_HOME"/${q(`workspaces/${opts.workspaceId}`)}`,
|
|
143
|
+
'mkdir -p "$WORKSPACE_DIR"',
|
|
144
|
+
'rsync -a --delete --exclude=node_modules --exclude=.agents/worktrees "$REPO_DIR/" "$WORKSPACE_DIR/"',
|
|
145
|
+
'cd "$WORKSPACE_DIR"',
|
|
146
|
+
].join('\n')
|
|
147
|
+
: '';
|
|
148
|
+
const isolatedHome = opts.workspaceId
|
|
149
|
+
? [
|
|
150
|
+
`export HOME="$BOX_HOME"/${q(leaseHomeDir(opts.workspaceId))}`,
|
|
151
|
+
'mkdir -p "$HOME/.agents"',
|
|
152
|
+
'ln -sfn "$BOX_HOME/.agents/.system" "$HOME/.agents/.system"',
|
|
153
|
+
'export PATH="$BOX_HOME/.local/bin:$PATH"',
|
|
154
|
+
].join('\n')
|
|
155
|
+
: '';
|
|
127
156
|
return [
|
|
128
157
|
'set -uo pipefail',
|
|
129
158
|
// crabbox has finished its workspace resync by the time this script runs;
|
|
130
159
|
// the sync sentinel marks the transition out of that (crabbox-driven) phase.
|
|
131
160
|
step('sync'),
|
|
161
|
+
workspace,
|
|
132
162
|
// Only meaningful on a tailnet lease — the box already joined during warmup.
|
|
133
163
|
opts.netMode === 'tailscale' ? step('joined-tailnet') : '',
|
|
134
164
|
step('install'),
|
|
135
165
|
ENSURE_AGENTS_CLI,
|
|
166
|
+
isolatedHome,
|
|
136
167
|
step('runtime'),
|
|
137
168
|
installRuntimes,
|
|
138
169
|
step('creds'),
|
|
@@ -287,6 +318,7 @@ export async function leaseAndRun(opts) {
|
|
|
287
318
|
secretsBundle: opts.secretsBundle,
|
|
288
319
|
onData: opts.onData,
|
|
289
320
|
refresh: false,
|
|
321
|
+
remoteDir: opts.workspaceId ? `${leaseHomeDir(opts.workspaceId)}/.agents/` : undefined,
|
|
290
322
|
});
|
|
291
323
|
}
|
|
292
324
|
catch {
|
|
@@ -303,10 +335,10 @@ export async function leaseAndRun(opts) {
|
|
|
303
335
|
});
|
|
304
336
|
}
|
|
305
337
|
finally {
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// --
|
|
309
|
-
if (!opts.keep && !reused) {
|
|
338
|
+
// Normal --lease establishes or reuses the warm pool, so the box outlives
|
|
339
|
+
// this run; credentials are still shredded inside the script above. Only
|
|
340
|
+
// --fresh requests the old one-shot lifecycle and tears its new box down.
|
|
341
|
+
if (!opts.keep && opts.fresh && !reused) {
|
|
310
342
|
opts.onPhase?.({ kind: 'teardown' });
|
|
311
343
|
toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
|
|
312
344
|
}
|
|
@@ -31,6 +31,11 @@ export interface CopySetupOptions {
|
|
|
31
31
|
secretsBundle?: string;
|
|
32
32
|
/** Override the local `~/.agents` dir (defaults to `getUserAgentsDir()`). */
|
|
33
33
|
userAgentsDir?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Destination relative to the box user's home. Lease runs set this to their
|
|
36
|
+
* isolated home; other callers retain the historical `~/.agents/` target.
|
|
37
|
+
*/
|
|
38
|
+
remoteDir?: string;
|
|
34
39
|
/** Receives combined stdout/stderr of the rsync + refresh, if set. */
|
|
35
40
|
onData?: (chunk: string) => void;
|
|
36
41
|
/**
|
|
@@ -109,9 +109,25 @@ export async function copySetupToBox(opts) {
|
|
|
109
109
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-setup-copy-'));
|
|
110
110
|
const listPath = path.join(tmp, 'files.lst');
|
|
111
111
|
try {
|
|
112
|
+
if (opts.remoteDir) {
|
|
113
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(opts.remoteDir) || opts.remoteDir.startsWith('/') || opts.remoteDir.split('/').includes('..')) {
|
|
114
|
+
throw new Error(`remote setup directory must stay under the box home: ${opts.remoteDir}`);
|
|
115
|
+
}
|
|
116
|
+
const remoteDir = opts.remoteDir.replace(/\/$/, '');
|
|
117
|
+
const prepareArgs = [
|
|
118
|
+
...sshArgv.slice(1),
|
|
119
|
+
'bash',
|
|
120
|
+
'-lc',
|
|
121
|
+
`mkdir -p "$HOME"/${JSON.stringify(remoteDir)}`,
|
|
122
|
+
];
|
|
123
|
+
const prepareExitCode = await runStreaming('ssh', prepareArgs, env, opts.onData);
|
|
124
|
+
if (prepareExitCode !== 0) {
|
|
125
|
+
return { files, pushExitCode: prepareExitCode, refreshExitCode: null };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
112
128
|
// NUL-separated list, matching `buildSetupRsyncArgs`'s `--from0`.
|
|
113
129
|
fs.writeFileSync(listPath, files.join('\0'), 'utf-8');
|
|
114
|
-
const rsyncArgs = buildSetupRsyncArgs({ rsh, host, filesFrom: listPath, source: dir });
|
|
130
|
+
const rsyncArgs = buildSetupRsyncArgs({ rsh, host, filesFrom: listPath, source: dir, remoteDir: opts.remoteDir });
|
|
115
131
|
const pushExitCode = await runStreaming('rsync', rsyncArgs, env, opts.onData);
|
|
116
132
|
let refreshExitCode = null;
|
|
117
133
|
if (pushExitCode === 0 && opts.refresh !== false) {
|
package/dist/lib/daemon.js
CHANGED
|
@@ -24,7 +24,9 @@ import { BrowserService } from './browser/service.js';
|
|
|
24
24
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
25
25
|
import { redactSecrets } from './redact.js';
|
|
26
26
|
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from './cli-entry.js';
|
|
27
|
-
import { isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
|
|
27
|
+
import { getConfigValue, isSchedulerEnabled, assertSchedulerEnabled } from './device-config.js';
|
|
28
|
+
import { reapTerminalRoutineProcesses } from './routine-process-cleanup.js';
|
|
29
|
+
import { runWatchdogPass } from './watchdog/service.js';
|
|
28
30
|
const PID_FILE = 'daemon.pid';
|
|
29
31
|
const LOCK_FILE = 'daemon.lock';
|
|
30
32
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -34,6 +36,7 @@ const LOG_ROTATE_COUNT = 3;
|
|
|
34
36
|
const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
35
37
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
36
38
|
const MONITOR_TICK_MS = 60_000;
|
|
39
|
+
const WATCHDOG_TICK_MS = 3 * 60_000;
|
|
37
40
|
/**
|
|
38
41
|
* How often to re-scan for missed fires. Deliberately slower than the monitor
|
|
39
42
|
* tick: detection walks a week of cron occurrences per routine
|
|
@@ -572,6 +575,24 @@ export async function runDaemon() {
|
|
|
572
575
|
}
|
|
573
576
|
if (schedulerEnabledAtBoot)
|
|
574
577
|
bootScheduler();
|
|
578
|
+
let watchdogInFlight = false;
|
|
579
|
+
const runDaemonWatchdog = async () => {
|
|
580
|
+
if (watchdogInFlight || getConfigValue('watchdog.enabled').value !== true)
|
|
581
|
+
return;
|
|
582
|
+
watchdogInFlight = true;
|
|
583
|
+
try {
|
|
584
|
+
const result = await runWatchdogPass({ nudge: true });
|
|
585
|
+
log('INFO', `watchdog: ${result.counts.total} live, ${result.counts.stalled} stalled, ${result.counts.nudged} nudged`);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
log('ERROR', `watchdog tick failed: ${err.message}`);
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
watchdogInFlight = false;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const watchdogInterval = setInterval(() => { void runDaemonWatchdog(); }, WATCHDOG_TICK_MS);
|
|
595
|
+
const watchdogKickoff = setTimeout(() => { void runDaemonWatchdog(); }, 30_000);
|
|
575
596
|
// Monitor engine: event-triggered watchers, beside the cron scheduler. Same
|
|
576
597
|
// daemon, same dispatch seam — a monitor is a routine whose trigger is a
|
|
577
598
|
// watched source instead of a clock. Reloads on SIGHUP alongside the scheduler.
|
|
@@ -682,6 +703,9 @@ export async function runDaemon() {
|
|
|
682
703
|
const monitorInterval = setInterval(() => {
|
|
683
704
|
writeHeartbeat();
|
|
684
705
|
monitorRunningJobs();
|
|
706
|
+
const reaped = reapTerminalRoutineProcesses();
|
|
707
|
+
if (reaped.length > 0)
|
|
708
|
+
log('WARN', `Reaped ${reaped.length} terminal routine process group(s): ${reaped.join(', ')}`);
|
|
685
709
|
}, MONITOR_TICK_MS);
|
|
686
710
|
// Resource safety check: heal gaps between what DotAgents repos define and
|
|
687
711
|
// what's actually installed in each agent home — the slow rot that nothing
|
|
@@ -1066,6 +1090,8 @@ export async function runDaemon() {
|
|
|
1066
1090
|
monitorEngine.stop();
|
|
1067
1091
|
await browserIPC.stop();
|
|
1068
1092
|
clearInterval(monitorInterval);
|
|
1093
|
+
clearInterval(watchdogInterval);
|
|
1094
|
+
clearTimeout(watchdogKickoff);
|
|
1069
1095
|
clearInterval(healInterval);
|
|
1070
1096
|
clearTimeout(healKickoff);
|
|
1071
1097
|
clearInterval(autoDispatchInterval);
|
|
@@ -67,6 +67,13 @@ export const CONFIG_KEYS = [
|
|
|
67
67
|
type: 'bool',
|
|
68
68
|
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'watchdog.enabled',
|
|
72
|
+
yamlKey: 'watchdogEnabled',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'bool',
|
|
75
|
+
description: 'Whether the daemon runs the watchdog pass on this device.',
|
|
76
|
+
},
|
|
70
77
|
{
|
|
71
78
|
name: 'browser.remote-control',
|
|
72
79
|
yamlKey: 'browserRemoteControl',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentId } from '../types.js';
|
|
2
2
|
import type { DuplicateVersionHook } from '../hooks.js';
|
|
3
3
|
import type { RcSecretFinding } from '../secrets/rc-hygiene.js';
|
|
4
|
+
import type { OwnerSinkStatus } from '../channels/owner-sink.js';
|
|
4
5
|
import type { SyncStatusRow, OrphanRow } from '../drift.js';
|
|
5
6
|
import type { FetchStatusMarker } from '../auto-pull.js';
|
|
6
7
|
import type { VersionResourceReport } from '../doctor-diff.js';
|
|
@@ -10,7 +11,7 @@ export type FindingSeverity = 'critical' | 'warning';
|
|
|
10
11
|
* the JSON consumer group by kind. */
|
|
11
12
|
/** Every finding class. Severity is NOT annotated here — {@link FINDING_SEVERITY}
|
|
12
13
|
* below owns it, and a second copy in these comments is a fourth place to drift. */
|
|
13
|
-
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "exec-policy", "stale-cli"];
|
|
14
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "env-secret-export", "exec-policy", "stale-cli", "owner-sink-unreachable"];
|
|
14
15
|
/**
|
|
15
16
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
16
17
|
* the builders below and asserted against both prose rubrics by
|
|
@@ -89,6 +90,12 @@ export interface LocalFindingInputs {
|
|
|
89
90
|
duplicateHooks?: DuplicateVersionHook[];
|
|
90
91
|
/** Credential-shaped exports found in the user's shell rc files (RUSH-1968). */
|
|
91
92
|
rcSecrets?: RcSecretFinding[];
|
|
93
|
+
/** True when the file-store master key is live in THIS process's environment.
|
|
94
|
+
* Distinct from `rcSecrets`, which scans FILES: a value inherited by a
|
|
95
|
+
* long-lived process survives deleting the rc line that set it, so the rc
|
|
96
|
+
* scan reports clean while the leak is still in flight (RUSH-1968). Never the
|
|
97
|
+
* value — only whether it is set. */
|
|
98
|
+
masterPassphraseInEnv?: boolean;
|
|
92
99
|
/** The effective PowerShell execution policy and the platform it was read on.
|
|
93
100
|
* Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
|
|
94
101
|
execPolicy?: {
|
|
@@ -100,6 +107,11 @@ export interface LocalFindingInputs {
|
|
|
100
107
|
* sweep deliberately skips isolated copies, so a collapsed row would print a
|
|
101
108
|
* remediation that does not fix them. */
|
|
102
109
|
isolatedVersions?: string[];
|
|
110
|
+
/** Whether the feed/notify owner-delivery lane can reach the owner from this box
|
|
111
|
+
* (RUSH-2262). Collected by `probeOwnerSink` in the command (it spawns the real
|
|
112
|
+
* `rush` transport, so it stays out of this pure module). Absent → no probe ran
|
|
113
|
+
* → no finding. */
|
|
114
|
+
ownerSink?: OwnerSinkStatus;
|
|
103
115
|
}
|
|
104
116
|
/**
|
|
105
117
|
* Fold this machine's signals into findings. Missing hooks/plugins and unwired
|
|
@@ -18,12 +18,13 @@
|
|
|
18
18
|
* with. Keep this list exhaustive; a kind missing from it is a doc that lies.
|
|
19
19
|
* CRITICAL — logged-out (provable) · missing-hook · missing-plugin ·
|
|
20
20
|
* unwired-hook (a hook on disk that settings.json never fires) ·
|
|
21
|
-
* cli-missing
|
|
21
|
+
* cli-missing · owner-sink-unreachable (the feed/notify owner lane
|
|
22
|
+
* cannot reach the owner from this box).
|
|
22
23
|
* WARNING — logout-unprovable (hedged) · missing-resource · content-drift ·
|
|
23
24
|
* never-synced · stale · repo-behind · repo-drift · version-skew ·
|
|
24
25
|
* fleet-resource-gap · orphan · duplicate-hook ·
|
|
25
26
|
* duplicate-hook-drift · host-cli-missing · host-cli-invalid ·
|
|
26
|
-
* rc-secret-export · exec-policy · stale-cli.
|
|
27
|
+
* rc-secret-export · env-secret-export · exec-policy · stale-cli.
|
|
27
28
|
* (RUSH-2162 moved never-synced and duplicate-hook-drift to WARNING: both are
|
|
28
29
|
* stale-sync states one `agents sync` resolves, not "needs you now".)
|
|
29
30
|
*
|
|
@@ -94,8 +95,10 @@ export const ALL_FINDING_KINDS = [
|
|
|
94
95
|
'duplicate-hook', // one hook materialized in several version homes, byte-identical
|
|
95
96
|
'duplicate-hook-drift', // …with differing content, so a stale copy can disagree
|
|
96
97
|
'rc-secret-export', // credential-shaped export in a shell rc file
|
|
98
|
+
'env-secret-export', // the file-store master key live in THIS process's env
|
|
97
99
|
'exec-policy', // Windows execution policy blocks agents.ps1
|
|
98
100
|
'stale-cli',
|
|
101
|
+
'owner-sink-unreachable', // the feed/notify owner-delivery lane can't reach the owner from this box
|
|
99
102
|
];
|
|
100
103
|
/**
|
|
101
104
|
* The severity each kind is emitted with - the SINGLE source of truth, read by
|
|
@@ -115,6 +118,9 @@ export const FINDING_SEVERITY = {
|
|
|
115
118
|
'missing-plugin': 'critical',
|
|
116
119
|
'unwired-hook': 'critical',
|
|
117
120
|
'cli-missing': 'critical',
|
|
121
|
+
// A factory that cannot escalate a blocked agent to the owner is not healthy,
|
|
122
|
+
// and the failure is otherwise silent until a block is filed (RUSH-2262/2258).
|
|
123
|
+
'owner-sink-unreachable': 'critical',
|
|
118
124
|
// Everything else is resolvable by a routine sync/cleanup and does not block
|
|
119
125
|
// the harness right now. RUSH-2162 moved never-synced and duplicate-hook-drift
|
|
120
126
|
// here: both are stale-sync states that one `agents sync` resolves.
|
|
@@ -133,6 +139,7 @@ export const FINDING_SEVERITY = {
|
|
|
133
139
|
'host-cli-missing': 'warning',
|
|
134
140
|
'host-cli-invalid': 'warning',
|
|
135
141
|
'rc-secret-export': 'warning',
|
|
142
|
+
'env-secret-export': 'warning',
|
|
136
143
|
'exec-policy': 'warning',
|
|
137
144
|
'stale-cli': 'warning',
|
|
138
145
|
};
|
|
@@ -240,10 +247,22 @@ export function remediationFor(finding) {
|
|
|
240
247
|
return 'fix the manifest';
|
|
241
248
|
case 'rc-secret-export':
|
|
242
249
|
return 'agents secrets add';
|
|
250
|
+
case 'env-secret-export':
|
|
251
|
+
// Not just "restart the shell": the value is in every long-lived parent
|
|
252
|
+
// that inherited it — an editor, a tmux server, the agents daemon — and
|
|
253
|
+
// each keeps handing it to new children until IT restarts.
|
|
254
|
+
return 'unset at the source, then restart every process that inherited it (shells, editor, tmux, agents daemon)';
|
|
243
255
|
case 'exec-policy':
|
|
244
256
|
return 'Set-ExecutionPolicy -Scope CurrentUser RemoteSigned';
|
|
245
257
|
case 'stale-cli':
|
|
246
258
|
return 'upgrade';
|
|
259
|
+
case 'owner-sink-unreachable':
|
|
260
|
+
// The lane delivers over the rush-backed owner channel, which needs rush on
|
|
261
|
+
// PATH AND a usable session in THIS context. Non-interactive shells miss a
|
|
262
|
+
// ~/.zshrc export (RUSH-2258), and the session is keychain-bound, not in
|
|
263
|
+
// ~/.rush/user.yaml (RUSH-2262) — so `rush login` here, or the Rush App for
|
|
264
|
+
// a GUI keychain, is what makes it reachable.
|
|
265
|
+
return "put rush on PATH for non-interactive shells (~/.zshenv) and run 'rush login'";
|
|
247
266
|
}
|
|
248
267
|
}
|
|
249
268
|
function finding(f) {
|
|
@@ -286,6 +305,22 @@ export function buildLocalFindings(input) {
|
|
|
286
305
|
message: `${agentName(agent)} binary not found`,
|
|
287
306
|
}));
|
|
288
307
|
}
|
|
308
|
+
// owner-sink-unreachable — the feed/notify owner-delivery lane can't reach the
|
|
309
|
+
// owner from this box (RUSH-2262). Only when owner delivery is CONFIGURED for the
|
|
310
|
+
// fleet; an un-opted-in box is not broken. The message names the concrete reason.
|
|
311
|
+
const sink = input.ownerSink;
|
|
312
|
+
if (sink?.configured && !sink.reachable) {
|
|
313
|
+
const chan = sink.channel ?? 'owner';
|
|
314
|
+
const why = sink.reason === 'rush-not-on-path'
|
|
315
|
+
? `rush CLI not on this box's PATH`
|
|
316
|
+
: sink.reason === 'rush-signed-out'
|
|
317
|
+
? 'rush has no usable session here'
|
|
318
|
+
: 'transport unreachable';
|
|
319
|
+
out.push(finding({
|
|
320
|
+
severity: FINDING_SEVERITY['owner-sink-unreachable'], kind: 'owner-sink-unreachable', device,
|
|
321
|
+
message: `${chan} → owner unreachable: ${why}`,
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
289
324
|
// Per-version resource reports → missing hook/plugin (critical), unwired hook
|
|
290
325
|
// (critical), other missing kinds (warning), content drift (warning).
|
|
291
326
|
for (const report of input.reports) {
|
|
@@ -450,6 +485,10 @@ export function buildLocalFindings(input) {
|
|
|
450
485
|
// into `agents secrets`.
|
|
451
486
|
for (const f of rcSecretFindings(device, input.rcSecrets ?? []))
|
|
452
487
|
out.push(f);
|
|
488
|
+
// The same key, live in this process's environment rather than in a file.
|
|
489
|
+
const envFinding = envSecretFinding(device, input.masterPassphraseInEnv ?? false);
|
|
490
|
+
if (envFinding)
|
|
491
|
+
out.push(envFinding);
|
|
453
492
|
// Windows execution policy blocking the generated agents.ps1 launcher.
|
|
454
493
|
const policyFinding = execPolicyFinding(device, input.execPolicy);
|
|
455
494
|
if (policyFinding)
|
|
@@ -572,6 +611,32 @@ function rcSecretFindings(device, rc) {
|
|
|
572
611
|
* policy itself. Returns null off Windows or on a permissive policy — pure, so
|
|
573
612
|
* both branches are testable without invoking PowerShell.
|
|
574
613
|
*/
|
|
614
|
+
/**
|
|
615
|
+
* The file-store master key sitting in THIS process's environment.
|
|
616
|
+
*
|
|
617
|
+
* `rc-hygiene.ts` scans FILES, which leaves a real hole: a value inherited by a
|
|
618
|
+
* long-lived process outlives the rc line that set it, so an operator who
|
|
619
|
+
* deletes `~/.zshenv:8` gets a clean `rc-secret-export` while every shell,
|
|
620
|
+
* editor and agent started before the edit still carries the key — and hands it
|
|
621
|
+
* to everything they spawn. Verified on a box with no rc export whose live
|
|
622
|
+
* environment still held the value, hashing identical to its key file.
|
|
623
|
+
*
|
|
624
|
+
* Warning, not critical: on the release home base the export is deliberate and
|
|
625
|
+
* scoped (`headless-sign-context.sh`), so the message names that exception
|
|
626
|
+
* rather than the check trying to detect it.
|
|
627
|
+
*/
|
|
628
|
+
function envSecretFinding(device, present) {
|
|
629
|
+
if (!present)
|
|
630
|
+
return null;
|
|
631
|
+
return finding({
|
|
632
|
+
severity: FINDING_SEVERITY['env-secret-export'], kind: 'env-secret-export', device,
|
|
633
|
+
// Never the value — only that it is set.
|
|
634
|
+
message: 'AGENTS_SECRETS_PASSPHRASE is set in this process environment — every '
|
|
635
|
+
+ 'child inherits it and any same-user process can read it from /proc/<pid>/environ. '
|
|
636
|
+
+ 'It outlives the shell rc line that set it, so deleting that line is not enough. '
|
|
637
|
+
+ '(Expected inside a release sign context, which sets it deliberately.)',
|
|
638
|
+
});
|
|
639
|
+
}
|
|
575
640
|
function execPolicyFinding(device, execPolicy) {
|
|
576
641
|
if (!execPolicy || execPolicy.platform !== 'win32')
|
|
577
642
|
return null;
|
|
@@ -745,6 +810,10 @@ function subjectLabel(f) {
|
|
|
745
810
|
return f.version ? `${f.agent} @${f.version}` : f.agent;
|
|
746
811
|
}
|
|
747
812
|
function critLabel(f) {
|
|
813
|
+
// Machine-level criticals with no agent get a category subject so the left
|
|
814
|
+
// column is not blank; the owner-sink row reads `owner …`, not an empty label.
|
|
815
|
+
if (f.kind === 'owner-sink-unreachable')
|
|
816
|
+
return { left: 'owner', account: '', message: f.message };
|
|
748
817
|
return {
|
|
749
818
|
left: subjectLabel(f),
|
|
750
819
|
account: f.account ?? '',
|
|
@@ -881,6 +950,8 @@ function warningSubject(f) {
|
|
|
881
950
|
return 'orphans';
|
|
882
951
|
if (f.kind === 'rc-secret-export')
|
|
883
952
|
return 'shell rc';
|
|
953
|
+
if (f.kind === 'env-secret-export')
|
|
954
|
+
return 'environment';
|
|
884
955
|
if (f.kind === 'exec-policy')
|
|
885
956
|
return 'PowerShell';
|
|
886
957
|
if (f.kind === 'fleet-resource-gap')
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -226,6 +226,15 @@ export declare function query(options: {
|
|
|
226
226
|
bundle?: string;
|
|
227
227
|
limit?: number;
|
|
228
228
|
}): EventRecord[];
|
|
229
|
+
/**
|
|
230
|
+
* Scan event logs once for a set of session IDs and return browser/computer
|
|
231
|
+
* usage flags for each. O(files) instead of O(N × files) — safe to call
|
|
232
|
+
* outside a SQLite write transaction.
|
|
233
|
+
*/
|
|
234
|
+
export declare function queryToolUsageForSessions(sessionIds: ReadonlySet<string>): Map<string, {
|
|
235
|
+
usedBrowser: boolean;
|
|
236
|
+
usedComputer: boolean;
|
|
237
|
+
}>;
|
|
229
238
|
/**
|
|
230
239
|
* Get performance stats for a specific label.
|
|
231
240
|
*/
|
package/dist/lib/events.js
CHANGED
|
@@ -1052,6 +1052,64 @@ export function query(options) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
return results;
|
|
1054
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Scan event logs once for a set of session IDs and return browser/computer
|
|
1057
|
+
* usage flags for each. O(files) instead of O(N × files) — safe to call
|
|
1058
|
+
* outside a SQLite write transaction.
|
|
1059
|
+
*/
|
|
1060
|
+
export function queryToolUsageForSessions(sessionIds) {
|
|
1061
|
+
const result = new Map();
|
|
1062
|
+
if (sessionIds.size === 0)
|
|
1063
|
+
return result;
|
|
1064
|
+
for (const id of sessionIds) {
|
|
1065
|
+
result.set(id, { usedBrowser: false, usedComputer: false });
|
|
1066
|
+
}
|
|
1067
|
+
const BROWSER_EVENTS = new Set(['browser.navigate', 'browser.screenshot']);
|
|
1068
|
+
const COMPUTER_EVENTS = new Set(['computer.action']);
|
|
1069
|
+
eventsPath();
|
|
1070
|
+
migrateLegacyEventLogs();
|
|
1071
|
+
const files = listEventLogFiles().sort((a, b) => Number(b.currentActive) - Number(a.currentActive) || b.mtimeMs - a.mtimeMs || b.path.localeCompare(a.path));
|
|
1072
|
+
let remaining = sessionIds.size * 2; // each session needs up to 2 flags set
|
|
1073
|
+
for (const file of files) {
|
|
1074
|
+
if (remaining <= 0)
|
|
1075
|
+
break;
|
|
1076
|
+
let content;
|
|
1077
|
+
if (file.gzip) {
|
|
1078
|
+
try {
|
|
1079
|
+
content = gunzipSync(fs.readFileSync(file.path)).toString('utf-8');
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
content = fs.readFileSync(file.path, 'utf-8');
|
|
1087
|
+
}
|
|
1088
|
+
for (const line of content.trim().split('\n')) {
|
|
1089
|
+
if (!line)
|
|
1090
|
+
continue;
|
|
1091
|
+
try {
|
|
1092
|
+
const record = JSON.parse(line);
|
|
1093
|
+
const sid = record.sessionId;
|
|
1094
|
+
if (!sid || !result.has(sid))
|
|
1095
|
+
continue;
|
|
1096
|
+
const entry = result.get(sid);
|
|
1097
|
+
if (BROWSER_EVENTS.has(record.event) && !entry.usedBrowser) {
|
|
1098
|
+
entry.usedBrowser = true;
|
|
1099
|
+
remaining--;
|
|
1100
|
+
}
|
|
1101
|
+
else if (COMPUTER_EVENTS.has(record.event) && !entry.usedComputer) {
|
|
1102
|
+
entry.usedComputer = true;
|
|
1103
|
+
remaining--;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
// skip malformed lines
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1055
1113
|
// ─── Stats ────────────────────────────────────────────────────────────────────
|
|
1056
1114
|
/**
|
|
1057
1115
|
* Get performance stats for a specific label.
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function headlessPlanStallCommand(args: {
|
|
|
42
42
|
* (every agent supports edit-like behavior as its default).
|
|
43
43
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
44
44
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
45
|
-
* like antigravity/
|
|
45
|
+
* like antigravity/kiro have no plan flag; hard-failing made
|
|
46
46
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
47
47
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
48
48
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -61,8 +61,8 @@ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
|
|
|
61
61
|
* `--prompt` + `--plan`, and grok's `--permission-mode plan` silently stalls at
|
|
62
62
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
63
63
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
64
|
-
* one-line stderr warning, mirroring the graceful plan→edit degrade
|
|
65
|
-
*
|
|
64
|
+
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
65
|
+
* and kiro get for having no plan flag at all. Interactive runs are never
|
|
66
66
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
67
67
|
* (agents run / teams) and the routine runner.
|
|
68
68
|
*/
|
package/dist/lib/exec.js
CHANGED
|
@@ -86,7 +86,7 @@ export function headlessPlanStallCommand(args) {
|
|
|
86
86
|
* (every agent supports edit-like behavior as its default).
|
|
87
87
|
* - `plan` on an agent without a read-only mode degrades to the agent's
|
|
88
88
|
* safest native mode (`capabilities.modes[0]`, typically `edit`). Agents
|
|
89
|
-
* like antigravity/
|
|
89
|
+
* like antigravity/kiro have no plan flag; hard-failing made
|
|
90
90
|
* multi-agent scripts (`--mode plan` for everyone) unusable and diverged
|
|
91
91
|
* from `agents teams add`, which already defaults to `edit`. Callers that
|
|
92
92
|
* care (the `agents run` CLI) must surface a warning when requested ≠
|
|
@@ -106,7 +106,7 @@ export function resolveMode(agent, requested) {
|
|
|
106
106
|
}
|
|
107
107
|
if (requested === 'plan') {
|
|
108
108
|
// No read-only mode on this agent. modes[0] is the declared safest mode
|
|
109
|
-
// (edit for antigravity/
|
|
109
|
+
// (edit for antigravity/kiro/…). Prefer that over hard-fail so
|
|
110
110
|
// uniform multi-agent `--mode plan` dispatches still run.
|
|
111
111
|
return supported[0];
|
|
112
112
|
}
|
|
@@ -121,8 +121,8 @@ export function resolveMode(agent, requested) {
|
|
|
121
121
|
* `--prompt` + `--plan`, and grok's `--permission-mode plan` silently stalls at
|
|
122
122
|
* its ExitPlanMode gate. For those agents, a headless plan request degrades to
|
|
123
123
|
* `auto` (kimi -p auto-runs; grok maps auto→edit via resolveMode) with a visible
|
|
124
|
-
* one-line stderr warning, mirroring the graceful plan→edit degrade
|
|
125
|
-
*
|
|
124
|
+
* one-line stderr warning, mirroring the graceful plan→edit degrade antigravity
|
|
125
|
+
* and kiro get for having no plan flag at all. Interactive runs are never
|
|
126
126
|
* downgraded. This is the single source of truth shared by buildExecCommand
|
|
127
127
|
* (agents run / teams) and the routine runner.
|
|
128
128
|
*/
|
|
@@ -142,12 +142,8 @@ export function resolveHeadlessMode(agent, requested, interactive, warningContex
|
|
|
142
142
|
if (mode !== requested) {
|
|
143
143
|
const subject = warningContext ? `${warningContext}: ` : '';
|
|
144
144
|
if (requested === 'plan') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: `${agent} has no read-only 'plan' mode`;
|
|
148
|
-
warn(`[agents] ${subject}${limitation}; ` +
|
|
149
|
-
`running '${mode}' (writable) instead${agent === 'cursor' ? ' (RUSH-2101)' : ''}. ` +
|
|
150
|
-
`Pass --mode ${mode} to silence this.\n`);
|
|
145
|
+
warn(`[agents] ${subject}${agent} has no read-only 'plan' mode; ` +
|
|
146
|
+
`running '${mode}' (writable) instead. Pass --mode ${mode} to silence this.\n`);
|
|
151
147
|
}
|
|
152
148
|
else {
|
|
153
149
|
warn(`[agents] ${subject}${agent} has no '${requested}' mode; using '${mode}'.\n`);
|
|
@@ -494,6 +490,7 @@ export const AGENT_COMMANDS = {
|
|
|
494
490
|
base: ['cursor-agent'],
|
|
495
491
|
promptFlag: '-p',
|
|
496
492
|
modeFlags: {
|
|
493
|
+
plan: ['--plan'],
|
|
497
494
|
edit: [],
|
|
498
495
|
skip: ['-f'],
|
|
499
496
|
},
|
|
@@ -687,6 +684,23 @@ export const AGENT_COMMANDS = {
|
|
|
687
684
|
// `resume` subcommand — special-cased in buildExecCommand.
|
|
688
685
|
resume: { flag: '--session-id' },
|
|
689
686
|
},
|
|
687
|
+
// Warp Agent CLI (`oz agent run`). Headless is `-p/--prompt "<task>"`; JSON is
|
|
688
|
+
// the global `--output-format json`. Autonomy comes from the selected agent
|
|
689
|
+
// profile (`--profile`), not a per-run permission flag, so the single `edit`
|
|
690
|
+
// mode maps to no flags (mirrors hermes). `--model` overrides the base model
|
|
691
|
+
// (`oz model list`). No `resume`: Oz's `oz agent run --conversation <id>`
|
|
692
|
+
// continues a SERVER-SIDE conversation by id, and warp is not session-tracked
|
|
693
|
+
// (absent from SESSION_AGENTS), so agents-cli has no local id to resume from —
|
|
694
|
+
// declaring it would make nativeResume(warp) true against an unreachable path.
|
|
695
|
+
warp: {
|
|
696
|
+
base: ['oz', 'agent', 'run'],
|
|
697
|
+
promptFlag: '-p',
|
|
698
|
+
modeFlags: {
|
|
699
|
+
edit: [],
|
|
700
|
+
},
|
|
701
|
+
jsonFlags: ['--output-format', 'json'],
|
|
702
|
+
modelFlag: '--model',
|
|
703
|
+
},
|
|
690
704
|
};
|
|
691
705
|
/**
|
|
692
706
|
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
@@ -89,6 +89,9 @@ export interface SessionOutcomeHint {
|
|
|
89
89
|
worktreeSlug?: string | null;
|
|
90
90
|
branch?: string | null;
|
|
91
91
|
project?: string | null;
|
|
92
|
+
host?: string | null;
|
|
93
|
+
origin?: 'cli' | 'routine' | null;
|
|
94
|
+
routineName?: string | null;
|
|
92
95
|
}
|
|
93
96
|
/**
|
|
94
97
|
* Overlay session meta onto a block when the block itself is missing ticket/PR/
|