@phnx-labs/agents-cli 1.22.75 → 1.22.77

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 (138) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +20 -8
  3. package/dist/bootstrap.js +7 -7
  4. package/dist/cli/command-registry.js +5 -0
  5. package/dist/commands/artifacts-setup.js +1 -1
  6. package/dist/commands/artifacts.js +1 -1
  7. package/dist/commands/auth.js +7 -1
  8. package/dist/commands/browser.js +104 -10
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/config.js +27 -4
  11. package/dist/commands/cost.js +6 -4
  12. package/dist/commands/doctor.d.ts +6 -5
  13. package/dist/commands/doctor.js +32 -274
  14. package/dist/commands/exec.d.ts +2 -0
  15. package/dist/commands/exec.js +9 -2
  16. package/dist/commands/harness.d.ts +1 -0
  17. package/dist/commands/harness.js +11 -3
  18. package/dist/commands/hooks.js +7 -6
  19. package/dist/commands/mcp.js +7 -6
  20. package/dist/commands/memory.js +7 -7
  21. package/dist/commands/monitors.js +3 -2
  22. package/dist/commands/open.d.ts +25 -12
  23. package/dist/commands/open.js +24 -10
  24. package/dist/commands/permissions.js +7 -6
  25. package/dist/commands/plugins.js +21 -17
  26. package/dist/commands/route.js +33 -16
  27. package/dist/commands/rules.js +7 -12
  28. package/dist/commands/sessions-share.js +1 -1
  29. package/dist/commands/setup-watchdog.js +2 -2
  30. package/dist/commands/setup.js +22 -1
  31. package/dist/commands/share.js +26 -10
  32. package/dist/commands/skills.js +7 -6
  33. package/dist/commands/subagents.js +7 -6
  34. package/dist/commands/sync.js +81 -10
  35. package/dist/commands/view.js +4 -1
  36. package/dist/commands/watchdog.d.ts +1 -1
  37. package/dist/commands/watchdog.js +10 -10
  38. package/dist/commands/webhook.d.ts +4 -0
  39. package/dist/commands/webhook.js +22 -4
  40. package/dist/commands/workflows.js +7 -6
  41. package/dist/lib/accounting/rotate.d.ts +3 -1
  42. package/dist/lib/accounting/rotate.js +8 -4
  43. package/dist/lib/auth-health.d.ts +2 -0
  44. package/dist/lib/auth-health.js +2 -0
  45. package/dist/lib/browser/chrome.d.ts +21 -0
  46. package/dist/lib/browser/chrome.js +60 -3
  47. package/dist/lib/browser/drivers/local.d.ts +21 -0
  48. package/dist/lib/browser/drivers/local.js +102 -9
  49. package/dist/lib/browser/profiles.d.ts +29 -1
  50. package/dist/lib/browser/profiles.js +50 -1
  51. package/dist/lib/browser/registry.js +10 -3
  52. package/dist/lib/browser/types.d.ts +18 -0
  53. package/dist/lib/config-keys.d.ts +7 -2
  54. package/dist/lib/config-keys.js +17 -2
  55. package/dist/lib/daemon/daemon.js +8 -0
  56. package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
  57. package/dist/lib/daemon/session-summarizer-service.js +39 -0
  58. package/dist/lib/daemon-services.d.ts +1 -1
  59. package/dist/lib/daemon-services.js +5 -0
  60. package/dist/lib/daemon-ticks.d.ts +2 -2
  61. package/dist/lib/daemon-ticks.js +2 -1
  62. package/dist/lib/deeplink/register.js +10 -9
  63. package/dist/lib/deeplink/url.d.ts +4 -4
  64. package/dist/lib/deeplink/url.js +4 -4
  65. package/dist/lib/device-config.js +25 -0
  66. package/dist/lib/devices/doctor-findings.d.ts +4 -4
  67. package/dist/lib/devices/doctor-findings.js +14 -8
  68. package/dist/lib/devices/registry.js +2 -0
  69. package/dist/lib/devices/stats-cache.d.ts +4 -0
  70. package/dist/lib/devices/stats-cache.js +19 -0
  71. package/dist/lib/drift-sync.d.ts +3 -1
  72. package/dist/lib/drift-sync.js +16 -5
  73. package/dist/lib/exec.d.ts +2 -0
  74. package/dist/lib/exec.js +16 -1
  75. package/dist/lib/fleet-shared-state.d.ts +8 -0
  76. package/dist/lib/git.js +6 -0
  77. package/dist/lib/heal.d.ts +4 -3
  78. package/dist/lib/heal.js +5 -4
  79. package/dist/lib/hosts/ready.d.ts +1 -1
  80. package/dist/lib/hosts/ready.js +16 -4
  81. package/dist/lib/hosts/reconnect.js +4 -2
  82. package/dist/lib/identity/client.d.ts +6 -0
  83. package/dist/lib/identity/index.d.ts +16 -0
  84. package/dist/lib/identity/index.js +25 -1
  85. package/dist/lib/installations/migrate.d.ts +10 -0
  86. package/dist/lib/installations/migrate.js +18 -0
  87. package/dist/lib/profiles.d.ts +2 -0
  88. package/dist/lib/profiles.js +28 -9
  89. package/dist/lib/project-resources.d.ts +23 -0
  90. package/dist/lib/project-resources.js +94 -0
  91. package/dist/lib/reconcile-and-repair.d.ts +109 -0
  92. package/dist/lib/reconcile-and-repair.js +267 -0
  93. package/dist/lib/routers.d.ts +12 -1
  94. package/dist/lib/routers.js +30 -1
  95. package/dist/lib/scheduling/routines.js +8 -2
  96. package/dist/lib/session/active.d.ts +13 -0
  97. package/dist/lib/session/db.d.ts +47 -7
  98. package/dist/lib/session/db.js +114 -12
  99. package/dist/lib/session/mirror.js +58 -0
  100. package/dist/lib/session/remote/watch.js +22 -2
  101. package/dist/lib/session/session-cache.d.ts +19 -0
  102. package/dist/lib/session/session-cache.js +46 -0
  103. package/dist/lib/session/types.d.ts +34 -0
  104. package/dist/lib/share/backend.d.ts +6 -4
  105. package/dist/lib/share/backend.js +10 -8
  106. package/dist/lib/share/config.d.ts +4 -3
  107. package/dist/lib/share/config.js +10 -1
  108. package/dist/lib/share/delete.d.ts +1 -1
  109. package/dist/lib/share/delete.js +1 -1
  110. package/dist/lib/share/html.d.ts +1 -1
  111. package/dist/lib/share/html.js +1 -1
  112. package/dist/lib/share/provision.d.ts +1 -1
  113. package/dist/lib/share/provision.js +2 -2
  114. package/dist/lib/share/publish.d.ts +23 -7
  115. package/dist/lib/share/publish.js +58 -12
  116. package/dist/lib/share/worker-template.js +221 -60
  117. package/dist/lib/startup/command-registry.js +2 -2
  118. package/dist/lib/state.d.ts +58 -1
  119. package/dist/lib/state.js +138 -16
  120. package/dist/lib/summarizer/config.d.ts +46 -0
  121. package/dist/lib/summarizer/config.js +83 -0
  122. package/dist/lib/summarizer/pass.d.ts +45 -0
  123. package/dist/lib/summarizer/pass.js +112 -0
  124. package/dist/lib/summarizer/summarize.d.ts +68 -0
  125. package/dist/lib/summarizer/summarize.js +120 -0
  126. package/dist/lib/teams/agents.d.ts +4 -3
  127. package/dist/lib/teams/agents.js +12 -4
  128. package/dist/lib/teams/scheduler.d.ts +4 -2
  129. package/dist/lib/teams/scheduler.js +6 -6
  130. package/dist/lib/tmux/session.d.ts +2 -0
  131. package/dist/lib/tmux/session.js +7 -1
  132. package/dist/lib/types.d.ts +20 -0
  133. package/dist/lib/verbs.d.ts +23 -0
  134. package/dist/lib/verbs.js +24 -0
  135. package/dist/lib/view-types.d.ts +4 -0
  136. package/dist/lib/watchdog/rotate.d.ts +1 -1
  137. package/dist/lib/watchdog/rotate.js +1 -1
  138. package/package.json +1 -1
@@ -15,11 +15,12 @@
15
15
  * unified diff body for each divergent file. Mirrors the resolution that
16
16
  * the shim drives at runtime: project > user > system > extras.
17
17
  *
18
- * Read-only by default: doctor diagnoses, it doesn't mutate. Pass `--fix` to
19
- * heal the gaps it finds (install missing resources, repair Claude-invalid
20
- * plugin manifests, refresh stale plugins, reconcile drift). Run
21
- * `agents prune cleanup` to act on orphan readouts, or just launch the agent to
22
- * apply pending sync.
18
+ * Read-only always: doctor diagnoses, it never mutates. To fix the gaps it
19
+ * finds — install missing resources, repair Claude-invalid plugin manifests,
20
+ * refresh stale plugins, reconcile drift, repair hook runtime shims — run
21
+ * `agents sync <agent>@all` (the one fixer, a superset of the old `doctor
22
+ * --fix`). Run `agents prune cleanup` to act on orphan readouts. `--fix` is a
23
+ * deprecated no-op that now errors with a pointer to `agents sync`.
23
24
  */
24
25
  import type { Command } from 'commander';
25
26
  import { type FleetInventory } from '../lib/devices/fleet-divergence.js';
@@ -4,7 +4,7 @@ import { addHostOption } from '../lib/hosts/option.js';
4
4
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
5
5
  import { loadDevices } from '../lib/devices/registry.js';
6
6
  import { fanOutDevices, planFleetTargets, remoteFleetTargets } from '../lib/devices/fleet.js';
7
- import { enterDoctorOverviewGate, invalidateDoctorOverviewCache, writeDoctorOverviewCache } from '../lib/devices/doctor-overview-cache.js';
7
+ import { enterDoctorOverviewGate, writeDoctorOverviewCache } from '../lib/devices/doctor-overview-cache.js';
8
8
  import { fleetDialTarget } from '../lib/devices/connect.js';
9
9
  import { compareFleetInventories, FLEET_HOOK_RUNTIME_STATES } from '../lib/devices/fleet-divergence.js';
10
10
  import { collectLocalFleetInventory } from '../lib/devices/fleet-inventory.js';
@@ -23,7 +23,7 @@ import { getVersionHomePath, listInstalledVersions, } from '../lib/installations
23
23
  import { resolveAgentTargets, AgentSpecError } from '../lib/agent-spec/index.js';
24
24
  import { loadManifest, isStale } from '../lib/staleness/index.js';
25
25
  import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
26
- import { checkVersionHookWiring, inspectDuplicateVersionHooks, registerHooksToSettings, repairManagedHookRuntimeArtifacts } from '../lib/hooks/install.js';
26
+ import { inspectDuplicateVersionHooks } from '../lib/hooks/install.js';
27
27
  import { inspectReservedAuthBundle } from '../lib/secrets/bundles.js';
28
28
  import { isVersionIsolated } from '../lib/installations/versions.js';
29
29
  import { computeDrift, checkSyncStatus, countOrphans, computeSourceBehind } from '../lib/drift.js';
@@ -33,18 +33,13 @@ import { probeOwnerSink } from '../lib/channels/owner-sink.js';
33
33
  import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
34
34
  import { listCliStatus, listCliStatusAsync } from '../lib/cli-resources.js';
35
35
  import { setHelpSections } from '../lib/help.js';
36
- import { heal, healChangedAnything } from '../lib/heal.js';
37
36
  import { getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
38
37
  import { auditWindowsSshEnrollment, diagnoseWindowsSshFailure } from '../lib/devices/windows-ssh-enrollment.js';
39
38
  import { scanUserRcFiles, masterPassphraseInEnv } from '../lib/secrets/rc-hygiene.js';
40
39
  import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
41
40
  import { readRepoBehindMarkers } from '../lib/auto-pull.js';
42
41
  import { detectAgentsBinaryShadows } from '../lib/binary-shadow.js';
43
- import { remediateStaleAgentsCliInstalls, resolveRunningPackageRoot, } from '../lib/self-update.js';
44
42
  import * as fs from 'fs';
45
- import * as path from 'path';
46
- import { fileURLToPath } from 'url';
47
- const __doctorDirname = path.dirname(fileURLToPath(import.meta.url));
48
43
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
49
44
  // ─── overview mode (no target) ────────────────────────────────────────────────
50
45
  function collapseWhitespace(s) {
@@ -521,7 +516,7 @@ function parseTargetArg(arg) {
521
516
  const agent = resolveAgentName(agentPart);
522
517
  if (!agent)
523
518
  return { error: formatAgentError(agentPart) };
524
- // No qualifier → non-isolated sweep of every installed version; --fix uses the non-isolated path
519
+ // No qualifier → diagnose every installed version of the agent.
525
520
  if (!qualifier) {
526
521
  const versions = listInstalledVersions(agent);
527
522
  if (versions.length === 0)
@@ -659,11 +654,8 @@ function readExpectedForDiff(kind, row) {
659
654
  return null;
660
655
  return safeRead(row.sourcePath);
661
656
  }
662
- // Family of agents whose hooks re-wire through registerHooksToSettings into a
663
- // Claude-style settings.json (matches checkVersionHookWiring's supported set).
664
- const HOOK_WIRING_FIX_AGENTS = ['claude', 'droid'];
665
- /** Categories `--fix` reconciles (vs. `agents repo pull` for a behind source, or
666
- * `agents prune cleanup` for an orphan). Drives the heal footer. */
657
+ /** Categories `agents sync` reconciles (vs. `agents repo pull` for a behind
658
+ * source, or `agents prune cleanup` for an orphan). Drives the heal footer. */
667
659
  const AUTO_FIXABLE_CATEGORIES = new Set([
668
660
  'hook-runtime-broken', 'unwired-hook', 'settings-missing', 'settings-unparseable', 'missing', 'divergent', 'stale', 'never-synced',
669
661
  ]);
@@ -693,8 +685,10 @@ function missingResourceSeverity(kind) {
693
685
  export function computeVerdict(report) {
694
686
  const issues = [];
695
687
  const idLabel = `${report.agent}@${report.version}`;
696
- const fixCmd = `agents doctor ${idLabel} --fix`;
697
- const syncCmd = `agents sync ${idLabel} --yes`;
688
+ // doctor diagnoses; `agents sync` fixes. Every auto-fixable finding points at
689
+ // the one fixer — a command sync can actually deliver (asserted in the tests).
690
+ const fixCmd = `agents sync ${idLabel} --yes`;
691
+ const syncCmd = fixCmd;
698
692
  // ── critical: settings.json / unwired hooks (silent breakage) ──
699
693
  const w = report.hookWiring;
700
694
  for (const issue of w?.runtimeBroken ?? []) {
@@ -897,7 +891,7 @@ export function computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, d
897
891
  issues.push({
898
892
  severity: 'critical', category: 'hook-runtime-broken', subject: label,
899
893
  impact: `${brokenRuntime} generated hook wrapper${brokenRuntime === 1 ? '' : 's'} unusable; affected hooks cannot run`,
900
- fix: `agents doctor ${row.agent}@${row.version} --fix`,
894
+ fix: `agents sync ${row.agent}@${row.version} --yes`,
901
895
  text: `${label} ${brokenRuntime} hook runtime broken`, color: 'red',
902
896
  });
903
897
  }
@@ -931,7 +925,7 @@ export function computeOverviewHealth(syncRows, orphanRows, repoBehindMarkers, d
931
925
  issues.push({
932
926
  severity: 'warning', category: 'stale', subject: label,
933
927
  impact: 'sources changed since last sync',
934
- fix: `agents doctor ${row.agent}@${row.version} --fix`,
928
+ fix: `agents sync ${row.agent}@${row.version} --yes`,
935
929
  text: `${label} stale`, color: 'yellow',
936
930
  });
937
931
  }
@@ -1040,247 +1034,16 @@ function renderTargetText(report, options) {
1040
1034
  }
1041
1035
  console.log();
1042
1036
  const verdict = computeVerdict(report);
1043
- // A source layer behind origin is healed by `agents repo pull`, not `--fix` —
1037
+ // A source layer behind origin is healed by `agents repo pull`, not by sync —
1044
1038
  // the per-issue fix already names the right command, so the heal footer only
1045
- // shows when something is genuinely `--fix`-able (never in the source-behind or
1039
+ // shows when something is genuinely sync-fixable (never in the source-behind or
1046
1040
  // orphan-only case, where it would mislead).
1047
1041
  const hooksWired = report.hookWiring?.supported ? ' · hooks wired' : '';
1048
1042
  renderHealthBlock(verdict, {
1049
1043
  healthySummary: `${verdict.reconciled} resource${verdict.reconciled === 1 ? '' : 's'} reconciled${hooksWired} · sources current`,
1050
- healFix: verdictIsAutoFixable(verdict) ? `agents doctor ${report.agent}@${report.version} --fix` : undefined,
1044
+ healFix: verdictIsAutoFixable(verdict) ? `agents sync ${report.agent}@${report.version} --yes` : undefined,
1051
1045
  });
1052
1046
  }
1053
- // ─── fix / heal mode ───────────────────────────────────────────────────────────
1054
- function renderHealText(result) {
1055
- for (const r of result.repairedManifests) {
1056
- console.log(` ${chalk.green('repair')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`— dropped invalid ${r.droppedFields.join(', ')} field`)}`);
1057
- }
1058
- for (const r of result.refreshedPlugins) {
1059
- console.log(` ${chalk.green('refresh')} plugin ${chalk.bold(r.plugin)} ${chalk.gray(`${r.from} → ${r.to}`)}`);
1060
- }
1061
- for (const s of result.skippedPlugins) {
1062
- const why = s.reason === 'modified'
1063
- ? `locally modified — left as-is (run \`agents plugins update ${s.plugin}\` to force)`
1064
- : `no baseline recorded — left as-is (run \`agents plugins update ${s.plugin}\` to adopt)`;
1065
- console.log(` ${chalk.yellow('hold ')} plugin ${chalk.bold(s.plugin)} ${chalk.gray(`${s.from} → ${s.upstream} available; ${why}`)}`);
1066
- }
1067
- for (const v of result.versions) {
1068
- const label = `${AGENT_NAMES[v.agent] || v.agent}@${v.version}`;
1069
- if (v.healed.length === 0 && v.skipped.length === 0)
1070
- continue;
1071
- const byKind = new Map();
1072
- for (const h of v.healed)
1073
- byKind.set(h.kind, (byKind.get(h.kind) ?? 0) + 1);
1074
- const parts = Array.from(byKind, ([k, n]) => `${n} ${k}`);
1075
- if (v.healed.length > 0) {
1076
- console.log(` ${chalk.green('fixed ')} ${label} ${chalk.gray(parts.join(', '))}`);
1077
- }
1078
- const drift = v.skipped.filter((s) => s.reason === 'drift');
1079
- const unres = v.skipped.filter((s) => s.reason === 'unreconcilable');
1080
- if (drift.length > 0) {
1081
- console.log(` ${chalk.yellow('drift ')} ${label} ${chalk.gray(`${drift.length} hand-edited — left as-is (use \`--diff\` to inspect)`)}`);
1082
- }
1083
- if (unres.length > 0) {
1084
- const names = unres.map((s) => `${s.kind}/${s.name}`).join(', ');
1085
- console.log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${unres.length} couldn't reconcile (${names}) — source/home mismatch the writer can't satisfy`)}`);
1086
- }
1087
- }
1088
- console.log();
1089
- const healed = result.versions.reduce((n, v) => n + v.healed.length, 0);
1090
- const touchedVersions = result.versions.filter((v) => v.healed.length > 0).length;
1091
- if (!healChangedAnything(result)) {
1092
- console.log(chalk.green('✓ Everything in sync — nothing to heal.'));
1093
- }
1094
- else {
1095
- const bits = [];
1096
- if (healed > 0)
1097
- bits.push(`${healed} resource${healed === 1 ? '' : 's'} across ${touchedVersions} version${touchedVersions === 1 ? '' : 's'}`);
1098
- if (result.repairedManifests.length > 0)
1099
- bits.push(`${result.repairedManifests.length} manifest${result.repairedManifests.length === 1 ? '' : 's'} repaired`);
1100
- if (result.refreshedPlugins.length > 0)
1101
- bits.push(`${result.refreshedPlugins.length} plugin${result.refreshedPlugins.length === 1 ? '' : 's'} refreshed`);
1102
- console.log(chalk.green(`✓ Healed ${bits.join(', ')}.`));
1103
- }
1104
- }
1105
- /**
1106
- * Re-wire hooks that reconcile as files but are absent from settings.json.
1107
- *
1108
- * heal() only re-syncs resources the diff flags missing/diff; a hook whose file
1109
- * is byte-identical to source but never referenced in settings.json is neither,
1110
- * so heal walks past it. registerHooksToSettings (the same call `agents sync`
1111
- * makes at versions.ts) regenerates the wiring, so run it for any Claude-family
1112
- * version this fix targets that has unwired hooks. Only claude/droid — the set
1113
- * checkVersionHookWiring can verify.
1114
- */
1115
- function rewireUnwiredHooks(parsed) {
1116
- const out = [];
1117
- const agents = parsed?.agent
1118
- ? (HOOK_WIRING_FIX_AGENTS.includes(parsed.agent) ? [parsed.agent] : [])
1119
- : HOOK_WIRING_FIX_AGENTS;
1120
- for (const agent of agents) {
1121
- // A named version is explicit consent; a sweep excludes isolated copies,
1122
- // mirroring heal().
1123
- const versions = parsed?.agent && parsed.versionExplicit
1124
- ? parsed.versions
1125
- : listInstalledVersions(agent).filter((v) => !isVersionIsolated(agent, v));
1126
- for (const version of versions) {
1127
- const before = checkVersionHookWiring(agent, version);
1128
- if (!before.supported)
1129
- continue;
1130
- const need = before.unwired.length + (before.settingsMissing ? (before.expected ?? 0) : 0);
1131
- if (need === 0)
1132
- continue;
1133
- try {
1134
- const registration = registerHooksToSettings(agent, getVersionHomePath(agent, version));
1135
- if (registration.errors.length > 0) {
1136
- out.push({ agent, version, rewired: 0, remaining: need, failure: 'register-failed' });
1137
- continue;
1138
- }
1139
- const after = checkVersionHookWiring(agent, version);
1140
- const remaining = after.unwired.length + (after.settingsMissing ? (after.expected ?? 0) : 0);
1141
- out.push({ agent, version, rewired: Math.max(0, need - remaining), remaining });
1142
- }
1143
- catch {
1144
- // A shim write can fail before the native config writer gets to return
1145
- // its own errors. Record the same stable class and keep doctor --fix
1146
- // moving to the one bounded runtime repair pass below.
1147
- out.push({ agent, version, rewired: 0, remaining: need, failure: 'register-failed' });
1148
- }
1149
- }
1150
- }
1151
- return out;
1152
- }
1153
- function renderHookRewireText(rewired) {
1154
- for (const r of rewired) {
1155
- const label = `${AGENT_NAMES[r.agent] || r.agent}@${r.version}`;
1156
- if (r.failure) {
1157
- console.log(` ${chalk.red('hold ')} ${label} ${chalk.gray('native hook wiring could not be updated')}`);
1158
- }
1159
- else if (r.remaining === 0) {
1160
- console.log(` ${chalk.green('rewired')} ${label} ${chalk.gray(`${r.rewired} hook${r.rewired === 1 ? '' : 's'} wired into settings.json`)}`);
1161
- }
1162
- else {
1163
- console.log(` ${chalk.yellow('hold ')} ${label} ${chalk.gray(`${r.remaining} hook${r.remaining === 1 ? '' : 's'} still unwired — run \`agents sync ${r.agent}@${r.version} --yes\``)}`);
1164
- }
1165
- }
1166
- }
1167
- function runtimeRepairFilter(parsed) {
1168
- if (!parsed)
1169
- return undefined;
1170
- // A concrete target can be narrowed to that version. Broad selectors (@all,
1171
- // @latest, agent-only) still make one bounded repair pass for that harness.
1172
- return {
1173
- agent: parsed.agent,
1174
- ...(parsed.versionExplicit && parsed.versions.length === 1 ? { version: parsed.versions[0] } : {}),
1175
- };
1176
- }
1177
- function renderHookRuntimeRepairText(repair) {
1178
- for (const fixed of repair.fixed) {
1179
- console.log(` ${chalk.green('fixed ')} ${chalk.gray(fixed)}`);
1180
- }
1181
- for (const unresolved of repair.needsAttention) {
1182
- console.log(` ${chalk.red('hold ')} ${chalk.gray(unresolved)}`);
1183
- }
1184
- }
1185
- /**
1186
- * RUSH-2415: delete npx-cache / unsafe-legacy / pre-1.22.30 agents-cli copies
1187
- * when a fixed peer already exists. Bare `doctor --fix` is the remediation
1188
- * surface the multi-install warning points at; a targeted
1189
- * `doctor <agent>@<version> --fix` only heals that version home and must not
1190
- * touch other CLI installs on the box.
1191
- */
1192
- function purgeStaleAgentsCliCopies(_opts) {
1193
- let runningRoot;
1194
- try {
1195
- // dist/commands/doctor.js (or src/commands/doctor.ts under vitest) —
1196
- // resolveRunningPackageRoot walks up until package.json names this package.
1197
- runningRoot = resolveRunningPackageRoot(__doctorDirname);
1198
- }
1199
- catch {
1200
- return null;
1201
- }
1202
- return remediateStaleAgentsCliInstalls({
1203
- runningRoot,
1204
- runningVersion: getCliVersion(),
1205
- });
1206
- }
1207
- function renderStaleInstallPurgeText(purge) {
1208
- if (purge.removed.length === 0 && purge.failed.length === 0 && purge.unresolved.length === 0)
1209
- return;
1210
- console.log(chalk.bold('\nStale agents-cli installs'));
1211
- for (const r of purge.removed) {
1212
- const why = r.reasons.join(', ');
1213
- console.log(` ${chalk.green('purged')} ${chalk.gray(`${r.packageRoot} ${r.version} (${why})`)}`);
1214
- }
1215
- for (const f of purge.failed) {
1216
- console.log(` ${chalk.red('hold ')} ${chalk.gray(`${f.packageRoot} ${f.version} — ${f.error}`)}`);
1217
- }
1218
- // RUSH-2705/2713: a duplicate --fix cannot auto-purge — either a healthy
1219
- // >=1.22.30 peer, OR a pre-1.22.30 copy left alone only because no fixed peer
1220
- // exists to fall back to (that one is NOT healthy). Either way, hand back the
1221
- // command that removes it instead of ending on a bare "everything in sync".
1222
- // Don't call it "healthy" — that would understate a genuinely vulnerable copy.
1223
- for (const u of purge.unresolved) {
1224
- console.log(` ${chalk.yellow('manual')} ${chalk.gray(`${u.packageRoot} ${u.version} — --fix will not delete this copy; remove it with:`)}`);
1225
- console.log(` ${chalk.bold(u.manualRemoveCommand)}`);
1226
- }
1227
- }
1228
- async function runFix(parsed, opts) {
1229
- // Heal targets the global install — project layer is irrelevant, so cwd is
1230
- // left to heal's neutral default rather than process.cwd().
1231
- if (!opts.json)
1232
- console.log(chalk.bold('Healing…'));
1233
- // `agents doctor <agent> --fix` is a SWEEP, same as the bare form — leave the
1234
- // version list to heal() so it applies its isolated-copy filter. Passing the
1235
- // enumerated list here would smuggle isolated versions past that filter.
1236
- // A named version (`<agent>@<version>`) is explicit consent and is passed through.
1237
- const result = await heal({
1238
- mode: 'full',
1239
- agent: parsed?.agent,
1240
- versions: parsed?.versionExplicit ? parsed.versions : undefined,
1241
- });
1242
- // Re-wire hooks the diff-driven heal leaves behind (present file, not wired).
1243
- const rewired = rewireUnwiredHooks(parsed);
1244
- // One inspect→generate→verify pass, after normal resource and native-wiring
1245
- // repair have settled. This routine never calls sync/register and never
1246
- // retries; unresolved wrappers remain an explicit non-zero doctor outcome.
1247
- const hookRuntimeRepair = repairManagedHookRuntimeArtifacts({ filter: runtimeRepairFilter(parsed) });
1248
- // Bare doctor --fix also purges latent pre-fix / legacy agents-cli copies
1249
- // that only warn today (RUSH-2415). A scoped agent@version fix leaves them
1250
- // alone — the multi-install surface is machine-wide, not per-agent.
1251
- const staleInstallPurge = parsed === null ? purgeStaleAgentsCliCopies(opts) : null;
1252
- const rewireFailed = rewired.some((entry) => entry.failure !== undefined);
1253
- if (healChangedAnything(result) ||
1254
- rewired.some((entry) => entry.rewired > 0 || entry.remaining > 0 || entry.failure !== undefined) ||
1255
- hookRuntimeRepair.attempts.length > 0 ||
1256
- (staleInstallPurge !== null && (staleInstallPurge.removed.length > 0 || staleInstallPurge.failed.length > 0))) {
1257
- invalidateDoctorOverviewCache();
1258
- }
1259
- if (opts.json) {
1260
- console.log(JSON.stringify({
1261
- ...result,
1262
- hookRewire: rewired,
1263
- hookRuntimeRepair,
1264
- ...(staleInstallPurge ? { staleInstallPurge } : {}),
1265
- }, null, 2));
1266
- if (rewireFailed
1267
- || hookRuntimeRepair.needsAttention.length > 0
1268
- || (staleInstallPurge !== null && staleInstallPurge.failed.length > 0)) {
1269
- process.exitCode = 1;
1270
- }
1271
- return;
1272
- }
1273
- renderHealText(result);
1274
- renderHookRewireText(rewired);
1275
- renderHookRuntimeRepairText(hookRuntimeRepair);
1276
- if (staleInstallPurge)
1277
- renderStaleInstallPurgeText(staleInstallPurge);
1278
- if (rewireFailed
1279
- || hookRuntimeRepair.needsAttention.length > 0
1280
- || (staleInstallPurge !== null && staleInstallPurge.failed.length > 0)) {
1281
- process.exitCode = 1;
1282
- }
1283
- }
1284
1047
  function checkLabel(row) {
1285
1048
  return `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
1286
1049
  }
@@ -1360,7 +1123,7 @@ function runCheckGate(opts, cwd) {
1360
1123
  }
1361
1124
  const hints = [];
1362
1125
  if (drift.staleCount > 0 || drift.neverSyncedCount > 0 || drift.unwiredHookVersions > 0 || drift.brokenHookRuntimeVersions > 0) {
1363
- hints.push('`agents doctor --fix` (or `agents doctor <agent>@<version> --fix`)');
1126
+ hints.push('`agents sync <agent>@all` (or `agents sync <agent>@<version>`)');
1364
1127
  }
1365
1128
  if (drift.sourceBehind.length > 0)
1366
1129
  hints.push('`agents repo pull <alias>` for a source layer behind origin');
@@ -1447,7 +1210,7 @@ async function runDevicesCheck(opts, cwd) {
1447
1210
  : [`${d.stale} stale`, `${d.neverSynced} never-synced`].filter((p) => !p.startsWith('0 ')).join(', ');
1448
1211
  console.error(` ${chalk.yellow(d.device.padEnd(18))} ${detail || 'drift'}`);
1449
1212
  }
1450
- console.error(chalk.gray('\nReconcile each device with `agents doctor --fix` or `agents repo pull user`.'));
1213
+ console.error(chalk.gray('\nReconcile each device with `agents sync <agent>@all` or `agents repo pull user`.'));
1451
1214
  }
1452
1215
  process.exit(1);
1453
1216
  }
@@ -1457,7 +1220,7 @@ export function registerDoctorCommand(program) {
1457
1220
  .description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
1458
1221
  .option('--json', 'Output machine-readable JSON')
1459
1222
  .option('--diff', 'In target mode, include unified diffs for divergent files')
1460
- .option('--fix', 'Heal gaps: install missing resources, repair invalid plugin manifests, refresh stale plugins, reconcile drift, and purge stale/legacy agents-cli installs (npx-cache, pre-1.22.30, unsafe helper installer) when a fixed peer exists')
1223
+ .option('--fix', '(deprecated) doctor is diagnose-only — this errors with a pointer to `agents sync`, the one fixer')
1461
1224
  .option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,workflows,memory)')
1462
1225
  .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
1463
1226
  .option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
@@ -1488,11 +1251,11 @@ export function registerDoctorCommand(program) {
1488
1251
  # Inspect only rules and hooks, with full diffs
1489
1252
  agents doctor claude@default --kind rules,hooks --diff
1490
1253
 
1491
- # Heal every gap across all installed versions
1492
- agents doctor --fix
1254
+ # Fix every gap across all installed versions (the one fixer)
1255
+ agents sync claude@all
1493
1256
 
1494
- # Heal just one agent (all its installed versions)
1495
- agents doctor claude --fix
1257
+ # Fix just one version
1258
+ agents sync claude@2.1.207
1496
1259
 
1497
1260
  # Fleet: agent readiness + cross-device divergence (missing plugins/skills,
1498
1261
  # agent-version gaps, .agents/.system repo drift) vs this machine
@@ -1503,7 +1266,7 @@ export function registerDoctorCommand(program) {
1503
1266
  agents doctor --check --quiet # just the verdict line
1504
1267
  agents doctor --check --json # machine-readable, for scripting
1505
1268
  agents doctor --check --devices # gate every registered device
1506
- agents doctor --check || { echo "resources drifted — run 'agents doctor --fix'"; exit 1; }
1269
+ agents doctor --check || { echo "resources drifted — run 'agents sync <agent>@all'"; exit 1; }
1507
1270
  `,
1508
1271
  });
1509
1272
  doctorCmd.action(async (target, opts) => {
@@ -1587,20 +1350,15 @@ export function registerDoctorCommand(program) {
1587
1350
  }
1588
1351
  return;
1589
1352
  }
1590
- // --fix turns the read-only diagnosis into a heal. With no target it heals
1591
- // every installed version; with a target it scopes to that agent.
1353
+ // `doctor --fix` moved to `agents sync` — doctor is diagnose-only now.
1354
+ // Keep the flag for one release as a signpost: it never heals, it points at
1355
+ // the one fixer and exits non-zero so scripts and muscle memory get a clear
1356
+ // redirect instead of a silent no-op.
1592
1357
  if (opts.fix) {
1593
- let scope = null;
1594
- if (target) {
1595
- const parsed = parseTargetArg(target);
1596
- if ('error' in parsed) {
1597
- console.error(chalk.red(parsed.error));
1598
- process.exit(1);
1599
- }
1600
- scope = parsed;
1601
- }
1602
- await runFix(scope, opts);
1603
- return;
1358
+ const where = target ? `agents sync ${target}` : 'agents sync <agent>@all';
1359
+ console.error(chalk.yellow('`agents doctor --fix` has moved to `agents sync`.'));
1360
+ console.error(chalk.gray(` doctor now only diagnoses. To fix, run: ${where}`));
1361
+ process.exit(1);
1604
1362
  }
1605
1363
  if (!target) {
1606
1364
  // Singleflight + short-TTL cache for the bare `doctor --json` overview.
@@ -1666,8 +1424,8 @@ export function registerDoctorCommand(program) {
1666
1424
  // A MANAGED agent (installed versions) whose binary won't resolve is a
1667
1425
  // real critical — an unmanaged-and-absent agent is not.
1668
1426
  const cliMissing = ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0 && clis[a] && !clis[a].installed);
1669
- // An isolated copy is skipped by the agent-wide `--fix` sweep, so its
1670
- // findings must never fold into a collapsed cross-version row.
1427
+ // An isolated copy is skipped by the agent-wide `agents sync` sweep, so
1428
+ // its findings must never fold into a collapsed cross-version row.
1671
1429
  const isolatedVersions = reports
1672
1430
  .filter((r) => isVersionIsolated(r.agent, r.version))
1673
1431
  .map((r) => `${r.agent}@${r.version}`);
@@ -8,6 +8,8 @@
8
8
  import { type Command } from 'commander';
9
9
  import type { ExecEffort } from '../lib/exec.js';
10
10
  import { RUN_AUTO_KEYWORD } from '../lib/types.js';
11
+ /** Validate a caller-supplied session id before it reaches tmux, paths, indexes, or remote dispatch. */
12
+ export declare function parseExplicitSessionId(value: string): string;
11
13
  export interface RunAccountPickerRequest {
12
14
  requested: boolean;
13
15
  normalizedAgentSpec: string;
@@ -5,7 +5,7 @@
5
5
  * or headlessly. Supports profile resolution, version rotation, secrets
6
6
  * injection, and multi-agent fallback chains for rate-limit resilience.
7
7
  */
8
- import { Option } from 'commander';
8
+ import { InvalidArgumentError, Option } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import { isTierToken } from '../lib/model-tiers.js';
11
11
  import { RUN_AUTO_KEYWORD } from '../lib/types.js';
@@ -28,6 +28,13 @@ import { isSessionTrackedAgent } from '../lib/session/types.js';
28
28
  import { applyActiveRulesPresetAtRun } from '../lib/rules/run-sync.js';
29
29
  import { handleBroadcast } from './run-broadcast.js';
30
30
  import { bootMark } from '../lib/boot-profile.js';
31
+ /** Validate a caller-supplied session id before it reaches tmux, paths, indexes, or remote dispatch. */
32
+ export function parseExplicitSessionId(value) {
33
+ if (!/^[A-Za-z0-9._-]+$/.test(value)) {
34
+ throw new InvalidArgumentError('must contain only ASCII letters, digits, dots, underscores, or hyphens');
35
+ }
36
+ return value;
37
+ }
31
38
  /** Distinguish a terminal account-picker marker from an explicit @version pin. */
32
39
  export function parseRunAccountPickerRequest(agentSpec) {
33
40
  const requested = agentSpec.endsWith('@');
@@ -514,7 +521,7 @@ export function registerRunCommand(program) {
514
521
  .option('--results [run-id]', 'With --broadcast: show one saved matrix run, or list saved runs newest first')
515
522
  .option('--concurrency <n>', 'With --broadcast: maximum cells running at once', '3')
516
523
  .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.')
517
- .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.')
524
+ .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.', parseExplicitSessionId)
518
525
  .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 --device runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
519
526
  .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).')
520
527
  .option('--no-trace-sync', 'Skip the run-exit trace auto-sync for this run. Auto-sync fires by default only for local runs and only once you have run `agents traces sync` at least once (also silenced by AGENTS_NO_TRACE_SYNC=1).')
@@ -22,6 +22,7 @@ import { type HarnessDraft } from './harness-wizard.js';
22
22
  export declare function renderHarnessDetail(name: string): void;
23
23
  /** Options accepted by `agents harness fork`. */
24
24
  export interface ForkOptions {
25
+ toHost?: string;
25
26
  model?: string;
26
27
  baseUrl?: string;
27
28
  authProvider?: string;
@@ -62,7 +62,11 @@ export function buildFork(source, name, opts) {
62
62
  if (opts.authProvider || opts.fromSecrets)
63
63
  throw new Error("Harnesses no longer own credentials. Add one with 'agents accounts add <name> --provider <provider> --auth <type>', then pass --account <name>.");
64
64
  if (profileExists(source)) {
65
+ const targetHost = opts.toHost ? (resolveAgentName(opts.toHost) ?? undefined) : undefined;
66
+ if (opts.toHost && !targetHost)
67
+ throw new Error(`Unknown target host '${opts.toHost}'.`);
65
68
  const profile = forkProfile(readProfile(source), name, {
69
+ host: targetHost,
66
70
  model: opts.model,
67
71
  baseUrl: opts.baseUrl,
68
72
  provider: opts.authProvider,
@@ -78,14 +82,17 @@ export function buildFork(source, name, opts) {
78
82
  }
79
83
  return profile;
80
84
  }
81
- const host = resolveAgentName(source);
82
- if (!host) {
85
+ const sourceHost = resolveAgentName(source);
86
+ if (!sourceHost) {
83
87
  throw new Error(`No harness or agent named '${source}'.\n` +
84
88
  `Fork from a custom harness (agents harness list) or a native one: ${ALL_AGENT_IDS.join(', ')}.`);
85
89
  }
86
90
  if (!opts.model) {
87
- throw new Error(`--model <id> is required when forking the native '${host}' harness (there is no model to inherit).`);
91
+ throw new Error(`--model <id> is required when forking the native '${sourceHost}' harness (there is no model to inherit).`);
88
92
  }
93
+ const host = opts.toHost ? resolveAgentName(opts.toHost) : sourceHost;
94
+ if (!host)
95
+ throw new Error(`Unknown target host '${opts.toHost}'.`);
89
96
  const profile = profileFromHostModel(name, host, opts.model, {
90
97
  version: opts.version,
91
98
  baseUrl: opts.baseUrl,
@@ -461,6 +468,7 @@ Examples:
461
468
  .command('fork [source] [name]')
462
469
  .description('Fork a native harness (claude, opencode, ...) or an existing custom one into a new named harness. Omit args in a terminal for the interactive wizard.')
463
470
  .option('--model <id>', 'Model to pin on the fork (required when forking a native harness)')
471
+ .option('--to-host <agent>', 'Translate the fork onto another native harness host (for example claude to codex)')
464
472
  .option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
465
473
  .option('--account <name>', 'Default durable credential account')
466
474
  .option('--auth-provider <provider>', 'Removed: use agents accounts add, then --account')
@@ -1,3 +1,4 @@
1
+ import { withAliases } from '../lib/verbs.js';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
@@ -40,8 +41,8 @@ When to use:
40
41
  - Notifications: ping Slack when agents complete long tasks
41
42
  - Team workflows: sync hooks via 'agents hooks add gh:team/hooks'
42
43
  `);
43
- hooksCmd
44
- .command('list [agent]')
44
+ withAliases(hooksCmd
45
+ .command('list [agent]'), 'list')
45
46
  .description('Show which hooks are installed and which events they respond to')
46
47
  .option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
47
48
  .option('-s, --scope <scope>', 'user (global), project (repo), or all', 'all')
@@ -387,8 +388,8 @@ Examples:
387
388
  process.exit(1);
388
389
  }
389
390
  });
390
- hooksCmd
391
- .command('remove [name]')
391
+ withAliases(hooksCmd
392
+ .command('remove [name]'), 'remove')
392
393
  .description('Delete a hook from agents (interactive picker if no name given)')
393
394
  .option('-a, --agents <list>', 'Limit removal to specific agents')
394
395
  .addHelpText('after', `
@@ -527,8 +528,8 @@ Examples:
527
528
  console.error(chalk.gray('Use: agents prune cleanup hooks (or `agents prune cleanup` for everything)'));
528
529
  process.exit(1);
529
530
  });
530
- hooksCmd
531
- .command('view [name]')
531
+ withAliases(hooksCmd
532
+ .command('view [name]'), 'view')
532
533
  .description('Read the shell script content for a hook')
533
534
  .addHelpText('after', `
534
535
  Examples:
@@ -1,3 +1,4 @@
1
+ import { withAliases } from '../lib/verbs.js';
1
2
  import chalk from 'chalk';
2
3
  import { truncate } from '../lib/format.js';
3
4
  import ora from 'ora';
@@ -143,8 +144,8 @@ When to use:
143
144
  - Version upgrade: 'agents mcp register' to sync servers to the new version
144
145
  - Team setup: commit mcp config to .agents and run 'agents mcp register'
145
146
  `);
146
- mcpCmd
147
- .command('list [agent]')
147
+ withAliases(mcpCmd
148
+ .command('list [agent]'), 'list')
148
149
  .description('Show which MCP servers are registered and which agent versions they are synced to')
149
150
  .option('-a, --agent <agent>', 'Filter to a specific agent (alternative to positional arg)')
150
151
  .option('--json', 'Emit machine-readable JSON instead of the table/picker')
@@ -337,8 +338,8 @@ Examples:
337
338
  console.log(chalk.green(`Added MCP server '${name}' to manifest`));
338
339
  console.log(chalk.gray('Run: agents mcp register to apply'));
339
340
  });
340
- mcpCmd
341
- .command('remove [name]')
341
+ withAliases(mcpCmd
342
+ .command('remove [name]'), 'remove')
342
343
  .description('Unregister an MCP server from agents (interactive picker if no name given)')
343
344
  .option('-a, --agents <list>', 'Limit removal to specific agents')
344
345
  .addHelpText('after', `
@@ -487,8 +488,8 @@ Examples:
487
488
  console.log(chalk.green(`\nRemoved ${removed} MCP server(s).`));
488
489
  }
489
490
  });
490
- mcpCmd
491
- .command('view [name]')
491
+ withAliases(mcpCmd
492
+ .command('view [name]'), 'view')
492
493
  .description('Show MCP server configuration (command, scope, registered agents)')
493
494
  .addHelpText('after', `
494
495
  Examples: