@getmarrow/install 0.1.53 → 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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/package.json +1 -1
  3. package/src/installer.js +613 -80
package/README.md CHANGED
@@ -64,6 +64,12 @@ export MARROW_API_KEY=mrw_live_...
64
64
 
65
65
  The bare command is the default-on path: it detects and byte-idempotently writes managed configuration, runs the authenticated activation self-test, and starts the supported persistent controller. The key stays process-only and is never written to generated configuration or controller state. Use `--dry-run` for a non-writing preview, `doctor` for a read-only health check, or `--no-controller` to install and self-test without starting the controller. The explicit `activate` command remains equivalent and supported.
66
66
 
67
+ ### MCP tool profiles
68
+
69
+ Ordinary setup leaves `MARROW_TOOL_PROFILE` unset, which selects the documented 17-tool `primary` surface. Set `MARROW_TOOL_PROFILE=core` only for the legacy seven-tool minimal surface, or `MARROW_TOOL_PROFILE=full` for the complete advanced/legacy catalog. Explicit `primary`, `core`, and `full` values are accepted; any other value fails with an exact bounded repair and never falls back to a broader profile.
70
+
71
+ Tool visibility is not authorization. Every visible call still reaches Marrow's backend authentication, tenant, key-permission, plan, proof, and policy enforcement. `doctor --self-test` reports the configured and effective profile, the expected visible count, reloaded MCP visible names/count, and non-authorizing backend entitlement/upgrade projection. Until the owning harness restarts and matching MCP status is observed, actual visibility stays unavailable and the profile remains not-live. The self-test reads backend availability status; it does not invoke paid write tools to discover access.
72
+
67
73
  ## Keeping Marrow Current
68
74
 
69
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.
@@ -93,7 +99,15 @@ npx @getmarrow/install controller stop
93
99
 
94
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.
95
101
 
96
- ## What's New in v0.1.53
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
107
+
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`.
109
+
110
+ ## Previous: v0.1.53
97
111
 
98
112
  v0.1.53 pins the native-gate MCP candidate `3.9.75` and reconciles only Marrow-owned native hook surfaces. Codex uses `.codex/hooks.json`; Cursor and Composer use `.cursor/hooks.json`; Cline uses bounded executables under `.clinerules/hooks/`; Windsurf uses `.windsurf/hooks.json`; Gemini CLI receives named BeforeTool, AfterTool, and AfterAgent groups in `.gemini/settings.json`; Grok receives trusted global hooks in `~/.grok/hooks/marrow.json`. Grok PreToolUse validates strict private allow/deny JSON and fails closed with exit `2` when the child cannot provide an exact decision; PostToolUse/PostToolUseFailure emit compact results; one nonblocking Stop hook closes the turn with no duplicate SessionEnd hook. Grok hooks remain user-toggleable, so restart plus `/hooks` inspection is required and configuration never proves observed coverage.
99
113
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.53",
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,8 +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.75';
14
- const MCP_ADAPTER_SOURCE_SHA = '7548a7ae2d7e95bb16c62470da41af06cb62c5c1';
13
+ const MCP_ADAPTER_VERSION = '3.9.79';
14
+ const MCP_ADAPTER_SOURCE_SHA = '11f00049043d0aba90704ecbf69f32d2278a4573';
15
+ const MCP_ADAPTER_INTEGRITY = 'sha512-LWfBWGot2PnluRbS5Hm3WA2DJCIkf2qVJfthGcCaZx8AoRXFKeg1pUO3rMVFqrOIWOB4nYZw6UgSL+7hDY1wuA==';
15
16
  const SDK_ADAPTER_VERSION = '3.7.62';
16
17
  const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
17
18
  const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
@@ -21,8 +22,8 @@ const ADAPTER_PROVENANCE = Object.freeze({
21
22
  package: '@getmarrow/mcp',
22
23
  version: MCP_ADAPTER_VERSION,
23
24
  source_sha: MCP_ADAPTER_SOURCE_SHA,
24
- integrity: null,
25
- integrity_state: 'registry_unavailable_until_publish',
25
+ integrity: MCP_ADAPTER_INTEGRITY,
26
+ integrity_state: 'sealed_local_candidate',
26
27
  }),
27
28
  sdk: Object.freeze({
28
29
  package: '@getmarrow/sdk',
@@ -30,6 +31,184 @@ const ADAPTER_PROVENANCE = Object.freeze({
30
31
  integrity: SDK_ADAPTER_INTEGRITY,
31
32
  }),
32
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
+ }
33
212
  const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp context-hook`;
34
213
  const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
35
214
  const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
@@ -91,6 +270,28 @@ const GEMINI_HOOK_TIMEOUT_MS = 5000;
91
270
  const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
92
271
  const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
93
272
  const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
273
+ const TOOL_PROFILES = new Set(['primary', 'core', 'full']);
274
+ const TOOL_PROFILE_EXPECTED_COUNTS = Object.freeze({ primary: 17, core: 7, full: null });
275
+ const TOOL_PROFILE_EXACT_FIX = 'Unset MARROW_TOOL_PROFILE to use primary, or set MARROW_TOOL_PROFILE=core or MARROW_TOOL_PROFILE=full, then restart the owning harness and run npx @getmarrow/install@latest doctor --self-test.';
276
+ const PRIMARY_TOOL_NAMES = Object.freeze([
277
+ 'marrow_agent_runtime',
278
+ 'marrow_arbitrate',
279
+ 'marrow_coordinate',
280
+ 'marrow_replay_compare',
281
+ 'marrow_decision_brief',
282
+ 'marrow_think',
283
+ 'marrow_commit',
284
+ 'marrow_workflow_gate',
285
+ 'marrow_completion_contracts',
286
+ 'marrow_evaluate_completion_contract',
287
+ 'marrow_agent_status',
288
+ 'marrow_value_report',
289
+ 'marrow_buyer_proof',
290
+ 'marrow_governance_timeline',
291
+ 'marrow_decision_trace',
292
+ 'marrow_fleet_lessons',
293
+ 'marrow_model_usage',
294
+ ]);
94
295
  const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
95
296
  { client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
96
297
  { client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
@@ -118,6 +319,148 @@ function explicitMcpVersion(command) {
118
319
  return match ? match[1] : null;
119
320
  }
120
321
 
322
+ function resolveToolProfile(value) {
323
+ const structured = value !== null && typeof value === 'object' && !Array.isArray(value);
324
+ const candidate = structured
325
+ ? value.configured_profile
326
+ : value;
327
+ const absent = !structured && candidate === undefined;
328
+ const structuredUnset = structured && candidate === 'unset';
329
+ const configuredProfile = absent || structuredUnset
330
+ ? 'unset'
331
+ : candidate;
332
+ if (!absent && !structuredUnset && !TOOL_PROFILES.has(configuredProfile)) {
333
+ throw new Error(`Invalid MARROW_TOOL_PROFILE. ${TOOL_PROFILE_EXACT_FIX}`);
334
+ }
335
+ const effectiveProfile = configuredProfile === 'unset' ? 'primary' : configuredProfile;
336
+ return {
337
+ configured_profile: configuredProfile,
338
+ effective_profile: effectiveProfile,
339
+ expected_visible_count: TOOL_PROFILE_EXPECTED_COUNTS[effectiveProfile],
340
+ };
341
+ }
342
+
343
+ function normalizePrimaryToolAvailability(value) {
344
+ if (!value || typeof value !== 'object' || Array.isArray(value) || value.profile !== 'primary') return null;
345
+ const evidence = value.entitlement_evidence;
346
+ const counts = value.counts;
347
+ const tools = Array.isArray(value.tools) ? value.tools : [];
348
+ if (!evidence || typeof evidence !== 'object' || evidence.authorizing !== false) return null;
349
+ if (!['available', 'unavailable'].includes(evidence.state)) return null;
350
+ if (!counts || counts.total !== 17 || !Number.isInteger(counts.entitled) || !Number.isInteger(counts.upgrade_required)) return null;
351
+ if (counts.entitled + counts.upgrade_required !== counts.total || tools.length !== counts.total) return null;
352
+ const normalizedTools = [];
353
+ const seen = new Set();
354
+ for (const tool of tools) {
355
+ if (!tool || typeof tool !== 'object' || !PRIMARY_TOOL_NAMES.includes(tool.name) || seen.has(tool.name)) return null;
356
+ if (!['entitled', 'upgrade_required'].includes(tool.state) || typeof tool.always_available !== 'boolean') return null;
357
+ seen.add(tool.name);
358
+ normalizedTools.push({
359
+ name: tool.name,
360
+ state: tool.state,
361
+ always_available: tool.always_available,
362
+ plan_feature: typeof tool.plan_feature === 'string' ? tool.plan_feature : null,
363
+ minimum_plan: typeof tool.minimum_plan === 'string' ? tool.minimum_plan : null,
364
+ owner_management_url: typeof tool.owner_management_url === 'string' ? tool.owner_management_url : '',
365
+ });
366
+ }
367
+ if (PRIMARY_TOOL_NAMES.some((name) => !seen.has(name))) return null;
368
+ const entitled = normalizedTools.filter((tool) => tool.state === 'entitled').length;
369
+ const upgradeRequired = normalizedTools.filter((tool) => tool.state === 'upgrade_required').length;
370
+ if (entitled !== counts.entitled || upgradeRequired !== counts.upgrade_required) return null;
371
+ return {
372
+ profile: 'primary',
373
+ current_plan: typeof value.current_plan === 'string' ? value.current_plan : null,
374
+ owner_management_url: typeof value.owner_management_url === 'string' ? value.owner_management_url : '',
375
+ entitlement_evidence: {
376
+ state: evidence.state,
377
+ source: typeof evidence.source === 'string' ? evidence.source : 'entitlement_read_unavailable',
378
+ authoritative: evidence.authoritative === true,
379
+ authorizing: false,
380
+ },
381
+ counts: { total: 17, entitled, upgrade_required: upgradeRequired },
382
+ tools: normalizedTools,
383
+ };
384
+ }
385
+
386
+ function backendEntitlementProjection(statusProfile, contextProjection) {
387
+ const freshProjection = normalizePrimaryToolAvailability(contextProjection);
388
+ if (freshProjection) {
389
+ return {
390
+ evidence_state: freshProjection.entitlement_evidence.state,
391
+ source: 'authenticated_backend',
392
+ authorizes_calls: false,
393
+ primary_tool_availability: freshProjection,
394
+ };
395
+ }
396
+ const envelope = statusProfile?.backend_entitlement_projection;
397
+ const projected = normalizePrimaryToolAvailability(envelope?.primary_tool_availability);
398
+ const source = ['authenticated_backend', 'cached_or_stale_status', 'backend_projection_not_provided'].includes(envelope?.source)
399
+ ? envelope.source
400
+ : 'backend_projection_not_provided';
401
+ const available = envelope?.authorizes_calls === false
402
+ && envelope?.evidence_state === 'available'
403
+ && source === 'authenticated_backend'
404
+ && projected?.entitlement_evidence.state === 'available';
405
+ return {
406
+ evidence_state: available ? 'available' : 'unavailable',
407
+ source,
408
+ authorizes_calls: false,
409
+ primary_tool_availability: projected,
410
+ };
411
+ }
412
+
413
+ function buildMcpToolProfileReport(value, statusProfile = null, contextProjection = null, forceReload = false) {
414
+ const expected = resolveToolProfile(value);
415
+ const reportedNames = Array.isArray(statusProfile?.visible_tool_names)
416
+ ? statusProfile.visible_tool_names.filter((name) => typeof name === 'string')
417
+ : [];
418
+ const reportedCount = statusProfile?.visible_tool_count;
419
+ const reportedConfigured = statusProfile?.configured_profile;
420
+ const reportedEffective = statusProfile?.effective_profile;
421
+ const uniqueNames = new Set(reportedNames);
422
+ const profileIdentityMatches = reportedConfigured === expected.configured_profile
423
+ && reportedEffective === expected.effective_profile;
424
+ const reportedCatalogIsConsistent = Number.isInteger(reportedCount)
425
+ && reportedCount >= 0
426
+ && reportedNames.length === reportedCount
427
+ && uniqueNames.size === reportedCount;
428
+ const expectedCount = expected.effective_profile === 'full'
429
+ && profileIdentityMatches
430
+ && reportedCatalogIsConsistent
431
+ ? reportedCount
432
+ : expected.expected_visible_count;
433
+ const expectedPrimaryNames = expected.effective_profile !== 'primary'
434
+ || (reportedNames.length === PRIMARY_TOOL_NAMES.length
435
+ && PRIMARY_TOOL_NAMES.every((name) => uniqueNames.has(name)));
436
+ const visibilityLive = !forceReload
437
+ && profileIdentityMatches
438
+ && statusProfile?.local_visibility_grants_entitlement === false
439
+ && reportedCatalogIsConsistent
440
+ && reportedCount === expectedCount
441
+ && expectedPrimaryNames;
442
+ return {
443
+ configured_profile: expected.configured_profile,
444
+ effective_profile: expected.effective_profile,
445
+ expected_visible_count: expectedCount,
446
+ visible_tool_count: visibilityLive ? reportedCount : null,
447
+ actual_visible_count: visibilityLive ? reportedCount : null,
448
+ visible_tool_names: visibilityLive ? reportedNames : [],
449
+ local_visibility_grants_entitlement: false,
450
+ visibility_live: visibilityLive,
451
+ reload_required: !visibilityLive,
452
+ reported_configured_profile: typeof reportedConfigured === 'string' ? reportedConfigured : null,
453
+ reported_effective_profile: typeof reportedEffective === 'string' ? reportedEffective : null,
454
+ backend_entitlement_projection: backendEntitlementProjection(statusProfile, contextProjection),
455
+ };
456
+ }
457
+
458
+ function initialToolProfileReport(value) {
459
+ return {
460
+ ...buildMcpToolProfileReport(value),
461
+ };
462
+ }
463
+
121
464
  function readMcpPackageVersion(packageRoot) {
122
465
  try {
123
466
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
@@ -201,24 +544,37 @@ function inspectMcpProcesses(options = {}) {
201
544
  .filter(isMcpProcessCommand)
202
545
  .map((command) => explicitMcpVersion(command) || packageMcpVersion(command) || 'unknown');
203
546
  const versions = [...new Set(active.filter((version) => version !== 'unknown'))].sort();
547
+ const expectedVersion = expectedMcpInspectionVersion(options);
204
548
  const unknownVersionProcesses = active.filter((version) => version === 'unknown').length;
205
- 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));
206
552
  const mixedVersions = versions.length > 1 || (versions.length > 0 && unknownVersionProcesses > 0);
207
553
  const stale = staleVersions.length > 0;
208
- const needsRepair = stale || mixedVersions || unknownVersionProcesses > 0;
209
- 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`;
210
558
  return {
211
559
  available: process.platform === 'linux' || Array.isArray(options.commands),
212
- expected_version: MCP_ADAPTER_VERSION,
560
+ expected_version: expectedVersion,
213
561
  active_processes: active.length,
214
562
  active_versions: versions,
215
563
  unknown_version_processes: unknownVersionProcesses,
216
564
  stale_versions: staleVersions,
565
+ ahead_unverified: aheadUnverified,
566
+ ahead_unverified_versions: aheadUnverifiedVersions,
217
567
  mixed_versions: mixedVersions,
218
568
  healthy: !needsRepair,
219
- exact_fix: needsRepair ? repairCommand : null,
220
- restart_required: needsRepair,
221
- 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,
222
578
  verification_command: needsRepair ? 'npx -y @getmarrow/install@latest doctor --self-test' : null,
223
579
  };
224
580
  }
@@ -257,20 +613,33 @@ function inspectMcpConfigurations(detection, options = {}) {
257
613
  }
258
614
  }
259
615
  const configuredVersions = [...new Set(versions)].sort();
260
- 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));
261
620
  const mixedVersions = configuredVersions.length > 1
262
621
  || (configuredVersions.length > 0 && unknownVersionConfigurations > 0);
263
- 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;
264
625
  return {
265
- expected_version: MCP_ADAPTER_VERSION,
626
+ expected_version: expectedVersion,
266
627
  files_checked: filesChecked,
267
628
  configurations_found: configurationsFound,
268
629
  configured_versions: configuredVersions,
269
630
  unknown_version_configurations: unknownVersionConfigurations,
270
631
  stale_versions: staleVersions,
632
+ ahead_unverified: aheadUnverified,
633
+ ahead_unverified_versions: aheadUnverifiedVersions,
271
634
  mixed_versions: mixedVersions,
272
635
  healthy,
273
- 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.`,
274
643
  verification_command: healthy ? null : 'npx -y @getmarrow/install@latest doctor --self-test',
275
644
  };
276
645
  }
@@ -307,7 +676,7 @@ function sourceClient() {
307
676
  return aliases[raw] || (SOURCE_CLIENTS.has(raw) ? raw : 'custom');
308
677
  }
309
678
 
310
- function parseArgs(argv) {
679
+ function parseArgs(argv, env = process.env) {
311
680
  const options = {
312
681
  cwd: process.cwd(),
313
682
  yes: false,
@@ -315,9 +684,10 @@ function parseArgs(argv) {
315
684
  doctor: false,
316
685
  repair: false,
317
686
  mode: 'auto',
318
- apiKey: process.env.MARROW_API_KEY || '',
319
- baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
320
- agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
687
+ apiKey: env.MARROW_API_KEY || '',
688
+ baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
689
+ agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
690
+ toolProfile: resolveToolProfile(env.MARROW_TOOL_PROFILE),
321
691
  selfTest: true,
322
692
  selfTestExplicitlyDisabled: false,
323
693
  json: false,
@@ -409,6 +779,7 @@ function parseArgs(argv) {
409
779
  if (options.activate && options.dryRun) {
410
780
  throw new Error('activate cannot be combined with --dry-run; use --dry-run without activate to preview changes');
411
781
  }
782
+ options.resolveMcpRegistry = Boolean(options.doctor || options.repair || options.update);
412
783
 
413
784
  return options;
414
785
  }
@@ -439,6 +810,10 @@ Options:
439
810
  --agent-id <id> Agent/fleet id for self-test headers
440
811
  --no-controller Do not start the local background controller during install/repair
441
812
  --no-self-test Skip API smoke/self-test
813
+
814
+ Environment:
815
+ MARROW_TOOL_PROFILE Leave unset for primary (17 tools), or explicitly set primary, core, or full.
816
+ Visibility never grants entitlement; backend plans and permissions authorize calls.
442
817
  `;
443
818
  }
444
819
 
@@ -727,6 +1102,7 @@ function passiveInstructions() {
727
1102
  Marrow should run passively after install:
728
1103
 
729
1104
  - Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
1105
+ - Leave \`MARROW_TOOL_PROFILE\` unset for the 17-tool primary surface. Set \`MARROW_TOOL_PROFILE=core\` or \`MARROW_TOOL_PROFILE=full\` only as an explicit opt-in; backend plans and permissions still enforce access to every visible tool.
730
1106
  - Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
731
1107
  - Native Claude hooks install only when \`.claude\` is present. Codex native hooks install into \`.codex/hooks.json\`; Cursor and Composer use \`.cursor/hooks.json\`; Cline uses non-overwriting executable files under \`.clinerules/hooks/\`; Windsurf uses \`.windsurf/hooks.json\`; Gemini CLI uses \`.gemini/settings.json\`; Grok uses trusted global \`~/.grok/hooks/marrow.json\`. Restart the host, enable/review hooks, disable Windsurf Restricted Mode where native hooks are required, and trust the workspace before claiming runtime coverage. The governed wrapper remains an explicit bounded fallback. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
732
1108
  - Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
@@ -790,6 +1166,7 @@ function envExample(options = {}) {
790
1166
  MARROW_BASE_URL=${JSON.stringify(baseUrl)}
791
1167
  MARROW_FLEET_AGENT_ID=${JSON.stringify(agentId)}
792
1168
  MARROW_CLIENT=${JSON.stringify(client)}
1169
+ # MARROW_TOOL_PROFILE is intentionally unset: ordinary setup uses primary. Set core or full only as an explicit opt-in.
793
1170
  MARROW_ENFORCEMENT_MODE=auto
794
1171
  MARROW_PASSIVE_BRIEF=auto
795
1172
  MARROW_PASSIVE_VALUE_REPORT=true
@@ -1431,8 +1808,12 @@ function activationProfile(detection, plan, changes, client) {
1431
1808
  const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
1432
1809
  ? ['pre_action', 'action_result', 'outcome_closure']
1433
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);
1434
1815
  const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
1435
- ? MCP_ADAPTER_VERSION
1816
+ ? mcpTargetVersion
1436
1817
  : capabilityLevel === 'sdk_passive_runtime'
1437
1818
  ? SDK_ADAPTER_VERSION
1438
1819
  : INSTALLER_ADAPTER_VERSION;
@@ -1444,71 +1825,71 @@ function activationProfile(detection, plan, changes, client) {
1444
1825
  const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
1445
1826
  const grokSettings = safeJsonObject(detection.paths.grokHooks);
1446
1827
  if (client === 'codex') {
1447
- if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1448
- if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
1449
- if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
1450
- 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');
1451
1832
  } else if (client === 'cursor' || client === 'composer') {
1452
- 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, {
1453
1834
  timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
1454
1835
  })) observedHooks.push('pre_action');
1455
- 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, {
1456
1837
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1457
- }) && 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, {
1458
1839
  timeout: CODEX_HOOK_TIMEOUT_SECONDS,
1459
1840
  })) observedHooks.push('action_result');
1460
- if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
1841
+ if (exactCursorHookConfigured(cursorSettings, 'stop', targetCommand(CURSOR_SESSION_END_HOOK_COMMAND), undefined, {
1461
1842
  timeout: CODEX_SESSION_TIMEOUT_SECONDS,
1462
1843
  })) observedHooks.push('outcome_closure');
1463
1844
  } else if (client === 'cline') {
1464
1845
  for (const hook of clineHookContract(detection)) {
1465
- if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
1846
+ if (exactExecutableFile(hook.path, targetCommand(hook.content))) observedHooks.push(hook.stage);
1466
1847
  }
1467
1848
  } else if (client === 'windsurf') {
1468
1849
  if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
1469
- windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
1850
+ windsurfSettings, event, targetCommand(WINDSURF_PRE_ACTION_HOOK_COMMAND),
1470
1851
  ))) observedHooks.push('pre_action');
1471
1852
  if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
1472
- windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
1853
+ windsurfSettings, event, targetCommand(WINDSURF_ACTION_RESULT_HOOK_COMMAND),
1473
1854
  ))) observedHooks.push('action_result');
1474
1855
  if (exactWindsurfHookConfigured(
1475
- windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
1856
+ windsurfSettings, 'post_cascade_response', targetCommand(WINDSURF_SESSION_END_HOOK_COMMAND),
1476
1857
  )) observedHooks.push('response_closeout');
1477
1858
  } else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
1478
1859
  if (exactGeminiHookConfigured(
1479
- geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
1860
+ geminiSettings, 'BeforeTool', 'marrow-before-tool', targetCommand(GEMINI_PRE_ACTION_HOOK_COMMAND),
1480
1861
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1481
1862
  )) observedHooks.push('pre_action');
1482
1863
  if (exactGeminiHookConfigured(
1483
- geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
1864
+ geminiSettings, 'AfterTool', 'marrow-after-tool', targetCommand(GEMINI_ACTION_RESULT_HOOK_COMMAND),
1484
1865
  GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
1485
1866
  )) observedHooks.push('action_result');
1486
1867
  if (exactGeminiHookConfigured(
1487
- geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
1868
+ geminiSettings, 'AfterAgent', 'marrow-after-agent', targetCommand(GEMINI_SESSION_END_HOOK_COMMAND),
1488
1869
  undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
1489
1870
  )) observedHooks.push('turn_closeout');
1490
1871
  } else if (client === 'grok') {
1491
1872
  if (exactGrokHookConfigured(
1492
- 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,
1493
1874
  )) observedHooks.push('pre_action');
1494
1875
  if (exactGrokHookConfigured(
1495
- 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,
1496
1877
  ) && exactGrokHookConfigured(
1497
- 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,
1498
1879
  )) observedHooks.push('action_result');
1499
1880
  if (exactGrokHookConfigured(
1500
- grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
1881
+ grokSettings, 'Stop', targetCommand(GROK_SESSION_END_HOOK_COMMAND), undefined, 3,
1501
1882
  ) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
1502
1883
  } else {
1503
1884
  if (capabilityLevel === 'native_hooks'
1504
- && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
1885
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', targetCommand(MCP_CONTEXT_HOOK_COMMAND))) observedHooks.push('prompt');
1505
1886
  if (capabilityLevel === 'native_hooks'
1506
- && 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');
1507
1888
  if (capabilityLevel === 'native_hooks'
1508
- && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
1509
- && 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');
1510
1891
  if (capabilityLevel === 'native_hooks'
1511
- && 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');
1512
1893
  }
1513
1894
  const passiveRuntime = safeRead(detection.paths.passiveRuntime);
1514
1895
  if (capabilityLevel === 'sdk_passive_runtime'
@@ -1524,7 +1905,7 @@ function activationProfile(detection, plan, changes, client) {
1524
1905
  if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
1525
1906
  config?.mcpServers?.marrow?.command === 'npx'
1526
1907
  && Array.isArray(config.mcpServers.marrow.args)
1527
- && 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`
1528
1909
  ))) observedHooks.push('mcp_tool_calls');
1529
1910
  const fingerprintMaterial = changes
1530
1911
  .filter((change) => change.applied || change.already_present)
@@ -1565,7 +1946,7 @@ function activationProfile(detection, plan, changes, client) {
1565
1946
  : client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
1566
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.'
1567
1948
  : client === 'grok'
1568
- ? `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.`
1569
1950
  : 'npx @getmarrow/install --repair';
1570
1951
  return {
1571
1952
  adapter_version: adapterVersion,
@@ -1622,13 +2003,18 @@ function upsertMcpServerConfig(filePath, options = {}) {
1622
2003
  const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
1623
2004
  ? config.mcpServers
1624
2005
  : {};
2006
+ const existingProfile = resolveToolProfile(servers.marrow?.env?.MARROW_TOOL_PROFILE).configured_profile;
2007
+ const requestedProfile = resolveToolProfile(options.toolProfile).configured_profile;
2008
+ const configuredProfile = requestedProfile === 'unset' ? existingProfile : requestedProfile;
2009
+ const env = {
2010
+ MARROW_BASE_URL: baseUrl,
2011
+ MARROW_FLEET_AGENT_ID: agentId,
2012
+ };
2013
+ if (configuredProfile !== 'unset') env.MARROW_TOOL_PROFILE = configuredProfile;
1625
2014
  servers.marrow = {
1626
2015
  command: 'npx',
1627
2016
  args: ['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp'],
1628
- env: {
1629
- MARROW_BASE_URL: baseUrl,
1630
- MARROW_FLEET_AGENT_ID: agentId,
1631
- },
2017
+ env,
1632
2018
  };
1633
2019
  config.mcpServers = servers;
1634
2020
  return JSON.stringify(config, null, 2) + '\n';
@@ -1849,6 +2235,8 @@ function buildPlan(detection, options) {
1849
2235
  const client = options.client || detectedClient(detection);
1850
2236
  const agentId = String(options.agentId || '').trim() || stableAgentId(detection.root, client);
1851
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);
1852
2240
  const mode = options.mode === 'auto'
1853
2241
  ? detection.node ? 'both' : 'mcp'
1854
2242
  : options.mode;
@@ -1876,7 +2264,7 @@ function buildPlan(detection, options) {
1876
2264
  type: 'json-transform',
1877
2265
  path: detection.paths.claudeSettings,
1878
2266
  label: 'Claude Code MCP passive hooks',
1879
- transform: upsertClaudeHooks,
2267
+ transform: (filePath) => retarget(upsertClaudeHooks(filePath)),
1880
2268
  });
1881
2269
  }
1882
2270
  if (detection.codex) {
@@ -1884,7 +2272,7 @@ function buildPlan(detection, options) {
1884
2272
  type: 'json-transform',
1885
2273
  path: detection.paths.codexHooks,
1886
2274
  label: 'Codex native hooks',
1887
- transform: upsertCodexHooks,
2275
+ transform: (filePath) => retarget(upsertCodexHooks(filePath)),
1888
2276
  });
1889
2277
  }
1890
2278
  if (detection.cline) {
@@ -1893,7 +2281,7 @@ function buildPlan(detection, options) {
1893
2281
  type: 'owned-executable',
1894
2282
  path: hook.path,
1895
2283
  label: hook.label,
1896
- content: hook.content,
2284
+ content: retarget(hook.content),
1897
2285
  mode: 0o755,
1898
2286
  conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
1899
2287
  });
@@ -1904,7 +2292,7 @@ function buildPlan(detection, options) {
1904
2292
  type: 'json-transform',
1905
2293
  path: detection.paths.windsurfHooks,
1906
2294
  label: 'Windsurf native hooks',
1907
- transform: upsertWindsurfHooks,
2295
+ transform: (filePath) => retarget(upsertWindsurfHooks(filePath)),
1908
2296
  });
1909
2297
  }
1910
2298
  if (detection.gemini) {
@@ -1912,27 +2300,27 @@ function buildPlan(detection, options) {
1912
2300
  type: 'json-transform',
1913
2301
  path: detection.paths.geminiSettings,
1914
2302
  label: 'Gemini CLI native hooks',
1915
- transform: upsertGeminiHooks,
2303
+ transform: (filePath) => retarget(upsertGeminiHooks(filePath)),
1916
2304
  });
1917
2305
  }
1918
2306
  writes.push({
1919
2307
  type: 'json-transform',
1920
2308
  path: detection.paths.mcpJson,
1921
2309
  label: 'Project MCP server config',
1922
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2310
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
1923
2311
  });
1924
2312
  if (detection.cursor) {
1925
2313
  writes.push({
1926
2314
  type: 'json-transform',
1927
2315
  path: detection.paths.cursorHooks,
1928
2316
  label: 'Cursor native hooks',
1929
- transform: upsertCursorHooks,
2317
+ transform: (filePath) => retarget(upsertCursorHooks(filePath)),
1930
2318
  });
1931
2319
  writes.push({
1932
2320
  type: 'json-transform',
1933
2321
  path: detection.paths.cursorMcp,
1934
2322
  label: 'Cursor MCP server config',
1935
- transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
2323
+ transform: (filePath) => retarget(upsertMcpServerConfig(filePath, { agentId, baseUrl, toolProfile: options.toolProfile })),
1936
2324
  });
1937
2325
  }
1938
2326
  }
@@ -1942,7 +2330,7 @@ function buildPlan(detection, options) {
1942
2330
  type: 'md-block',
1943
2331
  path: detection.paths.agentsMd,
1944
2332
  label: 'Agent instructions',
1945
- block: passiveInstructions(),
2333
+ block: retarget(passiveInstructions()),
1946
2334
  });
1947
2335
  }
1948
2336
 
@@ -1951,11 +2339,11 @@ function buildPlan(detection, options) {
1951
2339
  type: 'file',
1952
2340
  path: detection.paths.cursorRules,
1953
2341
  label: 'Cursor Marrow rule',
1954
- content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
2342
+ content: retarget(passiveInstructions()).replace(/<!--[^>]+-->/g, '').trim() + '\n',
1955
2343
  });
1956
2344
  }
1957
2345
 
1958
- return { mode, root: detection.root, writes };
2346
+ return { mode, root: detection.root, writes, mcp_target_version: mcpTargetVersion };
1959
2347
  }
1960
2348
 
1961
2349
  function assertContainedManagedTarget(root, targetPath) {
@@ -2021,9 +2409,16 @@ function applyPlan(plan, options) {
2021
2409
  const prepared = plan.writes.map((write) => {
2022
2410
  const fileExists = exists(write.path);
2023
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;
2024
2417
  let after;
2025
2418
  let hookConflict = false;
2026
- if (write.type === 'file') {
2419
+ if (automaticRepairSuppressed) {
2420
+ after = before;
2421
+ } else if (write.type === 'file') {
2027
2422
  if (write.overwrite === false && before) {
2028
2423
  after = before;
2029
2424
  } else {
@@ -2046,21 +2441,43 @@ function applyPlan(plan, options) {
2046
2441
 
2047
2442
  const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
2048
2443
  const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
2049
- return { write, before, after, hookConflict, modeChanged };
2444
+ return {
2445
+ write,
2446
+ before,
2447
+ after,
2448
+ hookConflict,
2449
+ modeChanged,
2450
+ automaticRepairSuppressed,
2451
+ aheadUnverifiedVersions,
2452
+ };
2050
2453
  });
2051
2454
 
2052
2455
  const changes = [];
2053
- 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) {
2054
2465
  const contentChanged = before !== after;
2055
- const changed = !hookConflict && (contentChanged || modeChanged);
2056
- 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);
2057
2469
  changes.push({
2058
2470
  path: write.path,
2059
2471
  label: write.label,
2060
2472
  changed,
2061
2473
  applied: changed && writeApplied,
2062
- already_present: !changed && !hookConflict,
2474
+ already_present: !changed && !hookConflict && !automaticRepairSuppressed,
2063
2475
  hook_conflict: hookConflict,
2476
+ automatic_repair_suppressed: automaticRepairSuppressed,
2477
+ ...(automaticRepairSuppressed ? {
2478
+ ahead_unverified_versions: aheadUnverifiedVersions,
2479
+ exact_fix: mcpRegistryVerificationAction(aheadUnverifiedVersions),
2480
+ } : {}),
2064
2481
  ...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
2065
2482
  });
2066
2483
  if (contentChanged && writeApplied) {
@@ -2106,12 +2523,14 @@ function runtimeGateVerified(runtime) {
2106
2523
  }
2107
2524
 
2108
2525
  async function runSelfTest(options) {
2109
- if (!options.selfTest) return { skipped: true, reason: 'disabled' };
2526
+ const initialProfile = initialToolProfileReport(options.toolProfile);
2527
+ if (!options.selfTest) return { skipped: true, reason: 'disabled', mcp_tool_profile: initialProfile };
2110
2528
  if (!options.apiKey) {
2111
2529
  return {
2112
2530
  skipped: true,
2113
2531
  reason: 'missing MARROW_API_KEY',
2114
2532
  exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install --repair',
2533
+ mcp_tool_profile: initialProfile,
2115
2534
  };
2116
2535
  }
2117
2536
 
@@ -2124,7 +2543,7 @@ async function runSelfTest(options) {
2124
2543
  'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
2125
2544
  'x-marrow-install-version': INSTALLER_ADAPTER_VERSION,
2126
2545
  'x-marrow-sdk-version': SDK_ADAPTER_VERSION,
2127
- 'x-marrow-mcp-version': MCP_ADAPTER_VERSION,
2546
+ 'x-marrow-mcp-version': executableMcpTarget(options).version,
2128
2547
  };
2129
2548
  if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
2130
2549
 
@@ -2157,6 +2576,14 @@ async function runSelfTest(options) {
2157
2576
  });
2158
2577
 
2159
2578
  const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
2579
+ const context = await requestJson(`${baseUrl}/v1/agent/context`, { headers })
2580
+ .catch(() => null);
2581
+ const toolProfile = buildMcpToolProfileReport(
2582
+ options.toolProfile,
2583
+ status.mcp_tool_profile,
2584
+ context?.primary_tool_availability,
2585
+ Boolean(options.activation),
2586
+ );
2160
2587
  const runtime = await requestJson(`${baseUrl}/v1/agent/runtime`, {
2161
2588
  method: 'POST',
2162
2589
  headers,
@@ -2291,6 +2718,7 @@ async function runSelfTest(options) {
2291
2718
  }
2292
2719
  return {
2293
2720
  skipped: false,
2721
+ mcp_tool_profile: toolProfile,
2294
2722
  decision_id: decisionId,
2295
2723
  active: Boolean(status.enabled ?? status.ok),
2296
2724
  health: status.health || null,
@@ -2480,11 +2908,34 @@ function printReport(report) {
2480
2908
 
2481
2909
  process.stdout.write('\nPlanned changes:\n');
2482
2910
  for (const change of report.changes) {
2483
- 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';
2484
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
+ }
2485
2918
  }
2486
2919
 
2487
2920
  process.stdout.write('\nSelf-test:\n');
2921
+ const toolProfile = report.selfTest.mcp_tool_profile || report.toolProfile;
2922
+ if (toolProfile) {
2923
+ process.stdout.write(`- configured tool profile: ${toolProfile.configured_profile}\n`);
2924
+ process.stdout.write(`- effective tool profile: ${toolProfile.effective_profile}\n`);
2925
+ process.stdout.write(`- expected visible tools: ${toolProfile.expected_visible_count == null ? 'complete catalog (awaiting reloaded MCP count)' : toolProfile.expected_visible_count}\n`);
2926
+ process.stdout.write(`- actual visible tools: ${toolProfile.actual_visible_count == null ? 'unavailable until process reload' : toolProfile.actual_visible_count}\n`);
2927
+ process.stdout.write(`- visible tool names: ${toolProfile.visibility_live ? toolProfile.visible_tool_names.join(', ') : 'unavailable until process reload'}\n`);
2928
+ process.stdout.write(`- profile live: ${toolProfile.visibility_live ? 'yes' : 'no'}\n`);
2929
+ const projection = toolProfile.backend_entitlement_projection;
2930
+ const availability = projection?.primary_tool_availability;
2931
+ if (projection?.evidence_state === 'available' && availability?.entitlement_evidence?.state === 'available') {
2932
+ process.stdout.write(`- backend-projected entitled tools: ${availability.counts.entitled}\n`);
2933
+ process.stdout.write(`- backend-projected upgrade-required tools: ${availability.counts.upgrade_required}\n`);
2934
+ process.stdout.write(`- backend projection source: ${projection.source}; authorizes calls: no\n`);
2935
+ } else {
2936
+ process.stdout.write(`- backend-projected entitlements: unavailable (source: ${projection?.source || 'backend_projection_not_provided'}; non-authorizing)\n`);
2937
+ }
2938
+ }
2488
2939
  if (report.selfTest.skipped) {
2489
2940
  process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
2490
2941
  if (report.selfTest.exact_fix) process.stdout.write(`- exact fix: ${report.selfTest.exact_fix}\n`);
@@ -2613,6 +3064,8 @@ function printReport(report) {
2613
3064
  const processes = report.doctor.mcpProcesses;
2614
3065
  process.stdout.write(`- MCP process versions: ${processes.active_versions.length ? processes.active_versions.join(', ') : processes.active_processes ? 'unknown' : 'none'}\n`);
2615
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');
2616
3069
  if (processes.restart_instruction) process.stdout.write(`- restart required: ${processes.restart_instruction}\n`);
2617
3070
  if (processes.verification_command) process.stdout.write(`- verify repair: ${processes.verification_command}\n`);
2618
3071
  }
@@ -2620,6 +3073,8 @@ function printReport(report) {
2620
3073
  const configurations = report.doctor.mcpConfigurations;
2621
3074
  process.stdout.write(`- configured MCP versions: ${configurations.configured_versions.length ? configurations.configured_versions.join(', ') : 'none pinned'}\n`);
2622
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');
2623
3078
  }
2624
3079
  if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
2625
3080
  process.stdout.write(`- live health: ${report.doctor.healthCommand}\n`);
@@ -2650,14 +3105,30 @@ async function install(options) {
2650
3105
  if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
2651
3106
  throw new Error('repair requires explicit write authorization (--yes)');
2652
3107
  }
3108
+ options.toolProfile = resolveToolProfile(options.toolProfile === undefined
3109
+ ? process.env.MARROW_TOOL_PROFILE
3110
+ : options.toolProfile);
2653
3111
  const detection = detectEnvironment(options.cwd);
2654
3112
  const client = detectedClient(detection);
2655
3113
  options.client = client;
2656
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;
2657
3128
  const plan = buildPlan(detection, options);
2658
3129
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
2659
3130
  const changes = applyPlan(plan, options);
2660
- const profile = activationProfile(detection, plan, changes, client);
3131
+ let profile = activationProfile(detection, plan, changes, client);
2661
3132
  options.activation = options.activate ? {
2662
3133
  harness: client,
2663
3134
  agent_id: options.agentId,
@@ -2688,19 +3159,66 @@ async function install(options) {
2688
3159
  ? repairConfigDiagnostics(configDiagnostics)
2689
3160
  : [];
2690
3161
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
2691
- const mcpProcesses = inspectMcpProcesses({ commands: options.processCommands });
2692
- 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
+ }
2693
3189
  let selfTest;
2694
3190
  try {
2695
3191
  selfTest = await runSelfTest(options);
2696
3192
  } catch (error) {
2697
3193
  const message = error instanceof Error ? error.message : String(error);
2698
3194
  if (options.activate) throw new Error(`Marrow activation failed: ${message}`);
2699
- selfTest = { skipped: false, active: false, error: message };
3195
+ selfTest = {
3196
+ skipped: false,
3197
+ active: false,
3198
+ error: message,
3199
+ mcp_tool_profile: initialToolProfileReport(options.toolProfile),
3200
+ };
2700
3201
  }
3202
+ selfTest = alignMcpRecommendationVersions(
3203
+ selfTest,
3204
+ mcpTarget.version,
3205
+ '',
3206
+ registryVerificationAction,
3207
+ );
2701
3208
  if (options.activate && !selfTest.activation_verified) {
2702
3209
  throw new Error('Marrow activation failed: server confirmation was not returned');
2703
3210
  }
3211
+ const harnessReload = harnessReloadPlan(detection, changes);
3212
+ if (harnessReload.required && selfTest.mcp_tool_profile) {
3213
+ selfTest.mcp_tool_profile = {
3214
+ ...selfTest.mcp_tool_profile,
3215
+ visible_tool_count: null,
3216
+ actual_visible_count: null,
3217
+ visible_tool_names: [],
3218
+ visibility_live: false,
3219
+ reload_required: true,
3220
+ };
3221
+ }
2704
3222
  const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
2705
3223
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
2706
3224
  const controllerPlatform = options.controllerPlatform || process.platform;
@@ -2747,7 +3265,10 @@ async function install(options) {
2747
3265
  attempted: true,
2748
3266
  fixedConfig: changedConfig,
2749
3267
  selfTestPassed,
2750
- message: selfTestPassed
3268
+ automaticMcpRepairSuppressed,
3269
+ message: automaticMcpRepairSuppressed
3270
+ ? registryVerificationAction
3271
+ : selfTestPassed
2751
3272
  ? selfTest.health === 'healthy'
2752
3273
  ? 'I fixed Marrow passive config, one-call runtime is active, and self-test passed.'
2753
3274
  : `I fixed Marrow passive config and self-test passed; status is ${selfTest.health || 'unknown'}${selfTest.next_action ? `. Next action: ${selfTest.next_action}` : ''}.`
@@ -2759,9 +3280,10 @@ async function install(options) {
2759
3280
 
2760
3281
  return {
2761
3282
  root: detection.root,
2762
- adapterProvenance: ADAPTER_PROVENANCE,
3283
+ adapterProvenance: adapterProvenanceForMcpTarget(mcpTarget),
2763
3284
  mode: plan.mode,
2764
3285
  writeMode,
3286
+ toolProfile: selfTest.mcp_tool_profile || initialToolProfileReport(options.toolProfile),
2765
3287
  detected: {
2766
3288
  node: detection.node,
2767
3289
  python: detection.python,
@@ -2784,7 +3306,7 @@ async function install(options) {
2784
3306
  receipt: selfTest.activation_receipt || null,
2785
3307
  profile,
2786
3308
  },
2787
- harnessReload: harnessReloadPlan(detection, changes),
3309
+ harnessReload,
2788
3310
  firstCapture: firstCapturePath(detection, options.agentId),
2789
3311
  changes,
2790
3312
  doctor: {
@@ -2794,7 +3316,10 @@ async function install(options) {
2794
3316
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
2795
3317
  mcpProcesses,
2796
3318
  mcpConfigurations,
2797
- 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
2798
3323
  ? envHints.length
2799
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.`
2800
3325
  : 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
@@ -2812,10 +3337,14 @@ async function install(options) {
2812
3337
  ...(options.keyFromArg
2813
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.']
2814
3339
  : []),
2815
- ...(!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
2816
3343
  ? ['Stale, mixed, or version-unknown Marrow MCP clients are active. Run the exact repair command, then restart every owning harness.']
2817
3344
  : []),
2818
- ...(!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
2819
3348
  ? ['Stale, mixed, or version-unknown Marrow MCP versions remain in owner configuration. Repair each owning workspace and restart its harness.']
2820
3349
  : []),
2821
3350
  ],
@@ -2866,6 +3395,10 @@ module.exports = {
2866
3395
  GROK_SESSION_END_HOOK_COMMAND,
2867
3396
  GROK_NATIVE_HOOK_MATCHER,
2868
3397
  printReport,
3398
+ buildMcpToolProfileReport,
3399
+ resolveMcpTargetVersion,
3400
+ resolveToolProfile,
3401
+ PRIMARY_TOOL_NAMES,
2869
3402
  ADAPTER_PROVENANCE,
2870
3403
  HARNESS_CAPABILITY_REGISTRY,
2871
3404
  defaultHarnessInstallMatrix,