@phnx-labs/agents-cli 1.20.26 → 1.20.28

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 (95) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/commands/doctor.d.ts +5 -2
  3. package/dist/commands/doctor.js +182 -30
  4. package/dist/commands/exec.d.ts +1 -1
  5. package/dist/commands/exec.js +177 -6
  6. package/dist/commands/hosts.d.ts +11 -0
  7. package/dist/commands/hosts.js +229 -0
  8. package/dist/commands/repo.d.ts +29 -0
  9. package/dist/commands/repo.js +174 -38
  10. package/dist/commands/secrets.d.ts +2 -7
  11. package/dist/commands/secrets.js +15 -23
  12. package/dist/commands/sessions.d.ts +2 -0
  13. package/dist/commands/sessions.js +27 -25
  14. package/dist/commands/sync.d.ts +2 -0
  15. package/dist/commands/sync.js +22 -5
  16. package/dist/commands/view.js +27 -11
  17. package/dist/index.js +4 -13
  18. package/dist/lib/agent-spec.d.ts +36 -0
  19. package/dist/lib/agent-spec.js +157 -0
  20. package/dist/lib/agents.d.ts +1 -0
  21. package/dist/lib/agents.js +45 -4
  22. package/dist/lib/browser/drivers/ssh.d.ts +47 -2
  23. package/dist/lib/browser/drivers/ssh.js +113 -24
  24. package/dist/lib/browser/profiles.js +28 -1
  25. package/dist/lib/browser/runtime-state.js +28 -8
  26. package/dist/lib/browser/types.d.ts +10 -1
  27. package/dist/lib/cli-resources.js +10 -1
  28. package/dist/lib/daemon.js +32 -0
  29. package/dist/lib/doctor-diff.d.ts +19 -0
  30. package/dist/lib/doctor-diff.js +107 -15
  31. package/dist/lib/exec.d.ts +27 -0
  32. package/dist/lib/exec.js +62 -19
  33. package/dist/lib/heal.d.ts +107 -0
  34. package/dist/lib/heal.js +279 -0
  35. package/dist/lib/hooks.d.ts +17 -0
  36. package/dist/lib/hooks.js +127 -3
  37. package/dist/lib/hosts/dispatch.d.ts +26 -0
  38. package/dist/lib/hosts/dispatch.js +71 -0
  39. package/dist/lib/hosts/progress.d.ts +21 -0
  40. package/dist/lib/hosts/progress.js +49 -0
  41. package/dist/lib/hosts/providers/local.d.ts +17 -0
  42. package/dist/lib/hosts/providers/local.js +81 -0
  43. package/dist/lib/hosts/ready.d.ts +37 -0
  44. package/dist/lib/hosts/ready.js +88 -0
  45. package/dist/lib/hosts/registry.d.ts +22 -0
  46. package/dist/lib/hosts/registry.js +65 -0
  47. package/dist/lib/hosts/ssh-config.d.ts +37 -0
  48. package/dist/lib/hosts/ssh-config.js +157 -0
  49. package/dist/lib/hosts/tasks.d.ts +32 -0
  50. package/dist/lib/hosts/tasks.js +58 -0
  51. package/dist/lib/hosts/types.d.ts +51 -0
  52. package/dist/lib/hosts/types.js +21 -0
  53. package/dist/lib/loop.d.ts +9 -0
  54. package/dist/lib/loop.js +13 -1
  55. package/dist/lib/mcp.js +12 -3
  56. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  57. package/dist/lib/migrate.js +9 -5
  58. package/dist/lib/platform/exec.d.ts +10 -0
  59. package/dist/lib/platform/exec.js +17 -0
  60. package/dist/lib/platform/index.d.ts +1 -0
  61. package/dist/lib/platform/index.js +1 -0
  62. package/dist/lib/platform/links.d.ts +15 -0
  63. package/dist/lib/platform/links.js +42 -0
  64. package/dist/lib/platform/paths.d.ts +18 -0
  65. package/dist/lib/platform/paths.js +22 -0
  66. package/dist/lib/platform/posixpath.d.ts +28 -0
  67. package/dist/lib/platform/posixpath.js +153 -0
  68. package/dist/lib/plugin-marketplace.d.ts +18 -0
  69. package/dist/lib/plugin-marketplace.js +67 -1
  70. package/dist/lib/plugins.d.ts +33 -1
  71. package/dist/lib/plugins.js +56 -11
  72. package/dist/lib/project-launch.js +6 -3
  73. package/dist/lib/sandbox.js +5 -2
  74. package/dist/lib/self-update.js +7 -2
  75. package/dist/lib/session/db.d.ts +23 -0
  76. package/dist/lib/session/db.js +76 -1
  77. package/dist/lib/session/discover.d.ts +26 -0
  78. package/dist/lib/session/discover.js +75 -4
  79. package/dist/lib/session/relative-time.d.ts +7 -0
  80. package/dist/lib/session/relative-time.js +28 -0
  81. package/dist/lib/session/remote.d.ts +61 -0
  82. package/dist/lib/session/remote.js +221 -0
  83. package/dist/lib/ssh-exec.d.ts +45 -0
  84. package/dist/lib/ssh-exec.js +61 -0
  85. package/dist/lib/staleness/detectors/commands.js +7 -6
  86. package/dist/lib/staleness/writers/commands.js +7 -12
  87. package/dist/lib/startup/command-registry.d.ts +1 -0
  88. package/dist/lib/startup/command-registry.js +2 -0
  89. package/dist/lib/startup/dev-build.d.ts +22 -0
  90. package/dist/lib/startup/dev-build.js +41 -0
  91. package/dist/lib/types.d.ts +28 -0
  92. package/dist/lib/versions.d.ts +9 -3
  93. package/dist/lib/versions.js +43 -7
  94. package/package.json +3 -3
  95. package/scripts/postinstall.js +62 -0
@@ -23,7 +23,9 @@ import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import { AGENTS, agentConfigDirName } from './agents.js';
25
25
  import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getEffectivePromptcutsPath, } from './state.js';
26
- import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, } from './versions.js';
26
+ import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, compareVersions, } from './versions.js';
27
+ import { discoverPlugins, marketplaceSpecForName } from './plugins.js';
28
+ import { pluginInstallDir, repairableManifestFields } from './plugin-marketplace.js';
27
29
  import { markdownToToml } from './convert.js';
28
30
  import { resolveImports, supportsRulesImports } from './rules/compile.js';
29
31
  import { listCommandsInVersionHome, getVersionCommandsDir } from './commands.js';
@@ -80,13 +82,13 @@ function buildLayerBases(cwd, kind, opts = {}) {
80
82
  return out;
81
83
  }
82
84
  // ─── commands ─────────────────────────────────────────────────────────────────
83
- function diffCommands(agent, version, cwd) {
85
+ function diffCommands(agent, version, cwd, excludeProject = false) {
84
86
  const agentConfig = AGENTS[agent];
85
87
  const isToml = agentConfig.format === 'toml';
86
88
  const ext = isToml ? '.toml' : '.md';
87
89
  const homeDir = getVersionCommandsDir(agent, version);
88
90
  const installed = new Set(listCommandsInVersionHome(agent, version));
89
- const layerBases = buildLayerBases(cwd, 'commands');
91
+ const layerBases = buildLayerBases(cwd, 'commands', { excludeProject });
90
92
  const sourceByName = new Map();
91
93
  for (const base of layerBases) {
92
94
  if (!fs.existsSync(base.path))
@@ -190,10 +192,10 @@ function dirsContentMatch(src, dst) {
190
192
  }
191
193
  return true;
192
194
  }
193
- function diffSkills(agent, version, cwd) {
195
+ function diffSkills(agent, version, cwd, excludeProject = false) {
194
196
  const homeDir = getVersionSkillsDir(agent, version);
195
197
  const installed = new Set(listSkillsInVersionHome(agent, version));
196
- const layerBases = buildLayerBases(cwd, 'skills');
198
+ const layerBases = buildLayerBases(cwd, 'skills', { excludeProject });
197
199
  const sourceByName = new Map();
198
200
  for (const base of layerBases) {
199
201
  if (!fs.existsSync(base.path))
@@ -298,8 +300,8 @@ function diffHooks(agent, version, cwd) {
298
300
  return rows.sort((a, b) => a.name.localeCompare(b.name));
299
301
  }
300
302
  // ─── rules / memory ───────────────────────────────────────────────────────────
301
- function listRulesNames(cwd) {
302
- const projectDir = getProjectAgentsDir(cwd);
303
+ function listRulesNames(cwd, excludeProject = false) {
304
+ const projectDir = excludeProject ? null : getProjectAgentsDir(cwd);
303
305
  const userRules = getUserRulesDir();
304
306
  const systemRules = getResolvedRulesDir();
305
307
  const extras = getEnabledExtraRepos();
@@ -349,11 +351,11 @@ function expectedRuleContent(agent, name, sourcePath) {
349
351
  }
350
352
  return readSafe(sourcePath);
351
353
  }
352
- function diffRules(agent, version, cwd) {
354
+ function diffRules(agent, version, cwd, excludeProject = false) {
353
355
  const agentConfig = AGENTS[agent];
354
356
  const versionHome = getVersionHomePath(agent, version);
355
357
  const configDir = path.join(versionHome, agentConfigDirName(agent));
356
- const sourcesByName = listRulesNames(cwd);
358
+ const sourcesByName = listRulesNames(cwd, excludeProject);
357
359
  // Files actually present in the version home.
358
360
  const homeFiles = new Set();
359
361
  if (fs.existsSync(configDir)) {
@@ -425,6 +427,91 @@ function diffPresenceOnly(kind, available, synced) {
425
427
  }
426
428
  return rows.sort((a, b) => a.name.localeCompare(b.name));
427
429
  }
430
+ // ─── plugins (content-aware) ───────────────────────────────────────────────
431
+ function listPluginSkillDirs(pluginDir) {
432
+ const d = path.join(pluginDir, 'skills');
433
+ try {
434
+ return fs.readdirSync(d, { withFileTypes: true })
435
+ .filter((e) => (e.isDirectory() || e.isSymbolicLink()) && fs.existsSync(path.join(d, e.name, 'SKILL.md')))
436
+ .map((e) => e.name);
437
+ }
438
+ catch {
439
+ return [];
440
+ }
441
+ }
442
+ function listPluginCommandFiles(pluginDir) {
443
+ const d = path.join(pluginDir, 'commands');
444
+ try {
445
+ return fs.readdirSync(d).filter((f) => f.endsWith('.md')).map((f) => f.replace(/\.md$/, ''));
446
+ }
447
+ catch {
448
+ return [];
449
+ }
450
+ }
451
+ /**
452
+ * Describe how a version's marketplace MIRROR of a plugin diverges from its
453
+ * central source — the detail presence-only checks miss. Surfaces a stale mirror
454
+ * version, a Claude-invalid manifest, and (the part users care about) the
455
+ * plugin's own skills/commands that never made it into the mirror. Returns null
456
+ * when the mirror faithfully matches source.
457
+ */
458
+ export function describePluginDrift(central, mirrorDir) {
459
+ if (!fs.existsSync(mirrorDir))
460
+ return 'mirror missing';
461
+ const parts = [];
462
+ let mManifest = null;
463
+ try {
464
+ mManifest = JSON.parse(fs.readFileSync(path.join(mirrorDir, '.claude-plugin', 'plugin.json'), 'utf-8'));
465
+ }
466
+ catch {
467
+ mManifest = null;
468
+ }
469
+ const mVer = mManifest && typeof mManifest.version === 'string' ? mManifest.version : undefined;
470
+ const cVer = central.manifest.version;
471
+ if (mVer && cVer && compareVersions(cVer, mVer) > 0)
472
+ parts.push(`${mVer}→${cVer}`);
473
+ if (mManifest && repairableManifestFields(mManifest).length > 0)
474
+ parts.push('invalid manifest');
475
+ const mirrorSkills = new Set(listPluginSkillDirs(mirrorDir));
476
+ const missSkills = listPluginSkillDirs(central.root).filter((s) => !mirrorSkills.has(s)).sort();
477
+ const mirrorCmds = new Set(listPluginCommandFiles(mirrorDir));
478
+ const missCmds = listPluginCommandFiles(central.root).filter((c) => !mirrorCmds.has(c)).sort();
479
+ if (missSkills.length)
480
+ parts.push(`missing skill${missSkills.length > 1 ? 's' : ''}: ${missSkills.join(', ')}`);
481
+ if (missCmds.length)
482
+ parts.push(`missing command${missCmds.length > 1 ? 's' : ''}: ${missCmds.join(', ')}`);
483
+ return parts.length ? parts.join(', ') : null;
484
+ }
485
+ function diffPlugins(agent, version, cwd) {
486
+ const versionHome = getVersionHomePath(agent, version);
487
+ const synced = new Set(getActuallySyncedResources(agent, version, { cwd }).plugins);
488
+ const rows = [];
489
+ const seen = new Set();
490
+ for (const p of discoverPlugins({ cwd })) {
491
+ if (seen.has(p.name))
492
+ continue; // dedupe across marketplaces for the readout
493
+ seen.add(p.name);
494
+ if (!synced.has(p.name)) {
495
+ rows.push({ kind: 'plugins', name: p.name, status: 'missing', sourcePath: p.root });
496
+ continue;
497
+ }
498
+ const mirror = pluginInstallDir(p, marketplaceSpecForName(p.marketplace), agent, versionHome);
499
+ const detail = describePluginDrift(p, mirror);
500
+ rows.push({
501
+ kind: 'plugins',
502
+ name: p.name,
503
+ status: detail ? 'diff' : 'ok',
504
+ sourcePath: p.root,
505
+ homePath: mirror,
506
+ ...(detail ? { detail } : {}),
507
+ });
508
+ }
509
+ for (const name of synced) {
510
+ if (!seen.has(name))
511
+ rows.push({ kind: 'plugins', name, status: 'extra' });
512
+ }
513
+ return rows.sort((a, b) => a.name.localeCompare(b.name));
514
+ }
428
515
  function diffPromptcuts() {
429
516
  const sourcePath = getEffectivePromptcutsPath();
430
517
  if (!fs.existsSync(sourcePath))
@@ -432,9 +519,14 @@ function diffPromptcuts() {
432
519
  return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath }];
433
520
  }
434
521
  export function diffVersionResources(agent, version, options = {}) {
435
- const cwd = options.cwd ?? process.cwd();
522
+ const rawCwd = options.cwd ?? process.cwd();
523
+ const excludeProject = options.excludeProject ?? false;
436
524
  const home = getVersionHomePath(agent, version);
437
525
  const requested = new Set(options.kinds ?? ALL_KINDS);
526
+ // When excluding the project layer, resolve every per-cwd lookup against a
527
+ // neutral cwd so no `<cwd>/.agents/` is ever discovered.
528
+ const cwd = rawCwd;
529
+ const projectDir = excludeProject ? null : getProjectAgentsDir(cwd);
438
530
  const available = getAvailableResources(cwd);
439
531
  const synced = getActuallySyncedResources(agent, version, { cwd });
440
532
  const empty = {
@@ -449,13 +541,13 @@ export function diffVersionResources(agent, version, options = {}) {
449
541
  promptcuts: [],
450
542
  };
451
543
  if (requested.has('commands'))
452
- empty.commands = diffCommands(agent, version, cwd);
544
+ empty.commands = diffCommands(agent, version, cwd, excludeProject);
453
545
  if (requested.has('skills'))
454
- empty.skills = diffSkills(agent, version, cwd);
546
+ empty.skills = diffSkills(agent, version, cwd, excludeProject);
455
547
  if (requested.has('hooks'))
456
548
  empty.hooks = diffHooks(agent, version, cwd);
457
549
  if (requested.has('rules'))
458
- empty.rules = diffRules(agent, version, cwd);
550
+ empty.rules = diffRules(agent, version, cwd, excludeProject);
459
551
  if (requested.has('mcp'))
460
552
  empty.mcp = diffPresenceOnly('mcp', available.mcp, synced.mcp);
461
553
  if (requested.has('permissions'))
@@ -463,7 +555,7 @@ export function diffVersionResources(agent, version, options = {}) {
463
555
  if (requested.has('subagents'))
464
556
  empty.subagents = diffPresenceOnly('subagents', available.subagents, synced.subagents);
465
557
  if (requested.has('plugins'))
466
- empty.plugins = diffPresenceOnly('plugins', available.plugins, synced.plugins);
558
+ empty.plugins = diffPlugins(agent, version, cwd);
467
559
  if (requested.has('promptcuts'))
468
560
  empty.promptcuts = diffPromptcuts();
469
561
  let ok = 0, diff = 0, missing = 0, extra = 0;
@@ -485,7 +577,7 @@ export function diffVersionResources(agent, version, options = {}) {
485
577
  home,
486
578
  cwd,
487
579
  layers: {
488
- project: getProjectAgentsDir(cwd),
580
+ project: projectDir,
489
581
  user: getUserAgentsDir(),
490
582
  system: getSystemAgentsDir(),
491
583
  extras: getEnabledExtraRepos().map((e) => ({ alias: e.alias, dir: e.dir })),
@@ -80,6 +80,14 @@ export interface ExecOptions {
80
80
  addDirs?: string[];
81
81
  timeout?: string;
82
82
  sessionId?: string;
83
+ /**
84
+ * Resume the conversation named by `sessionId` using the agent's NATIVE resume
85
+ * form (claude `--resume`, codex `resume`) instead of the default `--session-id`
86
+ * create. Only set for agents where `nativeResume` returns true; other agents
87
+ * resume via a `/continue <id>` first message (Tier 2), which needs no flag and
88
+ * leaves this unset.
89
+ */
90
+ resume?: boolean;
83
91
  verbose?: boolean;
84
92
  env?: Record<string, string>;
85
93
  /**
@@ -150,6 +158,19 @@ export interface AgentCommandTemplate {
150
158
  modelFlag?: string;
151
159
  printFlags?: string[];
152
160
  verboseFlag?: string;
161
+ /**
162
+ * How this agent natively resumes a prior conversation. Presence here is the
163
+ * single source of truth for `nativeResume(agent)` — agents without it fall
164
+ * back to the universal `/continue <id>` replay (Tier 2). Two shapes:
165
+ * { flag } — append `<flag> <id>` (e.g. claude `--resume <id>`)
166
+ * { subcommand } — replace the headless base subcommand with `<subcommand> <id>`
167
+ * (codex: `codex exec` -> `codex exec resume <id>`)
168
+ */
169
+ resume?: {
170
+ flag: string;
171
+ } | {
172
+ subcommand: string;
173
+ };
153
174
  }
154
175
  /**
155
176
  * CLI command templates for every supported agent.
@@ -158,6 +179,12 @@ export interface AgentCommandTemplate {
158
179
  * AGENTS[agent].capabilities.modes. A test in exec.test.ts asserts this.
159
180
  */
160
181
  export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
182
+ /**
183
+ * Whether `agent` has a native resume form (Tier 1). Derived solely from the
184
+ * command template's `resume` field — the single source of truth. Agents that
185
+ * return false resume via the universal Tier-2 `/continue` replay instead.
186
+ */
187
+ export declare function nativeResume(agent: AgentId): boolean;
161
188
  /** Assemble the full CLI argument array for an agent invocation. */
162
189
  export declare function buildExecCommand(options: ExecOptions): string[];
163
190
  /** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
package/dist/lib/exec.js CHANGED
@@ -261,10 +261,12 @@ export const AGENT_COMMANDS = {
261
261
  modelFlag: '--model',
262
262
  printFlags: ['--print'],
263
263
  verboseFlag: '--verbose',
264
+ resume: { flag: '--resume' },
264
265
  },
265
266
  codex: {
266
267
  base: ['codex', 'exec'],
267
268
  promptFlag: 'positional',
269
+ resume: { subcommand: 'resume' },
268
270
  modeFlags: {
269
271
  // NOTE: codex has no read-only mode in --sandbox; 'plan' here means
270
272
  // "workspace-write but no auto-approval" — closer to plan-as-restraint.
@@ -302,6 +304,9 @@ export const AGENT_COMMANDS = {
302
304
  opencode: {
303
305
  base: ['opencode', 'run'],
304
306
  promptFlag: 'positional',
307
+ // opencode's native resume is `opencode --session <id>` (NOT under `run`), so
308
+ // it does not compose with this headless `run` base. Until that's verified on
309
+ // a box with opencode installed, opencode resumes via Tier-2 `/continue`.
305
310
  modeFlags: {
306
311
  plan: ['--agent', 'plan'],
307
312
  edit: ['--agent', 'build'],
@@ -436,6 +441,14 @@ export const AGENT_COMMANDS = {
436
441
  modelFlag: '-m',
437
442
  },
438
443
  };
444
+ /**
445
+ * Whether `agent` has a native resume form (Tier 1). Derived solely from the
446
+ * command template's `resume` field — the single source of truth. Agents that
447
+ * return false resume via the universal Tier-2 `/continue` replay instead.
448
+ */
449
+ export function nativeResume(agent) {
450
+ return AGENT_COMMANDS[agent]?.resume !== undefined;
451
+ }
439
452
  /** Assemble the full CLI argument array for an agent invocation. */
440
453
  export function buildExecCommand(options) {
441
454
  const template = AGENT_COMMANDS[options.agent];
@@ -453,30 +466,36 @@ export function buildExecCommand(options) {
453
466
  cmd.splice(1, 1);
454
467
  }
455
468
  }
469
+ // Native resume with a `{ subcommand }` shape (codex) appends the resume verb
470
+ // to the base: `codex exec` -> `codex exec resume` (headless) and, after the
471
+ // interactive drop above, `codex` -> `codex resume` (TUI). The session id is
472
+ // pushed later as the first positional (before any prompt). `{ flag }` agents
473
+ // (claude) need no base change — the flag is appended with the id below.
474
+ const resumeSpec = options.resume ? template.resume : undefined;
475
+ if (resumeSpec && 'subcommand' in resumeSpec) {
476
+ cmd.push(resumeSpec.subcommand);
477
+ }
456
478
  // Use versioned alias if a specific version was requested (e.g., claude@2.1.98).
457
479
  // Resolve to the absolute path of the shim so spawn doesn't depend on PATH —
458
480
  // on Linux installs where the shims dir isn't on PATH, spawning the bare
459
481
  // versioned name fails with ENOENT even though `agents view` shows the agent.
460
482
  //
461
- // On Windows, shims are bash scripts and cannot be executed by spawn() directly.
462
- // buildExecEnv() already sets the isolation env vars (CLAUDE_CONFIG_DIR, CODEX_HOME,
463
- // etc.) that the bash shim would set, so we can skip the shim entirely and resolve
464
- // straight to the real binary via getBinaryPath.
483
+ // On Windows the shims dir holds a `.cmd` companion next to the bash alias
484
+ // (see createVersionedAlias); prefer it so spawn() can launch it (the bash
485
+ // script is not directly executable by cmd.exe). When no shim exists on disk
486
+ // we fall back to the bare versioned name, which spawnAgent() resolves via
487
+ // PATH (+ PATHEXT/shell on Windows).
465
488
  if (options.version && cmd.length > 0) {
466
- if (process.platform === 'win32') {
467
- const binaryPath = getBinaryPath(options.agent, options.version);
468
- const binaryPathCmd = binaryPath + '.cmd';
469
- if (fs.existsSync(binaryPathCmd)) {
470
- cmd[0] = binaryPathCmd;
471
- }
472
- else if (fs.existsSync(binaryPath)) {
473
- cmd[0] = binaryPath;
474
- }
489
+ const versionedName = `${cmd[0]}@${options.version}`;
490
+ const absPath = path.join(getShimsDir(), versionedName);
491
+ if (process.platform === 'win32' && fs.existsSync(absPath + '.cmd')) {
492
+ cmd[0] = absPath + '.cmd';
493
+ }
494
+ else if (fs.existsSync(absPath)) {
495
+ cmd[0] = absPath;
475
496
  }
476
497
  else {
477
- const versionedName = `${cmd[0]}@${options.version}`;
478
- const absPath = path.join(getShimsDir(), versionedName);
479
- cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
498
+ cmd[0] = versionedName;
480
499
  }
481
500
  }
482
501
  // Add reasoning effort flags (before mode flags for codex -c positioning)
@@ -505,15 +524,39 @@ export function buildExecCommand(options) {
505
524
  // AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
506
525
  throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
507
526
  }
508
- cmd.push(...modeFlags);
527
+ if (resumeSpec && 'subcommand' in resumeSpec) {
528
+ // codex `exec resume` / `resume` does NOT accept `--sandbox <mode>` (only the
529
+ // bypass flag, verified against `codex exec resume --help`). So the standard
530
+ // codex modeFlags can't be reused on resume. A non-plan HEADLESS resume needs
531
+ // the bypass or it stalls on approval prompts; plan and interactive resume
532
+ // inherit codex's default sandbox and pass no flag.
533
+ if (!interactive && resolvedMode !== 'plan') {
534
+ cmd.push('--dangerously-bypass-approvals-and-sandbox');
535
+ }
536
+ }
537
+ else {
538
+ cmd.push(...modeFlags);
539
+ }
509
540
  // Add print/headless flags only when a prompt is provided. Without a prompt
510
541
  // the caller wants an interactive REPL -- passing --print would immediately
511
542
  // wait on stdin and never render the TUI.
512
543
  if (!interactive && options.headless && template.printFlags) {
513
544
  cmd.push(...template.printFlags);
514
545
  }
515
- // Add session ID (Claude only)
516
- if (options.sessionId && options.agent === 'claude') {
546
+ // Resume vs. create. With `resume`, emit the agent's NATIVE resume reference:
547
+ // `{ flag }` agents append `<flag> <id>` (claude `--resume <id>`); `{ subcommand }`
548
+ // agents (codex) already pushed the verb above, so the id is the first
549
+ // positional here — placed before the prompt positional appended later. Without
550
+ // `resume`, the legacy claude-only `--session-id` CREATES a session with that id.
551
+ if (options.resume && options.sessionId && resumeSpec) {
552
+ if ('flag' in resumeSpec) {
553
+ cmd.push(resumeSpec.flag, options.sessionId);
554
+ }
555
+ else {
556
+ cmd.push(options.sessionId);
557
+ }
558
+ }
559
+ else if (options.sessionId && options.agent === 'claude') {
517
560
  cmd.push('--session-id', options.sessionId);
518
561
  }
519
562
  // Add model (only if explicitly provided by user)
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Resource heal engine — close the gap between what DotAgents repos DEFINE and
3
+ * what is actually present/valid in each installed agent home.
4
+ *
5
+ * Powers two callers:
6
+ * - `agents doctor --fix` — explicit, operator-driven. Mode 'full': fills
7
+ * missing, overwrites drifted content, and refreshes stale plugins even when
8
+ * the baseline is unknown (the operator asked for it).
9
+ * - the routines daemon's periodic safety check — Mode 'safe': fixes only the
10
+ * unambiguous gaps (missing resources, Claude-invalid plugin manifests, and
11
+ * provably-unmodified stale plugins). Drift and risky refreshes are reported,
12
+ * never clobbered.
13
+ *
14
+ * Built on the LIVE-home diff (`diffVersionResources`) — NOT the staleness
15
+ * manifest. `isStale()` only compares the last-synced manifest against the
16
+ * sources, so home-side rot (a deleted, corrupted, or Claude-rejected file in a
17
+ * version home whose source never changed) is invisible to it and to the sync
18
+ * fast-guard. The diff reads the actual home, so heal catches exactly that class
19
+ * of drift — the kind that silently broke the `code` plugin on a non-default
20
+ * Claude version.
21
+ *
22
+ * Heal FILLS and FIXES; it never deletes. Orphan/extra removal stays the job of
23
+ * `agents prune cleanup`, so a heal pass can never lose work.
24
+ */
25
+ import type { AgentId } from './types.js';
26
+ import { type DoctorKind, type DiffStatus } from './doctor-diff.js';
27
+ export interface HealedResource {
28
+ kind: DoctorKind;
29
+ name: string;
30
+ /** Why it was healed: 'missing' (filled) or 'diff' (overwritten / re-pushed). */
31
+ was: DiffStatus;
32
+ }
33
+ export interface SkippedResource {
34
+ kind: DoctorKind;
35
+ name: string;
36
+ /** 'drift': hand-edited content left untouched in 'safe' mode.
37
+ * 'unreconcilable': heal wrote it but the diff still flags it — a source/home
38
+ * asymmetry the writer can't satisfy (e.g. a hook sidecar the installer omits),
39
+ * surfaced honestly instead of "fixed" on every pass. */
40
+ reason: 'drift' | 'unreconcilable';
41
+ }
42
+ export interface VersionHealResult {
43
+ agent: AgentId;
44
+ version: string;
45
+ healed: HealedResource[];
46
+ skipped: SkippedResource[];
47
+ }
48
+ export interface ManifestRepairResult {
49
+ plugin: string;
50
+ /** Bare-name fields stripped from the source plugin.json (e.g. ["skills"]). */
51
+ droppedFields: string[];
52
+ }
53
+ export interface PluginRefreshResult {
54
+ plugin: string;
55
+ from: string;
56
+ to: string;
57
+ }
58
+ export interface PluginRefreshSkip {
59
+ plugin: string;
60
+ from: string;
61
+ upstream: string;
62
+ /** 'modified': central diverged from baseline. 'no-baseline': pre-tracking install. */
63
+ reason: 'modified' | 'no-baseline';
64
+ }
65
+ export interface HealResult {
66
+ versions: VersionHealResult[];
67
+ repairedManifests: ManifestRepairResult[];
68
+ refreshedPlugins: PluginRefreshResult[];
69
+ skippedPlugins: PluginRefreshSkip[];
70
+ }
71
+ export interface HealOptions {
72
+ /** 'full' (doctor --fix): fix drift + refresh unknown-baseline plugins.
73
+ * 'safe' (daemon): missing + invalid-manifest + unmodified refresh only. */
74
+ mode: 'full' | 'safe';
75
+ /** Resolution cwd. Defaults to the home dir so no project layer is ever
76
+ * resolved — heal targets the GLOBAL install, never a project. Tests override. */
77
+ cwd?: string;
78
+ /** Scope to one agent; omit to heal every installed agent. */
79
+ agent?: AgentId;
80
+ /** Scope to specific versions of `agent`; omit for all installed versions. */
81
+ versions?: string[];
82
+ /** Compute the plan without writing anything. */
83
+ dryRun?: boolean;
84
+ }
85
+ /** True when a heal pass made (or would make) any change at all. */
86
+ export declare function healChangedAnything(r: HealResult): boolean;
87
+ /** One-line summary of a heal pass for daemon logs. */
88
+ export declare function summarizeHeal(r: HealResult): string;
89
+ /**
90
+ * Fire a native desktop notification when a background heal did something
91
+ * noteworthy. Best-effort — missing `osascript`/`notify-send` or no display is
92
+ * swallowed. Silent when the pass auto-fixed everything and nothing needs the
93
+ * operator (no point pinging them for routine self-healing).
94
+ */
95
+ export declare function notifyHeal(r: HealResult): void;
96
+ /**
97
+ * Strip Claude-invalid bare-name `skills`/`commands` fields from every central
98
+ * plugin's SOURCE plugin.json. Unambiguously safe (Claude auto-discovers both
99
+ * from their directories) and the precondition for those plugins loading at all.
100
+ */
101
+ export declare function repairCentralPluginManifests(dryRun?: boolean): ManifestRepairResult[];
102
+ /**
103
+ * Run a heal pass. Repairs the central plugin layer once (manifest + stale
104
+ * refresh), then reconciles every targeted (agent, version) home against its
105
+ * live diff. Returns a full account of what changed (or would, under dryRun).
106
+ */
107
+ export declare function heal(opts: HealOptions): Promise<HealResult>;