@phnx-labs/agents-cli 1.22.29 → 1.22.31

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +44 -5
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/accounts.d.ts +13 -0
  5. package/dist/commands/accounts.js +32 -0
  6. package/dist/commands/daemon.d.ts +18 -0
  7. package/dist/commands/daemon.js +581 -0
  8. package/dist/commands/exec.js +66 -20
  9. package/dist/commands/focus.d.ts +4 -1
  10. package/dist/commands/focus.js +19 -4
  11. package/dist/commands/routines.js +29 -11
  12. package/dist/commands/secrets.d.ts +37 -0
  13. package/dist/commands/secrets.js +86 -105
  14. package/dist/commands/sessions-bookmark.d.ts +20 -0
  15. package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
  16. package/dist/commands/sessions-browser.d.ts +10 -8
  17. package/dist/commands/sessions-browser.js +61 -32
  18. package/dist/commands/sessions-picker.d.ts +33 -1
  19. package/dist/commands/sessions-picker.js +102 -27
  20. package/dist/commands/sessions-stats.js +1 -1
  21. package/dist/commands/sessions.d.ts +21 -8
  22. package/dist/commands/sessions.js +328 -74
  23. package/dist/commands/view.d.ts +11 -0
  24. package/dist/commands/view.js +56 -29
  25. package/dist/index.js +37 -2
  26. package/dist/lib/account-labels.d.ts +24 -0
  27. package/dist/lib/account-labels.js +72 -0
  28. package/dist/lib/agents.d.ts +32 -1
  29. package/dist/lib/agents.js +96 -31
  30. package/dist/lib/daemon-health.d.ts +24 -0
  31. package/dist/lib/daemon-health.js +84 -0
  32. package/dist/lib/daemon-ticks.d.ts +81 -0
  33. package/dist/lib/daemon-ticks.js +190 -0
  34. package/dist/lib/daemon.d.ts +68 -18
  35. package/dist/lib/daemon.js +303 -338
  36. package/dist/lib/device-config.d.ts +10 -0
  37. package/dist/lib/device-config.js +27 -0
  38. package/dist/lib/exec.d.ts +27 -0
  39. package/dist/lib/exec.js +49 -2
  40. package/dist/lib/hosts/dispatch.d.ts +4 -0
  41. package/dist/lib/hosts/dispatch.js +4 -0
  42. package/dist/lib/hosts/remote-cmd.js +1 -0
  43. package/dist/lib/hosts/run-target.d.ts +1 -0
  44. package/dist/lib/hosts/run-target.js +1 -0
  45. package/dist/lib/import.js +7 -6
  46. package/dist/lib/memory-cache.d.ts +19 -0
  47. package/dist/lib/memory-cache.js +31 -0
  48. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  49. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  50. package/dist/lib/migrate.d.ts +1 -1
  51. package/dist/lib/migrate.js +13 -2
  52. package/dist/lib/picker.d.ts +6 -3
  53. package/dist/lib/picker.js +7 -2
  54. package/dist/lib/routine-activation.d.ts +2 -0
  55. package/dist/lib/routine-activation.js +16 -0
  56. package/dist/lib/runner.d.ts +18 -0
  57. package/dist/lib/runner.js +52 -0
  58. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  59. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  60. package/dist/lib/secrets/agent.d.ts +19 -1
  61. package/dist/lib/secrets/agent.js +32 -6
  62. package/dist/lib/secrets/scope.d.ts +3 -3
  63. package/dist/lib/secrets/scope.js +3 -3
  64. package/dist/lib/secrets/session-store.d.ts +0 -4
  65. package/dist/lib/secrets/session-store.js +0 -5
  66. package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
  67. package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
  68. package/dist/lib/session/db.d.ts +15 -0
  69. package/dist/lib/session/db.js +90 -15
  70. package/dist/lib/session/discover.js +91 -39
  71. package/dist/lib/session/parse.d.ts +63 -0
  72. package/dist/lib/session/parse.js +165 -20
  73. package/dist/lib/session/session-cache.d.ts +9 -6
  74. package/dist/lib/session/session-cache.js +23 -6
  75. package/dist/lib/shims.js +12 -0
  76. package/dist/lib/startup/command-registry.d.ts +15 -1
  77. package/dist/lib/startup/command-registry.js +49 -0
  78. package/dist/lib/usage-refresh.js +3 -2
  79. package/dist/lib/usage.d.ts +12 -10
  80. package/dist/lib/usage.js +63 -144
  81. package/package.json +4 -1
  82. package/dist/commands/sessions-favorite.d.ts +0 -20
@@ -445,17 +445,18 @@ async function handleTerminalHandoff(agentSpec, options, prompt) {
445
445
  // the whole Kimi/DeepSeek/Qwen/GLM path — for `--terminal` runs only.
446
446
  const rawTarget = parseRunAccountPickerRequest(agentSpec).normalizedAgentSpec.split('@')[0];
447
447
  const knownAgent = resolveAgentName(rawTarget);
448
- if (knownAgent && isAgentHardDeprecated(knownAgent)) {
448
+ const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
449
+ import('../lib/profiles.js'),
450
+ import('../lib/workflows.js'),
451
+ ]);
452
+ const hasProfile = profileExists(rawTarget);
453
+ if (knownAgent && !hasProfile && isAgentHardDeprecated(knownAgent)) {
449
454
  console.error(chalk.red(hardDeprecationError(knownAgent)));
450
455
  process.exit(1);
451
456
  }
452
457
  if (!knownAgent) {
453
- const [{ profileExists }, { resolveWorkflowRef }] = await Promise.all([
454
- import('../lib/profiles.js'),
455
- import('../lib/workflows.js'),
456
- ]);
457
458
  const probeCwd = options.cwd ?? process.cwd();
458
- if (!profileExists(rawTarget) && !resolveWorkflowRef(rawTarget, probeCwd)) {
459
+ if (!hasProfile && !resolveWorkflowRef(rawTarget, probeCwd)) {
459
460
  console.error(chalk.red(`Unknown agent, profile, or workflow: ${rawTarget}. See \`agents list\` for the installed harnesses.`));
460
461
  process.exit(1);
461
462
  }
@@ -545,6 +546,7 @@ export function registerRunCommand(program) {
545
546
  .option('--fallback <agents>', 'Comma-separated agents to try on rate-limit failure. Each entry accepts an optional @version pin (e.g., codex@0.116.0,antigravity). The primary runs first; if it exits with a rate-limit error, the next agent picks up via /continue handoff.')
546
547
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Ignored when @version is pinned.')
547
548
  .option('--strategy <strategy>', 'Version/account selection strategy: pinned | available | balanced. Defaults to run.<agent>.strategy, then balanced (spreads load across healthy accounts and skips any that are rate-limited). (Legacy `rotate` accepted as alias for `balanced`.)')
549
+ .option('--account <label>', 'Run a healthy installed version currently signed into this named account (never falls back to another identity)')
548
550
  .option('--acp', 'Route through the Agent Client Protocol instead of direct exec. Supported for claude via @zed-industries/claude-code-acp adapter. Unified event stream; emits ndjson when --json.')
549
551
  .option('-y, --yes', 'Skip the interactive budget-confirm prompt (require_confirm_over). Never skips a hard budget block.', false)
550
552
  .option('--loop', 'Re-inject the prompt/entrypoint each iteration until a stop condition (issue #332). Guards (--max-iterations, --budget, --until) are enforced outside the agent. Writes a checkpoint after every iteration for --resume-checkpoint.')
@@ -812,8 +814,10 @@ export function registerRunCommand(program) {
812
814
  process.exit(1);
813
815
  }
814
816
  // Hard-deprecated harnesses cannot be run — point the user at the successor.
815
- const runBaseAgentId = resolveAgentName(normalizedAgentSpec.split('@')[0]);
816
- if (runBaseAgentId && isAgentHardDeprecated(runBaseAgentId)) {
817
+ const runBaseAgentName = normalizedAgentSpec.split('@')[0];
818
+ const runBaseAgentId = resolveAgentName(runBaseAgentName);
819
+ const { profileExists: runProfileExists } = await import('../lib/profiles.js');
820
+ if (runBaseAgentId && !runProfileExists(runBaseAgentName) && isAgentHardDeprecated(runBaseAgentId)) {
817
821
  console.error(chalk.red(hardDeprecationError(runBaseAgentId)));
818
822
  process.exit(1);
819
823
  }
@@ -1567,6 +1571,7 @@ export function registerRunCommand(program) {
1567
1571
  agent: runAgent,
1568
1572
  version: resumeId ? undefined : runVersion,
1569
1573
  strategy: resumeId ? undefined : runStrategy,
1574
+ account: resumeId ? undefined : options.account,
1570
1575
  fallback: options.fallback,
1571
1576
  prompt,
1572
1577
  mode: forwardedMode,
@@ -1654,6 +1659,7 @@ export function registerRunCommand(program) {
1654
1659
  agent: runAgent,
1655
1660
  version: resumeId ? undefined : runVersion,
1656
1661
  strategy: resumeId ? undefined : runStrategy,
1662
+ account: resumeId ? undefined : options.account,
1657
1663
  fallback: options.fallback,
1658
1664
  prompt,
1659
1665
  mode: forwardedMode,
@@ -1840,11 +1846,11 @@ export function registerRunCommand(program) {
1840
1846
  // degenerates to a no-op ("I'll wait for the completion notification").
1841
1847
  let workflowHasSubagents = false;
1842
1848
  const cwd = options.cwd ?? process.cwd();
1849
+ if (accountPickerRequested && profileExists(rawAgent)) {
1850
+ console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with @ instead.`));
1851
+ process.exit(1);
1852
+ }
1843
1853
  if (accountPickerRequested && !isValidAgent(rawAgent)) {
1844
- if (profileExists(rawAgent)) {
1845
- console.error(chalk.red(`Account selection is not available for custom harness '${rawAgent}'. Run its concrete host agent with @ instead.`));
1846
- process.exit(1);
1847
- }
1848
1854
  if (resolveWorkflowRef(rawAgent, cwd)) {
1849
1855
  console.error(chalk.red(`Account selection is not available for workflow '${rawAgent}'. Run a concrete agent with @ instead.`));
1850
1856
  process.exit(1);
@@ -1886,14 +1892,11 @@ export function registerRunCommand(program) {
1886
1892
  process.stderr.write(chalk.yellow(`[agents] --session-id ignored: auto picked ${agent} (only claude accepts a forced session id)\n`));
1887
1893
  }
1888
1894
  }
1889
- else if (isValidAgent(rawAgent)) {
1890
- agent = rawAgent;
1891
- }
1892
1895
  else if (profileExists(rawAgent)) {
1893
- // Not a known agent id, but a profile by this name exists. Profiles
1894
- // bind (host agent, version, env overrides, keychain-backed auth)
1895
- // so Chinese models (Kimi, DeepSeek, Qwen, GLM) can run inside
1896
- // Claude Code without a local proxy.
1896
+ // A profile by this exact name exists. Profiles bind (host agent,
1897
+ // version, env overrides, keychain-backed auth) so Chinese models
1898
+ // (Kimi, DeepSeek, Qwen, GLM) can run inside Claude Code without a
1899
+ // local proxy, including when the profile name matches a native id.
1897
1900
  try {
1898
1901
  const resolved = resolveProfileForRun(rawAgent, options.model);
1899
1902
  agent = resolved.agent;
@@ -1925,6 +1928,9 @@ export function registerRunCommand(program) {
1925
1928
  process.exit(1);
1926
1929
  }
1927
1930
  }
1931
+ else if (isValidAgent(rawAgent)) {
1932
+ agent = rawAgent;
1933
+ }
1928
1934
  else if (resolveWorkflowRef(rawAgent, cwd)) {
1929
1935
  // Workflow: explicit directory, project .agents/workflows/<name>, user, system, or extra repo.
1930
1936
  // Resolution follows resource precedence: direct path, then project > user > system > extras.
@@ -2148,6 +2154,23 @@ export function registerRunCommand(program) {
2148
2154
  }
2149
2155
  }
2150
2156
  version = resolveVersionAlias(agent, version);
2157
+ if (options.account) {
2158
+ if (options.cloud || options.provider || options.lease) {
2159
+ console.error(chalk.red('--account selects a local installed identity and cannot be combined with cloud or lease placement.'));
2160
+ process.exit(1);
2161
+ }
2162
+ const { resolveAccountLabel } = await import('../lib/account-labels.js');
2163
+ try {
2164
+ const selected = await resolveAccountLabel(agent, options.account);
2165
+ if (version && version !== selected)
2166
+ throw new Error(`${agent}@${version} is not a healthy match for account '${options.account}'.`);
2167
+ version ??= selected;
2168
+ }
2169
+ catch (err) {
2170
+ console.error(chalk.red(err.message));
2171
+ process.exit(1);
2172
+ }
2173
+ }
2151
2174
  // --resume: resolve a prior conversation and rewrite the run target to
2152
2175
  // continue it. `version` here is already the alias-resolved candidate-version
2153
2176
  // FILTER (undefined for default/any, concrete for @latest/@oldest/@x.y.z);
@@ -2320,7 +2343,7 @@ export function registerRunCommand(program) {
2320
2343
  // the bare primary still resolves through the strategy — otherwise every
2321
2344
  // `agents run claude --fallback codex` run lands on the pinned default
2322
2345
  // account and account rotation silently stops (the gh-monitor heal bug).
2323
- if (!accountPickerRequested && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
2346
+ if (!accountPickerRequested && !options.account && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
2324
2347
  if (version) {
2325
2348
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
2326
2349
  }
@@ -2434,6 +2457,29 @@ export function registerRunCommand(program) {
2434
2457
  version = healed;
2435
2458
  }
2436
2459
  }
2460
+ // The harness may simply not be on this machine. The self-heal above only
2461
+ // runs when a managed version resolved, so with nothing installed we used
2462
+ // to fall through and spawn the bare `cliCommand`, which dies as
2463
+ // `exec: cursor-agent: not found` (exit 127) after a misleading
2464
+ // "looks logged out" banner (RUSH-2339). Probe the executable
2465
+ // buildExecCommand will actually spawn and fail loud instead.
2466
+ //
2467
+ // This is an EXISTENCE probe, not "does agents-cli manage a version". A
2468
+ // harness the user installed themselves (Homebrew, a vendor `curl | sh`, a
2469
+ // distro package) has no version home and MUST still launch — the PATH
2470
+ // branch of resolveLaunchBinary is what keeps that working.
2471
+ {
2472
+ const { resolveLaunchBinary } = await import('../lib/exec.js');
2473
+ // `version` already carries the self-heal's resolution above (it assigns
2474
+ // `version = healed` whenever a version resolved at all), so re-deriving
2475
+ // it with resolveVersion here would be dead.
2476
+ if (!resolveLaunchBinary(agent, version)) {
2477
+ const target = version ? `${agent}@${version}` : agent;
2478
+ console.error(chalk.red(`agents: ${target} is not installed on this machine.`));
2479
+ console.error(chalk.yellow(`Install it with: agents add ${target}`));
2480
+ process.exit(1);
2481
+ }
2482
+ }
2437
2483
  const defaultVersion = version ?? resolveVersion(agent, cwd);
2438
2484
  // Re-apply the active rules preset before every launch (issue: preset
2439
2485
  // changes via `setActiveRulesPreset` only took effect after an explicit
@@ -42,7 +42,7 @@ export interface FocusOptions {
42
42
  since?: string;
43
43
  until?: string;
44
44
  limit?: string;
45
- favorites?: boolean;
45
+ bookmarks?: boolean;
46
46
  unmanaged?: boolean;
47
47
  sort?: string;
48
48
  working?: boolean;
@@ -74,6 +74,9 @@ export declare function selectFallback(attachOnly: boolean | undefined): Unreach
74
74
  export declare function focusAction(id: string | undefined, opts: FocusOptions): Promise<void>;
75
75
  /** A retained pane is not attachable merely because tmux can still display it. */
76
76
  export declare function isAttachableLiveSession(session: ActiveSession): boolean;
77
+ /** Focus one row selected by the shared session browser through the same
78
+ * attach/recover decision as `agents sessions focus <id>`. */
79
+ export declare function focusSelectedSession(meta: SessionMeta, active: ActiveSession | undefined, self: string): Promise<void>;
77
80
  /**
78
81
  * How a single selected live session opens in its new tab.
79
82
  * - `attach` — a live tmux pane joined in the tab (a second client, no fork),
@@ -32,7 +32,7 @@ const INHERITED_FOCUS_OPTIONS = [
32
32
  'local', 'host', 'device', 'active', 'agent',
33
33
  'claude', 'codex', 'kimi', 'antigravity', 'grok', 'opencode',
34
34
  'all', 'teams', 'inTeam', 'routine', 'project', 'skill', 'plugin',
35
- 'since', 'until', 'limit', 'favorites', 'unmanaged', 'sort',
35
+ 'since', 'until', 'limit', 'bookmarks', 'unmanaged', 'sort',
36
36
  'working', 'idle', 'waiting', 'orphan', 'orphaned', 'crashed',
37
37
  'closed', 'abandoned', 'queued', 'unknown',
38
38
  ];
@@ -93,7 +93,7 @@ export function registerFocusCommand(program) {
93
93
  .option('--since <time>', 'Only sessions newer than this (for example 7d)')
94
94
  .option('--until <time>', 'Only sessions older than this timestamp')
95
95
  .option('-n, --limit <n>', 'Maximum candidates to load', '500')
96
- .option('--favorites', 'Only favorited sessions')
96
+ .option('--bookmarks', 'Only bookmarked sessions')
97
97
  .option('--unmanaged', 'Also include native-home sessions outside managed versions')
98
98
  .option('--sort <field>', 'Order candidates by recent, cost, or duration', 'recent')
99
99
  .option('--working', 'Only live sessions currently doing work')
@@ -184,7 +184,7 @@ export async function focusAction(id, opts) {
184
184
  agent: agentSelector ?? focusOptionAgent(opts),
185
185
  teams: opts.teams === true,
186
186
  team: opts.inTeam,
187
- favorites: opts.favorites === true,
187
+ bookmarks: opts.bookmarks === true,
188
188
  projectScope: opts.all || hosts.length > 0 || !!opts.project || idLookup ? 'all' : 'repo',
189
189
  project: opts.project,
190
190
  window: opts.since ?? (opts.all || idLookup ? undefined : '30d'),
@@ -304,7 +304,7 @@ function looksLikeIdentitySelector(selector) {
304
304
  function hasFocusFilters(opts, statuses) {
305
305
  return statuses.length > 0 || !!(opts.active || opts.local || opts.host?.length || opts.device?.length || focusOptionAgent(opts) ||
306
306
  opts.all || opts.teams || opts.inTeam || opts.routine || opts.project || opts.skill || opts.plugin ||
307
- opts.since || opts.until || opts.favorites || opts.unmanaged || (opts.sort && opts.sort !== 'recent'));
307
+ opts.since || opts.until || opts.bookmarks || opts.unmanaged || (opts.sort && opts.sort !== 'recent'));
308
308
  }
309
309
  function focusSort(value) {
310
310
  if (!value || value === 'recent')
@@ -370,6 +370,21 @@ async function focusResolvedSession(meta, liveById, self, fallback, attachOnly)
370
370
  }
371
371
  await resumeSessionInPlace(meta);
372
372
  }
373
+ /** Focus one row selected by the shared session browser through the same
374
+ * attach/recover decision as `agents sessions focus <id>`. */
375
+ export async function focusSelectedSession(meta, active, self) {
376
+ if (active && !active.sessionId) {
377
+ if (isAttachableLiveSession(active)) {
378
+ await jumpTo(active, self, resumeInNewTab);
379
+ return;
380
+ }
381
+ console.log(chalk.yellow('This live session has no session id or living attach rail to focus.'));
382
+ process.exitCode = 1;
383
+ return;
384
+ }
385
+ const liveById = active ? new Map([[meta.id, active]]) : new Map();
386
+ await focusResolvedSession(meta, liveById, self, resumeInNewTab, false);
387
+ }
373
388
  function activeFromMeta(meta) {
374
389
  return {
375
390
  context: 'headless',
@@ -11,7 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as yaml from 'yaml';
13
13
  import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
14
- import { assertSchedulerEnabled } from '../lib/device-config.js';
14
+ import { assertSchedulerEnabled, assertDaemonEnabled, isDaemonEnabled } from '../lib/device-config.js';
15
15
  import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
16
16
  import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
17
17
  import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroup, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, } from '../lib/routines.js';
@@ -344,6 +344,7 @@ function parseRoutineTrigger(options) {
344
344
  function ensureSchedulerRunning(opts = {}) {
345
345
  const log = opts.stderr ? console.error : console.log;
346
346
  try {
347
+ assertDaemonEnabled();
347
348
  assertSchedulerEnabled();
348
349
  }
349
350
  catch (err) {
@@ -1282,11 +1283,18 @@ export function registerRoutinesCommands(program) {
1282
1283
  console.log(` ${chalk.cyan(job.name)} — missed ${chalk.gray(job.expectedAt.toLocaleString())}, last ran ${chalk.gray(last)}`);
1283
1284
  }
1284
1285
  // Need the daemon alive so spawned jobs are monitored and meta.json is
1285
- // finalized. Start it if it isn't already running.
1286
+ // finalized. Start it if it isn't already running — unless daemon.enabled
1287
+ // is off, in which case this auto-start is skipped with a stated reason.
1286
1288
  if (!options.dryRun && !isDaemonRunning()) {
1287
- const started = startDaemon();
1288
- if (started.pid) {
1289
- console.log(chalk.gray(`\nStarted scheduler (PID: ${started.pid}) so catchup runs are monitored.`));
1289
+ try {
1290
+ assertDaemonEnabled();
1291
+ const started = startDaemon();
1292
+ if (started.pid) {
1293
+ console.log(chalk.gray(`\nStarted scheduler (PID: ${started.pid}) so catchup runs are monitored.`));
1294
+ }
1295
+ }
1296
+ catch (err) {
1297
+ console.log(chalk.yellow(`\n${err.message}`));
1290
1298
  }
1291
1299
  }
1292
1300
  console.log(chalk.bold(options.dryRun ? '\nRecording misses...' : '\nTriggering catchup runs...'));
@@ -1372,11 +1380,18 @@ export function registerRoutinesCommands(program) {
1372
1380
  return;
1373
1381
  }
1374
1382
  // Fired jobs run detached via executeJobDetached (the same path cron
1375
- // uses). Keep the daemon alive so each run's meta.json is finalized.
1383
+ // uses). Keep the daemon alive so each run's meta.json is finalized
1384
+ // unless daemon.enabled is off, in which case this auto-start is
1385
+ // skipped with a stated reason.
1376
1386
  if (!isDaemonRunning()) {
1377
- const started = startDaemon();
1378
- if (started.pid) {
1379
- console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
1387
+ if (!isDaemonEnabled()) {
1388
+ console.log(chalk.yellow(`Daemon is disabled (daemon.enabled=false) — run(s) below fire but are not monitored. Re-enable with: agents daemon enable`));
1389
+ }
1390
+ else {
1391
+ const started = startDaemon();
1392
+ if (started.pid) {
1393
+ console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
1394
+ }
1380
1395
  }
1381
1396
  }
1382
1397
  const fired = await fireWebhookJobs(webhook);
@@ -1649,8 +1664,11 @@ export function registerRoutinesCommands(program) {
1649
1664
  .description('Start the background scheduler. Usually unnecessary — it auto-starts when you add your first routine.')
1650
1665
  .action(() => {
1651
1666
  try {
1652
- // A manual start on a scheduler-disabled device refuses with the same
1653
- // message the auto-start surfaces give.
1667
+ // A manual start on a disabled device refuses with the same message the
1668
+ // auto-start surfaces give — `agents routines start` is a convenience
1669
+ // wrapper around the daemon, not the deliberate override. Use
1670
+ // `agents daemon start` to bypass daemon.enabled explicitly.
1671
+ assertDaemonEnabled();
1654
1672
  assertSchedulerEnabled();
1655
1673
  }
1656
1674
  catch (err) {
@@ -12,6 +12,7 @@ import { bundleEnvToDotenv } from '../lib/secrets/push.js';
12
12
  export { bundleEnvToDotenv };
13
13
  import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
14
14
  import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
15
+ import { type SecretLease } from '../lib/secrets/lease.js';
15
16
  import { type BundleUsageSummary } from '../lib/secrets/usage-db.js';
16
17
  /** Read all available data from stdin synchronously, trimmed. */
17
18
  /**
@@ -51,6 +52,25 @@ export declare function parseImportSource(opts: {
51
52
  * without a live SSH session.
52
53
  */
53
54
  export declare function resolveUnlockTtlMs(ttl: string | undefined, until: string | undefined, now?: number): number;
55
+ /**
56
+ * Decide what an `unlock` holds: the whole bundle env, or — with --keys — only
57
+ * the resolved subset behind a lease that scopes the broker entry (agent.ts
58
+ * re-selects on load) and its own expiry. Fails closed: createSecretLease throws
59
+ * on an unknown or empty key subset. The unlock action's single scoping seam,
60
+ * exported so the whole-bundle vs scoped-subset decision is unit-testable without
61
+ * a live broker.
62
+ */
63
+ export declare function scopeHeldEnv(input: {
64
+ bundle: string;
65
+ env: Record<string, string>;
66
+ keys: string | null;
67
+ ttlMs: number;
68
+ harness: string;
69
+ sleepPersist: boolean;
70
+ }): {
71
+ heldEnv: Record<string, string>;
72
+ lease?: SecretLease;
73
+ };
54
74
  export declare function buildRemoteUnlockArgs(names: string[], opts: {
55
75
  all?: boolean;
56
76
  ttl?: string;
@@ -149,6 +169,23 @@ export declare function renderViewStatusLine(b: SecretsBundle, heldExpiresAt: nu
149
169
  * and a hard abort at inject time, i.e. after it had already broken something.
150
170
  */
151
171
  export declare function renderExpiringCol(b: SecretsBundle, now?: number): string;
172
+ /**
173
+ * Resolve an existing import target bundle (inheriting its backend) or create a
174
+ * new one with the requested backend. Refuses to silently downgrade a
175
+ * keychain-backed bundle to `file` — shared by every `import` source so the
176
+ * guard can't drift between them.
177
+ *
178
+ * `force` additionally recreates a bundle whose METADATA RECORD is present but
179
+ * undecryptable — a file store whose key was lost or rotated out from under it.
180
+ * That is precisely the state provisioning exists to repair (import is how a box
181
+ * gets its bundles back), and without this the import dies on `readBundle` and
182
+ * the only route left is deleting the record by hand on an already-degraded
183
+ * store. It is gated on `--force` on purpose: recreating unconditionally would
184
+ * destroy a perfectly healthy bundle for someone who merely forgot to set
185
+ * `AGENTS_SECRETS_PASSPHRASE`, which is the hazard `readBundleIfDecryptable`
186
+ * exists to describe. `--force` already means "overwrite what is there".
187
+ */
188
+ export declare function resolveImportBundle(name: string, backendOpt: string | undefined, synced?: boolean, force?: boolean): SecretsBundle;
152
189
  /** Register the `agents secrets` command tree. */
153
190
  export declare function registerSecretsCommands(program: Command): void;
154
191
  /** Validate a prompt-policy value, throwing a clear message on a bad one (the