@getmarrow/install 0.1.33 → 0.1.35

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
@@ -53,7 +53,7 @@ Verify current claims through the [public evidence manifest](https://getmarrow.a
53
53
  ## Install
54
54
 
55
55
  ```bash
56
- npx @getmarrow/install --yes
56
+ npx @getmarrow/install activate
57
57
  ```
58
58
 
59
59
  Required secret:
@@ -62,9 +62,41 @@ Required secret:
62
62
  export MARROW_API_KEY=mrw_live_...
63
63
  ```
64
64
 
65
- ## What's New in v0.1.33
65
+ ## Keeping Marrow Current
66
+
67
+ 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.
68
+
69
+ ```bash
70
+ npx -y @getmarrow/install@latest activate
71
+ npx -y @getmarrow/install@latest doctor
72
+
73
+ # Use only when doctor reports drift
74
+ npx -y @getmarrow/install@latest --repair
75
+ ```
76
+
77
+ `activate` reconciles Marrow-managed entries while retaining unrelated user hooks and configuration. Detection and notification are automatic; local changes remain explicit and subject to the operator's normal change policy.
66
78
 
67
- v0.1.33 adds agent-disagreement visibility to the Fleet Operator TUI. Operators can see open and review-required arbitration receipts and inspect the exact next action produced through the existing Marrow runtime. It preserves the machine-readable governance-fit contract introduced in v0.1.32 and the server-verified first-run activation introduced in v0.1.29:
79
+ ## What's New in v0.1.35
80
+
81
+ v0.1.35 turns server-side client update detection into a guided, low-friction operator workflow. Installer status, activation reports, and the Fleet Operator expose a request-specific advisory with the exact update and verification commands while keeping local mutation explicit:
82
+
83
+ - official installer requests identify the installed `@getmarrow/install` version;
84
+ - status, self-test, and Fleet Operator output show recommended, unrecognized, and security-required update states without conflating them;
85
+ - generated agent instructions tell the agent to notify the operator and obey local change policy;
86
+ - certified activation now pins MCP 3.9.51 and SDK 3.7.50, including the exact SDK registry integrity;
87
+ - `activate`, `doctor`, and `--repair` remain explicit commands and preserve unrelated hooks and configuration.
88
+
89
+ It preserves the passive-governance verification introduced in v0.1.34. Activation registers a bounded capability profile, a one-way configuration fingerprint, expected and observed hook surfaces, and a server-accepted lifecycle receipt. The Fleet Operator shows activation state, capture coverage, outcome closure, intervention follow-through, drift, and the exact repair:
90
+
91
+ - Claude Code installation includes exact `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, and `Stop` hooks;
92
+ - matching pre-action/result receipts use one tool correlation, and activation fingerprints the exact hook contract without uploading configuration contents;
93
+ - the capability registry distinguishes native hooks, MCP, SDK passive runtime, governed wrappers, and custom event contracts;
94
+ - `activate` fails when the local integration is incomplete or the server does not accept the exact activation profile;
95
+ - `doctor` and `--repair` use the same configuration evidence without exposing configuration contents;
96
+ - the harness certification suite prevents support claims from overstating what is automatic;
97
+ - unavailable coverage remains “insufficient data” instead of a synthetic percentage.
98
+
99
+ It preserves agent-disagreement visibility from v0.1.33 and the server-verified first-run activation introduced in v0.1.29:
68
100
 
69
101
  - GitHub and npm now advertise separate signed discovery placements;
70
102
  - package metadata identifies the installer as agent governance rather than a general memory utility;
@@ -104,10 +136,13 @@ With a valid key, `activate`:
104
136
  4. closes its outcome;
105
137
  5. sends the exact self-test decision ID to Marrow for server-side verification;
106
138
  6. reads agent status and the one-call runtime;
107
- 7. returns an activation receipt with capture, intervention, closure, first-value state, and the exact next action.
139
+ 7. registers the detected capability, expected hooks, and one-way configuration fingerprint;
140
+ 8. returns an activation receipt with capture, intervention, closure, first-value state, and the exact next action.
108
141
 
109
142
  Healthy output confirms the exact decision outcome exists under the authenticated account and agent. A local file write or client-supplied `verified: true` value cannot produce an active receipt.
110
143
 
144
+ The installer does not claim identical automation for every harness. Native hooks provide the broadest automatic coverage; MCP covers MCP-routed actions; the SDK covers owned Node processes; the governed runner covers commands launched through it; custom harnesses must map their own lifecycle events.
145
+
111
146
  ## Govern TUI
112
147
 
113
148
  Open the interactive setup panel:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -4,6 +4,7 @@ const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
6
  const readline = require('node:readline');
7
+ const { version: INSTALLER_PACKAGE_VERSION } = require('../package.json');
7
8
 
8
9
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
9
10
  const HIGH_RISK_TERMS = /\b(deploy|prod|production|publish|release|merge|migration|migrate|secret|token|key|cloudflare|wrangler|npm publish|gh pr merge|git push|terraform apply|kubectl apply|delete|destroy|drop)\b/i;
@@ -343,6 +344,8 @@ function headers(options) {
343
344
  'X-Marrow-Agent-Id': options.agentId,
344
345
  'X-Marrow-Session-Id': options.sessionId,
345
346
  'X-Marrow-Client': sourceClient(options.client),
347
+ 'X-Marrow-Package': '@getmarrow/install',
348
+ 'X-Marrow-Package-Version': INSTALLER_PACKAGE_VERSION,
346
349
  'User-Agent': '@getmarrow/install governed-runner',
347
350
  };
348
351
  return h;
@@ -657,6 +660,31 @@ async function statusOnly(parsed) {
657
660
  return requestJson(parsed.options, 'GET', '/v1/agent/status');
658
661
  }
659
662
 
663
+ function statusPanel(status = {}) {
664
+ const update = status.client_update && typeof status.client_update === 'object'
665
+ ? status.client_update
666
+ : null;
667
+ const lines = [
668
+ 'Marrow runtime status',
669
+ `Health: ${displayText(firstDefined(status.health, status.status, status.ok === false ? 'degraded' : 'unknown'), 40)}`,
670
+ ];
671
+ const notification = update?.notification_state || update?.notification;
672
+ const priority = notification === 'security_required' || notification === 'recommended'
673
+ ? notification
674
+ : update?.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown'
675
+ ? 'version_unknown'
676
+ : update?.priority || 'recommended';
677
+ if (update && (update.update_available === true || update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' || priority === 'security_required')) {
678
+ lines.push(`Client update: ${displayText(priority, 32)}; installed=${displayText(update.installed_version || update.current_version || 'unknown', 32)}; latest=${displayText(update.latest_version || 'unknown', 32)}`);
679
+ lines.push('Automatic notification: yes; automatic local mutation: no; operator policy applies.');
680
+ if (update.update_command || update.exact_update_command) lines.push(`Update: ${displayText(update.update_command || update.exact_update_command, 240)}`);
681
+ if (update.verification_command || update.exact_verification_command) lines.push(`Verify: ${displayText(update.verification_command || update.exact_verification_command, 240)}`);
682
+ } else {
683
+ lines.push('Client update: current or unavailable.');
684
+ }
685
+ return lines.join('\n');
686
+ }
687
+
660
688
  async function optionalRequestJson(options, method, route, body) {
661
689
  try {
662
690
  return { ok: true, data: await requestJson(options, method, route, body) };
@@ -806,6 +834,12 @@ function normalizeFixCommands(status, capacity) {
806
834
  add(status.route_contract?.exact_fix);
807
835
  add(status.auto_outcome_closure?.exact_fix);
808
836
  add(status.capture_coverage?.exact_fix);
837
+ add(status.activation_coverage?.exact_fix);
838
+ add(status.activation_coverage?.drift?.repair_command, true);
839
+ add(status.passive_activation?.exact_fix);
840
+ if (status.client_update?.update_available === true || status.client_update?.version_status === 'unknown' || status.client_update?.notification_state === 'unknown' || status.client_update?.notification === 'version_unknown') {
841
+ add(status.client_update?.update_command || status.client_update?.exact_update_command, true);
842
+ }
809
843
  add(capacity.exact_next_action, true);
810
844
  add(capacity.next_action, true);
811
845
  if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
@@ -813,6 +847,52 @@ function normalizeFixCommands(status, capacity) {
813
847
  return commands.slice(0, 6);
814
848
  }
815
849
 
850
+ function normalizeActivationCoverage(status, report, fleet) {
851
+ const source = firstDefined(
852
+ status.activation_coverage,
853
+ status.passive_activation,
854
+ report.activation_coverage,
855
+ report.passive_activation,
856
+ fleet.activation_coverage,
857
+ {},
858
+ ) || {};
859
+ const activation = source.activation || {};
860
+ const capture = source.capture_coverage || source.capture || source.coverage || {};
861
+ const closure = source.outcome_closure || source.closure || {};
862
+ const effectiveness = source.intervention_effectiveness || source.effectiveness || {};
863
+ const drift = source.drift && typeof source.drift === 'object' ? source.drift : {};
864
+ const available = source.available === true || capture.available === true;
865
+ const driftAvailable = available && drift.available === true && typeof drift.detected === 'boolean';
866
+ const percent = (value) => {
867
+ const number = Number(value);
868
+ if (!Number.isFinite(number)) return null;
869
+ return Math.max(0, Math.min(100, number));
870
+ };
871
+ const ratio = (value) => {
872
+ const number = Number(value);
873
+ if (!Number.isFinite(number)) return null;
874
+ return Math.max(0, Math.min(100, number * 100));
875
+ };
876
+ const metricPercent = (explicitPercent, rate, fallbackPercent) => {
877
+ const explicit = firstDefined(explicitPercent, fallbackPercent);
878
+ return explicit == null ? ratio(rate) : percent(explicit);
879
+ };
880
+ return {
881
+ available,
882
+ state: statusLabel(firstDefined(source.state, source.status), available ? 'active' : 'warming_up'),
883
+ capability_level: displayText(firstDefined(activation.capability_level, source.capability_level, source.capability, 'unknown'), 40),
884
+ capture_percent: metricPercent(capture.percent, capture.rate, source.capture_percent),
885
+ closure_percent: metricPercent(closure.percent, closure.rate, source.closure_percent),
886
+ effectiveness_percent: metricPercent(
887
+ effectiveness.followed_percent,
888
+ firstDefined(effectiveness.follow_through_rate, effectiveness.rate),
889
+ source.effectiveness_percent,
890
+ ),
891
+ drift: driftAvailable ? drift.detected : null,
892
+ exact_fix: displayText(firstDefined(drift.repair_command, source.exact_fix, source.repair_command, ''), 180),
893
+ };
894
+ }
895
+
816
896
  function normalizeFleetSnapshot(raw, options) {
817
897
  const status = raw.status?.data || {};
818
898
  const capacity = raw.capacity?.data || {};
@@ -873,6 +953,7 @@ function normalizeFleetSnapshot(raw, options) {
873
953
  const recentDecisions = normalizeRecentDecisions(status, report, fleet);
874
954
  const gates = normalizeGates(status, report);
875
955
  const arbitrations = normalizeArbitrations(status, report, fleet);
956
+ const activationCoverage = normalizeActivationCoverage(status, report, fleet);
876
957
  const fixCommands = normalizeFixCommands(status, capacity);
877
958
  const errors = Object.entries(raw)
878
959
  .filter(([, value]) => value && value.ok === false)
@@ -893,6 +974,8 @@ function normalizeFleetSnapshot(raw, options) {
893
974
  degraded_hooks: missedHooks,
894
975
  gates,
895
976
  arbitrations,
977
+ activation_coverage: activationCoverage,
978
+ client_update: status.client_update || null,
896
979
  agents,
897
980
  fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
898
981
  source_errors: errors,
@@ -944,12 +1027,17 @@ function fleetPanel(snapshot) {
944
1027
  '',
945
1028
  `Live agents: ${snapshot.live_agents ?? 'unknown'}`,
946
1029
  `Active workflows: ${snapshot.active_workflows ?? 'unknown'}`,
1030
+ `Passive activation: ${snapshot.activation_coverage.state} capability=${snapshot.activation_coverage.capability_level}`,
1031
+ `Passive coverage: ${snapshot.activation_coverage.capture_percent ?? 'insufficient data'}${snapshot.activation_coverage.capture_percent == null ? '' : '%'}; outcome closure=${snapshot.activation_coverage.closure_percent ?? 'insufficient data'}${snapshot.activation_coverage.closure_percent == null ? '' : '%'}; intervention follow-through=${snapshot.activation_coverage.effectiveness_percent ?? 'insufficient data'}${snapshot.activation_coverage.effectiveness_percent == null ? '' : '%'}`,
947
1032
  `Agent disagreements: open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
948
1033
  `Latest arbitration: ${arbitrationSummary}`,
949
1034
  `Risky actions waiting for proof: ${snapshot.proof_waiting}`,
950
1035
  `Failed/stale outcomes: ${snapshot.failed_stale_outcomes}`,
951
1036
  `Backpressure/capacity status: ${snapshot.backpressure_status}${snapshot.capacity_next_action ? ` - ${snapshot.capacity_next_action}` : ''}`,
952
1037
  `Degraded hooks: ${degraded}`,
1038
+ snapshot.client_update && (snapshot.client_update.update_available === true || snapshot.client_update.version_status === 'unknown' || snapshot.client_update.notification_state === 'unknown' || snapshot.client_update.notification === 'version_unknown')
1039
+ ? `Marrow client update: ${displayText(snapshot.client_update.notification_state === 'security_required' ? 'security_required' : snapshot.client_update.notification_state === 'recommended' ? 'recommended' : snapshot.client_update.version_status === 'unknown' || snapshot.client_update.notification_state === 'unknown' || snapshot.client_update.notification === 'version_unknown' ? 'version_unknown' : snapshot.client_update.priority || 'recommended', 32)}; installed=${displayText(snapshot.client_update.installed_version || snapshot.client_update.current_version || 'unknown', 32)}; latest=${displayText(snapshot.client_update.latest_version || 'unknown', 32)}; operator approval required`
1040
+ : 'Marrow client update: current or unavailable',
953
1041
  `Deploy/publish/merge gates: deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`,
954
1042
  '',
955
1043
  'Live agent roster:',
@@ -1361,6 +1449,10 @@ function renderFleetTui(state) {
1361
1449
  const snapshot = state.snapshot;
1362
1450
  const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
1363
1451
  const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
1452
+ const activation = snapshot.activation_coverage;
1453
+ const coverageValue = activation.available
1454
+ ? `${activation.state}; capture=${activation.capture_percent ?? 'n/a'}%; closure=${activation.closure_percent ?? 'n/a'}%`
1455
+ : `${activation.state}; insufficient data`;
1364
1456
  const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
1365
1457
  const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
1366
1458
  const recent = snapshot.recent_decisions[0] || 'none reported yet';
@@ -1404,9 +1496,13 @@ function renderFleetTui(state) {
1404
1496
  hint: 'Latest fleet decision signal returned by Marrow.',
1405
1497
  },
1406
1498
  {
1407
- label: 'Degraded hooks',
1408
- value: degraded,
1409
- hint: degraded === 'none' ? 'No missing hooks reported.' : 'Repair hooks before trusting passive coverage.',
1499
+ label: 'Passive activation / coverage',
1500
+ value: coverageValue,
1501
+ hint: activation.drift
1502
+ ? `Configuration drift detected. ${activation.exact_fix || fixCommand}`
1503
+ : degraded === 'none'
1504
+ ? `Capability=${activation.capability_level}; no missing hooks reported.`
1505
+ : `Degraded hooks: ${degraded}. Repair before trusting passive coverage.`,
1410
1506
  },
1411
1507
  {
1412
1508
  label: 'Deploy/publish/merge gates',
@@ -1725,9 +1821,8 @@ async function runFleetInteractive(options, input = process.stdin, output = proc
1725
1821
  ? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
1726
1822
  : 'No recent decisions returned yet.';
1727
1823
  } else if (state.cursor === 7) {
1728
- state.lastResult = state.snapshot.degraded_hooks.length
1729
- ? `Degraded hooks: ${state.snapshot.degraded_hooks.join(', ')}. Fix: ${state.snapshot.fix_commands[0]}`
1730
- : 'No degraded hooks reported.';
1824
+ const coverage = state.snapshot.activation_coverage;
1825
+ state.lastResult = `Passive activation=${coverage.state}; capability=${coverage.capability_level}; capture=${coverage.capture_percent ?? 'insufficient data'}; closure=${coverage.closure_percent ?? 'insufficient data'}; intervention follow-through=${coverage.effectiveness_percent ?? 'insufficient data'}${coverage.drift ? `. Drift detected. Fix: ${coverage.exact_fix || state.snapshot.fix_commands[0]}` : ''}`;
1731
1826
  } else if (state.cursor === 8) {
1732
1827
  state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
1733
1828
  } else {
@@ -1779,6 +1874,7 @@ async function runCli(argv) {
1779
1874
 
1780
1875
  if (parsed.options?.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1781
1876
  else if (result?.blocked) process.stderr.write(`BLOCKED: ${result.message || 'Marrow blocked this action.'}\n`);
1877
+ else if (parsed.command === 'status') process.stdout.write(`${statusPanel(result)}\n`);
1782
1878
  else if (!['run', 'fleet', 'hermes', 'openclaw', 'integrations'].includes(parsed.command)) process.stdout.write('Marrow command completed.\n');
1783
1879
 
1784
1880
  if (parsed.command === 'run' || result?.blocked) process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
@@ -1808,6 +1904,7 @@ module.exports = {
1808
1904
  gateOnly,
1809
1905
  proofOnly,
1810
1906
  statusOnly,
1907
+ statusPanel,
1811
1908
  runStatusCheck,
1812
1909
  runGateCheck,
1813
1910
  runGovernInteractive,
package/src/installer.js CHANGED
@@ -2,11 +2,42 @@ const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
  const os = require('node:os');
4
4
  const crypto = require('node:crypto');
5
+ const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
5
6
 
6
7
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
7
8
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
8
9
  const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
9
- const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'custom', 'unknown']);
10
+ const MCP_ADAPTER_VERSION = '3.9.51';
11
+ const SDK_ADAPTER_VERSION = '3.7.50';
12
+ const SDK_ADAPTER_INTEGRITY = 'sha512-TyCXUKZAaKRPdfVqXEuIvWnOt4GLcIUbwhpX+EopzdALNzq+t5wUGHrqJlkNSvopppPTTdFVLJ7O1vtwofqFqw==';
13
+ const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
14
+ const MCP_PACKAGE_SPEC = `@getmarrow/mcp@${MCP_ADAPTER_VERSION}`;
15
+ const MCP_CONTEXT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} context-hook`;
16
+ const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} pre-action-hook`;
17
+ const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} hook`;
18
+ const MCP_SESSION_END_HOOK_COMMAND = `npx -y ${MCP_PACKAGE_SPEC} session-hook`;
19
+ const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
20
+ const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
21
+ const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'custom', 'unknown']);
22
+ const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
23
+ { client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
24
+ { client: 'cursor', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
25
+ { client: 'composer', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
26
+ { client: 'cline', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
27
+ { client: 'windsurf', capability_level: 'mcp', automatic: ['mcp_tool_calls'], install_surface: 'mcp' },
28
+ { client: 'codex', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
29
+ { client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
30
+ { client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
31
+ { client: 'openclaw', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
32
+ { client: 'gemini', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
33
+ { client: 'grok', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
34
+ { client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
35
+ { client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
36
+ { client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
37
+ { client: 'minimax', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
38
+ { client: 'glm', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
39
+ { client: 'custom', capability_level: 'event_contract', automatic: [], install_surface: 'event_contract' },
40
+ ]);
10
41
 
11
42
  function sourceClient() {
12
43
  const raw = String(process.env.MARROW_CLIENT || process.env.MARROW_HARNESS || process.env.MARROW_AGENT_CLIENT || '').trim().toLowerCase().replace(/\s+/g, '-').replace(/^@/, '');
@@ -15,6 +46,7 @@ function sourceClient() {
15
46
  claude_code: 'claude-code',
16
47
  'claude-code': 'claude-code',
17
48
  cursor: 'cursor',
49
+ composer: 'composer',
18
50
  windsurf: 'windsurf',
19
51
  openclaw: 'openclaw',
20
52
  codex: 'codex',
@@ -348,6 +380,7 @@ Marrow should run passively after install:
348
380
  - Before risky work, use Marrow's decision brief or passive prompt hook.
349
381
  - After meaningful work, record the outcome so future agents learn from it.
350
382
  - Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
383
+ - When status/runtime returns a \`client_update\` notice, tell the operator and use its exact update and verification commands only when local change policy permits.
351
384
 
352
385
  Required environment:
353
386
 
@@ -422,38 +455,245 @@ function upsertBlock(content, block) {
422
455
  return `${content}${separator}${block}\n`;
423
456
  }
424
457
 
425
- function upsertClaudeHooks(settingsPath) {
426
- const settings = parseJsonObject(settingsPath);
427
- const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
428
- ? settings.hooks
429
- : {};
430
- const postToolUse = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
431
- const userPromptSubmit = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
432
-
433
- const hasPost = postToolUse.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp hook'));
434
- const hasPrompt = userPromptSubmit.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp context-hook'));
458
+ function exactHookConfigured(settings, eventName, command, matcher) {
459
+ const entries = settings?.hooks?.[eventName];
460
+ if (!Array.isArray(entries)) return false;
461
+ return entries.some((entry) => {
462
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
463
+ if (matcher != null && entry.matcher !== matcher) return false;
464
+ if (!Array.isArray(entry.hooks)) return false;
465
+ return entry.hooks.some((hook) => (
466
+ hook
467
+ && typeof hook === 'object'
468
+ && !Array.isArray(hook)
469
+ && hook.type === 'command'
470
+ && typeof hook.command === 'string'
471
+ && hook.command.trim() === command
472
+ ));
473
+ });
474
+ }
435
475
 
436
- if (!hasPost) {
437
- postToolUse.push({
438
- matcher: 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*',
439
- hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp hook' }],
476
+ function exactHookDescriptors(settings, eventName, command, matcher) {
477
+ const entries = settings?.hooks?.[eventName];
478
+ if (!Array.isArray(entries)) return [];
479
+ return entries.flatMap((entry) => {
480
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
481
+ if (matcher != null && entry.matcher !== matcher) return [];
482
+ if (!Array.isArray(entry.hooks)) return [];
483
+ return entry.hooks.flatMap((hook) => {
484
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
485
+ || hook.type !== 'command' || typeof hook.command !== 'string'
486
+ || hook.command.trim() !== command) return [];
487
+ return [{
488
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
489
+ command,
490
+ timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout)
491
+ ? hook.timeout
492
+ : null,
493
+ }];
440
494
  });
495
+ });
496
+ }
497
+
498
+ function marrowHookSubcommand(command) {
499
+ if (typeof command !== 'string') return null;
500
+ const match = command.trim().match(
501
+ /^npx\s+(?:-y\s+)?@getmarrow\/mcp(?:@[^\s]+)?\s+(context-hook|pre-action-hook|hook|session-hook)$/,
502
+ );
503
+ return match?.[1] || null;
504
+ }
505
+
506
+ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
507
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
508
+ let preferredHandler = null;
509
+ const retained = [];
510
+ for (const entry of original) {
511
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
512
+ retained.push(entry);
513
+ continue;
514
+ }
515
+ const remaining = [];
516
+ for (const hook of entry.hooks) {
517
+ const detected = hook && typeof hook === 'object' && !Array.isArray(hook)
518
+ && hook.type === 'command' ? marrowHookSubcommand(hook.command) : null;
519
+ if (detected) {
520
+ const exactMatcher = matcher == null ? entry.matcher === undefined : entry.matcher === matcher;
521
+ if (detected === subcommand && (!preferredHandler || (hook.command === command && exactMatcher))) {
522
+ preferredHandler = hook;
523
+ }
524
+ continue;
525
+ }
526
+ remaining.push(hook);
527
+ }
528
+ if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
441
529
  }
442
- if (!hasPrompt) {
443
- userPromptSubmit.push({
444
- hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp context-hook' }],
530
+ const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
531
+ if (matcher != null) canonical.matcher = matcher;
532
+ retained.push(canonical);
533
+ return retained;
534
+ }
535
+
536
+ function marrowHookDescriptors(settings, eventName, subcommand) {
537
+ const entries = settings?.hooks?.[eventName];
538
+ if (!Array.isArray(entries)) return [];
539
+ return entries.flatMap((entry) => {
540
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) return [];
541
+ return entry.hooks.flatMap((hook) => {
542
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
543
+ || hook.type !== 'command') return [];
544
+ const detected = marrowHookSubcommand(hook.command);
545
+ if (!detected || (subcommand && detected !== subcommand)) return [];
546
+ return [{
547
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
548
+ command: hook.command.trim(),
549
+ timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout) ? hook.timeout : null,
550
+ }];
445
551
  });
552
+ });
553
+ }
554
+
555
+ function safeJsonObject(filePath) {
556
+ try {
557
+ return parseJsonObject(filePath);
558
+ } catch {
559
+ return {};
446
560
  }
561
+ }
562
+
563
+ function claudeNativeHookFingerprint(settings) {
564
+ const contract = {
565
+ schema: 'marrow-claude-native-hooks.v3',
566
+ adapter_version: MCP_ADAPTER_VERSION,
567
+ expected_hooks: NATIVE_EXPECTED_HOOKS,
568
+ configured: {
569
+ prompt: exactHookConfigured(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
570
+ pre_action: exactHookConfigured(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
571
+ action_result_success: exactHookConfigured(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
572
+ action_result_failure: exactHookConfigured(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
573
+ session_end: exactHookConfigured(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
574
+ },
575
+ descriptors: {
576
+ prompt: exactHookDescriptors(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
577
+ pre_action: exactHookDescriptors(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
578
+ action_result_success: exactHookDescriptors(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
579
+ action_result_failure: exactHookDescriptors(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
580
+ session_end: exactHookDescriptors(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
581
+ },
582
+ active_marrow_handlers: {
583
+ prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
584
+ pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
585
+ action_result_success: marrowHookDescriptors(settings, 'PostToolUse'),
586
+ action_result_failure: marrowHookDescriptors(settings, 'PostToolUseFailure'),
587
+ session_end: marrowHookDescriptors(settings, 'Stop'),
588
+ },
589
+ };
590
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
591
+ }
592
+
593
+ function upsertClaudeHooks(settingsPath) {
594
+ const settings = parseJsonObject(settingsPath);
595
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
596
+ ? settings.hooks
597
+ : {};
598
+ const postToolUse = reconcileMarrowCommandHook(
599
+ settings, 'PostToolUse', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
600
+ );
601
+ const postToolUseFailure = reconcileMarrowCommandHook(
602
+ settings, 'PostToolUseFailure', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
603
+ );
604
+ const preToolUse = reconcileMarrowCommandHook(
605
+ settings, 'PreToolUse', 'pre-action-hook', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
606
+ );
607
+ const userPromptSubmit = reconcileMarrowCommandHook(
608
+ settings, 'UserPromptSubmit', 'context-hook', MCP_CONTEXT_HOOK_COMMAND,
609
+ );
610
+ const stop = reconcileMarrowCommandHook(
611
+ settings, 'Stop', 'session-hook', MCP_SESSION_END_HOOK_COMMAND,
612
+ );
447
613
 
448
614
  settings.hooks = {
449
615
  ...hooks,
616
+ PreToolUse: preToolUse,
450
617
  PostToolUse: postToolUse,
618
+ PostToolUseFailure: postToolUseFailure,
451
619
  UserPromptSubmit: userPromptSubmit,
620
+ Stop: stop,
452
621
  };
453
622
 
454
623
  return JSON.stringify(settings, null, 2) + '\n';
455
624
  }
456
625
 
626
+ function activationProfile(detection, plan, changes, client) {
627
+ const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
628
+ || HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
629
+ const sdkDependency = inspectSdkDependency(detection);
630
+ const capabilityLevel = client === 'custom' && (plan.mode === 'sdk' || plan.mode === 'both')
631
+ ? 'sdk_passive_runtime'
632
+ : registry.capability_level;
633
+ const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
634
+ ? ['pre_action', 'action_result', 'outcome_closure']
635
+ : [...registry.automatic];
636
+ const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
637
+ ? MCP_ADAPTER_VERSION
638
+ : capabilityLevel === 'sdk_passive_runtime'
639
+ ? SDK_ADAPTER_VERSION
640
+ : INSTALLER_ADAPTER_VERSION;
641
+ const observedHooks = [];
642
+ const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
643
+ if (capabilityLevel === 'native_hooks'
644
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
645
+ if (capabilityLevel === 'native_hooks'
646
+ && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
647
+ if (capabilityLevel === 'native_hooks'
648
+ && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
649
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
650
+ if (capabilityLevel === 'native_hooks'
651
+ && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
652
+ const passiveRuntime = safeRead(detection.paths.passiveRuntime);
653
+ if (capabilityLevel === 'sdk_passive_runtime'
654
+ && sdkDependency.present
655
+ && /await import\('@getmarrow\/sdk'\)/.test(passiveRuntime)
656
+ && /runtime\.install\(\)/.test(passiveRuntime)) {
657
+ for (const hook of ['pre_action', 'action_result', 'outcome_closure']) {
658
+ if (!observedHooks.includes(hook)) observedHooks.push(hook);
659
+ }
660
+ }
661
+ const mcpConfigs = [detection.paths.mcpJson, detection.paths.cursorMcp]
662
+ .map((filePath) => safeJsonObject(filePath));
663
+ if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
664
+ config?.mcpServers?.marrow?.command === 'npx'
665
+ && Array.isArray(config.mcpServers.marrow.args)
666
+ && config.mcpServers.marrow.args.join(' ') === `-y ${MCP_PACKAGE_SPEC}`
667
+ ))) observedHooks.push('mcp_tool_calls');
668
+ const fingerprintMaterial = changes
669
+ .filter((change) => change.applied || change.already_present)
670
+ .map((change) => `${change.label}:${crypto.createHash('sha256').update(safeRead(change.path)).digest('hex')}`)
671
+ .sort()
672
+ .join('|');
673
+ const configFingerprint = capabilityLevel === 'native_hooks'
674
+ ? claudeNativeHookFingerprint(claudeSettings)
675
+ : crypto.createHash('sha256')
676
+ .update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
677
+ .digest('hex');
678
+ const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
679
+ const exactFix = complete
680
+ ? null
681
+ : capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
682
+ ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
683
+ : capabilityLevel === 'governed_wrapper'
684
+ ? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
685
+ : 'npx @getmarrow/install --repair';
686
+ return {
687
+ adapter_version: adapterVersion,
688
+ capability_level: capabilityLevel,
689
+ config_fingerprint: configFingerprint,
690
+ expected_hooks: expectedHooks,
691
+ observed_hooks: observedHooks,
692
+ complete,
693
+ exact_fix: exactFix,
694
+ };
695
+ }
696
+
457
697
  function upsertMcpServerConfig(filePath) {
458
698
  const config = parseJsonObject(filePath);
459
699
  const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
@@ -461,7 +701,7 @@ function upsertMcpServerConfig(filePath) {
461
701
  : {};
462
702
  servers.marrow = {
463
703
  command: 'npx',
464
- args: ['-y', '@getmarrow/mcp'],
704
+ args: ['-y', MCP_PACKAGE_SPEC],
465
705
  env: {
466
706
  MARROW_API_KEY: '${MARROW_API_KEY}',
467
707
  MARROW_BASE_URL: '${MARROW_BASE_URL}',
@@ -485,7 +725,7 @@ function inspectSdkDependency(detection) {
485
725
  return {
486
726
  required: true,
487
727
  present: false,
488
- install_command: 'npm install @getmarrow/sdk',
728
+ install_command: `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
489
729
  warning: 'package.json could not be parsed; verify @getmarrow/sdk manually.',
490
730
  };
491
731
  }
@@ -496,11 +736,72 @@ function inspectSdkDependency(detection) {
496
736
  packageJson.optionalDependencies,
497
737
  packageJson.peerDependencies,
498
738
  ];
499
- const present = dependencyBlocks.some((deps) => deps && Object.prototype.hasOwnProperty.call(deps, '@getmarrow/sdk'));
739
+ const declaredSpec = dependencyBlocks
740
+ .map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
741
+ .find(Boolean) || null;
742
+ const objectTargetsSdk = (value) => {
743
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
744
+ return Object.entries(value).some(([key, nested]) => (
745
+ key.includes('@getmarrow/sdk') || objectTargetsSdk(nested)
746
+ ));
747
+ };
748
+ const overrideDetected = objectTargetsSdk(packageJson.overrides)
749
+ || objectTargetsSdk(packageJson.resolutions)
750
+ || objectTargetsSdk(packageJson.pnpm?.overrides);
751
+ let lockVerified = false;
752
+ try {
753
+ const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
754
+ const rootLock = lock?.packages?.[''];
755
+ const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
756
+ const lockedDeclaration = [
757
+ rootLock?.dependencies,
758
+ rootLock?.devDependencies,
759
+ rootLock?.optionalDependencies,
760
+ rootLock?.peerDependencies,
761
+ ].map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
762
+ .find(Boolean) || null;
763
+ lockVerified = [2, 3].includes(lock?.lockfileVersion)
764
+ && lockedDeclaration === declaredSpec
765
+ && lockedSdk?.version === SDK_ADAPTER_VERSION
766
+ && lockedSdk?.resolved === SDK_ADAPTER_TARBALL
767
+ && lockedSdk?.integrity === SDK_ADAPTER_INTEGRITY;
768
+ } catch {
769
+ lockVerified = false;
770
+ }
771
+ const installedPackagePath = findUp(
772
+ detection.root,
773
+ [path.join('node_modules', '@getmarrow', 'sdk', 'package.json')],
774
+ );
775
+ let installedVersion = null;
776
+ let installedName = null;
777
+ try {
778
+ const installedPackage = installedPackagePath ? JSON.parse(safeRead(installedPackagePath)) : null;
779
+ installedVersion = typeof installedPackage?.version === 'string' ? installedPackage.version : null;
780
+ installedName = typeof installedPackage?.name === 'string' ? installedPackage.name : null;
781
+ } catch {
782
+ installedVersion = null;
783
+ installedName = null;
784
+ }
785
+ const declarationTrusted = typeof declaredSpec === 'string'
786
+ && declaredSpec.trim().length > 0
787
+ && /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
788
+ const present = declarationTrusted
789
+ && !overrideDetected
790
+ && lockVerified
791
+ && installedName === '@getmarrow/sdk'
792
+ && installedVersion === SDK_ADAPTER_VERSION;
500
793
  return {
501
794
  required: true,
502
795
  present,
503
- install_command: present ? null : 'npm install @getmarrow/sdk',
796
+ declared: declaredSpec != null,
797
+ declared_spec: declaredSpec,
798
+ declaration_trusted: declarationTrusted,
799
+ override_detected: overrideDetected,
800
+ lock_verified: lockVerified,
801
+ installed_name: installedName,
802
+ installed_version: installedVersion,
803
+ expected_version: SDK_ADAPTER_VERSION,
804
+ install_command: present ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
504
805
  };
505
806
  }
506
807
 
@@ -569,8 +870,7 @@ function buildPlan(detection, options) {
569
870
  }
570
871
 
571
872
  function applyPlan(plan, options) {
572
- const changes = [];
573
- for (const write of plan.writes) {
873
+ const prepared = plan.writes.map((write) => {
574
874
  const before = safeRead(write.path);
575
875
  let after;
576
876
  if (write.type === 'file') {
@@ -587,6 +887,11 @@ function applyPlan(plan, options) {
587
887
  throw new Error(`Unknown write type: ${write.type}`);
588
888
  }
589
889
 
890
+ return { write, before, after };
891
+ });
892
+
893
+ const changes = [];
894
+ for (const { write, before, after } of prepared) {
590
895
  const changed = before !== after;
591
896
  const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor);
592
897
  changes.push({
@@ -628,7 +933,6 @@ function isCanonicalTimestamp(value) {
628
933
 
629
934
  function runtimeGateVerified(runtime) {
630
935
  if (!runtime || typeof runtime !== 'object') return false;
631
- if (runtime.ok === true) return true;
632
936
  const gate = runtime.risk_gate;
633
937
  if (!gate || typeof gate !== 'object') return false;
634
938
  if (typeof gate.allow === 'boolean' || typeof gate.allowed === 'boolean') return true;
@@ -651,6 +955,8 @@ async function runSelfTest(options) {
651
955
  'content-type': 'application/json',
652
956
  'x-marrow-session-id': `install-${Date.now()}`,
653
957
  'x-marrow-client': options.client || sourceClient(),
958
+ 'x-marrow-package': '@getmarrow/install',
959
+ 'x-marrow-package-version': INSTALLER_ADAPTER_VERSION,
654
960
  };
655
961
  if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
656
962
 
@@ -733,7 +1039,19 @@ async function runSelfTest(options) {
733
1039
  const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue, tokenValueProof);
734
1040
  let activationReceipt = null;
735
1041
  let activationVerified = false;
1042
+ let activationProfileReceipt = null;
736
1043
  if (options.activation) {
1044
+ const activationAdapterVersion = options.activation.adapter_version || INSTALLER_ADAPTER_VERSION;
1045
+ const activationCapabilityLevel = options.activation.capability_level || 'event_contract';
1046
+ const activationExpectedHooks = Array.isArray(options.activation.expected_hooks) ? options.activation.expected_hooks : [];
1047
+ const activationConfigFingerprint = options.activation.config_fingerprint || crypto.createHash('sha256')
1048
+ .update(JSON.stringify({
1049
+ harness: options.activation.harness || options.client || 'custom',
1050
+ install_surface: options.activation.install_surface || 'unknown',
1051
+ adapter_version: activationAdapterVersion,
1052
+ expected_hooks: activationExpectedHooks,
1053
+ }))
1054
+ .digest('hex');
737
1055
  activationReceipt = firstValue && firstValue.activation_receipt;
738
1056
  const receiptValid = activationReceipt
739
1057
  && typeof activationReceipt === 'object'
@@ -750,7 +1068,58 @@ async function runSelfTest(options) {
750
1068
  if (!receiptValid) {
751
1069
  throw new Error('activation receipt did not verify the exact self-test decision, agent, runtime gate, and closed successful outcome');
752
1070
  }
753
- activationVerified = Boolean(firstValue.active && runtimeGateVerified(runtime) && (status.enabled ?? status.ok));
1071
+ activationProfileReceipt = await requestJson(`${baseUrl}/v1/agent/integrations/events`, {
1072
+ method: 'POST',
1073
+ headers,
1074
+ body: JSON.stringify({
1075
+ event_id: `activation-${activationConfigFingerprint.slice(0, 32)}`,
1076
+ event_type: 'activation_profile_registered',
1077
+ harness: options.activation.harness,
1078
+ agent_id: options.agentId,
1079
+ session_id: headers['x-marrow-session-id'],
1080
+ adapter_version: activationAdapterVersion,
1081
+ capability_level: activationCapabilityLevel,
1082
+ config_fingerprint: activationConfigFingerprint,
1083
+ expected_hooks: activationExpectedHooks,
1084
+ action: 'passive integration activation profile registered',
1085
+ occurred_at: new Date().toISOString(),
1086
+ }),
1087
+ });
1088
+ const profileCoverage = activationProfileReceipt?.activation_coverage;
1089
+ const profileReceipt = profileCoverage?.profile_receipt;
1090
+ const accountBindingId = profileCoverage?.account_binding_id;
1091
+ const expectedConfigurationBindingId = typeof accountBindingId === 'string'
1092
+ ? crypto.createHash('sha256')
1093
+ .update(`agent-config-receipt:v2:${accountBindingId}:${activationConfigFingerprint}`)
1094
+ .digest('hex')
1095
+ : null;
1096
+ const expectedHooksMatch = Array.isArray(profileCoverage?.capture_coverage?.expected_hooks)
1097
+ && [...profileCoverage.capture_coverage.expected_hooks].sort().join(',') === [...activationExpectedHooks].sort().join(',');
1098
+ const profileBound = Boolean(
1099
+ activationProfileReceipt?.accepted === true
1100
+ && profileCoverage?.agent_id === options.agentId
1101
+ && profileCoverage?.harness === options.activation.harness
1102
+ && profileCoverage?.activation?.adapter_version === activationAdapterVersion
1103
+ && profileCoverage?.activation?.capability_level === activationCapabilityLevel
1104
+ && profileReceipt?.agent_id === options.agentId
1105
+ && profileReceipt?.harness === options.activation.harness
1106
+ && profileReceipt?.adapter_version === activationAdapterVersion
1107
+ && profileReceipt?.capability_level === activationCapabilityLevel
1108
+ && typeof accountBindingId === 'string'
1109
+ && accountBindingId.length === 64
1110
+ && profileReceipt?.account_binding_id === accountBindingId
1111
+ && profileReceipt?.config_fingerprint_verified === true
1112
+ && profileReceipt?.configuration_binding_id === expectedConfigurationBindingId
1113
+ && profileReceipt?.expected_hooks_verified === true
1114
+ && expectedHooksMatch
1115
+ );
1116
+ activationVerified = Boolean(
1117
+ firstValue.active
1118
+ && runtimeGateVerified(runtime)
1119
+ && (status.enabled ?? status.ok)
1120
+ && options.activation.complete === true
1121
+ && profileBound,
1122
+ );
754
1123
  if (!activationVerified) {
755
1124
  throw new Error('activation prerequisites were not all verified by the server');
756
1125
  }
@@ -769,10 +1138,13 @@ async function runSelfTest(options) {
769
1138
  runtime_before_you_act: runtime.before_you_act || null,
770
1139
  activation_verified: activationVerified,
771
1140
  activation_receipt: activationReceipt,
1141
+ activation_profile_receipt: activationProfileReceipt,
1142
+ activation_coverage: status.activation_coverage || firstValue.activation_coverage || null,
772
1143
  first_value: firstValue && firstValue.ok !== false ? firstValue : null,
773
1144
  first_value_signal: firstValueSignal,
774
1145
  install_value_moment: installValueMoment,
775
1146
  token_value_proof: tokenValueProof,
1147
+ client_update: status.client_update || runtime.client_update || runtime.status?.client_update || null,
776
1148
  performance_proof: performance && performance.ok !== false ? {
777
1149
  avoided_mistakes: performance.avoided_mistakes ?? performance.avoided_repeated_mistakes ?? 0,
778
1150
  reused_winning_decisions: performance.reused_winning_decisions ?? 0,
@@ -916,7 +1288,7 @@ function printReport(report) {
916
1288
 
917
1289
  process.stdout.write('\nPlanned changes:\n');
918
1290
  for (const change of report.changes) {
919
- const marker = change.changed ? (report.writeMode === 'write' ? 'wrote' : 'would write') : 'unchanged';
1291
+ const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
920
1292
  process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
921
1293
  }
922
1294
 
@@ -931,6 +1303,18 @@ function printReport(report) {
931
1303
  process.stdout.write(`- one-call runtime: ${report.selfTest.runtime_active ? 'active' : 'not verified'}\n`);
932
1304
  if (report.selfTest.error) process.stdout.write(`- error: ${report.selfTest.error}\n`);
933
1305
  if (report.selfTest.next_action) process.stdout.write(`- next action: ${report.selfTest.next_action}\n`);
1306
+ const update = report.selfTest.client_update;
1307
+ const notification = update?.notification_state || update?.notification;
1308
+ if (update && (update.update_available === true || update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' || notification === 'security_required')) {
1309
+ process.stdout.write('\nMarrow client update:\n');
1310
+ process.stdout.write(`- priority: ${notification === 'security_required' ? 'security_required' : notification === 'recommended' ? 'recommended' : update.version_status === 'unknown' || notification === 'unknown' || notification === 'version_unknown' ? 'version_unknown' : update.priority || 'recommended'}\n`);
1311
+ process.stdout.write(`- installed: ${update.installed_version || update.current_version || 'unknown'}\n`);
1312
+ process.stdout.write(`- latest: ${update.latest_version || 'unknown'}\n`);
1313
+ process.stdout.write('- automatic notification: yes\n');
1314
+ process.stdout.write('- automatic local mutation: no; operator policy applies\n');
1315
+ if (update.update_command || update.exact_update_command) process.stdout.write(`- update: ${update.update_command || update.exact_update_command}\n`);
1316
+ if (update.verification_command || update.exact_verification_command) process.stdout.write(`- verify: ${update.verification_command || update.exact_verification_command}\n`);
1317
+ }
934
1318
  if (report.selfTest.first_value_signal) {
935
1319
  process.stdout.write('\nFirst value:\n');
936
1320
  const valueMoment = report.selfTest.install_value_moment;
@@ -1025,6 +1409,9 @@ async function install(options) {
1025
1409
  if (options.activate && options.selfTest === false) {
1026
1410
  throw new Error('activate requires the server self-test');
1027
1411
  }
1412
+ if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
1413
+ throw new Error('repair requires explicit write authorization (--yes)');
1414
+ }
1028
1415
  const detection = detectEnvironment(options.cwd);
1029
1416
  const plan = buildPlan(detection, options);
1030
1417
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
@@ -1032,6 +1419,7 @@ async function install(options) {
1032
1419
  const client = detectedClient(detection);
1033
1420
  options.client = client;
1034
1421
  if (!options.agentId) options.agentId = stableAgentId(detection.root, client);
1422
+ const profile = activationProfile(detection, plan, changes, client);
1035
1423
  options.activation = options.activate ? {
1036
1424
  harness: client,
1037
1425
  agent_id: options.agentId,
@@ -1041,14 +1429,20 @@ async function install(options) {
1041
1429
  .filter((change) => change.changed && change.applied && /hook|runtime|rule|instruction|config/i.test(change.label))
1042
1430
  .map((change) => change.label)
1043
1431
  .slice(0, 20),
1044
- capture_verified: changes.every((change) => change.applied),
1432
+ capture_verified: changes.every((change) => change.applied || change.already_present),
1433
+ adapter_version: profile.adapter_version,
1434
+ capability_level: profile.capability_level,
1435
+ config_fingerprint: profile.config_fingerprint,
1436
+ expected_hooks: profile.expected_hooks,
1437
+ observed_hooks: profile.observed_hooks,
1438
+ complete: profile.complete,
1045
1439
  intervention_verified: false,
1046
1440
  closure_verified: false,
1047
1441
  } : null;
1048
1442
  const configInspection = inspectNpmTokenConfig();
1049
1443
  const sdkDependency = inspectSdkDependency(detection);
1050
1444
  const configDiagnostics = configInspection.safe;
1051
- const configRepairs = options.repair && !options.dryRun && !options.doctor
1445
+ const configRepairs = options.repair && options.yes && !options.dryRun && !options.doctor
1052
1446
  ? repairConfigDiagnostics(configDiagnostics)
1053
1447
  : [];
1054
1448
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
@@ -1063,7 +1457,7 @@ async function install(options) {
1063
1457
  if (options.activate && !selfTest.activation_verified) {
1064
1458
  throw new Error('Marrow activation failed: server confirmation was not returned');
1065
1459
  }
1066
- const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
1460
+ const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
1067
1461
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
1068
1462
  const remediation = options.repair
1069
1463
  ? {
@@ -1098,6 +1492,7 @@ async function install(options) {
1098
1492
  agent_id: options.agentId,
1099
1493
  server_confirmed: Boolean(selfTest.activation_verified),
1100
1494
  receipt: selfTest.activation_receipt || null,
1495
+ profile,
1101
1496
  },
1102
1497
  changes,
1103
1498
  doctor: {
@@ -1150,4 +1545,8 @@ module.exports = {
1150
1545
  buildInstallValueMoment,
1151
1546
  buildTokenValueProof,
1152
1547
  stableAgentId,
1548
+ activationProfile,
1549
+ claudeNativeHookFingerprint,
1550
+ printReport,
1551
+ HARNESS_CAPABILITY_REGISTRY,
1153
1552
  };