@getmarrow/install 0.1.55 → 0.1.56

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 (3) hide show
  1. package/README.md +13 -7
  2. package/package.json +1 -1
  3. package/src/installer.js +186 -33
package/README.md CHANGED
@@ -75,17 +75,19 @@ Tool visibility is not authorization. Every visible call still reaches Marrow's
75
75
  Marrow's hosted API, website, and dashboard update automatically; local SDK dependencies, generated runtime files, MCP hooks/configuration, and pinned package versions do not silently rewrite themselves. Keeping them current delivers new client-side features, compatibility improvements, and any published security fixes. Supported clients report their package version during authenticated status/runtime activity, and Marrow returns a `client_update` notice with the exact action when the version is behind or unknown.
76
76
 
77
77
  ```bash
78
- npx -y @getmarrow/install@latest activate
79
- npx -y @getmarrow/install@latest doctor
78
+ # When doctor reports stale or mixed MCP configuration
79
+ npx -y @getmarrow/install@latest update
80
+
81
+ # Restart the detected owning harnesses once, then verify
82
+ npx -y @getmarrow/install@latest doctor --self-test
80
83
 
81
84
  # Measured API read health and local backlog
82
85
  npx -y --package=@getmarrow/mcp@latest marrow-mcp ping
83
-
84
- # Use only when doctor reports drift
85
- npx -y @getmarrow/install@latest --repair
86
86
  ```
87
87
 
88
- `activate` reconciles Marrow-managed entries while retaining unrelated user hooks and configuration. After that explicit activation, the local controller can restore drifted Marrow-managed hooks and configuration. Package upgrades, owner policy, credentials, and unrelated local files remain explicit and subject to the operator's normal change policy.
88
+ `update` resolves official npm metadata once, selects one exact verified MCP target, and synchronizes every Marrow-managed surface in the detected owning workspace while retaining unrelated user hooks and configuration. Restart the detected owning harnesses once after it completes; running processes do not change before that restart. Run the doctor verification once after restart. Do not run separate `marrow-mcp setup` and restart cycles for the same detected workspace.
89
+
90
+ `activate` remains available for initial activation. After explicit activation, the local controller can restore drifted Marrow-managed hooks and configuration. Package upgrades, owner policy, credentials, explicitly disabled hooks, and unrelated local files remain explicit and subject to the operator's normal change policy.
89
91
 
90
92
  ## Automatic Controller
91
93
 
@@ -99,7 +101,11 @@ npx @getmarrow/install controller stop
99
101
 
100
102
  Persistent controller lifecycle is currently Linux-only. On macOS or Windows, activation still writes supported configuration and verifies one server-side install self-test without certifying that hooks continuously ran; run `npx @getmarrow/install sidecar` under an owner-managed service and pass `--no-controller`. The controller does not silently upgrade packages, change governance policy, rotate credentials, or modify unrelated project configuration.
101
103
 
102
- ## What's New in v0.1.55
104
+ ## What's New in v0.1.56
105
+
106
+ v0.1.56 pins the officially verified MCP `3.9.80` release from source `ee1eda3f201965a6530256accbdf175660dd8ad6` with registry integrity `sha512-uou3X18pESV39EMmddDrYN7yd6MrZzosrnQ1eRtvK5j3yuBLAlupj5kQVrrKEWAL6xwuLebXq2isivK2O/2WrA==`; SDK remains `3.7.62`. Doctor now directs stale, mixed, or version-unknown MCP installations through one `npx -y @getmarrow/install@latest update`, followed by one owning-harness restart and one `doctor --self-test` verification. The update resolves one official target and applies it consistently across detected Marrow-managed instructions, MCP launch configuration, and supported native hooks without claiming that already-running processes changed.
107
+
108
+ ## Previous: v0.1.55
103
109
 
104
110
  v0.1.55 pins sealed MCP candidate `3.9.79` from source `11f00049043d0aba90704ecbf69f32d2278a4573` for ordinary setup, generated launch configuration, and native hooks. Doctor, update, and repair retain exact-version resolution and never propagate a newer local version unless official registry metadata verifies it; offline operation preserves unverified-ahead owner surfaces and uses the sealed candidate for new managed targets.
105
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
package/src/installer.js CHANGED
@@ -10,9 +10,9 @@ const { evidence: localControlEvidence } = require('./control-state');
10
10
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
11
11
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
12
12
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
13
- const MCP_ADAPTER_VERSION = '3.9.79';
14
- const MCP_ADAPTER_SOURCE_SHA = '11f00049043d0aba90704ecbf69f32d2278a4573';
15
- const MCP_ADAPTER_INTEGRITY = 'sha512-LWfBWGot2PnluRbS5Hm3WA2DJCIkf2qVJfthGcCaZx8AoRXFKeg1pUO3rMVFqrOIWOB4nYZw6UgSL+7hDY1wuA==';
13
+ const MCP_ADAPTER_VERSION = '3.9.80';
14
+ const MCP_ADAPTER_SOURCE_SHA = 'ee1eda3f201965a6530256accbdf175660dd8ad6';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-uou3X18pESV39EMmddDrYN7yd6MrZzosrnQ1eRtvK5j3yuBLAlupj5kQVrrKEWAL6xwuLebXq2isivK2O/2WrA==';
16
16
  const SDK_ADAPTER_VERSION = '3.7.62';
17
17
  const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
18
18
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -33,6 +33,9 @@ const ADAPTER_PROVENANCE = Object.freeze({
33
33
  });
34
34
  const MCP_REGISTRY_LATEST_URL = 'https://registry.npmjs.org/%40getmarrow%2Fmcp/latest';
35
35
  const MCP_REGISTRY_VERIFICATION_COMMAND = 'npm view @getmarrow/mcp@latest name version dist.integrity dist.tarball --json --registry=https://registry.npmjs.org';
36
+ const INSTALLER_UPDATE_COMMAND = 'npx -y @getmarrow/install@latest update';
37
+ const INSTALLER_DOCTOR_COMMAND = 'npx -y @getmarrow/install@latest doctor --self-test';
38
+ const INSTALLER_RESTART_INSTRUCTION = 'After the update completes, restart the detected owning harnesses once. Running Marrow MCP processes remain unchanged until that restart.';
36
39
  const MCP_STABLE_VERSION_RE = /^(\d{1,6})\.(\d{1,6})\.(\d{1,9})$/;
37
40
  const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
38
41
  const VERIFIED_MCP_EXECUTABLE_TARGET = Symbol('verified_mcp_executable_target');
@@ -79,10 +82,19 @@ function mcpVersionsInText(value) {
79
82
  .map((match) => match[1]);
80
83
  }
81
84
 
85
+ function marrowManagedBlockInText(value) {
86
+ const content = String(value || '');
87
+ const start = content.indexOf(MARROW_BLOCK_START);
88
+ if (start < 0) return '';
89
+ const end = content.indexOf(MARROW_BLOCK_END, start + MARROW_BLOCK_START.length);
90
+ if (end < 0) return '';
91
+ return content.slice(start, end + MARROW_BLOCK_END.length);
92
+ }
93
+
82
94
  function unverifiedAheadMcpVersions(versions, targetVersion = MCP_ADAPTER_VERSION) {
83
- if (!compatibleMcpTargetVersion(targetVersion)) return [];
95
+ if (!parsedStableMcpVersion(targetVersion)) return [];
84
96
  return [...new Set((Array.isArray(versions) ? versions : [])
85
- .filter((version) => compatibleMcpTargetVersion(version)
97
+ .filter((version) => parsedStableMcpVersion(version)
86
98
  && compareMcpVersions(version, targetVersion) > 0))].sort((left, right) => compareMcpVersions(left, right));
87
99
  }
88
100
 
@@ -554,7 +566,6 @@ function inspectMcpProcesses(options = {}) {
554
566
  const aheadUnverified = aheadUnverifiedVersions.length > 0;
555
567
  const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0 || aheadUnverified;
556
568
  const automaticRepairSuppressed = aheadUnverified;
557
- const repairCommand = `npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup`;
558
569
  return {
559
570
  available: process.platform === 'linux' || Array.isArray(options.commands),
560
571
  expected_version: expectedVersion,
@@ -570,21 +581,30 @@ function inspectMcpProcesses(options = {}) {
570
581
  registry_verification_required: aheadUnverified,
571
582
  exact_fix: automaticRepairSuppressed
572
583
  ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
573
- : needsRepair ? repairCommand : null,
584
+ : needsRepair ? INSTALLER_UPDATE_COMMAND : null,
574
585
  restart_required: needsRepair && !automaticRepairSuppressed,
575
586
  restart_instruction: needsRepair && !automaticRepairSuppressed
576
- ? 'Restart every owning harness to replace its active Marrow MCP process.'
587
+ ? INSTALLER_RESTART_INSTRUCTION
577
588
  : null,
578
- verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
589
+ verification_command: needsRepair && !automaticRepairSuppressed ? INSTALLER_DOCTOR_COMMAND : null,
579
590
  };
580
591
  }
581
592
 
582
593
  function inspectMcpConfigurations(detection, options = {}) {
583
594
  const home = options.home || process.env.HOME || process.env.USERPROFILE || os.homedir();
584
595
  const configuredPaths = Array.isArray(options.paths) ? options.paths : [
596
+ detection?.paths?.agentsMd,
597
+ detection?.paths?.mcpJson,
585
598
  detection?.paths?.claudeSettings,
599
+ detection?.paths?.codexHooks,
600
+ detection?.paths?.cursorHooks,
586
601
  detection?.paths?.cursorMcp,
587
- detection?.paths?.mcpJson,
602
+ detection?.paths?.clinePreToolUseHook,
603
+ detection?.paths?.clinePostToolUseHook,
604
+ detection?.paths?.clineTaskCancelHook,
605
+ detection?.paths?.windsurfHooks,
606
+ detection?.paths?.geminiSettings,
607
+ detection?.paths?.grokHooks,
588
608
  path.join(home, '.claude', 'settings.json'),
589
609
  path.join(home, '.claude.json'),
590
610
  path.join(home, '.cursor', 'mcp.json'),
@@ -601,9 +621,11 @@ function inspectMcpConfigurations(detection, options = {}) {
601
621
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024) continue;
602
622
  filesChecked += 1;
603
623
  const raw = fs.readFileSync(filePath, 'utf8');
604
- const specs = [...raw.matchAll(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g)]
605
- .map((match) => match[1]);
606
- if (/@getmarrow\/mcp(?:@|["'\s])/.test(raw)) {
624
+ const inspected = path.basename(filePath) === 'AGENTS.md'
625
+ ? marrowManagedBlockInText(raw)
626
+ : raw;
627
+ const specs = mcpVersionsInText(inspected);
628
+ if (/@getmarrow\/mcp(?:@|["'\s])/.test(inspected)) {
607
629
  configurationsFound += 1;
608
630
  if (specs.length === 0) unknownVersionConfigurations += 1;
609
631
  }
@@ -639,8 +661,10 @@ function inspectMcpConfigurations(detection, options = {}) {
639
661
  ? null
640
662
  : aheadUnverified
641
663
  ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
642
- : `Run npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup in each owning workspace, update its MCP launch to npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp, then restart that harness.`,
643
- verification_command: healthy ? null : 'npx -y @getmarrow/install@latest doctor --self-test',
664
+ : INSTALLER_UPDATE_COMMAND,
665
+ restart_required: !healthy && !aheadUnverified,
666
+ restart_instruction: !healthy && !aheadUnverified ? INSTALLER_RESTART_INSTRUCTION : null,
667
+ verification_command: !healthy && !aheadUnverified ? INSTALLER_DOCTOR_COMMAND : null,
644
668
  };
645
669
  }
646
670
 
@@ -1691,6 +1715,75 @@ function grokNativeHookFingerprint(settings) {
1691
1715
  })).digest('hex');
1692
1716
  }
1693
1717
 
1718
+ function grokMarrowHookSubcommand(command) {
1719
+ const direct = marrowHookSubcommand(command);
1720
+ if (direct) return direct;
1721
+ if (typeof command !== 'string') return null;
1722
+ const guarded = command.match(
1723
+ /@getmarrow\/mcp@[^\s"',]+["'],["']marrow-mcp["'],["']grok-(pre-action-hook)["']/,
1724
+ );
1725
+ return guarded?.[1] || null;
1726
+ }
1727
+
1728
+ function reconcileGrokHook(settings, eventName, subcommand, command, matcher, timeout) {
1729
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1730
+ const retained = [];
1731
+ let preferredHandler = null;
1732
+ for (const entry of original) {
1733
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
1734
+ retained.push(entry);
1735
+ continue;
1736
+ }
1737
+ const remaining = [];
1738
+ for (const handler of entry.hooks) {
1739
+ const detected = handler && typeof handler === 'object' && !Array.isArray(handler)
1740
+ && handler.type === 'command' ? grokMarrowHookSubcommand(handler.command) : null;
1741
+ if (detected) {
1742
+ if (detected === subcommand && !preferredHandler) preferredHandler = handler;
1743
+ continue;
1744
+ }
1745
+ remaining.push(handler);
1746
+ }
1747
+ if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
1748
+ }
1749
+ if (!command) return retained;
1750
+ const canonical = {
1751
+ ...(matcher === undefined ? {} : { matcher }),
1752
+ hooks: [{ ...preferredHandler, type: 'command', command, timeout }],
1753
+ };
1754
+ return [...retained, canonical];
1755
+ }
1756
+
1757
+ function upsertGrokHooks(settingsPath) {
1758
+ const settings = parseJsonObject(settingsPath);
1759
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1760
+ ? settings.hooks
1761
+ : {};
1762
+ settings.hooks = {
1763
+ ...hooks,
1764
+ UserPromptSubmit: reconcileGrokHook(settings, 'UserPromptSubmit', 'context-hook', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
1765
+ PreToolUse: reconcileGrokHook(settings, 'PreToolUse', 'pre-action-hook', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
1766
+ PostToolUse: reconcileGrokHook(settings, 'PostToolUse', 'hook', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1767
+ PostToolUseFailure: reconcileGrokHook(settings, 'PostToolUseFailure', 'hook', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1768
+ Stop: reconcileGrokHook(settings, 'Stop', 'session-hook', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
1769
+ SessionEnd: reconcileGrokHook(settings, 'SessionEnd', 'session-hook', null, undefined, 3),
1770
+ };
1771
+ return JSON.stringify(settings, null, 2) + '\n';
1772
+ }
1773
+
1774
+ function managedGrokHooksFile(filePath) {
1775
+ if (!exists(filePath)) return false;
1776
+ try {
1777
+ const stat = fs.lstatSync(filePath);
1778
+ if (stat.isSymbolicLink() || !stat.isFile()) return true;
1779
+ if (stat.size > 2 * 1024 * 1024) return true;
1780
+ return /@getmarrow\/mcp@[^\s"']+[\s\S]{0,4096}marrow-mcp[\s\S]{0,512}grok-(?:context-hook|pre-action-hook|hook|session-hook)/
1781
+ .test(fs.readFileSync(filePath, 'utf8'));
1782
+ } catch {
1783
+ return true;
1784
+ }
1785
+ }
1786
+
1694
1787
  function upsertGeminiHooks(settingsPath) {
1695
1788
  const settings = parseJsonObject(settingsPath);
1696
1789
  const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
@@ -2303,6 +2396,15 @@ function buildPlan(detection, options) {
2303
2396
  transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
2304
2397
  });
2305
2398
  }
2399
+ if (options.update && detection.grok && managedGrokHooksFile(detection.paths.grokHooks)) {
2400
+ writes.push({
2401
+ type: 'json-transform',
2402
+ path: detection.paths.grokHooks,
2403
+ root: path.resolve(detection.paths.grokHooks, '..', '..', '..'),
2404
+ label: 'Grok native hooks',
2405
+ transform: (filePath) => retarget(upsertGrokHooks(filePath)),
2406
+ });
2407
+ }
2306
2408
  writes.push({
2307
2409
  type: 'json-transform',
2308
2410
  path: detection.paths.mcpJson,
@@ -2405,12 +2507,17 @@ function atomicWriteManagedFile(root, targetPath, contents) {
2405
2507
  function applyPlan(plan, options) {
2406
2508
  if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
2407
2509
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
2408
- for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
2510
+ for (const write of plan.writes) {
2511
+ assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2512
+ }
2409
2513
  const prepared = plan.writes.map((write) => {
2410
2514
  const fileExists = exists(write.path);
2411
2515
  const before = safeRead(write.path);
2516
+ const inspectedBefore = write.type === 'md-block'
2517
+ ? marrowManagedBlockInText(before)
2518
+ : before;
2412
2519
  const aheadUnverifiedVersions = unverifiedAheadMcpVersions(
2413
- mcpVersionsInText(before),
2520
+ mcpVersionsInText(inspectedBefore),
2414
2521
  plan.mcp_target_version,
2415
2522
  );
2416
2523
  const automaticRepairSuppressed = aheadUnverifiedVersions.length > 0;
@@ -2481,10 +2588,10 @@ function applyPlan(plan, options) {
2481
2588
  ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
2482
2589
  });
2483
2590
  if (contentChanged && writeApplied) {
2484
- atomicWriteManagedFile(root, write.path, after);
2591
+ atomicWriteManagedFile(path.resolve(write.root || root), write.path, after);
2485
2592
  }
2486
2593
  if (modeChanged && writeApplied) {
2487
- assertContainedManagedTarget(root, write.path);
2594
+ assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2488
2595
  fs.chmodSync(write.path, write.mode);
2489
2596
  }
2490
2597
  }
@@ -2997,12 +3104,12 @@ function printReport(report) {
2997
3104
  }
2998
3105
  }
2999
3106
 
3000
- if (report.harnessReload) {
3107
+ if (report.harnessReload && report.writeMode !== 'doctor') {
3001
3108
  process.stdout.write('\nHarness reload:\n');
3002
3109
  process.stdout.write(`- required: ${report.harnessReload.required ? 'yes' : 'no'}\n`);
3003
3110
  process.stdout.write(`- live in this process: ${report.harnessReload.live_in_this_process ? 'yes' : 'no'}\n`);
3004
- if (report.harnessReload.instruction) process.stdout.write(`- restart: ${report.harnessReload.instruction}\n`);
3005
- if (report.harnessReload.prove_command) process.stdout.write(`- prove after restart: ${report.harnessReload.prove_command}\n`);
3111
+ if (report.harnessReload.required && report.harnessReload.instruction) process.stdout.write(`- restart: ${report.harnessReload.instruction}\n`);
3112
+ if (report.harnessReload.required && report.harnessReload.prove_command) process.stdout.write(`- prove after restart: ${report.harnessReload.prove_command}\n`);
3006
3113
  }
3007
3114
  if (report.firstCapture) {
3008
3115
  process.stdout.write('\nFirst capture:\n');
@@ -3049,7 +3156,9 @@ function printReport(report) {
3049
3156
  if (report.controller?.exact_fix) process.stdout.write(`- exact fix: ${report.controller.exact_fix}\n`);
3050
3157
 
3051
3158
  if (report.writeMode === 'doctor') {
3052
- const liveHere = !report.harnessReload?.required;
3159
+ const liveHere = !report.harnessReload?.required
3160
+ && report.doctor.mcpProcesses?.healthy !== false
3161
+ && report.doctor.mcpConfigurations?.healthy !== false;
3053
3162
  const activeLabel = report.doctor.active && liveHere
3054
3163
  ? 'yes'
3055
3164
  : report.doctor.active
@@ -3066,8 +3175,6 @@ function printReport(report) {
3066
3175
  process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
3067
3176
  if (processes.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP clients: ${processes.ahead_unverified_versions.join(', ')}\n`);
3068
3177
  if (processes.automatic_repair_suppressed) process.stdout.write('- automatic MCP process repair: suppressed pending official registry verification\n');
3069
- if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
3070
- if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
3071
3178
  }
3072
3179
  if (report.doctor.mcpConfigurations) {
3073
3180
  const configurations = report.doctor.mcpConfigurations;
@@ -3077,6 +3184,10 @@ function printReport(report) {
3077
3184
  if (configurations.automatic_repair_suppressed) process.stdout.write('- automatic MCP configuration repair: suppressed pending official registry verification\n');
3078
3185
  }
3079
3186
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
3187
+ if (report.doctor.mcp_update_required) {
3188
+ process.stdout.write(`- restart once after update: ${report.doctor.restart_instruction}\n`);
3189
+ process.stdout.write(`- verify after restart: ${report.doctor.verification_command}\n`);
3190
+ }
3080
3191
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
3081
3192
  }
3082
3193
 
@@ -3176,6 +3287,8 @@ async function install(options) {
3176
3287
  const registryVerificationAction = automaticMcpRepairSuppressed
3177
3288
  ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
3178
3289
  : null;
3290
+ const mcpUpdateRequired = !automaticMcpRepairSuppressed
3291
+ && (!mcpConfigurations.healthy || (!options.update && !mcpProcesses.healthy));
3179
3292
  if (automaticMcpRepairSuppressed) {
3180
3293
  profile = {
3181
3294
  ...profile,
@@ -3208,7 +3321,40 @@ async function install(options) {
3208
3321
  if (options.activate && !selfTest.activation_verified) {
3209
3322
  throw new Error('Marrow activation failed: server confirmation was not returned');
3210
3323
  }
3211
- const harnessReload = harnessReloadPlan(detection, changes);
3324
+ const plannedHarnessReload = harnessReloadPlan(detection, options.doctor ? [] : changes);
3325
+ const updateNeedsRestart = Boolean(options.update
3326
+ && !automaticMcpRepairSuppressed
3327
+ && (plannedHarnessReload.required || (mcpConfigurations.healthy && !mcpProcesses.healthy)));
3328
+ const harnessReload = options.doctor && mcpUpdateRequired
3329
+ ? {
3330
+ ...plannedHarnessReload,
3331
+ required: true,
3332
+ live_in_this_process: false,
3333
+ instruction: INSTALLER_RESTART_INSTRUCTION,
3334
+ prove_command: INSTALLER_DOCTOR_COMMAND,
3335
+ }
3336
+ : updateNeedsRestart
3337
+ ? {
3338
+ ...plannedHarnessReload,
3339
+ required: true,
3340
+ live_in_this_process: false,
3341
+ instruction: INSTALLER_RESTART_INSTRUCTION,
3342
+ prove_command: INSTALLER_DOCTOR_COMMAND,
3343
+ }
3344
+ : plannedHarnessReload;
3345
+ const updateAwaitingRestart = Boolean(options.update
3346
+ && updateNeedsRestart
3347
+ && mcpConfigurations.healthy
3348
+ && !mcpProcesses.healthy
3349
+ && !automaticMcpRepairSuppressed);
3350
+ const reportedMcpProcesses = updateAwaitingRestart
3351
+ ? {
3352
+ ...mcpProcesses,
3353
+ update_completed: true,
3354
+ awaiting_restart: true,
3355
+ exact_fix: null,
3356
+ }
3357
+ : mcpProcesses;
3212
3358
  if (harnessReload.required && selfTest.mcp_tool_profile) {
3213
3359
  selfTest.mcp_tool_profile = {
3214
3360
  ...selfTest.mcp_tool_profile,
@@ -3268,6 +3414,8 @@ async function install(options) {
3268
3414
  automaticMcpRepairSuppressed,
3269
3415
  message: automaticMcpRepairSuppressed
3270
3416
  ? registryVerificationAction
3417
+ : options.update && harnessReload.required
3418
+ ? 'Managed Marrow configuration is synchronized. Running MCP processes are unchanged; complete the Harness reload steps below before treating the update as live.'
3271
3419
  : selfTestPassed
3272
3420
  ? selfTest.health === 'healthy'
3273
3421
  ? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
@@ -3277,7 +3425,6 @@ async function install(options) {
3277
3425
  : `Config repair ran, but self-test failed: ${selfTest.error || 'unknown error'}.`,
3278
3426
  }
3279
3427
  : null;
3280
-
3281
3428
  return {
3282
3429
  root: detection.root,
3283
3430
  adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
@@ -3314,12 +3461,18 @@ async function install(options) {
3314
3461
  missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
3315
3462
  envHints,
3316
3463
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
3317
- mcpProcesses,
3464
+ mcpProcesses: reportedMcpProcesses,
3318
3465
  mcpConfigurations,
3319
3466
  ahead_unverified_versions: aheadUnverifiedVersions,
3320
3467
  automatic_mcp_repair_suppressed: automaticMcpRepairSuppressed,
3321
3468
  registry_verification_action: registryVerificationAction,
3322
- recommendedFix: registryVerificationAction || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3469
+ mcp_update_required: mcpUpdateRequired,
3470
+ restart_required: mcpUpdateRequired || updateAwaitingRestart,
3471
+ restart_instruction: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null,
3472
+ verification_command: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_DOCTOR_COMMAND : null,
3473
+ recommendedFix: registryVerificationAction
3474
+ || (updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null)
3475
+ || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3323
3476
  ? envHints.length
3324
3477
  ? `MARROW_API_KEY was found in a likely env file at ${envHints[0]}. Load that key from trusted secret storage, export only MARROW_API_KEY, then run npx @getmarrow/install --repair.`
3325
3478
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -3339,13 +3492,13 @@ async function install(options) {
3339
3492
  : []),
3340
3493
  ...(mcpProcesses.ahead_unverified
3341
3494
  ? ['Registry-unverified ahead Marrow MCP clients are active. Automatic repair is suppressed; run the exact registry verification action before owner-directed replacement.']
3342
- : !mcpProcesses.healthy
3343
- ? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
3495
+ : !mcpProcesses.healthy && !options.update && !options.doctor
3496
+ ? [`Stale, mixed, or version-unknown Marrow MCP clients are active. Run ${INSTALLER_UPDATE_COMMAND}; the installer will report the one required restart and later doctor verification.`]
3344
3497
  : []),
3345
3498
  ...(mcpConfigurations.ahead_unverified || changes.some((change) => change.automatic_repair_suppressed)
3346
3499
  ? ['Registry-unverified ahead Marrow MCP configuration was preserved. It was not copied into other managed targets, and automatic repair is suppressed for that surface.']
3347
- : !mcpConfigurations.healthy
3348
- ? ['Stale, mixed, or version-unknown Marrow MCP versions remain in owner configuration. Repair each owning workspace and restart its harness.']
3500
+ : !mcpConfigurations.healthy && !options.update && !options.doctor
3501
+ ? [`Stale, mixed, or version-unknown Marrow MCP versions remain in the detected workspace. Run ${INSTALLER_UPDATE_COMMAND} once to synchronize its managed surfaces.`]
3349
3502
  : []),
3350
3503
  ],
3351
3504
  };