@phnx-labs/agents-cli 1.22.23 → 1.22.25
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 +359 -0
- package/README.md +19 -8
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +105 -9
- package/dist/commands/exec.js +99 -44
- package/dist/commands/feed.d.ts +23 -0
- package/dist/commands/feed.js +65 -2
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +301 -57
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +14 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +70 -47
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +36 -1
- package/dist/commands/sessions-browser.js +165 -19
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +28 -12
- package/dist/commands/sessions.d.ts +23 -1
- package/dist/commands/sessions.js +58 -61
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/teams.js +3 -2
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +38 -2
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.js +126 -21
- 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 +7 -1
- package/dist/lib/devices/doctor-findings.js +40 -1
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +15 -3
- package/dist/lib/exec.js +112 -19
- 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/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/passthrough.d.ts +21 -0
- package/dist/lib/hosts/passthrough.js +39 -12
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- 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 +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- 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/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +27 -22
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +158 -85
- package/dist/lib/secrets/filestore.d.ts +7 -3
- package/dist/lib/secrets/filestore.js +25 -8
- package/dist/lib/secrets/index.js +34 -8
- 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 +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.js +8 -2
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- 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/shims.d.ts +2 -2
- package/dist/lib/shims.js +45 -6
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +44 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -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
package/dist/lib/exec.js
CHANGED
|
@@ -17,7 +17,7 @@ import { isTierToken, resolveTier } from './model-tiers.js';
|
|
|
17
17
|
import { createTimer, redactPrompt, redactArgs } from './events.js';
|
|
18
18
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
|
-
import { getShimsDir, getHistoryDir } from './state.js';
|
|
20
|
+
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
21
21
|
import { resolveCodexHome } from './codex-home.js';
|
|
22
22
|
import { readCodexConfiguredModel } from './shims.js';
|
|
23
23
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
@@ -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`);
|
|
@@ -362,6 +358,28 @@ export function buildExecEnv(options) {
|
|
|
362
358
|
delete result.CODEX_HOME;
|
|
363
359
|
delete result.COPILOT_HOME;
|
|
364
360
|
}
|
|
361
|
+
else if (options.agent === 'muse') {
|
|
362
|
+
// Muse has no MUSE_CONFIG_DIR. Config is XDG-based:
|
|
363
|
+
// $XDG_CONFIG_HOME/muse (settings, skills, hooks, auth)
|
|
364
|
+
// $XDG_DATA_HOME/muse (sessions, plugins)
|
|
365
|
+
// Pin both into the version home so multi-version isolation matches
|
|
366
|
+
// Claude's CLAUDE_CONFIG_DIR / Codex's CODEX_HOME, and so Muse never
|
|
367
|
+
// resolves through the adopt-time ~/.config/muse symlink (SymlinkOrReparse).
|
|
368
|
+
const cwd = options.cwd || process.cwd();
|
|
369
|
+
const resolvedVersion = options.version ?? resolveVersion('muse', cwd);
|
|
370
|
+
const version = options.version
|
|
371
|
+
? resolvedVersion
|
|
372
|
+
: (resolvedVersion && isVersionInstalled('muse', resolvedVersion) ? resolvedVersion : null);
|
|
373
|
+
if (version) {
|
|
374
|
+
const versionHome = getVersionHomePath('muse', version);
|
|
375
|
+
result.XDG_CONFIG_HOME = path.join(versionHome, '.config');
|
|
376
|
+
result.XDG_DATA_HOME = path.join(versionHome, '.local', 'share');
|
|
377
|
+
}
|
|
378
|
+
delete result.CLAUDE_CONFIG_DIR;
|
|
379
|
+
delete result.CODEX_HOME;
|
|
380
|
+
delete result.COPILOT_HOME;
|
|
381
|
+
delete result.KIMI_CODE_HOME;
|
|
382
|
+
}
|
|
365
383
|
else {
|
|
366
384
|
delete result.CLAUDE_CONFIG_DIR;
|
|
367
385
|
delete result.CODEX_HOME;
|
|
@@ -472,6 +490,7 @@ export const AGENT_COMMANDS = {
|
|
|
472
490
|
base: ['cursor-agent'],
|
|
473
491
|
promptFlag: '-p',
|
|
474
492
|
modeFlags: {
|
|
493
|
+
plan: ['--plan'],
|
|
475
494
|
edit: [],
|
|
476
495
|
skip: ['-f'],
|
|
477
496
|
},
|
|
@@ -665,6 +684,23 @@ export const AGENT_COMMANDS = {
|
|
|
665
684
|
// `resume` subcommand — special-cased in buildExecCommand.
|
|
666
685
|
resume: { flag: '--session-id' },
|
|
667
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
|
+
},
|
|
668
704
|
};
|
|
669
705
|
/**
|
|
670
706
|
* Whether `agent` has a native resume form (Tier 1). Derived solely from the
|
|
@@ -1167,13 +1203,53 @@ export function shouldWrapInTmux(ctx) {
|
|
|
1167
1203
|
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
1168
1204
|
*/
|
|
1169
1205
|
export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
|
|
1206
|
+
const agentCmd = [executable, ...args].map(shellQuote).join(' ');
|
|
1207
|
+
// envFile: source the values instead of inlining them, so no VALUE ever lands
|
|
1208
|
+
// in the pane's argv. `exec env K=V …` put every resolved secret into the
|
|
1209
|
+
// process table, readable by any process of this user — on one fleet box six
|
|
1210
|
+
// live processes carried the secrets-store master passphrase, which decrypts
|
|
1211
|
+
// every file-backed bundle including the Claude OAuth tokens (RUSH-2100).
|
|
1212
|
+
// `set -a` exports what the file assigns; the file is unlinked before `exec`,
|
|
1213
|
+
// so it exists only for the sourcing itself. A missing file aborts the pane
|
|
1214
|
+
// rather than silently launching with a half-built env.
|
|
1215
|
+
if (opts.envFile) {
|
|
1216
|
+
const f = shellQuote(opts.envFile);
|
|
1217
|
+
// Remove the file whether or not sourcing succeeds — a bare `. f || exit 1`
|
|
1218
|
+
// strands the plaintext env (incl. the secrets-store master passphrase) on
|
|
1219
|
+
// disk on any source failure, worse than the argv leak this replaces
|
|
1220
|
+
// (RUSH-2100). Capture the source rc, unlink, then honor it.
|
|
1221
|
+
return `set -a; . ${f}; __agents_rc=$?; set +a; rm -f ${f}; [ "$__agents_rc" -eq 0 ] || exit 1; exec ${agentCmd}`;
|
|
1222
|
+
}
|
|
1170
1223
|
const envPrefix = Object.entries(env)
|
|
1171
1224
|
.filter(([k, v]) => v !== undefined && EXEC_ENV_KEY_PATTERN.test(k))
|
|
1172
1225
|
.map(([k, v]) => `${k}=${opts.redactEnvValues ? '<redacted>' : shellQuote(String(v))}`)
|
|
1173
1226
|
.join(' ');
|
|
1174
|
-
const agentCmd = [executable, ...args].map(shellQuote).join(' ');
|
|
1175
1227
|
return `exec env ${envPrefix} ${agentCmd}`;
|
|
1176
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Serialize the pane env to a shell-sourceable file, created 0600 and exclusively
|
|
1231
|
+
* (`wx`) so it can never adopt a pre-existing file's mode or content.
|
|
1232
|
+
*
|
|
1233
|
+
* EVERY key goes in the file, not a curated "secret-bearing" subset: a denylist
|
|
1234
|
+
* has to be updated for each new credential and is wrong the moment someone
|
|
1235
|
+
* forgets, whereas routing all of it through the file makes the guarantee hold by
|
|
1236
|
+
* construction. Keys are filtered to valid shell identifiers for the same reason
|
|
1237
|
+
* `env` needed it — an exported function (`BASH_FUNC_x%%`) is not assignable.
|
|
1238
|
+
*/
|
|
1239
|
+
export function writeTmuxEnvFile(env, filePath) {
|
|
1240
|
+
const body = Object.entries(env)
|
|
1241
|
+
.filter(([k, v]) => v !== undefined && EXEC_ENV_KEY_PATTERN.test(k))
|
|
1242
|
+
.map(([k, v]) => `${k}=${shellQuote(String(v))}`)
|
|
1243
|
+
.join('\n');
|
|
1244
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1245
|
+
const fd = fs.openSync(filePath, 'wx', 0o600);
|
|
1246
|
+
try {
|
|
1247
|
+
fs.writeSync(fd, `${body}\n`);
|
|
1248
|
+
}
|
|
1249
|
+
finally {
|
|
1250
|
+
fs.closeSync(fd);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1177
1253
|
/**
|
|
1178
1254
|
* Trim a raw `tmux capture-pane` dump to its last `maxLines` non-empty lines
|
|
1179
1255
|
* (right-stripping each). Used by runInTmux to recap a fast-failed agent's
|
|
@@ -1231,15 +1307,32 @@ async function runInTmux(options, executable, args) {
|
|
|
1231
1307
|
// SessionStart learns some harness IDs only after launch. Carry the wrapper
|
|
1232
1308
|
// name into that hook so it can bind both identities durably.
|
|
1233
1309
|
const execEnv = { ...buildExecEnv(options), AGENT_TMUX_SESSION_NAME: name };
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1310
|
+
// The pane sources its env from a 0600 file it unlinks before exec, so no
|
|
1311
|
+
// resolved secret VALUE reaches the process table (RUSH-2100). SessionMeta.cmd
|
|
1312
|
+
// keeps the value-redacted inline form — the human-readable record of what ran,
|
|
1313
|
+
// which never carried real values anyway (RUSH-1758).
|
|
1314
|
+
const envFile = path.join(getRuntimeStateDir(), 'tmux-env', `${name}-${randomUUID().slice(0, 8)}.env`);
|
|
1315
|
+
writeTmuxEnvFile(execEnv, envFile);
|
|
1316
|
+
const cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
|
|
1238
1317
|
const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
|
|
1239
1318
|
const labels = { agent: options.agent };
|
|
1240
1319
|
if (options.sessionId)
|
|
1241
1320
|
labels.sessionId = options.sessionId;
|
|
1242
|
-
|
|
1321
|
+
// Only a launched pane sources-and-unlinks the env file. If createSession
|
|
1322
|
+
// throws, the pane never runs, so the resolved secrets (incl. the master
|
|
1323
|
+
// passphrase) would linger on disk — remove it on that failure path
|
|
1324
|
+
// (RUSH-2100). On success the detached pane owns the unlink.
|
|
1325
|
+
let meta;
|
|
1326
|
+
try {
|
|
1327
|
+
meta = await createSession({ name, cmd, metaCmd, cwd, socket, source: 'cli', labels });
|
|
1328
|
+
}
|
|
1329
|
+
catch (err) {
|
|
1330
|
+
try {
|
|
1331
|
+
fs.rmSync(envFile, { force: true });
|
|
1332
|
+
}
|
|
1333
|
+
catch { /* best-effort */ }
|
|
1334
|
+
throw err;
|
|
1335
|
+
}
|
|
1243
1336
|
const pane = meta.pane;
|
|
1244
1337
|
if (options.sessionId)
|
|
1245
1338
|
writeSessionAliasRecord(options.sessionId, name);
|
|
@@ -1317,7 +1410,7 @@ async function runInTmux(options, executable, args) {
|
|
|
1317
1410
|
};
|
|
1318
1411
|
// The agent could exit before we attach (fast failure). Don't attach to an
|
|
1319
1412
|
// already-dead pane — surface its output + status directly and tear down.
|
|
1320
|
-
const before = pane ? await paneExitStatus(pane, socket) : { dead: false };
|
|
1413
|
+
const before = pane ? await paneExitStatus(pane, socket) : { found: false, dead: false, status: undefined };
|
|
1321
1414
|
if (before.dead) {
|
|
1322
1415
|
// F2 (RUSH-2185 / EXEC-23a): for interactive runs, ALWAYS recap — a clean
|
|
1323
1416
|
// exit-0 before attach means the harness has no interactive REPL and the
|
|
@@ -1343,7 +1436,7 @@ async function runInTmux(options, executable, args) {
|
|
|
1343
1436
|
return false;
|
|
1344
1437
|
}
|
|
1345
1438
|
};
|
|
1346
|
-
const after = pane ? await paneExitStatus(pane, socket) : { dead: false };
|
|
1439
|
+
const after = pane ? await paneExitStatus(pane, socket) : { found: false, dead: false, status: undefined };
|
|
1347
1440
|
if (after.dead) {
|
|
1348
1441
|
// Nonzero exit after attach → the agent crashed rather than the user
|
|
1349
1442
|
// detaching cleanly (a clean detach leaves the pane ALIVE, handled below).
|
|
@@ -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/
|
package/dist/lib/feed-outcome.js
CHANGED
|
@@ -152,21 +152,22 @@ export function groupBlocksByOutcome(blocks) {
|
|
|
152
152
|
const groups = [];
|
|
153
153
|
for (const { outcome, blocks: members } of byKey.values()) {
|
|
154
154
|
const mailboxes = new Set(members.map((b) => b.mailboxId));
|
|
155
|
-
|
|
156
|
-
let answered = 0;
|
|
157
|
-
let parked = 0;
|
|
155
|
+
const states = new Map();
|
|
158
156
|
for (const b of members) {
|
|
159
|
-
|
|
160
|
-
parked
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
const next = b.parkedAt
|
|
158
|
+
? 'parked'
|
|
159
|
+
: b.answer || b.defaultedAt || b.continuedAt ? 'answered' : 'open';
|
|
160
|
+
const current = states.get(b.mailboxId);
|
|
161
|
+
// One agent occupies one state. Its most actionable block wins.
|
|
162
|
+
if (!current || next === 'open' || (next === 'parked' && current === 'answered')) {
|
|
163
|
+
states.set(b.mailboxId, next);
|
|
164
|
+
}
|
|
165
165
|
}
|
|
166
|
+
const count = (state) => [...states.values()].filter((value) => value === state).length;
|
|
166
167
|
groups.push({
|
|
167
168
|
outcome,
|
|
168
169
|
blocks: members,
|
|
169
|
-
counts: { agents: mailboxes.size, open, answered, parked },
|
|
170
|
+
counts: { agents: mailboxes.size, open: count('open'), answered: count('answered'), parked: count('parked') },
|
|
170
171
|
});
|
|
171
172
|
}
|
|
172
173
|
groups.sort((a, b) => {
|
|
@@ -222,6 +223,13 @@ export function enrichBlockFromSession(block, hint) {
|
|
|
222
223
|
next.worktreeSlug = hint.worktreeSlug;
|
|
223
224
|
if (!next.project && hint.project)
|
|
224
225
|
next.project = hint.project;
|
|
226
|
+
if (hint.host && hint.host !== 'terminal' && next.runtime === 'terminal')
|
|
227
|
+
next.runtime = hint.host;
|
|
228
|
+
if (hint.origin === 'routine') {
|
|
229
|
+
next.origin = 'routine';
|
|
230
|
+
if (hint.routineName)
|
|
231
|
+
next.routineName = hint.routineName;
|
|
232
|
+
}
|
|
225
233
|
return next;
|
|
226
234
|
}
|
|
227
235
|
/**
|
package/dist/lib/feed.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface OpenBlock {
|
|
|
36
36
|
mailboxId: string;
|
|
37
37
|
host: string;
|
|
38
38
|
runtime: string;
|
|
39
|
+
/** Indexed launch origin, added at read time when the live session is known. */
|
|
40
|
+
origin?: 'cli' | 'routine';
|
|
41
|
+
/** Routine definition name when origin is `routine`. */
|
|
42
|
+
routineName?: string;
|
|
39
43
|
/** Project/repo name this block belongs to (derived from cwd, worktree-aware). */
|
|
40
44
|
project?: string;
|
|
41
45
|
ts: string;
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -463,6 +463,9 @@ except Exception: pass' 2>/dev/null || true)"
|
|
|
463
463
|
;;
|
|
464
464
|
esac
|
|
465
465
|
CACHE_FILE="$CACHE_DIR/$HOOK_NAME$cache_suffix.out"
|
|
466
|
+
# Failure sentinel: touched on bg-refresh failure; mtime drives the backoff window.
|
|
467
|
+
FAIL_FILE="$CACHE_FILE.fail"
|
|
468
|
+
BACKOFF_SEC=60
|
|
466
469
|
|
|
467
470
|
# Monotonic-ish nanosecond timer (macOS \`date\` has no %N).
|
|
468
471
|
now_ns() { "$PY" -c 'import time; print(int(time.time()*1e9))'; }
|
|
@@ -491,9 +494,38 @@ if [ "$CACHE_STATUS" = miss ]; then
|
|
|
491
494
|
# Stale-while-revalidate: serve stale immediately, refresh in detached child.
|
|
492
495
|
cat "$CACHE_FILE"
|
|
493
496
|
CACHE_STATUS=stale-prefetch
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
+
# Fix 2: backoff — skip bg refresh if last attempt failed within BACKOFF_SEC.
|
|
498
|
+
_in_backoff=0
|
|
499
|
+
if [ -f "$FAIL_FILE" ]; then
|
|
500
|
+
_fail_mtime=$("$PY" -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$FAIL_FILE" 2>/dev/null || echo 0)
|
|
501
|
+
_now_s=$(date +%s)
|
|
502
|
+
_fail_age=$((_now_s - \${_fail_mtime:-0}))
|
|
503
|
+
[ "$_fail_age" -lt "$BACKOFF_SEC" ] && _in_backoff=1
|
|
504
|
+
fi
|
|
505
|
+
if [ "$_in_backoff" -eq 0 ]; then
|
|
506
|
+
# Fix 1: lockdir — only one background refresh runs at a time.
|
|
507
|
+
LOCK_DIR="$CACHE_FILE.bg.lck"
|
|
508
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
509
|
+
tmp="$CACHE_FILE.new.$$"
|
|
510
|
+
# Fix 3: background subshell captures and logs its own real exit code.
|
|
511
|
+
(
|
|
512
|
+
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
513
|
+
_bg_exit=0
|
|
514
|
+
if printf '%s' "$STDIN_PAYLOAD" | "$SOURCE" >"$tmp" 2>/dev/null; then
|
|
515
|
+
mv -f "$tmp" "$CACHE_FILE" && rm -f "$FAIL_FILE"
|
|
516
|
+
else
|
|
517
|
+
_bg_exit=$?
|
|
518
|
+
rm -f "$tmp"
|
|
519
|
+
touch "$FAIL_FILE"
|
|
520
|
+
fi
|
|
521
|
+
_bg_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
522
|
+
_bg_log_file="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
523
|
+
printf '{"ts":"%s","event":"hook.cache.refresh","hook":"%s","cache":"bg-refresh","exit":%d}\\n' \\
|
|
524
|
+
"$_bg_ts" "$HOOK_NAME" "$_bg_exit" >>"$_bg_log_file" 2>/dev/null || true
|
|
525
|
+
) >/dev/null 2>&1 &
|
|
526
|
+
disown 2>/dev/null || true
|
|
527
|
+
fi
|
|
528
|
+
fi
|
|
497
529
|
else
|
|
498
530
|
# Synchronous fetch + cache.
|
|
499
531
|
tmp="$CACHE_FILE.new.$$"
|
|
@@ -501,6 +533,7 @@ if [ "$CACHE_STATUS" = miss ]; then
|
|
|
501
533
|
EXIT=0
|
|
502
534
|
cat "$tmp"
|
|
503
535
|
mv -f "$tmp" "$CACHE_FILE"
|
|
536
|
+
rm -f "$FAIL_FILE"
|
|
504
537
|
else
|
|
505
538
|
EXIT=$?
|
|
506
539
|
rm -f "$tmp"
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export declare function hookExists(agentId: AgentId, hookName: string): boolean;
|
|
|
73
73
|
* Compares both script file and data file (if present).
|
|
74
74
|
*/
|
|
75
75
|
export declare function hookContentMatches(agentId: AgentId, hookName: string, sourceEntry: HookEntry): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Hooks dir for an agent under an arbitrary home (version home or effective
|
|
78
|
+
* home). An agent whose `hooksDir` is configured absolute under `$HOME` (grok,
|
|
79
|
+
* kimi) must be translated to config-dir-relative first — a raw `path.join`
|
|
80
|
+
* would embed the absolute path as a relative segment and produce a hybrid
|
|
81
|
+
* path that never exists (RUSH-2237).
|
|
82
|
+
*/
|
|
83
|
+
export declare function getHooksDirInHome(agentId: AgentId, home: string): string;
|
|
76
84
|
export declare function listInstalledHooksWithScope(agentId: AgentId, cwd?: string, options?: {
|
|
77
85
|
home?: string;
|
|
78
86
|
}): InstalledHook[];
|
|
@@ -99,7 +107,7 @@ export interface HookWiringIssue {
|
|
|
99
107
|
* group). Real hooks scope by matcher — ask-user-question-guard=AskUserQuestion,
|
|
100
108
|
* user-message-guard=Bash — so wiring is verified per (event, matcher). */
|
|
101
109
|
matcher: string;
|
|
102
|
-
/** The command
|
|
110
|
+
/** The command the harness-native config should reference under `event`. */
|
|
103
111
|
command: string;
|
|
104
112
|
}
|
|
105
113
|
export interface HookWiringReport {
|
|
@@ -109,18 +117,22 @@ export interface HookWiringReport {
|
|
|
109
117
|
settingsPath?: string;
|
|
110
118
|
/** Number of hooks the manifest says should be wired for this version. */
|
|
111
119
|
expected?: number;
|
|
112
|
-
/**
|
|
120
|
+
/** Native hook config does not exist — nothing declared can be wired. */
|
|
113
121
|
settingsMissing?: boolean;
|
|
114
|
-
/**
|
|
122
|
+
/** Native hook config cannot be parsed — wiring can't be verified. */
|
|
115
123
|
settingsUnparseable?: boolean;
|
|
116
124
|
/** Hooks whose file is present/resolvable but that are NOT referenced in the
|
|
117
|
-
* event
|
|
125
|
+
* native event group/entry should carry them in. */
|
|
118
126
|
unwired: HookWiringIssue[];
|
|
127
|
+
/** Expected hooks that ARE referenced in native config (expected − unwired).
|
|
128
|
+
* Empty whenever wiring cannot be verified (unsupported family, missing or
|
|
129
|
+
* unparseable settings). */
|
|
130
|
+
wired: HookWiringIssue[];
|
|
119
131
|
}
|
|
120
132
|
/**
|
|
121
|
-
* Verify that every hook the manifest says should be wired
|
|
122
|
-
*
|
|
123
|
-
*
|
|
133
|
+
* Verify that every hook the manifest says should be wired is actually
|
|
134
|
+
* referenced in that version's harness-native config, not merely present as a
|
|
135
|
+
* file on disk.
|
|
124
136
|
*
|
|
125
137
|
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
126
138
|
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
package/dist/lib/hooks.js
CHANGED
|
@@ -673,6 +673,20 @@ export function hookContentMatches(agentId, hookName, sourceEntry) {
|
|
|
673
673
|
return false;
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Hooks dir for an agent under an arbitrary home (version home or effective
|
|
678
|
+
* home). An agent whose `hooksDir` is configured absolute under `$HOME` (grok,
|
|
679
|
+
* kimi) must be translated to config-dir-relative first — a raw `path.join`
|
|
680
|
+
* would embed the absolute path as a relative segment and produce a hybrid
|
|
681
|
+
* path that never exists (RUSH-2237).
|
|
682
|
+
*/
|
|
683
|
+
export function getHooksDirInHome(agentId, home) {
|
|
684
|
+
const config = AGENTS[agentId];
|
|
685
|
+
const hooksDir = path.isAbsolute(config.hooksDir)
|
|
686
|
+
? path.relative(config.configDir, config.hooksDir)
|
|
687
|
+
: config.hooksDir;
|
|
688
|
+
return path.join(home, agentConfigDirName(agentId), hooksDir);
|
|
689
|
+
}
|
|
676
690
|
export function listInstalledHooksWithScope(agentId, cwd = process.cwd(), options) {
|
|
677
691
|
const agent = AGENTS[agentId];
|
|
678
692
|
if (!agent.supportsHooks) {
|
|
@@ -702,7 +716,7 @@ export function listInstalledHooksWithScope(agentId, cwd = process.cwd(), option
|
|
|
702
716
|
}
|
|
703
717
|
// User-scoped hooks (version-aware when home is provided)
|
|
704
718
|
const home = options?.home || getEffectiveHome(agentId);
|
|
705
|
-
const userDir =
|
|
719
|
+
const userDir = getHooksDirInHome(agentId, home);
|
|
706
720
|
const userHooks = listHookEntriesFromDir(userDir);
|
|
707
721
|
for (const hook of userHooks) {
|
|
708
722
|
addHook(hook, 'user', agentId);
|
|
@@ -740,8 +754,7 @@ export async function installHooks(source, agents, options = {}) {
|
|
|
740
754
|
* Path to the hooks dir of a specific version home (not the active one).
|
|
741
755
|
*/
|
|
742
756
|
export function getVersionHooksDir(agent, version) {
|
|
743
|
-
|
|
744
|
-
return path.join(home, agentConfigDirName(agent), AGENTS[agent].hooksDir);
|
|
757
|
+
return getHooksDirInHome(agent, getVersionHomePath(agent, version));
|
|
745
758
|
}
|
|
746
759
|
/**
|
|
747
760
|
* List hook entries in a specific version home.
|
|
@@ -749,20 +762,20 @@ export function getVersionHooksDir(agent, version) {
|
|
|
749
762
|
export function listHooksInVersionHome(agent, version) {
|
|
750
763
|
return listHookEntriesFromDir(getVersionHooksDir(agent, version));
|
|
751
764
|
}
|
|
752
|
-
// ─── wiring inspection
|
|
765
|
+
// ─── wiring inspection ────────────────────────────────────────────────────────
|
|
753
766
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* map (Gemini/Antigravity settings.json variants, Codex config.toml, the OpenCode
|
|
759
|
-
* plugin, …), so it reports them unsupported rather than risk a false verdict.
|
|
767
|
+
* Native hook-config families understood by this read-only inspector. Claude,
|
|
768
|
+
* Droid, and Muse share settings.json; Grok uses the same grouped event shape
|
|
769
|
+
* in hooks/hooks.json; Kimi stores one hook per [[hooks]] config.toml table.
|
|
770
|
+
* Other harnesses report unsupported rather than risk a false verdict.
|
|
760
771
|
*/
|
|
761
772
|
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid', 'muse'];
|
|
773
|
+
const HOOKS_JSON_HOOK_FAMILY = ['grok'];
|
|
774
|
+
const TOML_ARRAY_HOOK_FAMILY = ['kimi'];
|
|
762
775
|
/**
|
|
763
|
-
* Verify that every hook the manifest says should be wired
|
|
764
|
-
*
|
|
765
|
-
*
|
|
776
|
+
* Verify that every hook the manifest says should be wired is actually
|
|
777
|
+
* referenced in that version's harness-native config, not merely present as a
|
|
778
|
+
* file on disk.
|
|
766
779
|
*
|
|
767
780
|
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
768
781
|
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
@@ -774,11 +787,18 @@ const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid', 'muse'];
|
|
|
774
787
|
* resolveHookCommand performs, so it never mutates the version home.
|
|
775
788
|
*/
|
|
776
789
|
export function checkVersionHookWiring(agent, version) {
|
|
777
|
-
if (!AGENTS[agent].supportsHooks ||
|
|
778
|
-
|
|
790
|
+
if (!AGENTS[agent].supportsHooks ||
|
|
791
|
+
(!SETTINGS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
792
|
+
!HOOKS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
793
|
+
!TOML_ARRAY_HOOK_FAMILY.includes(agent))) {
|
|
794
|
+
return { supported: false, unwired: [], wired: [] };
|
|
779
795
|
}
|
|
780
796
|
const versionHome = getVersionHomePath(agent, version);
|
|
781
|
-
const settingsPath =
|
|
797
|
+
const settingsPath = HOOKS_JSON_HOOK_FAMILY.includes(agent)
|
|
798
|
+
? path.join(versionHome, '.grok', 'hooks', 'hooks.json')
|
|
799
|
+
: TOML_ARRAY_HOOK_FAMILY.includes(agent)
|
|
800
|
+
? path.join(versionHome, '.kimi-code', 'config.toml')
|
|
801
|
+
: path.join(versionHome, agentConfigDirName(agent), 'settings.json');
|
|
782
802
|
const localHooksDir = getVersionHooksDir(agent, version);
|
|
783
803
|
// Resolve ONLY to a script that was actually synced for THIS agent+version: the
|
|
784
804
|
// copy in the version home hooks dir, or an absolute subrule-dir path (those are
|
|
@@ -815,11 +835,17 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
815
835
|
const command = expectedCommand(name, hookDef);
|
|
816
836
|
if (!command)
|
|
817
837
|
continue; // script unresolved — a file gap, reported by diffHooks
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
838
|
+
for (const event of hookDef.events) {
|
|
839
|
+
if (HOOKS_JSON_HOOK_FAMILY.includes(agent)) {
|
|
840
|
+
const matcher = GROK_MATCHER_EVENTS.has(event)
|
|
841
|
+
? (GROK_MATCHER_ALIASES[hookDef.matcher || ''] ?? hookDef.matcher ?? '')
|
|
842
|
+
: '';
|
|
843
|
+
expected.push({ name, event, matcher, command });
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
expected.push({ name, event, matcher: hookDef.matcher || '', command });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
823
849
|
}
|
|
824
850
|
if (!fs.existsSync(settingsPath)) {
|
|
825
851
|
return {
|
|
@@ -828,11 +854,15 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
828
854
|
expected: expected.length,
|
|
829
855
|
settingsMissing: expected.length > 0,
|
|
830
856
|
unwired: [],
|
|
857
|
+
wired: [],
|
|
831
858
|
};
|
|
832
859
|
}
|
|
833
860
|
let config;
|
|
834
861
|
try {
|
|
835
|
-
|
|
862
|
+
const raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
863
|
+
config = TOML_ARRAY_HOOK_FAMILY.includes(agent)
|
|
864
|
+
? TOML.parse(raw)
|
|
865
|
+
: JSON.parse(raw);
|
|
836
866
|
}
|
|
837
867
|
catch {
|
|
838
868
|
return {
|
|
@@ -841,6 +871,7 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
841
871
|
expected: expected.length,
|
|
842
872
|
settingsUnparseable: true,
|
|
843
873
|
unwired: [],
|
|
874
|
+
wired: [],
|
|
844
875
|
};
|
|
845
876
|
}
|
|
846
877
|
// Command strings actually referenced, keyed by (event, matcher) — a hook wired
|
|
@@ -848,30 +879,49 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
848
879
|
// not just by event.
|
|
849
880
|
const wiredByGroup = new Map();
|
|
850
881
|
const groupKey = (event, matcher) => `${event}\n${matcher}`;
|
|
851
|
-
|
|
852
|
-
? config.hooks
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (!Array.isArray(groups))
|
|
856
|
-
continue;
|
|
857
|
-
for (const group of groups) {
|
|
858
|
-
if (!group || !Array.isArray(group.hooks))
|
|
882
|
+
if (TOML_ARRAY_HOOK_FAMILY.includes(agent)) {
|
|
883
|
+
const hooks = Array.isArray(config.hooks) ? config.hooks : [];
|
|
884
|
+
for (const hook of hooks) {
|
|
885
|
+
if (typeof hook.event !== 'string' || typeof hook.command !== 'string')
|
|
859
886
|
continue;
|
|
860
|
-
const matcher = typeof
|
|
861
|
-
const key = groupKey(event, matcher);
|
|
887
|
+
const matcher = typeof hook.matcher === 'string' ? hook.matcher : '';
|
|
888
|
+
const key = groupKey(hook.event, matcher);
|
|
862
889
|
let cmds = wiredByGroup.get(key);
|
|
863
890
|
if (!cmds) {
|
|
864
891
|
cmds = new Set();
|
|
865
892
|
wiredByGroup.set(key, cmds);
|
|
866
893
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
894
|
+
cmds.add(hook.command);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
const hooks = config.hooks && typeof config.hooks === 'object'
|
|
899
|
+
? config.hooks
|
|
900
|
+
: {};
|
|
901
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
902
|
+
if (!Array.isArray(groups))
|
|
903
|
+
continue;
|
|
904
|
+
for (const group of groups) {
|
|
905
|
+
if (!group || !Array.isArray(group.hooks))
|
|
906
|
+
continue;
|
|
907
|
+
const matcher = typeof group.matcher === 'string' ? group.matcher : '';
|
|
908
|
+
const key = groupKey(event, matcher);
|
|
909
|
+
let cmds = wiredByGroup.get(key);
|
|
910
|
+
if (!cmds) {
|
|
911
|
+
cmds = new Set();
|
|
912
|
+
wiredByGroup.set(key, cmds);
|
|
913
|
+
}
|
|
914
|
+
for (const h of group.hooks) {
|
|
915
|
+
if (h && typeof h.command === 'string')
|
|
916
|
+
cmds.add(h.command);
|
|
917
|
+
}
|
|
870
918
|
}
|
|
871
919
|
}
|
|
872
920
|
}
|
|
873
|
-
const
|
|
874
|
-
|
|
921
|
+
const isWired = (entry) => wiredByGroup.get(groupKey(entry.event, entry.matcher))?.has(entry.command) ?? false;
|
|
922
|
+
const unwired = expected.filter((entry) => !isWired(entry));
|
|
923
|
+
const wired = expected.filter(isWired);
|
|
924
|
+
return { supported: true, settingsPath, expected: expected.length, unwired, wired };
|
|
875
925
|
}
|
|
876
926
|
/**
|
|
877
927
|
* Check if a hook installed in a specific version matches central content.
|
|
@@ -1402,7 +1452,7 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
1402
1452
|
// Scripts are copied into the version home during sync — prefer that stable
|
|
1403
1453
|
// local path so registered commands don't break when source dirs change.
|
|
1404
1454
|
const localHooksDir = !overrideRoots
|
|
1405
|
-
?
|
|
1455
|
+
? getHooksDirInHome(agentId, versionHome)
|
|
1406
1456
|
: null;
|
|
1407
1457
|
const resolveScript = (script) => {
|
|
1408
1458
|
// Subrule-dir hooks declare an already-absolute script path. Use it
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* table or, when `--host`/`--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
|
+
import { type Host } from './types.js';
|
|
19
20
|
import { type DeviceProfile, type DeviceRegistry } from '../devices/registry.js';
|
|
20
21
|
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
21
22
|
/** Per-command remote behaviour. Absence from this map = not host-routable here. */
|
|
@@ -60,4 +61,24 @@ export declare function runFleetPassthrough(command: string, allArgs: string[],
|
|
|
60
61
|
* @param allArgs `process.argv.slice(2)` — the command name followed by its args.
|
|
61
62
|
*/
|
|
62
63
|
export declare function maybeRunOnHost(command: string, allArgs: string[], opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Run `agents <forwardedArgs>` on `host` over SSH, streaming its output, and
|
|
66
|
+
* return the exit code. The single place the SSH hop is built, so every remote
|
|
67
|
+
* `agents` invocation carries identical env semantics.
|
|
68
|
+
*
|
|
69
|
+
* Forwards actor provenance (`AGENTS_ACTOR`/`GIT_` vars) across the hop, merged UNDER
|
|
70
|
+
* `extraEnv` so a caller-supplied PATH still wins — without this the remote
|
|
71
|
+
* re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
72
|
+
* (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
73
|
+
* AGENTS_FLEET_REMOTE marks this as a fleet-dispatched run so the far side can
|
|
74
|
+
* gate consent-sensitive actions — the browser consent gate
|
|
75
|
+
* (lib/browser/remote-control.ts) reads it to allow/deny a cross-machine drive.
|
|
76
|
+
*/
|
|
77
|
+
export declare function streamAgentsOnHost(host: Host, forwardedArgs: string[], opts?: {
|
|
78
|
+
remoteCwd?: string;
|
|
79
|
+
interactive?: boolean;
|
|
80
|
+
extraEnv?: Record<string, string>;
|
|
81
|
+
remoteOs?: string;
|
|
82
|
+
target?: string;
|
|
83
|
+
}): number;
|
|
63
84
|
export {};
|