@getmarrow/install 0.1.54 → 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 +17 -7
  2. package/package.json +1 -1
  3. package/src/installer.js +545 -88
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.54
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
109
+
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.
111
+
112
+ ## Previous: v0.1.54
103
113
 
104
114
  v0.1.54 pins sealed MCP candidate `3.9.77` from source `1e782d8ba6bbb54bfaa322f0300565c7176f1969` and makes ordinary setup use the primary tool surface without writing a profile variable. Explicit `core` and `full` selections remain preserved, invalid values fail closed with a bounded repair, and human/JSON self-test output distinguishes configured/effective profile, expected visibility, actual post-reload visibility, and backend-projected entitlement state. Backend projections are status evidence only and always report `authorizes_calls: false`.
105
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.54",
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.77';
14
- const MCP_ADAPTER_SOURCE_SHA = '1e782d8ba6bbb54bfaa322f0300565c7176f1969';
15
- const MCP_ADAPTER_INTEGRITY = 'sha512-4SVvnyTaDh/mhVUry1LaGCYKXSB/mt5NCHijIOLP336jILSqe7ezjZ5/EwEOMTQ2JvLm6w0QE/jmGbcksqe86w==';
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`;
@@ -31,6 +31,196 @@ const ADAPTER_PROVENANCE = Object.freeze({
31
31
  integrity: SDK_ADAPTER_INTEGRITY,
32
32
  }),
33
33
  });
34
+ const MCP_REGISTRY_LATEST_URL = 'https://registry.npmjs.org/%40getmarrow%2Fmcp/latest';
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.';
39
+ const MCP_STABLE_VERSION_RE = /^(\d{1,6})\.(\d{1,6})\.(\d{1,9})$/;
40
+ const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
41
+ const VERIFIED_MCP_EXECUTABLE_TARGET = Symbol('verified_mcp_executable_target');
42
+
43
+ function validSha512Integrity(value) {
44
+ if (!SHA512_INTEGRITY_RE.test(value)) return false;
45
+ const encoded = value.slice('sha512-'.length);
46
+ try {
47
+ const digest = Buffer.from(encoded, 'base64');
48
+ return digest.length === 64 && digest.toString('base64') === encoded;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function parsedStableMcpVersion(value) {
55
+ const match = String(value || '').match(MCP_STABLE_VERSION_RE);
56
+ if (!match) return null;
57
+ const parts = match.slice(1).map(Number);
58
+ return parts.every(Number.isSafeInteger) ? parts : null;
59
+ }
60
+
61
+ function compareMcpVersions(left, right) {
62
+ const leftParts = parsedStableMcpVersion(left);
63
+ const rightParts = parsedStableMcpVersion(right);
64
+ if (!leftParts || !rightParts) return null;
65
+ for (let index = 0; index < leftParts.length; index += 1) {
66
+ if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
67
+ }
68
+ return 0;
69
+ }
70
+
71
+ function compatibleMcpTargetVersion(value) {
72
+ const candidate = parsedStableMcpVersion(value);
73
+ const sealed = parsedStableMcpVersion(MCP_ADAPTER_VERSION);
74
+ return Boolean(candidate && sealed
75
+ && candidate[0] === sealed[0]
76
+ && candidate[1] === sealed[1]
77
+ && compareMcpVersions(value, MCP_ADAPTER_VERSION) >= 0);
78
+ }
79
+
80
+ function mcpVersionsInText(value) {
81
+ return [...String(value || '').matchAll(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g)]
82
+ .map((match) => match[1]);
83
+ }
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
+
94
+ function unverifiedAheadMcpVersions(versions, targetVersion = MCP_ADAPTER_VERSION) {
95
+ if (!parsedStableMcpVersion(targetVersion)) return [];
96
+ return [...new Set((Array.isArray(versions) ? versions : [])
97
+ .filter((version) => parsedStableMcpVersion(version)
98
+ && compareMcpVersions(version, targetVersion) > 0))].sort((left, right) => compareMcpVersions(left, right));
99
+ }
100
+
101
+ function mcpRegistryVerificationAction(versions) {
102
+ const ahead = [...new Set(Array.isArray(versions) ? versions : [])].sort((left, right) => compareMcpVersions(left, right));
103
+ const targets = ahead.map((version) => `@getmarrow/mcp@${version}`).join(', ');
104
+ return `Run ${MCP_REGISTRY_VERIFICATION_COMMAND} with official npm registry access, then rerun npx -y @getmarrow/install@latest doctor --self-test. Automatic repair is suppressed${targets ? ` for ${targets}` : ''}; preserve each existing surface until registry metadata verifies it or the owner chooses the sealed or verified version.`;
105
+ }
106
+
107
+ function verifiedMcpRegistryMetadata(value) {
108
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
109
+ const version = typeof value.version === 'string' ? value.version : '';
110
+ const integrity = typeof value.dist?.integrity === 'string' ? value.dist.integrity : '';
111
+ const tarball = typeof value.dist?.tarball === 'string' ? value.dist.tarball : '';
112
+ const expectedTarball = `https://registry.npmjs.org/@getmarrow/mcp/-/mcp-${version}.tgz`;
113
+ if (value.name !== '@getmarrow/mcp'
114
+ || !compatibleMcpTargetVersion(version)
115
+ || !validSha512Integrity(integrity)
116
+ || tarball !== expectedTarball) return null;
117
+ return { version, integrity, tarball };
118
+ }
119
+
120
+ function resolveMcpTargetVersion(options = {}) {
121
+ const registry = verifiedMcpRegistryMetadata(options.registryMetadata);
122
+ if (registry) {
123
+ const target = {
124
+ version: registry.version,
125
+ source: 'verified_npm_registry',
126
+ integrity: registry.integrity,
127
+ source_sha: null,
128
+ };
129
+ Object.defineProperty(target, VERIFIED_MCP_EXECUTABLE_TARGET, { value: true });
130
+ return target;
131
+ }
132
+ return {
133
+ version: MCP_ADAPTER_VERSION,
134
+ source: 'sealed_installer',
135
+ integrity: MCP_ADAPTER_INTEGRITY,
136
+ source_sha: MCP_ADAPTER_SOURCE_SHA,
137
+ };
138
+ }
139
+
140
+ function executableMcpTarget(options = {}) {
141
+ const target = options.mcpTarget;
142
+ if (target?.[VERIFIED_MCP_EXECUTABLE_TARGET] === true
143
+ && target.source === 'verified_npm_registry'
144
+ && compatibleMcpTargetVersion(target.version)
145
+ && validSha512Integrity(target.integrity)
146
+ && target.source_sha === null) return target;
147
+ return resolveMcpTargetVersion();
148
+ }
149
+
150
+ function expectedMcpInspectionVersion(options = {}) {
151
+ if (options.expectedVersion === MCP_ADAPTER_VERSION) return MCP_ADAPTER_VERSION;
152
+ return executableMcpTarget({ mcpTarget: options.expectedTarget }).version;
153
+ }
154
+
155
+ async function readMcpRegistryMetadata(options = {}) {
156
+ if (Object.prototype.hasOwnProperty.call(options, 'mcpRegistryMetadata')) {
157
+ return options.mcpRegistryMetadata;
158
+ }
159
+ if (options.resolveMcpRegistry !== true) return null;
160
+ const controller = new AbortController();
161
+ const timer = setTimeout(() => controller.abort(), 2500);
162
+ try {
163
+ const fetcher = typeof options.registryFetch === 'function' ? options.registryFetch : fetch;
164
+ const response = await fetcher(MCP_REGISTRY_LATEST_URL, {
165
+ headers: { accept: 'application/json' },
166
+ redirect: 'error',
167
+ signal: controller.signal,
168
+ });
169
+ if (!response.ok) return null;
170
+ return await response.json();
171
+ } catch {
172
+ return null;
173
+ } finally {
174
+ clearTimeout(timer);
175
+ }
176
+ }
177
+
178
+ function adapterProvenanceForMcpTarget(target) {
179
+ if (!target || target.version === MCP_ADAPTER_VERSION) return ADAPTER_PROVENANCE;
180
+ return {
181
+ mcp: {
182
+ package: '@getmarrow/mcp',
183
+ version: target.version,
184
+ source_sha: target.source_sha,
185
+ integrity: target.integrity,
186
+ integrity_state: target.source === 'verified_npm_registry'
187
+ ? 'verified_npm_registry_metadata'
188
+ : 'exact_current_configuration',
189
+ },
190
+ sdk: ADAPTER_PROVENANCE.sdk,
191
+ };
192
+ }
193
+
194
+ function retargetMcpPackageSpec(value, version) {
195
+ const target = compatibleMcpTargetVersion(version) ? version : MCP_ADAPTER_VERSION;
196
+ return String(value).split(MCP_PACKAGE_SPEC).join(`@getmarrow/mcp@${target}`);
197
+ }
198
+
199
+ function retargetMcpDowngradeRecommendation(value, targetVersion) {
200
+ if (typeof value !== 'string' || !compatibleMcpTargetVersion(targetVersion)) return value;
201
+ return value.replace(/@getmarrow\/mcp@(\d+\.\d+\.\d+)/g, (match, version) => (
202
+ parsedStableMcpVersion(version) && version !== targetVersion
203
+ ? `@getmarrow/mcp@${targetVersion}`
204
+ : match
205
+ ));
206
+ }
207
+
208
+ function alignMcpRecommendationVersions(value, targetVersion, key = '', verificationAction = null) {
209
+ if (Array.isArray(value)) {
210
+ return value.map((entry) => alignMcpRecommendationVersions(entry, targetVersion, key, verificationAction));
211
+ }
212
+ if (!value || typeof value !== 'object') {
213
+ return /(?:command|fix|instruction|next_action|notice)$/i.test(key)
214
+ ? verificationAction && typeof value === 'string' && /@getmarrow\/mcp@\d+\.\d+\.\d+/.test(value)
215
+ ? verificationAction
216
+ : retargetMcpDowngradeRecommendation(value, targetVersion)
217
+ : value;
218
+ }
219
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
220
+ entryKey,
221
+ alignMcpRecommendationVersions(entryValue, targetVersion, entryKey, verificationAction),
222
+ ]));
223
+ }
34
224
  const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp context-hook`;
35
225
  const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
36
226
  const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
@@ -366,34 +556,55 @@ function inspectMcpProcesses(options = {}) {
366
556
  .filter(isMcpProcessCommand)
367
557
  .map((command) => explicitMcpVersion(command) || packageMcpVersion(command) || 'unknown');
368
558
  const versions = [...new Set(active.filter((version) => version !== 'unknown'))].sort();
559
+ const expectedVersion = expectedMcpInspectionVersion(options);
369
560
  const unknownVersionProcesses = active.filter((version) => version === 'unknown').length;
370
- const staleVersions = versions.filter((version) => version !== MCP_ADAPTER_VERSION);
561
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(versions, expectedVersion);
562
+ const staleVersions = versions.filter((version) => version !== expectedVersion
563
+ && !aheadUnverifiedVersions.includes(version));
371
564
  const mixedVersions = versions.length > 1 || (versions.length > 0 && unknownVersionProcesses > 0);
372
565
  const stale = staleVersions.length > 0;
373
- const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0;
374
- const repairCommand = `npx -y --package=@getmarrow/mcp@${MCP_ADAPTER_VERSION} marrow-mcp setup`;
566
+ const aheadUnverified = aheadUnverifiedVersions.length > 0;
567
+ const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0 || aheadUnverified;
568
+ const automaticRepairSuppressed = aheadUnverified;
375
569
  return {
376
570
  available: process.platform === 'linux' || Array.isArray(options.commands),
377
- expected_version: MCP_ADAPTER_VERSION,
571
+ expected_version: expectedVersion,
378
572
  active_processes: active.length,
379
573
  active_versions: versions,
380
574
  unknown_version_processes: unknownVersionProcesses,
381
575
  stale_versions: staleVersions,
576
+ ahead_unverified: aheadUnverified,
577
+ ahead_unverified_versions: aheadUnverifiedVersions,
382
578
  mixed_versions: mixedVersions,
383
579
  healthy: !needsRepair,
384
- exact_fix: needsRepair ? repairCommand : null,
385
- restart_required: needsRepair,
386
- restart_instruction: needsRepair ? 'Restart every owning harness to replace its active Marrow MCP process.' : null,
387
- verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
580
+ automatic_repair_suppressed: automaticRepairSuppressed,
581
+ registry_verification_required: aheadUnverified,
582
+ exact_fix: automaticRepairSuppressed
583
+ ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
584
+ : needsRepair ? INSTALLER_UPDATE_COMMAND : null,
585
+ restart_required: needsRepair && !automaticRepairSuppressed,
586
+ restart_instruction: needsRepair && !automaticRepairSuppressed
587
+ ? INSTALLER_RESTART_INSTRUCTION
588
+ : null,
589
+ verification_command: needsRepair && !automaticRepairSuppressed ? INSTALLER_DOCTOR_COMMAND : null,
388
590
  };
389
591
  }
390
592
 
391
593
  function inspectMcpConfigurations(detection, options = {}) {
392
594
  const home = options.home || process.env.HOME || process.env.USERPROFILE || os.homedir();
393
595
  const configuredPaths = Array.isArray(options.paths) ? options.paths : [
596
+ detection?.paths?.agentsMd,
597
+ detection?.paths?.mcpJson,
394
598
  detection?.paths?.claudeSettings,
599
+ detection?.paths?.codexHooks,
600
+ detection?.paths?.cursorHooks,
395
601
  detection?.paths?.cursorMcp,
396
- 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,
397
608
  path.join(home, '.claude', 'settings.json'),
398
609
  path.join(home, '.claude.json'),
399
610
  path.join(home, '.cursor', 'mcp.json'),
@@ -410,9 +621,11 @@ function inspectMcpConfigurations(detection, options = {}) {
410
621
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024) continue;
411
622
  filesChecked += 1;
412
623
  const raw = fs.readFileSync(filePath, 'utf8');
413
- const specs = [...raw.matchAll(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g)]
414
- .map((match) => match[1]);
415
- 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)) {
416
629
  configurationsFound += 1;
417
630
  if (specs.length === 0) unknownVersionConfigurations += 1;
418
631
  }
@@ -422,21 +635,36 @@ function inspectMcpConfigurations(detection, options = {}) {
422
635
  }
423
636
  }
424
637
  const configuredVersions = [...new Set(versions)].sort();
425
- const staleVersions = configuredVersions.filter((version) => version !== MCP_ADAPTER_VERSION);
638
+ const expectedVersion = expectedMcpInspectionVersion(options);
639
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(configuredVersions, expectedVersion);
640
+ const staleVersions = configuredVersions.filter((version) => version !== expectedVersion
641
+ && !aheadUnverifiedVersions.includes(version));
426
642
  const mixedVersions = configuredVersions.length > 1
427
643
  || (configuredVersions.length > 0 && unknownVersionConfigurations > 0);
428
- const healthy = staleVersions.length === 0 && !mixedVersions && unknownVersionConfigurations === 0;
644
+ const aheadUnverified = aheadUnverifiedVersions.length > 0;
645
+ const healthy = staleVersions.length === 0 && !mixedVersions
646
+ && unknownVersionConfigurations === 0 && !aheadUnverified;
429
647
  return {
430
- expected_version: MCP_ADAPTER_VERSION,
648
+ expected_version: expectedVersion,
431
649
  files_checked: filesChecked,
432
650
  configurations_found: configurationsFound,
433
651
  configured_versions: configuredVersions,
434
652
  unknown_version_configurations: unknownVersionConfigurations,
435
653
  stale_versions: staleVersions,
654
+ ahead_unverified: aheadUnverified,
655
+ ahead_unverified_versions: aheadUnverifiedVersions,
436
656
  mixed_versions: mixedVersions,
437
657
  healthy,
438
- exact_fix: healthy ? null : `Run npx -y --package=@getmarrow/mcp@${MCP_ADAPTER_VERSION} marrow-mcp setup in each owning workspace, update its MCP launch to npx -y --package=@getmarrow/mcp@${MCP_ADAPTER_VERSION} marrow-mcp, then restart that harness.`,
439
- verification_command: healthy ? null : 'npx -y @getmarrow/install@latest doctor --self-test',
658
+ automatic_repair_suppressed: aheadUnverified,
659
+ registry_verification_required: aheadUnverified,
660
+ exact_fix: healthy
661
+ ? null
662
+ : aheadUnverified
663
+ ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
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,
440
668
  };
441
669
  }
442
670
 
@@ -575,6 +803,7 @@ function parseArgs(argv, env = process.env) {
575
803
  if (options.activate && options.dryRun) {
576
804
  throw new Error('activate cannot be combined with --dry-run; use --dry-run without activate to preview changes');
577
805
  }
806
+ options.resolveMcpRegistry = Boolean(options.doctor || options.repair || options.update);
578
807
 
579
808
  return options;
580
809
  }
@@ -1486,6 +1715,75 @@ function grokNativeHookFingerprint(settings) {
1486
1715
  })).digest('hex');
1487
1716
  }
1488
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
+
1489
1787
  function upsertGeminiHooks(settingsPath) {
1490
1788
  const settings = parseJsonObject(settingsPath);
1491
1789
  const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
@@ -1603,8 +1901,12 @@ function activationProfile(detection, plan, changes, client) {
1603
1901
  const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
1604
1902
  ? ['pre_action', 'action_result', 'outcome_closure']
1605
1903
  : [...registry.automatic];
1904
+ const mcpTargetVersion = compatibleMcpTargetVersion(plan.mcp_target_version)
1905
+ ? plan.mcp_target_version
1906
+ : MCP_ADAPTER_VERSION;
1907
+ const targetCommand = (command) => retargetMcpPackageSpec(command, mcpTargetVersion);
1606
1908
  const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
1607
- ? MCP_ADAPTER_VERSION
1909
+ ? mcpTargetVersion
1608
1910
  : capabilityLevel === 'sdk_passive_runtime'
1609
1911
  ? SDK_ADAPTER_VERSION
1610
1912
  : INSTALLER_ADAPTER_VERSION;
@@ -1616,71 +1918,71 @@ function activationProfile(detection, plan, changes, client) {
1616
1918
  const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
1617
1919
  const grokSettings = safeJsonObject(detection.paths.grokHooks);
1618
1920
  if (client === 'codex') {
1619
- if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1620
- if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1621
- if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1622
- if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1921
+ if (exactHookConfigured(codexSettings, 'UserPromptSubmit', targetCommand(CODEX_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
1922
+ if (exactHookConfigured(codexSettings, 'PreToolUse', targetCommand(CODEX_PRE_ACTION_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1923
+ if (exactHookConfigured(codexSettings, 'PostToolUse', targetCommand(CODEX_ACTION_RESULT_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1924
+ if (exactHookConfigured(codexSettings, 'SessionEnd', targetCommand(CODEX_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
1623
1925
  } else if (client === 'cursor' || client === 'composer') {
1624
- if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1926
+ if (exactCursorHookConfigured(cursorSettings, 'preToolUse', targetCommand(CURSOR_PRE_ACTION_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1625
1927
  timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1626
1928
  })) observedHooks.push('pre_action');
1627
- if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1929
+ if (exactCursorHookConfigured(cursorSettings, 'postToolUse', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1628
1930
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1629
- }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1931
+ }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1630
1932
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1631
1933
  })) observedHooks.push('action_result');
1632
- if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1934
+ if (exactCursorHookConfigured(cursorSettings, 'stop', targetCommand(CURSOR_SESSION_END_HOOK_COMMAND), undefined, {
1633
1935
  timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1634
1936
  })) observedHooks.push('outcome_closure');
1635
1937
  } else if (client === 'cline') {
1636
1938
  for (const hook of clineHookContract(detection)) {
1637
- if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
1939
+ if (exactExecutableFile(hook.path, targetCommand(hook.content))) observedHooks.push(hook.stage);
1638
1940
  }
1639
1941
  } else if (client === 'windsurf') {
1640
1942
  if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1641
- windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1943
+ windsurfSettings, event, targetCommand(WINDSURF_PRE_ACTION_HOOK_COMMAND),
1642
1944
  ))) observedHooks.push('pre_action');
1643
1945
  if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1644
- windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1946
+ windsurfSettings, event, targetCommand(WINDSURF_ACTION_RESULT_HOOK_COMMAND),
1645
1947
  ))) observedHooks.push('action_result');
1646
1948
  if (exactWindsurfHookConfigured(
1647
- windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1949
+ windsurfSettings, 'post_cascade_response', targetCommand(WINDSURF_SESSION_END_HOOK_COMMAND),
1648
1950
  )) observedHooks.push('response_closeout');
1649
1951
  } else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
1650
1952
  if (exactGeminiHookConfigured(
1651
- geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1953
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', targetCommand(GEMINI_PRE_ACTION_HOOK_COMMAND),
1652
1954
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1653
1955
  )) observedHooks.push('pre_action');
1654
1956
  if (exactGeminiHookConfigured(
1655
- geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1957
+ geminiSettings, 'AfterTool', 'marrow-after-tool', targetCommand(GEMINI_ACTION_RESULT_HOOK_COMMAND),
1656
1958
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1657
1959
  )) observedHooks.push('action_result');
1658
1960
  if (exactGeminiHookConfigured(
1659
- geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1961
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', targetCommand(GEMINI_SESSION_END_HOOK_COMMAND),
1660
1962
  undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1661
1963
  )) observedHooks.push('turn_closeout');
1662
1964
  } else if (client === 'grok') {
1663
1965
  if (exactGrokHookConfigured(
1664
- grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1966
+ grokSettings, 'PreToolUse', targetCommand(GROK_PRE_ACTION_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 7,
1665
1967
  )) observedHooks.push('pre_action');
1666
1968
  if (exactGrokHookConfigured(
1667
- grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1969
+ grokSettings, 'PostToolUse', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
1668
1970
  ) && exactGrokHookConfigured(
1669
- grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1971
+ grokSettings, 'PostToolUseFailure', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
1670
1972
  )) observedHooks.push('action_result');
1671
1973
  if (exactGrokHookConfigured(
1672
- grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1974
+ grokSettings, 'Stop', targetCommand(GROK_SESSION_END_HOOK_COMMAND), undefined, 3,
1673
1975
  ) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
1674
1976
  } else {
1675
1977
  if (capabilityLevel === 'native_hooks'
1676
- && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1978
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', targetCommand(MCP_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
1677
1979
  if (capabilityLevel === 'native_hooks'
1678
- && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1980
+ && exactHookConfigured(claudeSettings, 'PreToolUse', targetCommand(MCP_PRE_ACTION_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1679
1981
  if (capabilityLevel === 'native_hooks'
1680
- && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
1681
- && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1982
+ && exactHookConfigured(claudeSettings, 'PostToolUse', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)
1983
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1682
1984
  if (capabilityLevel === 'native_hooks'
1683
- && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1985
+ && exactHookConfigured(claudeSettings, 'Stop', targetCommand(MCP_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
1684
1986
  }
1685
1987
  const passiveRuntime = safeRead(detection.paths.passiveRuntime);
1686
1988
  if (capabilityLevel === 'sdk_passive_runtime'
@@ -1696,7 +1998,7 @@ function activationProfile(detection, plan, changes, client) {
1696
1998
  if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
1697
1999
  config?.mcpServers?.marrow?.command === 'npx'
1698
2000
  && Array.isArray(config.mcpServers.marrow.args)
1699
- && config.mcpServers.marrow.args.join(' ') === `-y --package=${MCP_PACKAGE_SPEC} marrow-mcp`
2001
+ && config.mcpServers.marrow.args.join(' ') === `-y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp`
1700
2002
  ))) observedHooks.push('mcp_tool_calls');
1701
2003
  const fingerprintMaterial = changes
1702
2004
  .filter((change) => change.applied || change.already_present)
@@ -1737,7 +2039,7 @@ function activationProfile(detection, plan, changes, client) {
1737
2039
  : client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
1738
2040
  ? 'Hooks are explicitly disabled. After owner review, run /hooks enable-all, open /hooks panel, review and approve the project hook fingerprints, then restart Gemini CLI.'
1739
2041
  : client === 'grok'
1740
- ? `Run npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
2042
+ ? `Run npx -y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
1741
2043
  : 'npx @getmarrow/install --repair';
1742
2044
  return {
1743
2045
  adapter_version: adapterVersion,
@@ -2026,6 +2328,8 @@ function buildPlan(detection, options) {
2026
2328
  const client = options.client || detectedClient(detection);
2027
2329
  const agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
2028
2330
  const baseUrl = String(options.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL;
2331
+ const mcpTargetVersion = executableMcpTarget(options).version;
2332
+ const retarget = (value) => retargetMcpPackageSpec(value, mcpTargetVersion);
2029
2333
  const mode = options.mode === 'auto'
2030
2334
  ? detection.node ? 'both' : 'mcp'
2031
2335
  : options.mode;
@@ -2053,7 +2357,7 @@ function buildPlan(detection, options) {
2053
2357
  type: 'json-transform',
2054
2358
  path: detection.paths.claudeSettings,
2055
2359
  label: 'Claude Code MCP passive hooks',
2056
- transform: upsertClaudeHooks,
2360
+ transform: (filePath) => retarget(upsertClaudeHooks(filePath)),
2057
2361
  });
2058
2362
  }
2059
2363
  if (detection.codex) {
@@ -2061,7 +2365,7 @@ function buildPlan(detection, options) {
2061
2365
  type: 'json-transform',
2062
2366
  path: detection.paths.codexHooks,
2063
2367
  label: 'Codex native hooks',
2064
- transform: upsertCodexHooks,
2368
+ transform: (filePath) => retarget(upsertCodexHooks(filePath)),
2065
2369
  });
2066
2370
  }
2067
2371
  if (detection.cline) {
@@ -2070,7 +2374,7 @@ function buildPlan(detection, options) {
2070
2374
  type: 'owned-executable',
2071
2375
  path: hook.path,
2072
2376
  label: hook.label,
2073
- content: hook.content,
2377
+ content: retarget(hook.content),
2074
2378
  mode: 0o755,
2075
2379
  conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
2076
2380
  });
@@ -2081,7 +2385,7 @@ function buildPlan(detection, options) {
2081
2385
  type: 'json-transform',
2082
2386
  path: detection.paths.windsurfHooks,
2083
2387
  label: 'Windsurf native hooks',
2084
- transform: upsertWindsurfHooks,
2388
+ transform: (filePath) => retarget(upsertWindsurfHooks(filePath)),
2085
2389
  });
2086
2390
  }
2087
2391
  if (detection.gemini) {
@@ -2089,27 +2393,36 @@ function buildPlan(detection, options) {
2089
2393
  type: 'json-transform',
2090
2394
  path: detection.paths.geminiSettings,
2091
2395
  label: 'Gemini CLI native hooks',
2092
- transform: upsertGeminiHooks,
2396
+ transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
2397
+ });
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)),
2093
2406
  });
2094
2407
  }
2095
2408
  writes.push({
2096
2409
  type: 'json-transform',
2097
2410
  path: detection.paths.mcpJson,
2098
2411
  label: 'Project MCP server config',
2099
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
2412
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
2100
2413
  });
2101
2414
  if (detection.cursor) {
2102
2415
  writes.push({
2103
2416
  type: 'json-transform',
2104
2417
  path: detection.paths.cursorHooks,
2105
2418
  label: 'Cursor native hooks',
2106
- transform: upsertCursorHooks,
2419
+ transform: (filePath) => retarget(upsertCursorHooks(filePath)),
2107
2420
  });
2108
2421
  writes.push({
2109
2422
  type: 'json-transform',
2110
2423
  path: detection.paths.cursorMcp,
2111
2424
  label: 'Cursor MCP server config',
2112
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
2425
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
2113
2426
  });
2114
2427
  }
2115
2428
  }
@@ -2119,7 +2432,7 @@ function buildPlan(detection, options) {
2119
2432
  type: 'md-block',
2120
2433
  path: detection.paths.agentsMd,
2121
2434
  label: 'Agent instructions',
2122
- block: passiveInstructions(),
2435
+ block: retarget(passiveInstructions()),
2123
2436
  });
2124
2437
  }
2125
2438
 
@@ -2128,11 +2441,11 @@ function buildPlan(detection, options) {
2128
2441
  type: 'file',
2129
2442
  path: detection.paths.cursorRules,
2130
2443
  label: 'Cursor Marrow rule',
2131
- content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
2444
+ content: retarget(passiveInstructions()).replace(/<!--[^>]+-->/g, '').trim() + '\n',
2132
2445
  });
2133
2446
  }
2134
2447
 
2135
- return { mode, root: detection.root, writes };
2448
+ return { mode, root: detection.root, writes, mcp_target_version: mcpTargetVersion };
2136
2449
  }
2137
2450
 
2138
2451
  function assertContainedManagedTarget(root, targetPath) {
@@ -2194,13 +2507,25 @@ function atomicWriteManagedFile(root, targetPath, contents) {
2194
2507
  function applyPlan(plan, options) {
2195
2508
  if (!Array.isArray(plan?.writes) || plan.writes.length === 0) return [];
2196
2509
  const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
2197
- 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
+ }
2198
2513
  const prepared = plan.writes.map((write) => {
2199
2514
  const fileExists = exists(write.path);
2200
2515
  const before = safeRead(write.path);
2516
+ const inspectedBefore = write.type === 'md-block'
2517
+ ? marrowManagedBlockInText(before)
2518
+ : before;
2519
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(
2520
+ mcpVersionsInText(inspectedBefore),
2521
+ plan.mcp_target_version,
2522
+ );
2523
+ const automaticRepairSuppressed = aheadUnverifiedVersions.length > 0;
2201
2524
  let after;
2202
2525
  let hookConflict = false;
2203
- if (write.type === 'file') {
2526
+ if (automaticRepairSuppressed) {
2527
+ after = before;
2528
+ } else if (write.type === 'file') {
2204
2529
  if (write.overwrite === false && before) {
2205
2530
  after = before;
2206
2531
  } else {
@@ -2223,28 +2548,50 @@ function applyPlan(plan, options) {
2223
2548
 
2224
2549
  const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
2225
2550
  const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
2226
- return { write, before, after, hookConflict, modeChanged };
2551
+ return {
2552
+ write,
2553
+ before,
2554
+ after,
2555
+ hookConflict,
2556
+ modeChanged,
2557
+ automaticRepairSuppressed,
2558
+ aheadUnverifiedVersions,
2559
+ };
2227
2560
  });
2228
2561
 
2229
2562
  const changes = [];
2230
- for (const { write, before, after, hookConflict, modeChanged } of prepared) {
2563
+ for (const {
2564
+ write,
2565
+ before,
2566
+ after,
2567
+ hookConflict,
2568
+ modeChanged,
2569
+ automaticRepairSuppressed,
2570
+ aheadUnverifiedVersions,
2571
+ } of prepared) {
2231
2572
  const contentChanged = before !== after;
2232
- const changed = !hookConflict && (contentChanged || modeChanged);
2233
- const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
2573
+ const changed = !automaticRepairSuppressed && !hookConflict && (contentChanged || modeChanged);
2574
+ const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor
2575
+ && !hookConflict && !automaticRepairSuppressed);
2234
2576
  changes.push({
2235
2577
  path: write.path,
2236
2578
  label: write.label,
2237
2579
  changed,
2238
2580
  applied: changed && writeApplied,
2239
- already_present: !changed && !hookConflict,
2581
+ already_present: !changed && !hookConflict && !automaticRepairSuppressed,
2240
2582
  hook_conflict: hookConflict,
2583
+ automatic_repair_suppressed: automaticRepairSuppressed,
2584
+ ...(automaticRepairSuppressed ? {
2585
+ ahead_unverified_versions: aheadUnverifiedVersions,
2586
+ exact_fix: mcpRegistryVerificationAction(aheadUnverifiedVersions),
2587
+ } : {}),
2241
2588
  ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
2242
2589
  });
2243
2590
  if (contentChanged && writeApplied) {
2244
- atomicWriteManagedFile(root, write.path, after);
2591
+ atomicWriteManagedFile(path.resolve(write.root || root), write.path, after);
2245
2592
  }
2246
2593
  if (modeChanged && writeApplied) {
2247
- assertContainedManagedTarget(root, write.path);
2594
+ assertContainedManagedTarget(path.resolve(write.root || root), write.path);
2248
2595
  fs.chmodSync(write.path, write.mode);
2249
2596
  }
2250
2597
  }
@@ -2303,7 +2650,7 @@ async function runSelfTest(options) {
2303
2650
  'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
2304
2651
  'x-marrow-install-version': INSTALLER_ADAPTER_VERSION,
2305
2652
  'x-marrow-sdk-version': SDK_ADAPTER_VERSION,
2306
- 'x-marrow-mcp-version': MCP_ADAPTER_VERSION,
2653
+ 'x-marrow-mcp-version': executableMcpTarget(options).version,
2307
2654
  };
2308
2655
  if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
2309
2656
 
@@ -2668,8 +3015,13 @@ function printReport(report) {
2668
3015
 
2669
3016
  process.stdout.write('\nPlanned changes:\n');
2670
3017
  for (const change of report.changes) {
2671
- const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
3018
+ const marker = change.automatic_repair_suppressed
3019
+ ? 'preserved unverified-ahead surface; repair suppressed'
3020
+ : change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
2672
3021
  process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
3022
+ if (change.automatic_repair_suppressed && change.exact_fix) {
3023
+ process.stdout.write(` exact verification: ${change.exact_fix}\n`);
3024
+ }
2673
3025
  }
2674
3026
 
2675
3027
  process.stdout.write('\nSelf-test:\n');
@@ -2752,12 +3104,12 @@ function printReport(report) {
2752
3104
  }
2753
3105
  }
2754
3106
 
2755
- if (report.harnessReload) {
3107
+ if (report.harnessReload && report.writeMode !== 'doctor') {
2756
3108
  process.stdout.write('\nHarness reload:\n');
2757
3109
  process.stdout.write(`- required: ${report.harnessReload.required ? 'yes' : 'no'}\n`);
2758
3110
  process.stdout.write(`- live in this process: ${report.harnessReload.live_in_this_process ? 'yes' : 'no'}\n`);
2759
- if (report.harnessReload.instruction) process.stdout.write(`- restart: ${report.harnessReload.instruction}\n`);
2760
- 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`);
2761
3113
  }
2762
3114
  if (report.firstCapture) {
2763
3115
  process.stdout.write('\nFirst capture:\n');
@@ -2804,7 +3156,9 @@ function printReport(report) {
2804
3156
  if (report.controller?.exact_fix) process.stdout.write(`- exact fix: ${report.controller.exact_fix}\n`);
2805
3157
 
2806
3158
  if (report.writeMode === 'doctor') {
2807
- const liveHere = !report.harnessReload?.required;
3159
+ const liveHere = !report.harnessReload?.required
3160
+ && report.doctor.mcpProcesses?.healthy !== false
3161
+ && report.doctor.mcpConfigurations?.healthy !== false;
2808
3162
  const activeLabel = report.doctor.active && liveHere
2809
3163
  ? 'yes'
2810
3164
  : report.doctor.active
@@ -2819,15 +3173,21 @@ function printReport(report) {
2819
3173
  const processes = report.doctor.mcpProcesses;
2820
3174
  process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
2821
3175
  process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
2822
- if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
2823
- if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
3176
+ if (processes.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP clients: ${processes.ahead_unverified_versions.join(', ')}\n`);
3177
+ if (processes.automatic_repair_suppressed) process.stdout.write('- automatic MCP process repair: suppressed pending official registry verification\n');
2824
3178
  }
2825
3179
  if (report.doctor.mcpConfigurations) {
2826
3180
  const configurations = report.doctor.mcpConfigurations;
2827
3181
  process.stdout.write(`- configured MCP versions: ${configurations.configured_versions.length ? configurations.configured_versions.join(', ') : 'none pinned'}\n`);
2828
3182
  process.stdout.write(`- stale/mixed/version-unknown MCP configuration: ${configurations.healthy ? 'no' : 'yes'}\n`);
3183
+ if (configurations.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP configuration: ${configurations.ahead_unverified_versions.join(', ')}\n`);
3184
+ if (configurations.automatic_repair_suppressed) process.stdout.write('- automatic MCP configuration repair: suppressed pending official registry verification\n');
2829
3185
  }
2830
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
+ }
2831
3191
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
2832
3192
  }
2833
3193
 
@@ -2863,10 +3223,23 @@ async function install(options) {
2863
3223
  const client = detectedClient(detection);
2864
3224
  options.client = client;
2865
3225
  options.agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
3226
+ const observedMcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
3227
+ const observedMcpConfigurations = inspectMcpConfigurations(detection, { paths: options.mcpConfigPaths });
3228
+ const registryMetadata = await readMcpRegistryMetadata(options);
3229
+ const latestTargetOperation = Boolean(options.doctor || options.repair || options.update);
3230
+ const mcpTarget = resolveMcpTargetVersion({
3231
+ currentVersions: latestTargetOperation ? [
3232
+ ...observedMcpProcesses.active_versions,
3233
+ ...observedMcpConfigurations.configured_versions,
3234
+ ] : [],
3235
+ registryMetadata: latestTargetOperation ? registryMetadata : null,
3236
+ });
3237
+ options.mcpTarget = mcpTarget;
3238
+ options.mcpTargetVersion = mcpTarget.version;
2866
3239
  const plan = buildPlan(detection, options);
2867
3240
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
2868
3241
  const changes = applyPlan(plan, options);
2869
- const profile = activationProfile(detection, plan, changes, client);
3242
+ let profile = activationProfile(detection, plan, changes, client);
2870
3243
  options.activation = options.activate ? {
2871
3244
  harness: client,
2872
3245
  agent_id: options.agentId,
@@ -2897,8 +3270,35 @@ async function install(options) {
2897
3270
  ? repairConfigDiagnostics(configDiagnostics)
2898
3271
  : [];
2899
3272
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
2900
- const mcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
2901
- const mcpConfigurations = inspectMcpConfigurations(detection, { paths: options.mcpConfigPaths });
3273
+ const mcpProcesses = inspectMcpProcesses({
3274
+ commands: options.processCommands,
3275
+ expectedTarget: mcpTarget,
3276
+ });
3277
+ const mcpConfigurations = inspectMcpConfigurations(detection, {
3278
+ paths: options.mcpConfigPaths,
3279
+ expectedTarget: mcpTarget,
3280
+ });
3281
+ const aheadUnverifiedVersions = [...new Set([
3282
+ ...mcpProcesses.ahead_unverified_versions,
3283
+ ...mcpConfigurations.ahead_unverified_versions,
3284
+ ...changes.flatMap((change) => change.ahead_unverified_versions || []),
3285
+ ])].sort((left, right) => compareMcpVersions(left, right));
3286
+ const automaticMcpRepairSuppressed = aheadUnverifiedVersions.length > 0;
3287
+ const registryVerificationAction = automaticMcpRepairSuppressed
3288
+ ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
3289
+ : null;
3290
+ const mcpUpdateRequired = !automaticMcpRepairSuppressed
3291
+ && (!mcpConfigurations.healthy || (!options.update && !mcpProcesses.healthy));
3292
+ if (automaticMcpRepairSuppressed) {
3293
+ profile = {
3294
+ ...profile,
3295
+ configuration_complete: false,
3296
+ complete: false,
3297
+ automatic_repair_suppressed: true,
3298
+ ahead_unverified_versions: aheadUnverifiedVersions,
3299
+ exact_fix: registryVerificationAction,
3300
+ };
3301
+ }
2902
3302
  let selfTest;
2903
3303
  try {
2904
3304
  selfTest = await runSelfTest(options);
@@ -2912,10 +3312,49 @@ async function install(options) {
2912
3312
  mcp_tool_profile: initialToolProfileReport(options.toolProfile),
2913
3313
  };
2914
3314
  }
3315
+ selfTest = alignMcpRecommendationVersions(
3316
+ selfTest,
3317
+ mcpTarget.version,
3318
+ '',
3319
+ registryVerificationAction,
3320
+ );
2915
3321
  if (options.activate && !selfTest.activation_verified) {
2916
3322
  throw new Error('Marrow activation failed: server confirmation was not returned');
2917
3323
  }
2918
- 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;
2919
3358
  if (harnessReload.required && selfTest.mcp_tool_profile) {
2920
3359
  selfTest.mcp_tool_profile = {
2921
3360
  ...selfTest.mcp_tool_profile,
@@ -2972,7 +3411,12 @@ async function install(options) {
2972
3411
  attempted: true,
2973
3412
  fixedConfig: changedConfig,
2974
3413
  selfTestPassed,
2975
- message: selfTestPassed
3414
+ automaticMcpRepairSuppressed,
3415
+ message: automaticMcpRepairSuppressed
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.'
3419
+ : selfTestPassed
2976
3420
  ? selfTest.health === 'healthy'
2977
3421
  ? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
2978
3422
  : `I fixed Marrow passive config and self-test passed; status is ${selfTest.health || 'unknown'}${selfTest.next_action ? `. Next action: ${selfTest.next_action}` : ''}.`
@@ -2981,10 +3425,9 @@ async function install(options) {
2981
3425
  : `Config repair ran, but self-test failed: ${selfTest.error || 'unknown error'}.`,
2982
3426
  }
2983
3427
  : null;
2984
-
2985
3428
  return {
2986
3429
  root: detection.root,
2987
- adapterProvenance: ADAPTER_PROVENANCE,
3430
+ adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
2988
3431
  mode: plan.mode,
2989
3432
  writeMode,
2990
3433
  toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
@@ -3018,9 +3461,18 @@ async function install(options) {
3018
3461
  missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
3019
3462
  envHints,
3020
3463
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
3021
- mcpProcesses,
3464
+ mcpProcesses: reportedMcpProcesses,
3022
3465
  mcpConfigurations,
3023
- recommendedFix: mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3466
+ ahead_unverified_versions: aheadUnverifiedVersions,
3467
+ automatic_mcp_repair_suppressed: automaticMcpRepairSuppressed,
3468
+ registry_verification_action: registryVerificationAction,
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
3024
3476
  ? envHints.length
3025
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.`
3026
3478
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -3038,11 +3490,15 @@ async function install(options) {
3038
3490
  ...(options.keyFromArg
3039
3491
  ? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
3040
3492
  : []),
3041
- ...(!mcpProcesses.healthy
3042
- ? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
3493
+ ...(mcpProcesses.ahead_unverified
3494
+ ? ['Registry-unverified ahead Marrow MCP clients are active. Automatic repair is suppressed; run the exact registry verification action before owner-directed replacement.']
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.`]
3043
3497
  : []),
3044
- ...(!mcpConfigurations.healthy
3045
- ? ['Stale, mixed, or version-unknown Marrow MCP versions remain in owner configuration. Repair each owning workspace and restart its harness.']
3498
+ ...(mcpConfigurations.ahead_unverified || changes.some((change) => change.automatic_repair_suppressed)
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.']
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.`]
3046
3502
  : []),
3047
3503
  ],
3048
3504
  };
@@ -3093,6 +3549,7 @@ module.exports = {
3093
3549
  GROK_NATIVE_HOOK_MATCHER,
3094
3550
  printReport,
3095
3551
  buildMcpToolProfileReport,
3552
+ resolveMcpTargetVersion,
3096
3553
  resolveToolProfile,
3097
3554
  PRIMARY_TOOL_NAMES,
3098
3555
  ADAPTER_PROVENANCE,