@getmarrow/install 0.1.55 → 0.1.57

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 +17 -7
  2. package/package.json +1 -1
  3. package/src/installer.js +220 -42
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,15 @@ 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.57
105
+
106
+ v0.1.57 pins the npm-verified MCP `3.9.88` and SDK `3.7.63` packages. Doctor now recognizes an installed and lockfile-verified SDK `3.7.63` instead of suggesting `3.7.62`. If a workspace has a newer stable SDK version, doctor preserves it and asks for official registry verification before replacement. The generated MCP setup, hooks, and update instructions target `3.9.88`; restart the owning harness and run `npx @getmarrow/install@latest doctor --self-test` after activation.
107
+
108
+ ## Previous: v0.1.56
109
+
110
+ 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.
111
+
112
+ ## Previous: v0.1.55
103
113
 
104
114
  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
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
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,11 +10,11 @@ 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==';
16
- const SDK_ADAPTER_VERSION = '3.7.62';
17
- const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
13
+ const MCP_ADAPTER_VERSION = '3.9.88';
14
+ const MCP_ADAPTER_SOURCE_SHA = '2385f0554dd9455e6a7d2c0396058710ab56611a';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-1WQtVU8oT1XLGFa9aINm9Ypn/xhNh/mPLpK2bziPIxRURsAGrs8TAfb23JL7HXDcmpXFgMLd8R6gmltVbBbHIg==';
16
+ const SDK_ADAPTER_VERSION = '3.7.63';
17
+ const SDK_ADAPTER_INTEGRITY = 'sha512-5BiV1P0J1NMVdgjqxqULfM+zGrTjHBuWm1amKZWba4yW6C3t10VlhIlj/A+4YKbKqvyZe8gXgUwW+VrYDbVQrQ==';
18
18
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
19
19
  const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
20
20
  const ADAPTER_PROVENANCE = Object.freeze({
@@ -23,7 +23,7 @@ const ADAPTER_PROVENANCE = Object.freeze({
23
23
  version: MCP_ADAPTER_VERSION,
24
24
  source_sha: MCP_ADAPTER_SOURCE_SHA,
25
25
  integrity: MCP_ADAPTER_INTEGRITY,
26
- integrity_state: 'sealed_local_candidate',
26
+ integrity_state: 'verified_npm_registry_metadata',
27
27
  }),
28
28
  sdk: Object.freeze({
29
29
  package: '@getmarrow/sdk',
@@ -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');
@@ -65,6 +68,13 @@ function compareMcpVersions(left, right) {
65
68
  return 0;
66
69
  }
67
70
 
71
+ function declaredStableSdkLowerBound(spec) {
72
+ if (typeof spec !== 'string') return null;
73
+ const match = spec.trim().match(/^(v|\^|~|>=|>)?(\d{1,6}\.\d{1,6}\.\d{1,9})$/);
74
+ if (!match || !parsedStableMcpVersion(match[2])) return null;
75
+ return { version: match[2], exclusive: match[1] === '>' };
76
+ }
77
+
68
78
  function compatibleMcpTargetVersion(value) {
69
79
  const candidate = parsedStableMcpVersion(value);
70
80
  const sealed = parsedStableMcpVersion(MCP_ADAPTER_VERSION);
@@ -79,10 +89,19 @@ function mcpVersionsInText(value) {
79
89
  .map((match) => match[1]);
80
90
  }
81
91
 
92
+ function marrowManagedBlockInText(value) {
93
+ const content = String(value || '');
94
+ const start = content.indexOf(MARROW_BLOCK_START);
95
+ if (start < 0) return '';
96
+ const end = content.indexOf(MARROW_BLOCK_END, start + MARROW_BLOCK_START.length);
97
+ if (end < 0) return '';
98
+ return content.slice(start, end + MARROW_BLOCK_END.length);
99
+ }
100
+
82
101
  function unverifiedAheadMcpVersions(versions, targetVersion = MCP_ADAPTER_VERSION) {
83
- if (!compatibleMcpTargetVersion(targetVersion)) return [];
102
+ if (!parsedStableMcpVersion(targetVersion)) return [];
84
103
  return [...new Set((Array.isArray(versions) ? versions : [])
85
- .filter((version) => compatibleMcpTargetVersion(version)
104
+ .filter((version) => parsedStableMcpVersion(version)
86
105
  && compareMcpVersions(version, targetVersion) > 0))].sort((left, right) => compareMcpVersions(left, right));
87
106
  }
88
107
 
@@ -554,7 +573,6 @@ function inspectMcpProcesses(options = {}) {
554
573
  const aheadUnverified = aheadUnverifiedVersions.length > 0;
555
574
  const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0 || aheadUnverified;
556
575
  const automaticRepairSuppressed = aheadUnverified;
557
- const repairCommand = `npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup`;
558
576
  return {
559
577
  available: process.platform === 'linux' || Array.isArray(options.commands),
560
578
  expected_version: expectedVersion,
@@ -570,21 +588,30 @@ function inspectMcpProcesses(options = {}) {
570
588
  registry_verification_required: aheadUnverified,
571
589
  exact_fix: automaticRepairSuppressed
572
590
  ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
573
- : needsRepair ? repairCommand : null,
591
+ : needsRepair ? INSTALLER_UPDATE_COMMAND : null,
574
592
  restart_required: needsRepair && !automaticRepairSuppressed,
575
593
  restart_instruction: needsRepair && !automaticRepairSuppressed
576
- ? 'Restart every owning harness to replace its active Marrow MCP process.'
594
+ ? INSTALLER_RESTART_INSTRUCTION
577
595
  : null,
578
- verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
596
+ verification_command: needsRepair && !automaticRepairSuppressed ? INSTALLER_DOCTOR_COMMAND : null,
579
597
  };
580
598
  }
581
599
 
582
600
  function inspectMcpConfigurations(detection, options = {}) {
583
601
  const home = options.home || process.env.HOME || process.env.USERPROFILE || os.homedir();
584
602
  const configuredPaths = Array.isArray(options.paths) ? options.paths : [
603
+ detection?.paths?.agentsMd,
604
+ detection?.paths?.mcpJson,
585
605
  detection?.paths?.claudeSettings,
606
+ detection?.paths?.codexHooks,
607
+ detection?.paths?.cursorHooks,
586
608
  detection?.paths?.cursorMcp,
587
- detection?.paths?.mcpJson,
609
+ detection?.paths?.clinePreToolUseHook,
610
+ detection?.paths?.clinePostToolUseHook,
611
+ detection?.paths?.clineTaskCancelHook,
612
+ detection?.paths?.windsurfHooks,
613
+ detection?.paths?.geminiSettings,
614
+ detection?.paths?.grokHooks,
588
615
  path.join(home, '.claude', 'settings.json'),
589
616
  path.join(home, '.claude.json'),
590
617
  path.join(home, '.cursor', 'mcp.json'),
@@ -601,9 +628,11 @@ function inspectMcpConfigurations(detection, options = {}) {
601
628
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024) continue;
602
629
  filesChecked += 1;
603
630
  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)) {
631
+ const inspected = path.basename(filePath) === 'AGENTS.md'
632
+ ? marrowManagedBlockInText(raw)
633
+ : raw;
634
+ const specs = mcpVersionsInText(inspected);
635
+ if (/@getmarrow\/mcp(?:@|["'\s])/.test(inspected)) {
607
636
  configurationsFound += 1;
608
637
  if (specs.length === 0) unknownVersionConfigurations += 1;
609
638
  }
@@ -639,8 +668,10 @@ function inspectMcpConfigurations(detection, options = {}) {
639
668
  ? null
640
669
  : aheadUnverified
641
670
  ? 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',
671
+ : INSTALLER_UPDATE_COMMAND,
672
+ restart_required: !healthy && !aheadUnverified,
673
+ restart_instruction: !healthy && !aheadUnverified ? INSTALLER_RESTART_INSTRUCTION : null,
674
+ verification_command: !healthy && !aheadUnverified ? INSTALLER_DOCTOR_COMMAND : null,
644
675
  };
645
676
  }
646
677
 
@@ -1691,6 +1722,75 @@ function grokNativeHookFingerprint(settings) {
1691
1722
  })).digest('hex');
1692
1723
  }
1693
1724
 
1725
+ function grokMarrowHookSubcommand(command) {
1726
+ const direct = marrowHookSubcommand(command);
1727
+ if (direct) return direct;
1728
+ if (typeof command !== 'string') return null;
1729
+ const guarded = command.match(
1730
+ /@getmarrow\/mcp@[^\s"',]+["'],["']marrow-mcp["'],["']grok-(pre-action-hook)["']/,
1731
+ );
1732
+ return guarded?.[1] || null;
1733
+ }
1734
+
1735
+ function reconcileGrokHook(settings, eventName, subcommand, command, matcher, timeout) {
1736
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
1737
+ const retained = [];
1738
+ let preferredHandler = null;
1739
+ for (const entry of original) {
1740
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
1741
+ retained.push(entry);
1742
+ continue;
1743
+ }
1744
+ const remaining = [];
1745
+ for (const handler of entry.hooks) {
1746
+ const detected = handler && typeof handler === 'object' && !Array.isArray(handler)
1747
+ && handler.type === 'command' ? grokMarrowHookSubcommand(handler.command) : null;
1748
+ if (detected) {
1749
+ if (detected === subcommand && !preferredHandler) preferredHandler = handler;
1750
+ continue;
1751
+ }
1752
+ remaining.push(handler);
1753
+ }
1754
+ if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
1755
+ }
1756
+ if (!command) return retained;
1757
+ const canonical = {
1758
+ ...(matcher === undefined ? {} : { matcher }),
1759
+ hooks: [{ ...preferredHandler, type: 'command', command, timeout }],
1760
+ };
1761
+ return [...retained, canonical];
1762
+ }
1763
+
1764
+ function upsertGrokHooks(settingsPath) {
1765
+ const settings = parseJsonObject(settingsPath);
1766
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
1767
+ ? settings.hooks
1768
+ : {};
1769
+ settings.hooks = {
1770
+ ...hooks,
1771
+ UserPromptSubmit: reconcileGrokHook(settings, 'UserPromptSubmit', 'context-hook', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
1772
+ PreToolUse: reconcileGrokHook(settings, 'PreToolUse', 'pre-action-hook', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
1773
+ PostToolUse: reconcileGrokHook(settings, 'PostToolUse', 'hook', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1774
+ PostToolUseFailure: reconcileGrokHook(settings, 'PostToolUseFailure', 'hook', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
1775
+ Stop: reconcileGrokHook(settings, 'Stop', 'session-hook', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
1776
+ SessionEnd: reconcileGrokHook(settings, 'SessionEnd', 'session-hook', null, undefined, 3),
1777
+ };
1778
+ return JSON.stringify(settings, null, 2) + '\n';
1779
+ }
1780
+
1781
+ function managedGrokHooksFile(filePath) {
1782
+ if (!exists(filePath)) return false;
1783
+ try {
1784
+ const stat = fs.lstatSync(filePath);
1785
+ if (stat.isSymbolicLink() || !stat.isFile()) return true;
1786
+ if (stat.size > 2 * 1024 * 1024) return true;
1787
+ return /@getmarrow\/mcp@[^\s"']+[\s\S]{0,4096}marrow-mcp[\s\S]{0,512}grok-(?:context-hook|pre-action-hook|hook|session-hook)/
1788
+ .test(fs.readFileSync(filePath, 'utf8'));
1789
+ } catch {
1790
+ return true;
1791
+ }
1792
+ }
1793
+
1694
1794
  function upsertGeminiHooks(settingsPath) {
1695
1795
  const settings = parseJsonObject(settingsPath);
1696
1796
  const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
@@ -1938,7 +2038,9 @@ function activationProfile(detection, plan, changes, client) {
1938
2038
  ? 'Restart Grok, inspect the installed global hooks with /hooks, and confirm they are enabled. Configuration remains client-self-reported and does not verify observed coverage.'
1939
2039
  : null
1940
2040
  : capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
1941
- ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
2041
+ ? sdkDependency.install_command
2042
+ ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
2043
+ : sdkDependency.warning
1942
2044
  : capabilityLevel === 'governed_wrapper'
1943
2045
  ? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
1944
2046
  : client === 'cline' && clineConflicts.length > 0
@@ -2057,10 +2159,12 @@ function inspectSdkDependency(detection) {
2057
2159
  || objectTargetsSdk(packageJson.resolutions)
2058
2160
  || objectTargetsSdk(packageJson.pnpm?.overrides);
2059
2161
  let lockVerified = false;
2162
+ let lockedVersion = null;
2060
2163
  try {
2061
2164
  const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
2062
2165
  const rootLock = lock?.packages?.[''];
2063
2166
  const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
2167
+ lockedVersion = typeof lockedSdk?.version === 'string' ? lockedSdk.version : null;
2064
2168
  const lockedDeclaration = [
2065
2169
  rootLock?.dependencies,
2066
2170
  rootLock?.devDependencies,
@@ -2090,9 +2194,16 @@ function inspectSdkDependency(detection) {
2090
2194
  installedVersion = null;
2091
2195
  installedName = null;
2092
2196
  }
2093
- const declarationTrusted = typeof declaredSpec === 'string'
2094
- && declaredSpec.trim().length > 0
2095
- && /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
2197
+ const declaredLowerBound = declaredStableSdkLowerBound(declaredSpec);
2198
+ const declarationTrusted = declaredLowerBound !== null;
2199
+ const unsupportedDeclaration = declaredSpec !== null && !declarationTrusted;
2200
+ const declaredComparison = declaredLowerBound
2201
+ ? compareMcpVersions(declaredLowerBound.version, SDK_ADAPTER_VERSION) : null;
2202
+ const declaredAhead = declaredComparison !== null
2203
+ && (declaredComparison > 0 || (declaredLowerBound.exclusive && declaredComparison === 0));
2204
+ const aheadVersions = [installedVersion, lockedVersion]
2205
+ .filter((version) => compareMcpVersions(version, SDK_ADAPTER_VERSION) > 0);
2206
+ const aheadUnverified = aheadVersions.length > 0 || declaredAhead;
2096
2207
  const present = declarationTrusted
2097
2208
  && !overrideDetected
2098
2209
  && lockVerified
@@ -2108,8 +2219,15 @@ function inspectSdkDependency(detection) {
2108
2219
  lock_verified: lockVerified,
2109
2220
  installed_name: installedName,
2110
2221
  installed_version: installedVersion,
2222
+ ahead_unverified: aheadUnverified,
2111
2223
  expected_version: SDK_ADAPTER_VERSION,
2112
- install_command: present ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
2224
+ install_command: present || aheadUnverified || unsupportedDeclaration
2225
+ ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
2226
+ ...(aheadUnverified ? {
2227
+ warning: 'A newer SDK version is configured or installed. Preserve it and verify its exact version and integrity against the official npm registry before changing it.',
2228
+ } : unsupportedDeclaration ? {
2229
+ warning: 'The configured SDK version range is unsupported or ambiguous. Preserve it and verify the intended version against the official npm registry before changing it.',
2230
+ } : {}),
2113
2231
  };
2114
2232
  }
2115
2233
 
@@ -2303,6 +2421,15 @@ function buildPlan(detection, options) {
2303
2421
  transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
2304
2422
  });
2305
2423
  }
2424
+ if (options.update && detection.grok && managedGrokHooksFile(detection.paths.grokHooks)) {
2425
+ writes.push({
2426
+ type: 'json-transform',
2427
+ path: detection.paths.grokHooks,
2428
+ root: path.resolve(detection.paths.grokHooks, '..', '..', '..'),
2429
+ label: 'Grok native hooks',
2430
+ transform: (filePath) => retarget(upsertGrokHooks(filePath)),
2431
+ });
2432
+ }
2306
2433
  writes.push({
2307
2434
  type: 'json-transform',
2308
2435
  path: detection.paths.mcpJson,
@@ -2405,12 +2532,17 @@ function atomicWriteManagedFile(root, targetPath, contents) {
2405
2532
  function applyPlan(plan, options) {
2406
2533
  if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
2407
2534
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
2408
- for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
2535
+ for (const write of plan.writes) {
2536
+ assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2537
+ }
2409
2538
  const prepared = plan.writes.map((write) => {
2410
2539
  const fileExists = exists(write.path);
2411
2540
  const before = safeRead(write.path);
2541
+ const inspectedBefore = write.type === 'md-block'
2542
+ ? marrowManagedBlockInText(before)
2543
+ : before;
2412
2544
  const aheadUnverifiedVersions = unverifiedAheadMcpVersions(
2413
- mcpVersionsInText(before),
2545
+ mcpVersionsInText(inspectedBefore),
2414
2546
  plan.mcp_target_version,
2415
2547
  );
2416
2548
  const automaticRepairSuppressed = aheadUnverifiedVersions.length > 0;
@@ -2481,10 +2613,10 @@ function applyPlan(plan, options) {
2481
2613
  ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
2482
2614
  });
2483
2615
  if (contentChanged && writeApplied) {
2484
- atomicWriteManagedFile(root, write.path, after);
2616
+ atomicWriteManagedFile(path.resolve(write.root || root), write.path, after);
2485
2617
  }
2486
2618
  if (modeChanged && writeApplied) {
2487
- assertContainedManagedTarget(root, write.path);
2619
+ assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2488
2620
  fs.chmodSync(write.path, write.mode);
2489
2621
  }
2490
2622
  }
@@ -2997,12 +3129,12 @@ function printReport(report) {
2997
3129
  }
2998
3130
  }
2999
3131
 
3000
- if (report.harnessReload) {
3132
+ if (report.harnessReload && report.writeMode !== 'doctor') {
3001
3133
  process.stdout.write('\nHarness reload:\n');
3002
3134
  process.stdout.write(`- required: ${report.harnessReload.required ? 'yes' : 'no'}\n`);
3003
3135
  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`);
3136
+ if (report.harnessReload.required && report.harnessReload.instruction) process.stdout.write(`- restart: ${report.harnessReload.instruction}\n`);
3137
+ if (report.harnessReload.required && report.harnessReload.prove_command) process.stdout.write(`- prove after restart: ${report.harnessReload.prove_command}\n`);
3006
3138
  }
3007
3139
  if (report.firstCapture) {
3008
3140
  process.stdout.write('\nFirst capture:\n');
@@ -3038,7 +3170,7 @@ function printReport(report) {
3038
3170
 
3039
3171
  if (report.sdkDependency?.required) {
3040
3172
  process.stdout.write('\nSDK dependency:\n');
3041
- process.stdout.write(`- @getmarrow/sdk: ${report.sdkDependency.present ? 'present' : 'missing'}\n`);
3173
+ process.stdout.write(`- @getmarrow/sdk: ${report.sdkDependency.present ? 'present' : report.sdkDependency.ahead_unverified ? 'newer version requires verification' : 'missing'}\n`);
3042
3174
  if (report.sdkDependency.install_command) process.stdout.write(`- exact fix: ${report.sdkDependency.install_command}\n`);
3043
3175
  if (report.sdkDependency.warning) process.stdout.write(`- warning: ${report.sdkDependency.warning}\n`);
3044
3176
  }
@@ -3049,7 +3181,9 @@ function printReport(report) {
3049
3181
  if (report.controller?.exact_fix) process.stdout.write(`- exact fix: ${report.controller.exact_fix}\n`);
3050
3182
 
3051
3183
  if (report.writeMode === 'doctor') {
3052
- const liveHere = !report.harnessReload?.required;
3184
+ const liveHere = !report.harnessReload?.required
3185
+ && report.doctor.mcpProcesses?.healthy !== false
3186
+ && report.doctor.mcpConfigurations?.healthy !== false;
3053
3187
  const activeLabel = report.doctor.active && liveHere
3054
3188
  ? 'yes'
3055
3189
  : report.doctor.active
@@ -3066,8 +3200,6 @@ function printReport(report) {
3066
3200
  process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
3067
3201
  if (processes.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP clients: ${processes.ahead_unverified_versions.join(', ')}\n`);
3068
3202
  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
3203
  }
3072
3204
  if (report.doctor.mcpConfigurations) {
3073
3205
  const configurations = report.doctor.mcpConfigurations;
@@ -3077,6 +3209,10 @@ function printReport(report) {
3077
3209
  if (configurations.automatic_repair_suppressed) process.stdout.write('- automatic MCP configuration repair: suppressed pending official registry verification\n');
3078
3210
  }
3079
3211
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
3212
+ if (report.doctor.mcp_update_required) {
3213
+ process.stdout.write(`- restart once after update: ${report.doctor.restart_instruction}\n`);
3214
+ process.stdout.write(`- verify after restart: ${report.doctor.verification_command}\n`);
3215
+ }
3080
3216
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
3081
3217
  }
3082
3218
 
@@ -3176,6 +3312,8 @@ async function install(options) {
3176
3312
  const registryVerificationAction = automaticMcpRepairSuppressed
3177
3313
  ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
3178
3314
  : null;
3315
+ const mcpUpdateRequired = !automaticMcpRepairSuppressed
3316
+ && (!mcpConfigurations.healthy || (!options.update && !mcpProcesses.healthy));
3179
3317
  if (automaticMcpRepairSuppressed) {
3180
3318
  profile = {
3181
3319
  ...profile,
@@ -3208,7 +3346,40 @@ async function install(options) {
3208
3346
  if (options.activate && !selfTest.activation_verified) {
3209
3347
  throw new Error('Marrow activation failed: server confirmation was not returned');
3210
3348
  }
3211
- const harnessReload = harnessReloadPlan(detection, changes);
3349
+ const plannedHarnessReload = harnessReloadPlan(detection, options.doctor ? [] : changes);
3350
+ const updateNeedsRestart = Boolean(options.update
3351
+ && !automaticMcpRepairSuppressed
3352
+ && (plannedHarnessReload.required || (mcpConfigurations.healthy && !mcpProcesses.healthy)));
3353
+ const harnessReload = options.doctor && mcpUpdateRequired
3354
+ ? {
3355
+ ...plannedHarnessReload,
3356
+ required: true,
3357
+ live_in_this_process: false,
3358
+ instruction: INSTALLER_RESTART_INSTRUCTION,
3359
+ prove_command: INSTALLER_DOCTOR_COMMAND,
3360
+ }
3361
+ : updateNeedsRestart
3362
+ ? {
3363
+ ...plannedHarnessReload,
3364
+ required: true,
3365
+ live_in_this_process: false,
3366
+ instruction: INSTALLER_RESTART_INSTRUCTION,
3367
+ prove_command: INSTALLER_DOCTOR_COMMAND,
3368
+ }
3369
+ : plannedHarnessReload;
3370
+ const updateAwaitingRestart = Boolean(options.update
3371
+ && updateNeedsRestart
3372
+ && mcpConfigurations.healthy
3373
+ && !mcpProcesses.healthy
3374
+ && !automaticMcpRepairSuppressed);
3375
+ const reportedMcpProcesses = updateAwaitingRestart
3376
+ ? {
3377
+ ...mcpProcesses,
3378
+ update_completed: true,
3379
+ awaiting_restart: true,
3380
+ exact_fix: null,
3381
+ }
3382
+ : mcpProcesses;
3212
3383
  if (harnessReload.required && selfTest.mcp_tool_profile) {
3213
3384
  selfTest.mcp_tool_profile = {
3214
3385
  ...selfTest.mcp_tool_profile,
@@ -3268,6 +3439,8 @@ async function install(options) {
3268
3439
  automaticMcpRepairSuppressed,
3269
3440
  message: automaticMcpRepairSuppressed
3270
3441
  ? registryVerificationAction
3442
+ : options.update && harnessReload.required
3443
+ ? 'Managed Marrow configuration is synchronized. Running MCP processes are unchanged; complete the Harness reload steps below before treating the update as live.'
3271
3444
  : selfTestPassed
3272
3445
  ? selfTest.health === 'healthy'
3273
3446
  ? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
@@ -3277,7 +3450,6 @@ async function install(options) {
3277
3450
  : `Config repair ran, but self-test failed: ${selfTest.error || 'unknown error'}.`,
3278
3451
  }
3279
3452
  : null;
3280
-
3281
3453
  return {
3282
3454
  root: detection.root,
3283
3455
  adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
@@ -3314,12 +3486,18 @@ async function install(options) {
3314
3486
  missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
3315
3487
  envHints,
3316
3488
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
3317
- mcpProcesses,
3489
+ mcpProcesses: reportedMcpProcesses,
3318
3490
  mcpConfigurations,
3319
3491
  ahead_unverified_versions: aheadUnverifiedVersions,
3320
3492
  automatic_mcp_repair_suppressed: automaticMcpRepairSuppressed,
3321
3493
  registry_verification_action: registryVerificationAction,
3322
- recommendedFix: registryVerificationAction || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3494
+ mcp_update_required: mcpUpdateRequired,
3495
+ restart_required: mcpUpdateRequired || updateAwaitingRestart,
3496
+ restart_instruction: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null,
3497
+ verification_command: mcpUpdateRequired || updateAwaitingRestart ? INSTALLER_DOCTOR_COMMAND : null,
3498
+ recommendedFix: registryVerificationAction
3499
+ || (updateAwaitingRestart ? INSTALLER_RESTART_INSTRUCTION : null)
3500
+ || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3323
3501
  ? envHints.length
3324
3502
  ? `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
3503
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -3339,13 +3517,13 @@ async function install(options) {
3339
3517
  : []),
3340
3518
  ...(mcpProcesses.ahead_unverified
3341
3519
  ? ['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.']
3520
+ : !mcpProcesses.healthy && !options.update && !options.doctor
3521
+ ? [`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
3522
  : []),
3345
3523
  ...(mcpConfigurations.ahead_unverified || changes.some((change) => change.automatic_repair_suppressed)
3346
3524
  ? ['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.']
3525
+ : !mcpConfigurations.healthy && !options.update && !options.doctor
3526
+ ? [`Stale, mixed, or version-unknown Marrow MCP versions remain in the detected workspace. Run ${INSTALLER_UPDATE_COMMAND} once to synchronize its managed surfaces.`]
3349
3527
  : []),
3350
3528
  ],
3351
3529
  };