@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -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/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- 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/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- 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 +44 -0
- 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/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- 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 +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- 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 +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- 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 +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- 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/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -848,6 +848,11 @@ function renderTargetText(report, options) {
|
|
|
848
848
|
// sees what was checked. options.requestedKinds drives this.
|
|
849
849
|
if (options.requestedKinds && !options.requestedKinds.has(kind))
|
|
850
850
|
continue;
|
|
851
|
+
if (kind === 'hooks' && report.hookInventory) {
|
|
852
|
+
const wired = report.hookInventory.wiringSupported ? String(report.hookInventory.wired.length) : 'unknown';
|
|
853
|
+
const unmanaged = report.hookInventory.unmanaged.length > 0 ? ` · unmanaged ${report.hookInventory.unmanaged.length}` : '';
|
|
854
|
+
console.log(chalk.gray(` inventory: capable ${report.hookInventory.capable ? 'yes' : 'no'} · on-disk ${report.hookInventory.onDisk.length} · wired ${wired}${unmanaged}`));
|
|
855
|
+
}
|
|
851
856
|
renderKindSection(kind, rows, report.layers, options);
|
|
852
857
|
// A hook file can reconcile "ok" above yet be absent from settings.json — a
|
|
853
858
|
// present-but-dead hook. Surface that right under the hooks section.
|
|
@@ -1022,10 +1027,13 @@ function runCheckGate(opts, cwd) {
|
|
|
1022
1027
|
const orphanNote = drift.orphanVersionCount > 0
|
|
1023
1028
|
? chalk.gray(` (${drift.orphanVersionCount} version(s) carry orphans — run \`agents prune cleanup\`)`)
|
|
1024
1029
|
: '';
|
|
1025
|
-
console.log(`${chalk.green('ok')}
|
|
1030
|
+
console.log(`${chalk.gray('check:')} ${chalk.green('ok')} — ${drift.syncRows.length} version(s) in sync${orphanNote}`);
|
|
1026
1031
|
process.exit(0);
|
|
1027
1032
|
}
|
|
1028
|
-
// Drift: one-line verdict always, per-version detail unless --quiet.
|
|
1033
|
+
// Drift: one-line verdict always, per-version detail unless --quiet. The
|
|
1034
|
+
// `check:` prefix and total count make every verdict line grep/parse-alike
|
|
1035
|
+
// whether it's clean or drifted, instead of the bare `drift <parts>` shape
|
|
1036
|
+
// that gave CI logs no anchor to search on.
|
|
1029
1037
|
const parts = [];
|
|
1030
1038
|
if (drift.staleCount > 0)
|
|
1031
1039
|
parts.push(`${drift.staleCount} stale`);
|
|
@@ -1035,16 +1043,21 @@ function runCheckGate(opts, cwd) {
|
|
|
1035
1043
|
parts.push(`${drift.unwiredHookVersions} with unwired hooks`);
|
|
1036
1044
|
if (drift.sourceBehind.length > 0)
|
|
1037
1045
|
parts.push(`${drift.sourceBehind.length} source layer(s) behind origin`);
|
|
1038
|
-
console.error(`${chalk.red('drift')}
|
|
1046
|
+
console.error(`${chalk.gray('check:')} ${chalk.red('drift')} — ${parts.join(', ')} across ${drift.syncRows.length} version(s)`);
|
|
1039
1047
|
if (!opts.quiet) {
|
|
1048
|
+
// Fixed-width, left-aligned so rows form a real column; the badge text is
|
|
1049
|
+
// the actual status name ('never-synced'), not the unrelated 'cold' label
|
|
1050
|
+
// that used to hide it. Width is 'never-synced'.length + a 2-space gap so
|
|
1051
|
+
// the longest badge still separates cleanly from the label that follows.
|
|
1052
|
+
const STATUS_BADGE_WIDTH = 'never-synced'.length + 2;
|
|
1040
1053
|
for (const row of drift.syncRows) {
|
|
1041
1054
|
const unwired = (row.unwiredHooks ?? 0) > 0;
|
|
1042
1055
|
if (row.status === 'fresh' && !unwired)
|
|
1043
1056
|
continue;
|
|
1044
|
-
const tag = row.status === 'stale' ? chalk.yellow('stale')
|
|
1045
|
-
: row.status === 'never-synced' ? chalk.gray('
|
|
1046
|
-
: chalk.red('unwired'); // fresh but hooks not wired into settings.json
|
|
1047
|
-
console.error(` ${tag}
|
|
1057
|
+
const tag = row.status === 'stale' ? chalk.yellow('stale'.padEnd(STATUS_BADGE_WIDTH))
|
|
1058
|
+
: row.status === 'never-synced' ? chalk.gray('never-synced'.padEnd(STATUS_BADGE_WIDTH))
|
|
1059
|
+
: chalk.red('unwired'.padEnd(STATUS_BADGE_WIDTH)); // fresh but hooks not wired into settings.json
|
|
1060
|
+
console.error(` ${tag}${checkLabel(row)}`);
|
|
1048
1061
|
for (const line of row.divergence ?? []) {
|
|
1049
1062
|
console.error(chalk.gray(` ${line}`));
|
|
1050
1063
|
}
|
package/dist/commands/exec.js
CHANGED
|
@@ -516,7 +516,7 @@ export function registerRunCommand(program) {
|
|
|
516
516
|
const runCmd = program
|
|
517
517
|
.command('run <agent> [prompt]')
|
|
518
518
|
.description('Execute an agent. Pass a prompt for headless runs; omit it to launch the agent interactively.')
|
|
519
|
-
.option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). \'full\' accepted as alias for skip.', 'plan')
|
|
519
|
+
.option('-m, --mode <mode>', 'How much the agent can do: plan (read-only), edit (can write files), auto (smart classifier auto-approves safe ops, prompts for risky), skip (bypass all permission prompts). Omitted Codex mode defaults to safe writable edit; other harnesses default to plan. \'full\' accepted as alias for skip.', 'plan')
|
|
520
520
|
.option('-e, --effort <effort>', 'Reasoning effort: low | medium | high | xhigh | max | auto (claude and codex only)', 'auto')
|
|
521
521
|
.option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete model id; tiers resolve per harness+version to a supported model')
|
|
522
522
|
.option('--env <key=value>', 'Pass environment variable to the agent (repeatable, e.g., --env DEBUG=1 --env API_KEY=xyz)', (val, prev) => [...prev, val], [])
|
|
@@ -532,7 +532,7 @@ export function registerRunCommand(program) {
|
|
|
532
532
|
.option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
|
|
533
533
|
.option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
|
|
534
534
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
535
|
-
.option('--resume [id]', '
|
|
535
|
+
.option('--resume [id]', 'Recover a previous conversation on its origin device. The exact healthy origin uses native resume; otherwise a healthy version of the same harness replays via /continue. Pair with a prompt to continue headlessly.')
|
|
536
536
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
537
537
|
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --host runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
538
538
|
.option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
|
|
@@ -647,12 +647,14 @@ export function registerRunCommand(program) {
|
|
|
647
647
|
agents run claude "fix the bug" -- --custom-flag
|
|
648
648
|
`,
|
|
649
649
|
notes: `
|
|
650
|
-
Modes (not every agent supports every mode —
|
|
650
|
+
Modes (not every agent supports every mode — run \`agents modes <agent>\`):
|
|
651
651
|
plan read-only investigation; no writes, no shell side-effects
|
|
652
652
|
edit may edit files; prompts for shell / risky operations
|
|
653
653
|
auto smart classifier auto-approves safe ops, prompts for risky (claude, copilot)
|
|
654
654
|
skip bypass every permission prompt (dangerously-skip-permissions)
|
|
655
655
|
Legacy 'full' is silently rewritten to 'skip'.
|
|
656
|
+
List per-harness support + native flags: agents modes · agents modes claude
|
|
657
|
+
Models (cheap|default|best|ultra): agents models <agent[@version]>
|
|
656
658
|
|
|
657
659
|
Headless plan support (a prompt makes the run headless):
|
|
658
660
|
plan works headless on claude, codex, droid, opencode.
|
|
@@ -689,7 +691,7 @@ export function registerRunCommand(program) {
|
|
|
689
691
|
given. --cloud is mutually exclusive with --host/--lease and with
|
|
690
692
|
local-run flags (--loop, --resume, --secrets, --terminal, …).
|
|
691
693
|
|
|
692
|
-
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and
|
|
694
|
+
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and recovers on the source device with its cwd/mode. The exact healthy origin version uses native resume; otherwise a healthy version of the same harness replays via /continue. agents resume <id> infers the harness too.
|
|
693
695
|
|
|
694
696
|
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
695
697
|
agents run kimi -- --plan --some-native-flag value
|
|
@@ -835,6 +837,7 @@ export function registerRunCommand(program) {
|
|
|
835
837
|
}
|
|
836
838
|
let autoHarnessRequested = normalizedAgentSpec === RUN_AUTO_KEYWORD;
|
|
837
839
|
let resolvedResumeSource;
|
|
840
|
+
let resolvedRecoveryTarget;
|
|
838
841
|
// Concrete resume ids resolve BEFORE placement. Full UUIDs take the local
|
|
839
842
|
// SQLite fast path; only a local miss fans out to the fleet. This lets a
|
|
840
843
|
// command entered on zion discover that the owning version-home is on a
|
|
@@ -882,38 +885,48 @@ export function registerRunCommand(program) {
|
|
|
882
885
|
// the normal new-run default as a real value, so consult its provenance
|
|
883
886
|
// rather than mistaking the default for an explicit override.
|
|
884
887
|
if (command.getOptionValueSource('mode') === 'default') {
|
|
885
|
-
if (resolvedResumeSource.mode)
|
|
888
|
+
if (resolvedResumeSource.mode) {
|
|
886
889
|
options.mode = resolvedResumeSource.mode;
|
|
890
|
+
command.setOptionValueWithSource('mode', resolvedResumeSource.mode, 'implied');
|
|
891
|
+
}
|
|
887
892
|
else if (!options.quiet)
|
|
888
893
|
process.stderr.write(chalk.yellow(`[agents] session ${resolvedResumeSource.shortId} predates stored launch modes; using --mode ${options.mode}\n`));
|
|
889
894
|
}
|
|
890
895
|
const { machineId } = await import('../lib/machine-id.js');
|
|
896
|
+
const { sessionRecoveryDestinationMatches, sessionRecoveryPeer, } = await import('../lib/session/recovery.js');
|
|
891
897
|
const sourceMachine = resolvedResumeSource.machine;
|
|
898
|
+
const sourcePeer = sessionRecoveryPeer(resolvedResumeSource);
|
|
892
899
|
const explicitPlacement = hostTargetGiven(options).length > 0;
|
|
893
|
-
if (
|
|
894
|
-
options.host =
|
|
900
|
+
if (sourcePeer && !explicitPlacement) {
|
|
901
|
+
options.host = sourcePeer;
|
|
895
902
|
}
|
|
896
|
-
else if (
|
|
897
|
-
console.error(chalk.red(`
|
|
898
|
-
`
|
|
903
|
+
else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
|
|
904
|
+
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its indexed transcript and version history are owned; ` +
|
|
905
|
+
`the requested device was ${hostTargetGiven(options).join(', ')}.`));
|
|
899
906
|
process.exit(1);
|
|
900
907
|
}
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const {
|
|
909
|
-
|
|
910
|
-
if (!health.exhausted && health.version === resolvedResumeSource.version) {
|
|
911
|
-
normalizedAgentSpec = `${sourceAgent}@${resolvedResumeSource.version}`;
|
|
912
|
-
autoHarnessRequested = false;
|
|
913
|
-
if (!options.quiet)
|
|
914
|
-
process.stderr.write(chalk.gray(`[agents] auto resume → native ${normalizedAgentSpec} on ${sourceMachine ?? machineId()}\n`));
|
|
915
|
-
}
|
|
908
|
+
// Recovery is resolved on the device that owns the transcript. A remote
|
|
909
|
+
// dispatch is pinned to the source HARNESS (not `run auto`'s cross-harness
|
|
910
|
+
// picker); the peer repeats this block with the injected SessionMeta and
|
|
911
|
+
// chooses its own healthy version. Locally, resolve it now.
|
|
912
|
+
const sourceAgent = resolvedResumeSource.agent;
|
|
913
|
+
if (!sourcePeer) {
|
|
914
|
+
try {
|
|
915
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
916
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(resolvedResumeSource);
|
|
916
917
|
}
|
|
918
|
+
catch (err) {
|
|
919
|
+
console.error(chalk.red(err.message));
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
normalizedAgentSpec = `${resolvedRecoveryTarget.agent}@${resolvedRecoveryTarget.version}`;
|
|
923
|
+
autoHarnessRequested = false;
|
|
924
|
+
if (!options.quiet)
|
|
925
|
+
process.stderr.write(chalk.gray(`[agents] session recovery → ${resolvedRecoveryTarget.mode} ${normalizedAgentSpec} on ${sourceMachine ?? machineId()} · ${resolvedRecoveryTarget.reason}\n`));
|
|
926
|
+
}
|
|
927
|
+
else if (autoHarnessRequested) {
|
|
928
|
+
normalizedAgentSpec = sourceAgent;
|
|
929
|
+
autoHarnessRequested = false;
|
|
917
930
|
}
|
|
918
931
|
}
|
|
919
932
|
if (autoHarnessRequested) {
|
|
@@ -1540,13 +1553,15 @@ export function registerRunCommand(program) {
|
|
|
1540
1553
|
});
|
|
1541
1554
|
}
|
|
1542
1555
|
const isRaw = options.raw || options.tmux === false || options.disableTmux === true;
|
|
1556
|
+
const { modeForRemoteDispatch } = await import('../lib/codex-policy.js');
|
|
1557
|
+
const forwardedMode = modeForRemoteDispatch(options.mode, command.getOptionValueSource('mode'));
|
|
1543
1558
|
const exitCode = await runInteractiveOnHost(host, {
|
|
1544
1559
|
agent: runAgent,
|
|
1545
1560
|
version: resumeId ? undefined : runVersion,
|
|
1546
1561
|
strategy: resumeId ? undefined : runStrategy,
|
|
1547
1562
|
fallback: options.fallback,
|
|
1548
1563
|
prompt,
|
|
1549
|
-
mode:
|
|
1564
|
+
mode: forwardedMode,
|
|
1550
1565
|
model: options.model,
|
|
1551
1566
|
effort: options.effort,
|
|
1552
1567
|
env: hostEnv,
|
|
@@ -1625,13 +1640,15 @@ export function registerRunCommand(program) {
|
|
|
1625
1640
|
}
|
|
1626
1641
|
// Session-id mint, detached dispatch, and local session-index
|
|
1627
1642
|
// registration all live in the shared helper (lib/hosts/run-target.ts).
|
|
1643
|
+
const { modeForRemoteDispatch } = await import('../lib/codex-policy.js');
|
|
1644
|
+
const forwardedMode = modeForRemoteDispatch(options.mode, command.getOptionValueSource('mode'));
|
|
1628
1645
|
const { task, exitCode } = await dispatchPromptToHost(host, {
|
|
1629
1646
|
agent: runAgent,
|
|
1630
1647
|
version: resumeId ? undefined : runVersion,
|
|
1631
1648
|
strategy: resumeId ? undefined : runStrategy,
|
|
1632
1649
|
fallback: options.fallback,
|
|
1633
1650
|
prompt,
|
|
1634
|
-
mode:
|
|
1651
|
+
mode: forwardedMode,
|
|
1635
1652
|
model: options.model,
|
|
1636
1653
|
effort: options.effort,
|
|
1637
1654
|
env: options.env,
|
|
@@ -1770,7 +1787,7 @@ export function registerRunCommand(program) {
|
|
|
1770
1787
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
1771
1788
|
process.exit(resumeExit);
|
|
1772
1789
|
}
|
|
1773
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
1790
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, implicitModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports, capableAgents }, { shareRuntimeEnv },] = await Promise.all([
|
|
1774
1791
|
import('../lib/exec.js'),
|
|
1775
1792
|
import('../lib/agents.js'),
|
|
1776
1793
|
import('../lib/profiles.js'),
|
|
@@ -2211,10 +2228,44 @@ export function registerRunCommand(program) {
|
|
|
2211
2228
|
session = picked.session;
|
|
2212
2229
|
forceInteractive = true; // bare resume always lands in the agent's TUI
|
|
2213
2230
|
}
|
|
2214
|
-
//
|
|
2215
|
-
//
|
|
2216
|
-
//
|
|
2217
|
-
|
|
2231
|
+
// Bare `run <harness> --resume` learns the chosen SessionMeta only after
|
|
2232
|
+
// the host-placement phase above. If the picker chose a synced session
|
|
2233
|
+
// from another device, route the recovery command now; resolving local
|
|
2234
|
+
// candidates would otherwise native-resume through this device's
|
|
2235
|
+
// unrelated isolated home.
|
|
2236
|
+
if (!resolvedResumeSource) {
|
|
2237
|
+
const { sessionRecoveryPeer, sessionRecoveryRunArgs, } = await import('../lib/session/recovery.js');
|
|
2238
|
+
const peer = sessionRecoveryPeer(session);
|
|
2239
|
+
if (peer) {
|
|
2240
|
+
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
2241
|
+
const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true });
|
|
2242
|
+
if (routed === 'no-target') {
|
|
2243
|
+
console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
|
|
2244
|
+
process.exitCode = 1;
|
|
2245
|
+
}
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
// Bare interactive --resume selects the SessionMeta only down here, so
|
|
2250
|
+
// it has not gone through the early concrete-id resolver. Resolve it now;
|
|
2251
|
+
// concrete ids reuse the exact same target chosen above.
|
|
2252
|
+
if (!resolvedRecoveryTarget) {
|
|
2253
|
+
try {
|
|
2254
|
+
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
2255
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(session);
|
|
2256
|
+
}
|
|
2257
|
+
catch (err) {
|
|
2258
|
+
console.error(chalk.red(err.message));
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (resolvedRecoveryTarget.agent !== agent) {
|
|
2263
|
+
console.error(chalk.red(`Session ${session.shortId} belongs to ${resolvedRecoveryTarget.agent}, not ${agent}. ` +
|
|
2264
|
+
`Use: agents run auto --resume ${session.id}`));
|
|
2265
|
+
process.exit(1);
|
|
2266
|
+
}
|
|
2267
|
+
version = resolvedRecoveryTarget.version;
|
|
2268
|
+
const canResumeNatively = resolvedRecoveryTarget.mode === 'native';
|
|
2218
2269
|
if (canResumeNatively) {
|
|
2219
2270
|
version = session.version;
|
|
2220
2271
|
resumeNative = true;
|
|
@@ -2324,6 +2375,12 @@ export function registerRunCommand(program) {
|
|
|
2324
2375
|
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
|
|
2325
2376
|
process.exit(1);
|
|
2326
2377
|
}
|
|
2378
|
+
if (resolvedRecoveryTarget && healed !== launchTarget) {
|
|
2379
|
+
console.error(chalk.red(`agents: session recovery target ${agent}@${launchTarget} became unavailable on ` +
|
|
2380
|
+
`${resolvedResumeSource?.machine ?? 'this device'}; refusing to resume through another version home. ` +
|
|
2381
|
+
`Retry the command so recovery can select a healthy ${agent} version.`));
|
|
2382
|
+
process.exit(1);
|
|
2383
|
+
}
|
|
2327
2384
|
// Always adopt the healed version explicitly. In the version-undefined
|
|
2328
2385
|
// path a fallback re-pins the GLOBAL default, but `resolveVersion`
|
|
2329
2386
|
// prefers a PROJECT pin — so leaving `version` undefined would let the
|
|
@@ -2407,7 +2464,14 @@ export function registerRunCommand(program) {
|
|
|
2407
2464
|
// `skip` still hard-fails when unsupported — pretending we bypassed
|
|
2408
2465
|
// permissions would be unsafe.
|
|
2409
2466
|
const modeIsDefault = modeSource === 'default';
|
|
2410
|
-
|
|
2467
|
+
let requestedMode = normalizeMode(mode);
|
|
2468
|
+
// Codex's intrinsic omitted-mode default is safe writable: workspace plus
|
|
2469
|
+
// common caches, network enabled, approvals on request. An explicit
|
|
2470
|
+
// --mode plan and a configured run default remain read-only.
|
|
2471
|
+
const { modeWasImplicit } = await import('../lib/codex-policy.js');
|
|
2472
|
+
const wasModeImplicit = modeWasImplicit(modeSource, modeFromRunDefault);
|
|
2473
|
+
if (wasModeImplicit)
|
|
2474
|
+
requestedMode = implicitModeFor(agent);
|
|
2411
2475
|
let resolvedMode;
|
|
2412
2476
|
try {
|
|
2413
2477
|
resolvedMode = resolveMode(agent, requestedMode);
|
|
@@ -2434,7 +2498,8 @@ export function registerRunCommand(program) {
|
|
|
2434
2498
|
console.error(chalk.gray(`Pass --mode plan explicitly if you really want a read-only run.`));
|
|
2435
2499
|
process.exit(1);
|
|
2436
2500
|
}
|
|
2437
|
-
const
|
|
2501
|
+
const effortSource = runCmd.getOptionValueSource('effort');
|
|
2502
|
+
const effort = (effortSource === 'default' && runDefaults.effort ? runDefaults.effort : options.effort);
|
|
2438
2503
|
if (!['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(effort)) {
|
|
2439
2504
|
console.error(chalk.red(`Invalid effort: ${effort}. Use 'low', 'medium', 'high', 'xhigh', 'max', or 'auto'`));
|
|
2440
2505
|
process.exit(1);
|
|
@@ -2531,6 +2596,7 @@ export function registerRunCommand(program) {
|
|
|
2531
2596
|
prompt,
|
|
2532
2597
|
interactive: options.interactive || forceInteractive,
|
|
2533
2598
|
mode: requestedMode,
|
|
2599
|
+
modeWasImplicit: wasModeImplicit,
|
|
2534
2600
|
effort,
|
|
2535
2601
|
cwd: options.cwd,
|
|
2536
2602
|
model,
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -60,6 +60,24 @@ export declare function sessionHintsFromActive(sessions: Array<{
|
|
|
60
60
|
slug?: string;
|
|
61
61
|
};
|
|
62
62
|
}>): SessionOutcomeHint[];
|
|
63
|
+
/**
|
|
64
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
65
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
66
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
67
|
+
*/
|
|
68
|
+
export declare function isSqliteBusyError(err: unknown): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
71
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
72
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
73
|
+
*
|
|
74
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
75
|
+
* mocking the session module.
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadSessionMetasForFeedEnrichment<T>(load: () => Promise<T[]>): Promise<{
|
|
78
|
+
metas: T[];
|
|
79
|
+
skippedLock: boolean;
|
|
80
|
+
}>;
|
|
63
81
|
export declare function registerFeedCommand(program: Command): void;
|
|
64
82
|
/** Feed view selector (RUSH-2015): decisions, progress, or both. */
|
|
65
83
|
export type FeedFilter = 'needs' | 'updates' | 'all';
|
package/dist/commands/feed.js
CHANGED
|
@@ -253,6 +253,39 @@ export function sessionHintsFromActive(sessions) {
|
|
|
253
253
|
project: s.cwd ? projectKeyFromCwd(s.cwd) : undefined,
|
|
254
254
|
}));
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* True when a SQLite open/query failed because another writer holds the lock.
|
|
258
|
+
* better-sqlite3 surfaces this as message "database is locked" and/or code
|
|
259
|
+
* SQLITE_BUSY (RUSH-2006).
|
|
260
|
+
*/
|
|
261
|
+
export function isSqliteBusyError(err) {
|
|
262
|
+
if (err == null)
|
|
263
|
+
return false;
|
|
264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
+
const code = typeof err === 'object' && err !== null && 'code' in err
|
|
266
|
+
? String(err.code ?? '')
|
|
267
|
+
: '';
|
|
268
|
+
return /SQLITE_BUSY|database is locked/i.test(msg) || /SQLITE_BUSY/i.test(code);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Load session metas for feed outcome enrichment. The sessions index can be
|
|
272
|
+
* locked by a concurrent scanner/daemon — enrichment is best-effort, so a lock
|
|
273
|
+
* degrades to an empty list instead of crashing `agents feed` (RUSH-2006).
|
|
274
|
+
*
|
|
275
|
+
* `load` is injected so tests can throw a real lock-shaped error without
|
|
276
|
+
* mocking the session module.
|
|
277
|
+
*/
|
|
278
|
+
export async function loadSessionMetasForFeedEnrichment(load) {
|
|
279
|
+
try {
|
|
280
|
+
return { metas: await load(), skippedLock: false };
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (isSqliteBusyError(err)) {
|
|
284
|
+
return { metas: [], skippedLock: true };
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
256
289
|
export function registerFeedCommand(program) {
|
|
257
290
|
const feed = program
|
|
258
291
|
.command('feed')
|
|
@@ -446,7 +479,17 @@ export function registerFeedCommand(program) {
|
|
|
446
479
|
if (includeLocal) {
|
|
447
480
|
sessions = await getActiveSessions();
|
|
448
481
|
}
|
|
449
|
-
|
|
482
|
+
// discoverSessions touches sessions.db; under concurrent scan pressure it
|
|
483
|
+
// can throw SQLITE_BUSY. Outcome enrichment is best-effort — degrade with
|
|
484
|
+
// a warning rather than crash the whole feed (RUSH-2006).
|
|
485
|
+
let sessionMetas = [];
|
|
486
|
+
if (includeLocal && sessions.length > 0) {
|
|
487
|
+
const loaded = await loadSessionMetasForFeedEnrichment(() => discoverSessions({ all: true, limit: 5000 }));
|
|
488
|
+
if (loaded.skippedLock) {
|
|
489
|
+
console.error(chalk.yellow('Feed: session index is locked; skipping local outcome enrichment'));
|
|
490
|
+
}
|
|
491
|
+
sessionMetas = loaded.metas;
|
|
492
|
+
}
|
|
450
493
|
const localSignals = buildSessionSignals(sessions, sessionMetas);
|
|
451
494
|
if (opts.pause || opts.kill) {
|
|
452
495
|
if (!includeLocal) {
|
package/dist/commands/focus.d.ts
CHANGED
|
@@ -13,9 +13,57 @@
|
|
|
13
13
|
* why `--tmux`-wrapped launches are worth it for sessions you'll want back live.
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
|
-
import { type UnreachableFallback } from './go.js';
|
|
16
|
+
import { probeAttachRail, type AttachRailLiveness, type UnreachableFallback } from './go.js';
|
|
17
17
|
import type { ActiveSession } from '../lib/session/active.js';
|
|
18
|
-
import type
|
|
18
|
+
import { type SessionMeta } from '../lib/session/types.js';
|
|
19
|
+
import { type LiveStatusFilter } from './sessions.js';
|
|
20
|
+
import { openSurfaces, type Backend } from '../lib/terminal/index.js';
|
|
21
|
+
/** Options for `sessions focus` — device/host scope + the `--active` live-state filters. */
|
|
22
|
+
export interface FocusOptions {
|
|
23
|
+
local?: boolean;
|
|
24
|
+
attachOnly?: boolean;
|
|
25
|
+
host?: string[];
|
|
26
|
+
device?: string[];
|
|
27
|
+
active?: boolean;
|
|
28
|
+
agent?: string;
|
|
29
|
+
claude?: boolean;
|
|
30
|
+
codex?: boolean;
|
|
31
|
+
kimi?: boolean;
|
|
32
|
+
antigravity?: boolean;
|
|
33
|
+
grok?: boolean;
|
|
34
|
+
opencode?: boolean;
|
|
35
|
+
all?: boolean;
|
|
36
|
+
teams?: boolean;
|
|
37
|
+
inTeam?: string;
|
|
38
|
+
routine?: boolean;
|
|
39
|
+
project?: string;
|
|
40
|
+
skill?: string;
|
|
41
|
+
plugin?: string;
|
|
42
|
+
since?: string;
|
|
43
|
+
until?: string;
|
|
44
|
+
limit?: string;
|
|
45
|
+
favorites?: boolean;
|
|
46
|
+
unmanaged?: boolean;
|
|
47
|
+
sort?: string;
|
|
48
|
+
working?: boolean;
|
|
49
|
+
idle?: boolean;
|
|
50
|
+
waiting?: boolean;
|
|
51
|
+
orphan?: boolean;
|
|
52
|
+
orphaned?: boolean;
|
|
53
|
+
crashed?: boolean;
|
|
54
|
+
closed?: boolean;
|
|
55
|
+
abandoned?: boolean;
|
|
56
|
+
queued?: boolean;
|
|
57
|
+
unknown?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Commander gives overlapping `sessions` flags to the parent command, even
|
|
60
|
+
* when they appear after `focus`. Fold only values explicitly provided there
|
|
61
|
+
* into the child options; child defaults (limit/sort) otherwise stay intact. */
|
|
62
|
+
export declare function inheritFocusOptions(child: FocusOptions, parent?: Command): FocusOptions;
|
|
63
|
+
/** `--device` is an alias of `--host`; both are repeatable. Merge into one host list. */
|
|
64
|
+
export declare function mergeFocusHosts(opts: FocusOptions): string[];
|
|
65
|
+
/** Picker header that reflects the active filter + device, e.g. "Focus orphaned sessions on yosemite-s0:". */
|
|
66
|
+
export declare function focusHeader(statuses: LiveStatusFilter[], hosts: string[]): string;
|
|
19
67
|
export declare function registerFocusCommand(program: Command): void;
|
|
20
68
|
/**
|
|
21
69
|
* Which fallback fires when a session has no attach rail. `--attach-only` (the old
|
|
@@ -23,9 +71,64 @@ export declare function registerFocusCommand(program: Command): void;
|
|
|
23
71
|
* without touching `jumpTo`'s side effects.
|
|
24
72
|
*/
|
|
25
73
|
export declare function selectFallback(attachOnly: boolean | undefined): UnreachableFallback;
|
|
26
|
-
export declare function focusAction(id: string | undefined, opts:
|
|
27
|
-
|
|
74
|
+
export declare function focusAction(id: string | undefined, opts: FocusOptions): Promise<void>;
|
|
75
|
+
/** A retained pane is not attachable merely because tmux can still display it. */
|
|
76
|
+
export declare function isAttachableLiveSession(session: ActiveSession): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* How a single selected live session opens in its new tab.
|
|
79
|
+
* - `attach` — a live tmux pane joined in the tab (a second client, no fork),
|
|
80
|
+
* local (`sh -c`) or remote (`ssh -tt`). tmux is the only rail that can be
|
|
81
|
+
* *joined* without forking (see the file header).
|
|
82
|
+
* - `resume` — no attach rail, so resume a copy in the tab (the original keeps
|
|
83
|
+
* running); never a silent drop.
|
|
84
|
+
* - `skip` — not resumable AND no rail: reported, not dropped.
|
|
85
|
+
*/
|
|
86
|
+
export type FocusSurfacePlan = {
|
|
87
|
+
kind: 'attach';
|
|
88
|
+
command: string[];
|
|
89
|
+
note: string;
|
|
90
|
+
} | {
|
|
91
|
+
kind: 'resume';
|
|
92
|
+
command: string[];
|
|
93
|
+
note: string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: 'skip';
|
|
96
|
+
note: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Shell that resolves a tmux pane to its session and attaches it — the exact form
|
|
100
|
+
* `jumpTo` uses, minus the pre-select-window nicety, so a batch tab joins the live
|
|
101
|
+
* session (a second client) without forking. Reused for local and (wrapped in ssh)
|
|
102
|
+
* remote panes.
|
|
103
|
+
*/
|
|
104
|
+
export declare function tmuxAttachScript(mux: {
|
|
105
|
+
socket?: string;
|
|
106
|
+
pane: string;
|
|
107
|
+
}): string;
|
|
108
|
+
/**
|
|
109
|
+
* Decide how one live session opens as a tab. Pure over the session + a
|
|
110
|
+
* `resumeCommandFor` resolver (injected so the local version-pinned resume command
|
|
111
|
+
* and the tests stay decoupled from `discoverSessions`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function planFocusSurface(s: ActiveSession, self: string, resumeCommandFor: (s: ActiveSession) => string[] | null, rail?: AttachRailLiveness): FocusSurfacePlan;
|
|
114
|
+
/** The engine seam — real `openSurfaces`, overridable in tests to assert the tab requests. */
|
|
115
|
+
export type OpenSurfacesFn = typeof openSurfaces;
|
|
116
|
+
/** Test seams for `openFocusTabs`: inject the engine boundary + force a backend. */
|
|
117
|
+
export interface OpenFocusTabsDeps {
|
|
118
|
+
open?: OpenSurfacesFn;
|
|
119
|
+
/** Skip `resolveBackend` (which needs a live terminal) when set — tests pass 'tmux'. */
|
|
120
|
+
backend?: Backend | 'inplace';
|
|
121
|
+
/** Rich rows selected by the shared browser pipeline (including remote history). */
|
|
122
|
+
metas?: SessionMeta[];
|
|
123
|
+
/** Liveness boundary; production probes tmux, tests exercise planning deterministically. */
|
|
124
|
+
probe?: typeof probeAttachRail;
|
|
125
|
+
/** Strict focus mode: open living attach rails only; never recover a copy. */
|
|
28
126
|
attachOnly?: boolean;
|
|
29
|
-
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Open each selected live session as a tab: attach its live pane where one exists,
|
|
130
|
+
* else resume a copy. Reuses `resume`'s backend resolution + flood guard.
|
|
131
|
+
*/
|
|
132
|
+
export declare function openFocusTabs(targets: ActiveSession[], self: string, deps?: OpenFocusTabsDeps): Promise<void>;
|
|
30
133
|
/** Minimal SessionMeta for a live session, enough for `buildResumeCommand` + placement. */
|
|
31
134
|
export declare function metaFromActive(s: ActiveSession): SessionMeta;
|