@getmarrow/install 0.1.54 → 0.1.55

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.
package/README.md CHANGED
@@ -99,7 +99,11 @@ npx @getmarrow/install controller stop
99
99
 
100
100
  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
101
 
102
- ## What's New in v0.1.54
102
+ ## What's New in v0.1.55
103
+
104
+ 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
+
106
+ ## Previous: v0.1.54
103
107
 
104
108
  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
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
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.79';
14
+ const MCP_ADAPTER_SOURCE_SHA = '11f00049043d0aba90704ecbf69f32d2278a4573';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-LWfBWGot2PnluRbS5Hm3WA2DJCIkf2qVJfthGcCaZx8AoRXFKeg1pUO3rMVFqrOIWOB4nYZw6UgSL+7hDY1wuA==';
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,184 @@ 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 MCP_STABLE_VERSION_RE = /^(\d{1,6})\.(\d{1,6})\.(\d{1,9})$/;
37
+ const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
38
+ const VERIFIED_MCP_EXECUTABLE_TARGET = Symbol('verified_mcp_executable_target');
39
+
40
+ function validSha512Integrity(value) {
41
+ if (!SHA512_INTEGRITY_RE.test(value)) return false;
42
+ const encoded = value.slice('sha512-'.length);
43
+ try {
44
+ const digest = Buffer.from(encoded, 'base64');
45
+ return digest.length === 64 && digest.toString('base64') === encoded;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function parsedStableMcpVersion(value) {
52
+ const match = String(value || '').match(MCP_STABLE_VERSION_RE);
53
+ if (!match) return null;
54
+ const parts = match.slice(1).map(Number);
55
+ return parts.every(Number.isSafeInteger) ? parts : null;
56
+ }
57
+
58
+ function compareMcpVersions(left, right) {
59
+ const leftParts = parsedStableMcpVersion(left);
60
+ const rightParts = parsedStableMcpVersion(right);
61
+ if (!leftParts || !rightParts) return null;
62
+ for (let index = 0; index < leftParts.length; index += 1) {
63
+ if (leftParts[index] !== rightParts[index]) return leftParts[index] - rightParts[index];
64
+ }
65
+ return 0;
66
+ }
67
+
68
+ function compatibleMcpTargetVersion(value) {
69
+ const candidate = parsedStableMcpVersion(value);
70
+ const sealed = parsedStableMcpVersion(MCP_ADAPTER_VERSION);
71
+ return Boolean(candidate && sealed
72
+ && candidate[0] === sealed[0]
73
+ && candidate[1] === sealed[1]
74
+ && compareMcpVersions(value, MCP_ADAPTER_VERSION) >= 0);
75
+ }
76
+
77
+ function mcpVersionsInText(value) {
78
+ return [...String(value || '').matchAll(/@getmarrow\/mcp@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g)]
79
+ .map((match) => match[1]);
80
+ }
81
+
82
+ function unverifiedAheadMcpVersions(versions, targetVersion = MCP_ADAPTER_VERSION) {
83
+ if (!compatibleMcpTargetVersion(targetVersion)) return [];
84
+ return [...new Set((Array.isArray(versions) ? versions : [])
85
+ .filter((version) => compatibleMcpTargetVersion(version)
86
+ && compareMcpVersions(version, targetVersion) > 0))].sort((left, right) => compareMcpVersions(left, right));
87
+ }
88
+
89
+ function mcpRegistryVerificationAction(versions) {
90
+ const ahead = [...new Set(Array.isArray(versions) ? versions : [])].sort((left, right) => compareMcpVersions(left, right));
91
+ const targets = ahead.map((version) => `@getmarrow/mcp@${version}`).join(', ');
92
+ 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.`;
93
+ }
94
+
95
+ function verifiedMcpRegistryMetadata(value) {
96
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
97
+ const version = typeof value.version === 'string' ? value.version : '';
98
+ const integrity = typeof value.dist?.integrity === 'string' ? value.dist.integrity : '';
99
+ const tarball = typeof value.dist?.tarball === 'string' ? value.dist.tarball : '';
100
+ const expectedTarball = `https://registry.npmjs.org/@getmarrow/mcp/-/mcp-${version}.tgz`;
101
+ if (value.name !== '@getmarrow/mcp'
102
+ || !compatibleMcpTargetVersion(version)
103
+ || !validSha512Integrity(integrity)
104
+ || tarball !== expectedTarball) return null;
105
+ return { version, integrity, tarball };
106
+ }
107
+
108
+ function resolveMcpTargetVersion(options = {}) {
109
+ const registry = verifiedMcpRegistryMetadata(options.registryMetadata);
110
+ if (registry) {
111
+ const target = {
112
+ version: registry.version,
113
+ source: 'verified_npm_registry',
114
+ integrity: registry.integrity,
115
+ source_sha: null,
116
+ };
117
+ Object.defineProperty(target, VERIFIED_MCP_EXECUTABLE_TARGET, { value: true });
118
+ return target;
119
+ }
120
+ return {
121
+ version: MCP_ADAPTER_VERSION,
122
+ source: 'sealed_installer',
123
+ integrity: MCP_ADAPTER_INTEGRITY,
124
+ source_sha: MCP_ADAPTER_SOURCE_SHA,
125
+ };
126
+ }
127
+
128
+ function executableMcpTarget(options = {}) {
129
+ const target = options.mcpTarget;
130
+ if (target?.[VERIFIED_MCP_EXECUTABLE_TARGET] === true
131
+ && target.source === 'verified_npm_registry'
132
+ && compatibleMcpTargetVersion(target.version)
133
+ && validSha512Integrity(target.integrity)
134
+ && target.source_sha === null) return target;
135
+ return resolveMcpTargetVersion();
136
+ }
137
+
138
+ function expectedMcpInspectionVersion(options = {}) {
139
+ if (options.expectedVersion === MCP_ADAPTER_VERSION) return MCP_ADAPTER_VERSION;
140
+ return executableMcpTarget({ mcpTarget: options.expectedTarget }).version;
141
+ }
142
+
143
+ async function readMcpRegistryMetadata(options = {}) {
144
+ if (Object.prototype.hasOwnProperty.call(options, 'mcpRegistryMetadata')) {
145
+ return options.mcpRegistryMetadata;
146
+ }
147
+ if (options.resolveMcpRegistry !== true) return null;
148
+ const controller = new AbortController();
149
+ const timer = setTimeout(() => controller.abort(), 2500);
150
+ try {
151
+ const fetcher = typeof options.registryFetch === 'function' ? options.registryFetch : fetch;
152
+ const response = await fetcher(MCP_REGISTRY_LATEST_URL, {
153
+ headers: { accept: 'application/json' },
154
+ redirect: 'error',
155
+ signal: controller.signal,
156
+ });
157
+ if (!response.ok) return null;
158
+ return await response.json();
159
+ } catch {
160
+ return null;
161
+ } finally {
162
+ clearTimeout(timer);
163
+ }
164
+ }
165
+
166
+ function adapterProvenanceForMcpTarget(target) {
167
+ if (!target || target.version === MCP_ADAPTER_VERSION) return ADAPTER_PROVENANCE;
168
+ return {
169
+ mcp: {
170
+ package: '@getmarrow/mcp',
171
+ version: target.version,
172
+ source_sha: target.source_sha,
173
+ integrity: target.integrity,
174
+ integrity_state: target.source === 'verified_npm_registry'
175
+ ? 'verified_npm_registry_metadata'
176
+ : 'exact_current_configuration',
177
+ },
178
+ sdk: ADAPTER_PROVENANCE.sdk,
179
+ };
180
+ }
181
+
182
+ function retargetMcpPackageSpec(value, version) {
183
+ const target = compatibleMcpTargetVersion(version) ? version : MCP_ADAPTER_VERSION;
184
+ return String(value).split(MCP_PACKAGE_SPEC).join(`@getmarrow/mcp@${target}`);
185
+ }
186
+
187
+ function retargetMcpDowngradeRecommendation(value, targetVersion) {
188
+ if (typeof value !== 'string' || !compatibleMcpTargetVersion(targetVersion)) return value;
189
+ return value.replace(/@getmarrow\/mcp@(\d+\.\d+\.\d+)/g, (match, version) => (
190
+ parsedStableMcpVersion(version) && version !== targetVersion
191
+ ? `@getmarrow/mcp@${targetVersion}`
192
+ : match
193
+ ));
194
+ }
195
+
196
+ function alignMcpRecommendationVersions(value, targetVersion, key = '', verificationAction = null) {
197
+ if (Array.isArray(value)) {
198
+ return value.map((entry) => alignMcpRecommendationVersions(entry, targetVersion, key, verificationAction));
199
+ }
200
+ if (!value || typeof value !== 'object') {
201
+ return /(?:command|fix|instruction|next_action|notice)$/i.test(key)
202
+ ? verificationAction && typeof value === 'string' && /@getmarrow\/mcp@\d+\.\d+\.\d+/.test(value)
203
+ ? verificationAction
204
+ : retargetMcpDowngradeRecommendation(value, targetVersion)
205
+ : value;
206
+ }
207
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
208
+ entryKey,
209
+ alignMcpRecommendationVersions(entryValue, targetVersion, entryKey, verificationAction),
210
+ ]));
211
+ }
34
212
  const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp context-hook`;
35
213
  const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
36
214
  const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
@@ -366,24 +544,37 @@ function inspectMcpProcesses(options = {}) {
366
544
  .filter(isMcpProcessCommand)
367
545
  .map((command) => explicitMcpVersion(command) || packageMcpVersion(command) || 'unknown');
368
546
  const versions = [...new Set(active.filter((version) => version !== 'unknown'))].sort();
547
+ const expectedVersion = expectedMcpInspectionVersion(options);
369
548
  const unknownVersionProcesses = active.filter((version) => version === 'unknown').length;
370
- const staleVersions = versions.filter((version) => version !== MCP_ADAPTER_VERSION);
549
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(versions, expectedVersion);
550
+ const staleVersions = versions.filter((version) => version !== expectedVersion
551
+ && !aheadUnverifiedVersions.includes(version));
371
552
  const mixedVersions = versions.length > 1 || (versions.length > 0 && unknownVersionProcesses > 0);
372
553
  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`;
554
+ const aheadUnverified = aheadUnverifiedVersions.length > 0;
555
+ const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0 || aheadUnverified;
556
+ const automaticRepairSuppressed = aheadUnverified;
557
+ const repairCommand = `npx -y --package=@getmarrow/mcp@${expectedVersion} marrow-mcp setup`;
375
558
  return {
376
559
  available: process.platform === 'linux' || Array.isArray(options.commands),
377
- expected_version: MCP_ADAPTER_VERSION,
560
+ expected_version: expectedVersion,
378
561
  active_processes: active.length,
379
562
  active_versions: versions,
380
563
  unknown_version_processes: unknownVersionProcesses,
381
564
  stale_versions: staleVersions,
565
+ ahead_unverified: aheadUnverified,
566
+ ahead_unverified_versions: aheadUnverifiedVersions,
382
567
  mixed_versions: mixedVersions,
383
568
  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,
569
+ automatic_repair_suppressed: automaticRepairSuppressed,
570
+ registry_verification_required: aheadUnverified,
571
+ exact_fix: automaticRepairSuppressed
572
+ ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
573
+ : needsRepair ? repairCommand : null,
574
+ restart_required: needsRepair && !automaticRepairSuppressed,
575
+ restart_instruction: needsRepair && !automaticRepairSuppressed
576
+ ? 'Restart every owning harness to replace its active Marrow MCP process.'
577
+ : null,
387
578
  verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
388
579
  };
389
580
  }
@@ -422,20 +613,33 @@ function inspectMcpConfigurations(detection, options = {}) {
422
613
  }
423
614
  }
424
615
  const configuredVersions = [...new Set(versions)].sort();
425
- const staleVersions = configuredVersions.filter((version) => version !== MCP_ADAPTER_VERSION);
616
+ const expectedVersion = expectedMcpInspectionVersion(options);
617
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(configuredVersions, expectedVersion);
618
+ const staleVersions = configuredVersions.filter((version) => version !== expectedVersion
619
+ && !aheadUnverifiedVersions.includes(version));
426
620
  const mixedVersions = configuredVersions.length > 1
427
621
  || (configuredVersions.length > 0 && unknownVersionConfigurations > 0);
428
- const healthy = staleVersions.length === 0 && !mixedVersions && unknownVersionConfigurations === 0;
622
+ const aheadUnverified = aheadUnverifiedVersions.length > 0;
623
+ const healthy = staleVersions.length === 0 && !mixedVersions
624
+ && unknownVersionConfigurations === 0 && !aheadUnverified;
429
625
  return {
430
- expected_version: MCP_ADAPTER_VERSION,
626
+ expected_version: expectedVersion,
431
627
  files_checked: filesChecked,
432
628
  configurations_found: configurationsFound,
433
629
  configured_versions: configuredVersions,
434
630
  unknown_version_configurations: unknownVersionConfigurations,
435
631
  stale_versions: staleVersions,
632
+ ahead_unverified: aheadUnverified,
633
+ ahead_unverified_versions: aheadUnverifiedVersions,
436
634
  mixed_versions: mixedVersions,
437
635
  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.`,
636
+ automatic_repair_suppressed: aheadUnverified,
637
+ registry_verification_required: aheadUnverified,
638
+ exact_fix: healthy
639
+ ? null
640
+ : aheadUnverified
641
+ ? 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.`,
439
643
  verification_command: healthy ? null : 'npx -y @getmarrow/install@latest doctor --self-test',
440
644
  };
441
645
  }
@@ -575,6 +779,7 @@ function parseArgs(argv, env = process.env) {
575
779
  if (options.activate && options.dryRun) {
576
780
  throw new Error('activate cannot be combined with --dry-run; use --dry-run without activate to preview changes');
577
781
  }
782
+ options.resolveMcpRegistry = Boolean(options.doctor || options.repair || options.update);
578
783
 
579
784
  return options;
580
785
  }
@@ -1603,8 +1808,12 @@ function activationProfile(detection, plan, changes, client) {
1603
1808
  const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
1604
1809
  ? ['pre_action', 'action_result', 'outcome_closure']
1605
1810
  : [...registry.automatic];
1811
+ const mcpTargetVersion = compatibleMcpTargetVersion(plan.mcp_target_version)
1812
+ ? plan.mcp_target_version
1813
+ : MCP_ADAPTER_VERSION;
1814
+ const targetCommand = (command) => retargetMcpPackageSpec(command, mcpTargetVersion);
1606
1815
  const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
1607
- ? MCP_ADAPTER_VERSION
1816
+ ? mcpTargetVersion
1608
1817
  : capabilityLevel === 'sdk_passive_runtime'
1609
1818
  ? SDK_ADAPTER_VERSION
1610
1819
  : INSTALLER_ADAPTER_VERSION;
@@ -1616,71 +1825,71 @@ function activationProfile(detection, plan, changes, client) {
1616
1825
  const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
1617
1826
  const grokSettings = safeJsonObject(detection.paths.grokHooks);
1618
1827
  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');
1828
+ if (exactHookConfigured(codexSettings, 'UserPromptSubmit', targetCommand(CODEX_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
1829
+ if (exactHookConfigured(codexSettings, 'PreToolUse', targetCommand(CODEX_PRE_ACTION_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1830
+ if (exactHookConfigured(codexSettings, 'PostToolUse', targetCommand(CODEX_ACTION_RESULT_HOOK_COMMAND), CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1831
+ if (exactHookConfigured(codexSettings, 'SessionEnd', targetCommand(CODEX_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
1623
1832
  } else if (client === 'cursor' || client === 'composer') {
1624
- if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1833
+ if (exactCursorHookConfigured(cursorSettings, 'preToolUse', targetCommand(CURSOR_PRE_ACTION_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1625
1834
  timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1626
1835
  })) observedHooks.push('pre_action');
1627
- if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1836
+ if (exactCursorHookConfigured(cursorSettings, 'postToolUse', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1628
1837
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1629
- }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
1838
+ }) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', targetCommand(CURSOR_ACTION_RESULT_HOOK_COMMAND), CURSOR_NATIVE_HOOK_MATCHER, {
1630
1839
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1631
1840
  })) observedHooks.push('action_result');
1632
- if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1841
+ if (exactCursorHookConfigured(cursorSettings, 'stop', targetCommand(CURSOR_SESSION_END_HOOK_COMMAND), undefined, {
1633
1842
  timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1634
1843
  })) observedHooks.push('outcome_closure');
1635
1844
  } else if (client === 'cline') {
1636
1845
  for (const hook of clineHookContract(detection)) {
1637
- if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
1846
+ if (exactExecutableFile(hook.path, targetCommand(hook.content))) observedHooks.push(hook.stage);
1638
1847
  }
1639
1848
  } else if (client === 'windsurf') {
1640
1849
  if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1641
- windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1850
+ windsurfSettings, event, targetCommand(WINDSURF_PRE_ACTION_HOOK_COMMAND),
1642
1851
  ))) observedHooks.push('pre_action');
1643
1852
  if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1644
- windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1853
+ windsurfSettings, event, targetCommand(WINDSURF_ACTION_RESULT_HOOK_COMMAND),
1645
1854
  ))) observedHooks.push('action_result');
1646
1855
  if (exactWindsurfHookConfigured(
1647
- windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1856
+ windsurfSettings, 'post_cascade_response', targetCommand(WINDSURF_SESSION_END_HOOK_COMMAND),
1648
1857
  )) observedHooks.push('response_closeout');
1649
1858
  } else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
1650
1859
  if (exactGeminiHookConfigured(
1651
- geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1860
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', targetCommand(GEMINI_PRE_ACTION_HOOK_COMMAND),
1652
1861
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1653
1862
  )) observedHooks.push('pre_action');
1654
1863
  if (exactGeminiHookConfigured(
1655
- geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1864
+ geminiSettings, 'AfterTool', 'marrow-after-tool', targetCommand(GEMINI_ACTION_RESULT_HOOK_COMMAND),
1656
1865
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1657
1866
  )) observedHooks.push('action_result');
1658
1867
  if (exactGeminiHookConfigured(
1659
- geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1868
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', targetCommand(GEMINI_SESSION_END_HOOK_COMMAND),
1660
1869
  undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1661
1870
  )) observedHooks.push('turn_closeout');
1662
1871
  } else if (client === 'grok') {
1663
1872
  if (exactGrokHookConfigured(
1664
- grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
1873
+ grokSettings, 'PreToolUse', targetCommand(GROK_PRE_ACTION_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 7,
1665
1874
  )) observedHooks.push('pre_action');
1666
1875
  if (exactGrokHookConfigured(
1667
- grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1876
+ grokSettings, 'PostToolUse', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
1668
1877
  ) && exactGrokHookConfigured(
1669
- grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
1878
+ grokSettings, 'PostToolUseFailure', targetCommand(GROK_ACTION_RESULT_HOOK_COMMAND), GROK_NATIVE_HOOK_MATCHER, 5,
1670
1879
  )) observedHooks.push('action_result');
1671
1880
  if (exactGrokHookConfigured(
1672
- grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1881
+ grokSettings, 'Stop', targetCommand(GROK_SESSION_END_HOOK_COMMAND), undefined, 3,
1673
1882
  ) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
1674
1883
  } else {
1675
1884
  if (capabilityLevel === 'native_hooks'
1676
- && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1885
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', targetCommand(MCP_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
1677
1886
  if (capabilityLevel === 'native_hooks'
1678
- && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1887
+ && exactHookConfigured(claudeSettings, 'PreToolUse', targetCommand(MCP_PRE_ACTION_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1679
1888
  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');
1889
+ && exactHookConfigured(claudeSettings, 'PostToolUse', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)
1890
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', targetCommand(MCP_ACTION_RESULT_HOOK_COMMAND), NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1682
1891
  if (capabilityLevel === 'native_hooks'
1683
- && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
1892
+ && exactHookConfigured(claudeSettings, 'Stop', targetCommand(MCP_SESSION_END_HOOK_COMMAND))) observedHooks.push('session_end');
1684
1893
  }
1685
1894
  const passiveRuntime = safeRead(detection.paths.passiveRuntime);
1686
1895
  if (capabilityLevel === 'sdk_passive_runtime'
@@ -1696,7 +1905,7 @@ function activationProfile(detection, plan, changes, client) {
1696
1905
  if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
1697
1906
  config?.mcpServers?.marrow?.command === 'npx'
1698
1907
  && Array.isArray(config.mcpServers.marrow.args)
1699
- && config.mcpServers.marrow.args.join(' ') === `-y --package=${MCP_PACKAGE_SPEC} marrow-mcp`
1908
+ && config.mcpServers.marrow.args.join(' ') === `-y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp`
1700
1909
  ))) observedHooks.push('mcp_tool_calls');
1701
1910
  const fingerprintMaterial = changes
1702
1911
  .filter((change) => change.applied || change.already_present)
@@ -1737,7 +1946,7 @@ function activationProfile(detection, plan, changes, client) {
1737
1946
  : client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
1738
1947
  ? '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
1948
  : 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.`
1949
+ ? `Run npx -y --package=@getmarrow/mcp@${mcpTargetVersion} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
1741
1950
  : 'npx @getmarrow/install --repair';
1742
1951
  return {
1743
1952
  adapter_version: adapterVersion,
@@ -2026,6 +2235,8 @@ function buildPlan(detection, options) {
2026
2235
  const client = options.client || detectedClient(detection);
2027
2236
  const agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
2028
2237
  const baseUrl = String(options.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL;
2238
+ const mcpTargetVersion = executableMcpTarget(options).version;
2239
+ const retarget = (value) => retargetMcpPackageSpec(value, mcpTargetVersion);
2029
2240
  const mode = options.mode === 'auto'
2030
2241
  ? detection.node ? 'both' : 'mcp'
2031
2242
  : options.mode;
@@ -2053,7 +2264,7 @@ function buildPlan(detection, options) {
2053
2264
  type: 'json-transform',
2054
2265
  path: detection.paths.claudeSettings,
2055
2266
  label: 'Claude Code MCP passive hooks',
2056
- transform: upsertClaudeHooks,
2267
+ transform: (filePath) => retarget(upsertClaudeHooks(filePath)),
2057
2268
  });
2058
2269
  }
2059
2270
  if (detection.codex) {
@@ -2061,7 +2272,7 @@ function buildPlan(detection, options) {
2061
2272
  type: 'json-transform',
2062
2273
  path: detection.paths.codexHooks,
2063
2274
  label: 'Codex native hooks',
2064
- transform: upsertCodexHooks,
2275
+ transform: (filePath) => retarget(upsertCodexHooks(filePath)),
2065
2276
  });
2066
2277
  }
2067
2278
  if (detection.cline) {
@@ -2070,7 +2281,7 @@ function buildPlan(detection, options) {
2070
2281
  type: 'owned-executable',
2071
2282
  path: hook.path,
2072
2283
  label: hook.label,
2073
- content: hook.content,
2284
+ content: retarget(hook.content),
2074
2285
  mode: 0o755,
2075
2286
  conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
2076
2287
  });
@@ -2081,7 +2292,7 @@ function buildPlan(detection, options) {
2081
2292
  type: 'json-transform',
2082
2293
  path: detection.paths.windsurfHooks,
2083
2294
  label: 'Windsurf native hooks',
2084
- transform: upsertWindsurfHooks,
2295
+ transform: (filePath) => retarget(upsertWindsurfHooks(filePath)),
2085
2296
  });
2086
2297
  }
2087
2298
  if (detection.gemini) {
@@ -2089,27 +2300,27 @@ function buildPlan(detection, options) {
2089
2300
  type: 'json-transform',
2090
2301
  path: detection.paths.geminiSettings,
2091
2302
  label: 'Gemini CLI native hooks',
2092
- transform: upsertGeminiHooks,
2303
+ transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
2093
2304
  });
2094
2305
  }
2095
2306
  writes.push({
2096
2307
  type: 'json-transform',
2097
2308
  path: detection.paths.mcpJson,
2098
2309
  label: 'Project MCP server config',
2099
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
2310
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
2100
2311
  });
2101
2312
  if (detection.cursor) {
2102
2313
  writes.push({
2103
2314
  type: 'json-transform',
2104
2315
  path: detection.paths.cursorHooks,
2105
2316
  label: 'Cursor native hooks',
2106
- transform: upsertCursorHooks,
2317
+ transform: (filePath) => retarget(upsertCursorHooks(filePath)),
2107
2318
  });
2108
2319
  writes.push({
2109
2320
  type: 'json-transform',
2110
2321
  path: detection.paths.cursorMcp,
2111
2322
  label: 'Cursor MCP server config',
2112
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile }),
2323
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
2113
2324
  });
2114
2325
  }
2115
2326
  }
@@ -2119,7 +2330,7 @@ function buildPlan(detection, options) {
2119
2330
  type: 'md-block',
2120
2331
  path: detection.paths.agentsMd,
2121
2332
  label: 'Agent instructions',
2122
- block: passiveInstructions(),
2333
+ block: retarget(passiveInstructions()),
2123
2334
  });
2124
2335
  }
2125
2336
 
@@ -2128,11 +2339,11 @@ function buildPlan(detection, options) {
2128
2339
  type: 'file',
2129
2340
  path: detection.paths.cursorRules,
2130
2341
  label: 'Cursor Marrow rule',
2131
- content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
2342
+ content: retarget(passiveInstructions()).replace(/<!--[^>]+-->/g, '').trim() + '\n',
2132
2343
  });
2133
2344
  }
2134
2345
 
2135
- return { mode, root: detection.root, writes };
2346
+ return { mode, root: detection.root, writes, mcp_target_version: mcpTargetVersion };
2136
2347
  }
2137
2348
 
2138
2349
  function assertContainedManagedTarget(root, targetPath) {
@@ -2198,9 +2409,16 @@ function applyPlan(plan, options) {
2198
2409
  const prepared = plan.writes.map((write) => {
2199
2410
  const fileExists = exists(write.path);
2200
2411
  const before = safeRead(write.path);
2412
+ const aheadUnverifiedVersions = unverifiedAheadMcpVersions(
2413
+ mcpVersionsInText(before),
2414
+ plan.mcp_target_version,
2415
+ );
2416
+ const automaticRepairSuppressed = aheadUnverifiedVersions.length > 0;
2201
2417
  let after;
2202
2418
  let hookConflict = false;
2203
- if (write.type === 'file') {
2419
+ if (automaticRepairSuppressed) {
2420
+ after = before;
2421
+ } else if (write.type === 'file') {
2204
2422
  if (write.overwrite === false && before) {
2205
2423
  after = before;
2206
2424
  } else {
@@ -2223,21 +2441,43 @@ function applyPlan(plan, options) {
2223
2441
 
2224
2442
  const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
2225
2443
  const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
2226
- return { write, before, after, hookConflict, modeChanged };
2444
+ return {
2445
+ write,
2446
+ before,
2447
+ after,
2448
+ hookConflict,
2449
+ modeChanged,
2450
+ automaticRepairSuppressed,
2451
+ aheadUnverifiedVersions,
2452
+ };
2227
2453
  });
2228
2454
 
2229
2455
  const changes = [];
2230
- for (const { write, before, after, hookConflict, modeChanged } of prepared) {
2456
+ for (const {
2457
+ write,
2458
+ before,
2459
+ after,
2460
+ hookConflict,
2461
+ modeChanged,
2462
+ automaticRepairSuppressed,
2463
+ aheadUnverifiedVersions,
2464
+ } of prepared) {
2231
2465
  const contentChanged = before !== after;
2232
- const changed = !hookConflict && (contentChanged || modeChanged);
2233
- const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
2466
+ const changed = !automaticRepairSuppressed && !hookConflict && (contentChanged || modeChanged);
2467
+ const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor
2468
+ && !hookConflict && !automaticRepairSuppressed);
2234
2469
  changes.push({
2235
2470
  path: write.path,
2236
2471
  label: write.label,
2237
2472
  changed,
2238
2473
  applied: changed && writeApplied,
2239
- already_present: !changed && !hookConflict,
2474
+ already_present: !changed && !hookConflict && !automaticRepairSuppressed,
2240
2475
  hook_conflict: hookConflict,
2476
+ automatic_repair_suppressed: automaticRepairSuppressed,
2477
+ ...(automaticRepairSuppressed ? {
2478
+ ahead_unverified_versions: aheadUnverifiedVersions,
2479
+ exact_fix: mcpRegistryVerificationAction(aheadUnverifiedVersions),
2480
+ } : {}),
2241
2481
  ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
2242
2482
  });
2243
2483
  if (contentChanged && writeApplied) {
@@ -2303,7 +2543,7 @@ async function runSelfTest(options) {
2303
2543
  'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
2304
2544
  'x-marrow-install-version': INSTALLER_ADAPTER_VERSION,
2305
2545
  'x-marrow-sdk-version': SDK_ADAPTER_VERSION,
2306
- 'x-marrow-mcp-version': MCP_ADAPTER_VERSION,
2546
+ 'x-marrow-mcp-version': executableMcpTarget(options).version,
2307
2547
  };
2308
2548
  if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
2309
2549
 
@@ -2668,8 +2908,13 @@ function printReport(report) {
2668
2908
 
2669
2909
  process.stdout.write('\nPlanned changes:\n');
2670
2910
  for (const change of report.changes) {
2671
- const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
2911
+ const marker = change.automatic_repair_suppressed
2912
+ ? 'preserved unverified-ahead surface; repair suppressed'
2913
+ : change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
2672
2914
  process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
2915
+ if (change.automatic_repair_suppressed && change.exact_fix) {
2916
+ process.stdout.write(` exact verification: ${change.exact_fix}\n`);
2917
+ }
2673
2918
  }
2674
2919
 
2675
2920
  process.stdout.write('\nSelf-test:\n');
@@ -2819,6 +3064,8 @@ function printReport(report) {
2819
3064
  const processes = report.doctor.mcpProcesses;
2820
3065
  process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
2821
3066
  process.stdout.write(`- stale/mixed/version-unknown MCP clients: ${processes.healthy ? 'no' : 'yes'}\n`);
3067
+ if (processes.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP clients: ${processes.ahead_unverified_versions.join(', ')}\n`);
3068
+ if (processes.automatic_repair_suppressed) process.stdout.write('- automatic MCP process repair: suppressed pending official registry verification\n');
2822
3069
  if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
2823
3070
  if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
2824
3071
  }
@@ -2826,6 +3073,8 @@ function printReport(report) {
2826
3073
  const configurations = report.doctor.mcpConfigurations;
2827
3074
  process.stdout.write(`- configured MCP versions: ${configurations.configured_versions.length ? configurations.configured_versions.join(', ') : 'none pinned'}\n`);
2828
3075
  process.stdout.write(`- stale/mixed/version-unknown MCP configuration: ${configurations.healthy ? 'no' : 'yes'}\n`);
3076
+ if (configurations.ahead_unverified_versions.length) process.stdout.write(`- unverified-ahead MCP configuration: ${configurations.ahead_unverified_versions.join(', ')}\n`);
3077
+ if (configurations.automatic_repair_suppressed) process.stdout.write('- automatic MCP configuration repair: suppressed pending official registry verification\n');
2829
3078
  }
2830
3079
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
2831
3080
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
@@ -2863,10 +3112,23 @@ async function install(options) {
2863
3112
  const client = detectedClient(detection);
2864
3113
  options.client = client;
2865
3114
  options.agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
3115
+ const observedMcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
3116
+ const observedMcpConfigurations = inspectMcpConfigurations(detection, { paths: options.mcpConfigPaths });
3117
+ const registryMetadata = await readMcpRegistryMetadata(options);
3118
+ const latestTargetOperation = Boolean(options.doctor || options.repair || options.update);
3119
+ const mcpTarget = resolveMcpTargetVersion({
3120
+ currentVersions: latestTargetOperation ? [
3121
+ ...observedMcpProcesses.active_versions,
3122
+ ...observedMcpConfigurations.configured_versions,
3123
+ ] : [],
3124
+ registryMetadata: latestTargetOperation ? registryMetadata : null,
3125
+ });
3126
+ options.mcpTarget = mcpTarget;
3127
+ options.mcpTargetVersion = mcpTarget.version;
2866
3128
  const plan = buildPlan(detection, options);
2867
3129
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
2868
3130
  const changes = applyPlan(plan, options);
2869
- const profile = activationProfile(detection, plan, changes, client);
3131
+ let profile = activationProfile(detection, plan, changes, client);
2870
3132
  options.activation = options.activate ? {
2871
3133
  harness: client,
2872
3134
  agent_id: options.agentId,
@@ -2897,8 +3159,33 @@ async function install(options) {
2897
3159
  ? repairConfigDiagnostics(configDiagnostics)
2898
3160
  : [];
2899
3161
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
2900
- const mcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
2901
- const mcpConfigurations = inspectMcpConfigurations(detection, { paths: options.mcpConfigPaths });
3162
+ const mcpProcesses = inspectMcpProcesses({
3163
+ commands: options.processCommands,
3164
+ expectedTarget: mcpTarget,
3165
+ });
3166
+ const mcpConfigurations = inspectMcpConfigurations(detection, {
3167
+ paths: options.mcpConfigPaths,
3168
+ expectedTarget: mcpTarget,
3169
+ });
3170
+ const aheadUnverifiedVersions = [...new Set([
3171
+ ...mcpProcesses.ahead_unverified_versions,
3172
+ ...mcpConfigurations.ahead_unverified_versions,
3173
+ ...changes.flatMap((change) => change.ahead_unverified_versions || []),
3174
+ ])].sort((left, right) => compareMcpVersions(left, right));
3175
+ const automaticMcpRepairSuppressed = aheadUnverifiedVersions.length > 0;
3176
+ const registryVerificationAction = automaticMcpRepairSuppressed
3177
+ ? mcpRegistryVerificationAction(aheadUnverifiedVersions)
3178
+ : null;
3179
+ if (automaticMcpRepairSuppressed) {
3180
+ profile = {
3181
+ ...profile,
3182
+ configuration_complete: false,
3183
+ complete: false,
3184
+ automatic_repair_suppressed: true,
3185
+ ahead_unverified_versions: aheadUnverifiedVersions,
3186
+ exact_fix: registryVerificationAction,
3187
+ };
3188
+ }
2902
3189
  let selfTest;
2903
3190
  try {
2904
3191
  selfTest = await runSelfTest(options);
@@ -2912,6 +3199,12 @@ async function install(options) {
2912
3199
  mcp_tool_profile: initialToolProfileReport(options.toolProfile),
2913
3200
  };
2914
3201
  }
3202
+ selfTest = alignMcpRecommendationVersions(
3203
+ selfTest,
3204
+ mcpTarget.version,
3205
+ '',
3206
+ registryVerificationAction,
3207
+ );
2915
3208
  if (options.activate && !selfTest.activation_verified) {
2916
3209
  throw new Error('Marrow activation failed: server confirmation was not returned');
2917
3210
  }
@@ -2972,7 +3265,10 @@ async function install(options) {
2972
3265
  attempted: true,
2973
3266
  fixedConfig: changedConfig,
2974
3267
  selfTestPassed,
2975
- message: selfTestPassed
3268
+ automaticMcpRepairSuppressed,
3269
+ message: automaticMcpRepairSuppressed
3270
+ ? registryVerificationAction
3271
+ : selfTestPassed
2976
3272
  ? selfTest.health === 'healthy'
2977
3273
  ? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
2978
3274
  : `I fixed Marrow passive config and self-test passed; status is ${selfTest.health || 'unknown'}${selfTest.next_action ? `. Next action: ${selfTest.next_action}` : ''}.`
@@ -2984,7 +3280,7 @@ async function install(options) {
2984
3280
 
2985
3281
  return {
2986
3282
  root: detection.root,
2987
- adapterProvenance: ADAPTER_PROVENANCE,
3283
+ adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
2988
3284
  mode: plan.mode,
2989
3285
  writeMode,
2990
3286
  toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
@@ -3020,7 +3316,10 @@ async function install(options) {
3020
3316
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
3021
3317
  mcpProcesses,
3022
3318
  mcpConfigurations,
3023
- recommendedFix: mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3319
+ ahead_unverified_versions: aheadUnverifiedVersions,
3320
+ automatic_mcp_repair_suppressed: automaticMcpRepairSuppressed,
3321
+ registry_verification_action: registryVerificationAction,
3322
+ recommendedFix: registryVerificationAction || mcpProcesses.exact_fix || mcpConfigurations.exact_fix || configDiagnostics.npm_token.recommended_fix || (!options.apiKey
3024
3323
  ? envHints.length
3025
3324
  ? `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
3325
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -3038,10 +3337,14 @@ async function install(options) {
3038
3337
  ...(options.keyFromArg
3039
3338
  ? ['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
3339
  : []),
3041
- ...(!mcpProcesses.healthy
3340
+ ...(mcpProcesses.ahead_unverified
3341
+ ? ['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
3042
3343
  ? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
3043
3344
  : []),
3044
- ...(!mcpConfigurations.healthy
3345
+ ...(mcpConfigurations.ahead_unverified || changes.some((change) => change.automatic_repair_suppressed)
3346
+ ? ['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
3045
3348
  ? ['Stale, mixed, or version-unknown Marrow MCP versions remain in owner configuration. Repair each owning workspace and restart its harness.']
3046
3349
  : []),
3047
3350
  ],
@@ -3093,6 +3396,7 @@ module.exports = {
3093
3396
  GROK_NATIVE_HOOK_MATCHER,
3094
3397
  printReport,
3095
3398
  buildMcpToolProfileReport,
3399
+ resolveMcpTargetVersion,
3096
3400
  resolveToolProfile,
3097
3401
  PRIMARY_TOOL_NAMES,
3098
3402
  ADAPTER_PROVENANCE,