@getmarrow/install 0.1.32 → 0.1.34

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,19 @@ Required secret:
62
62
  export MARROW_API_KEY=mrw_live_...
63
63
  ```
64
64
 
65
- ## What's New in v0.1.32
65
+ ## What's New in v0.1.34
66
+
67
+ v0.1.34 verifies whether passive governance is actually active after install. Activation now 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:
66
68
 
67
- v0.1.32 adds one machine-readable governance-fit contract across discovery, evidence, and integration surfaces while preserving the server-verified first-run activation introduced in v0.1.29:
69
+ - Claude Code installation includes exact `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, and `Stop` hooks;
70
+ - matching pre-action/result receipts use one tool correlation, and activation fingerprints the exact hook contract without uploading configuration contents;
71
+ - the capability registry distinguishes native hooks, MCP, SDK passive runtime, governed wrappers, and custom event contracts;
72
+ - `activate` fails when the local integration is incomplete or the server does not accept the exact activation profile;
73
+ - `doctor` and `--repair` use the same configuration evidence without exposing configuration contents;
74
+ - the harness certification suite prevents support claims from overstating what is automatic;
75
+ - unavailable coverage remains “insufficient data” instead of a synthetic percentage.
76
+
77
+ It preserves agent-disagreement visibility from v0.1.33 and the server-verified first-run activation introduced in v0.1.29:
68
78
 
69
79
  - GitHub and npm now advertise separate signed discovery placements;
70
80
  - package metadata identifies the installer as agent governance rather than a general memory utility;
@@ -104,10 +114,13 @@ With a valid key, `activate`:
104
114
  4. closes its outcome;
105
115
  5. sends the exact self-test decision ID to Marrow for server-side verification;
106
116
  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.
117
+ 7. registers the detected capability, expected hooks, and one-way configuration fingerprint;
118
+ 8. returns an activation receipt with capture, intervention, closure, first-value state, and the exact next action.
108
119
 
109
120
  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
121
 
122
+ 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.
123
+
111
124
  ## Govern TUI
112
125
 
113
126
  Open the interactive setup panel:
@@ -160,7 +173,7 @@ npx @getmarrow/install --repair
160
173
  npx @getmarrow/install fleet
161
174
  ```
162
175
 
163
- The fleet view shows live agents, active workflows, risky actions waiting for proof, stale or failed outcomes, capture health, recent decisions, and exact repair commands. It is an operator surface for the authenticated account, not a public status dashboard.
176
+ The fleet view shows live agents, active workflows, agent disagreements and their latest arbitration receipt, risky actions waiting for proof, stale or failed outcomes, capture health, recent decisions, and exact repair commands. Press Enter on **Agent disagreements** to inspect the bound decision, selected proposal, whether Marrow selected a proposal, synthesized a safe sequence, held the action for owner review, or blocked the conflicting actions. Review-required work must be approved from an authenticated Marrow dashboard session; the TUI does not let an agent approve itself. It is an operator surface for the authenticated account, not a public status dashboard.
164
177
 
165
178
  ## Integration Paths
166
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Universal installer and governed runner for Marrow agent fleets.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -8,7 +8,7 @@ const readline = require('node:readline');
8
8
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
9
9
  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;
10
10
  const GOVERN_TUI_ROW_COUNT = 7;
11
- const FLEET_TUI_ROW_COUNT = 11;
11
+ const FLEET_TUI_ROW_COUNT = 12;
12
12
  function usage() {
13
13
  return `Usage:
14
14
  npx @getmarrow/install run --agent deploy-agent -- npm test
@@ -761,6 +761,38 @@ function normalizeGates(status, report) {
761
761
  };
762
762
  }
763
763
 
764
+ function normalizeArbitrations(status, report, fleet) {
765
+ const source = firstDefined(
766
+ report.arbitrations,
767
+ report.report?.arbitrations,
768
+ fleet.arbitrations,
769
+ status.arbitrations,
770
+ {},
771
+ ) || {};
772
+ const receipts = listValue(
773
+ source.receipts,
774
+ source.items,
775
+ report.recent_arbitrations,
776
+ fleet.recent_arbitrations,
777
+ status.recent_arbitrations,
778
+ ).slice(0, 8).map((receipt) => ({
779
+ id: displayText(firstDefined(receipt?.id, receipt?.receipt_id, 'arbitration'), 80),
780
+ decision_id: displayText(firstDefined(receipt?.decision_id, ''), 80),
781
+ resolution: statusLabel(receipt?.resolution, 'unknown'),
782
+ conflict_type: displayText(firstDefined(receipt?.conflict_type, 'action_conflict'), 60),
783
+ selected_proposal_id: displayText(firstDefined(receipt?.selected_proposal_id, ''), 80),
784
+ requesting_agent_id: displayText(firstDefined(receipt?.requesting_agent_id, ''), 80),
785
+ exact_next_action: displayText(firstDefined(receipt?.exact_next_action, ''), 180),
786
+ owner_approval_required: Boolean(receipt?.owner_approval_required),
787
+ created_at: displayText(firstDefined(receipt?.created_at, ''), 80),
788
+ }));
789
+ return {
790
+ open_count: numberValue(firstDefined(source.open_count, source.open, receipts.filter((item) => item.resolution !== 'selected' && item.resolution !== 'synthesized').length), 0),
791
+ review_required_count: numberValue(firstDefined(source.review_required_count, source.review_required, receipts.filter((item) => item.resolution === 'review_required').length), 0),
792
+ receipts,
793
+ };
794
+ }
795
+
764
796
  function normalizeFixCommands(status, capacity) {
765
797
  const commands = [];
766
798
  const add = (value, commandOnly = false) => {
@@ -774,6 +806,9 @@ function normalizeFixCommands(status, capacity) {
774
806
  add(status.route_contract?.exact_fix);
775
807
  add(status.auto_outcome_closure?.exact_fix);
776
808
  add(status.capture_coverage?.exact_fix);
809
+ add(status.activation_coverage?.exact_fix);
810
+ add(status.activation_coverage?.drift?.repair_command, true);
811
+ add(status.passive_activation?.exact_fix);
777
812
  add(capacity.exact_next_action, true);
778
813
  add(capacity.next_action, true);
779
814
  if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
@@ -781,6 +816,52 @@ function normalizeFixCommands(status, capacity) {
781
816
  return commands.slice(0, 6);
782
817
  }
783
818
 
819
+ function normalizeActivationCoverage(status, report, fleet) {
820
+ const source = firstDefined(
821
+ status.activation_coverage,
822
+ status.passive_activation,
823
+ report.activation_coverage,
824
+ report.passive_activation,
825
+ fleet.activation_coverage,
826
+ {},
827
+ ) || {};
828
+ const activation = source.activation || {};
829
+ const capture = source.capture_coverage || source.capture || source.coverage || {};
830
+ const closure = source.outcome_closure || source.closure || {};
831
+ const effectiveness = source.intervention_effectiveness || source.effectiveness || {};
832
+ const drift = source.drift && typeof source.drift === 'object' ? source.drift : {};
833
+ const available = source.available === true || capture.available === true;
834
+ const driftAvailable = available && drift.available === true && typeof drift.detected === 'boolean';
835
+ const percent = (value) => {
836
+ const number = Number(value);
837
+ if (!Number.isFinite(number)) return null;
838
+ return Math.max(0, Math.min(100, number));
839
+ };
840
+ const ratio = (value) => {
841
+ const number = Number(value);
842
+ if (!Number.isFinite(number)) return null;
843
+ return Math.max(0, Math.min(100, number * 100));
844
+ };
845
+ const metricPercent = (explicitPercent, rate, fallbackPercent) => {
846
+ const explicit = firstDefined(explicitPercent, fallbackPercent);
847
+ return explicit == null ? ratio(rate) : percent(explicit);
848
+ };
849
+ return {
850
+ available,
851
+ state: statusLabel(firstDefined(source.state, source.status), available ? 'active' : 'warming_up'),
852
+ capability_level: displayText(firstDefined(activation.capability_level, source.capability_level, source.capability, 'unknown'), 40),
853
+ capture_percent: metricPercent(capture.percent, capture.rate, source.capture_percent),
854
+ closure_percent: metricPercent(closure.percent, closure.rate, source.closure_percent),
855
+ effectiveness_percent: metricPercent(
856
+ effectiveness.followed_percent,
857
+ firstDefined(effectiveness.follow_through_rate, effectiveness.rate),
858
+ source.effectiveness_percent,
859
+ ),
860
+ drift: driftAvailable ? drift.detected : null,
861
+ exact_fix: displayText(firstDefined(drift.repair_command, source.exact_fix, source.repair_command, ''), 180),
862
+ };
863
+ }
864
+
784
865
  function normalizeFleetSnapshot(raw, options) {
785
866
  const status = raw.status?.data || {};
786
867
  const capacity = raw.capacity?.data || {};
@@ -840,6 +921,8 @@ function normalizeFleetSnapshot(raw, options) {
840
921
  ), raw.capacity?.ok ? 'ok' : 'unknown');
841
922
  const recentDecisions = normalizeRecentDecisions(status, report, fleet);
842
923
  const gates = normalizeGates(status, report);
924
+ const arbitrations = normalizeArbitrations(status, report, fleet);
925
+ const activationCoverage = normalizeActivationCoverage(status, report, fleet);
843
926
  const fixCommands = normalizeFixCommands(status, capacity);
844
927
  const errors = Object.entries(raw)
845
928
  .filter(([, value]) => value && value.ok === false)
@@ -859,6 +942,8 @@ function normalizeFleetSnapshot(raw, options) {
859
942
  recent_decisions: recentDecisions,
860
943
  degraded_hooks: missedHooks,
861
944
  gates,
945
+ arbitrations,
946
+ activation_coverage: activationCoverage,
862
947
  agents,
863
948
  fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
864
949
  source_errors: errors,
@@ -898,6 +983,10 @@ function fleetPanel(snapshot) {
898
983
  ? snapshot.agents.slice(0, 5).map((agent) => ` - ${agent.id}${agent.role ? ` (${agent.role})` : ''}: ${agent.status}${agent.last_seen_at ? ` last_seen=${agent.last_seen_at}` : ''}`).join('\n')
899
984
  : ' - no agent roster returned yet';
900
985
  const fixes = snapshot.fix_commands.map((command) => ` - ${command}`).join('\n');
986
+ const arbitration = snapshot.arbitrations.receipts[0];
987
+ const arbitrationSummary = arbitration
988
+ ? `${arbitration.resolution} (${arbitration.conflict_type})${arbitration.exact_next_action ? ` - ${arbitration.exact_next_action}` : ''}`
989
+ : 'none reported yet';
901
990
  return [
902
991
  'Marrow Fleet Operator',
903
992
  '',
@@ -906,6 +995,10 @@ function fleetPanel(snapshot) {
906
995
  '',
907
996
  `Live agents: ${snapshot.live_agents ?? 'unknown'}`,
908
997
  `Active workflows: ${snapshot.active_workflows ?? 'unknown'}`,
998
+ `Passive activation: ${snapshot.activation_coverage.state} capability=${snapshot.activation_coverage.capability_level}`,
999
+ `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 ? '' : '%'}`,
1000
+ `Agent disagreements: open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
1001
+ `Latest arbitration: ${arbitrationSummary}`,
909
1002
  `Risky actions waiting for proof: ${snapshot.proof_waiting}`,
910
1003
  `Failed/stale outcomes: ${snapshot.failed_stale_outcomes}`,
911
1004
  `Backpressure/capacity status: ${snapshot.backpressure_status}${snapshot.capacity_next_action ? ` - ${snapshot.capacity_next_action}` : ''}`,
@@ -1321,9 +1414,14 @@ function renderFleetTui(state) {
1321
1414
  const snapshot = state.snapshot;
1322
1415
  const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
1323
1416
  const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
1417
+ const activation = snapshot.activation_coverage;
1418
+ const coverageValue = activation.available
1419
+ ? `${activation.state}; capture=${activation.capture_percent ?? 'n/a'}%; closure=${activation.closure_percent ?? 'n/a'}%`
1420
+ : `${activation.state}; insufficient data`;
1324
1421
  const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
1325
1422
  const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
1326
1423
  const recent = snapshot.recent_decisions[0] || 'none reported yet';
1424
+ const arbitration = snapshot.arbitrations.receipts[0];
1327
1425
  const rows = [
1328
1426
  {
1329
1427
  label: 'Live agents',
@@ -1335,6 +1433,13 @@ function renderFleetTui(state) {
1335
1433
  value: String(snapshot.active_workflows ?? 'unknown'),
1336
1434
  hint: 'Current account-scoped workflow pressure from Marrow.',
1337
1435
  },
1436
+ {
1437
+ label: 'Agent disagreements',
1438
+ value: `open=${snapshot.arbitrations.open_count} review_required=${snapshot.arbitrations.review_required_count}`,
1439
+ hint: arbitration
1440
+ ? `${arbitration.resolution}: ${arbitration.exact_next_action || arbitration.conflict_type}`
1441
+ : 'No conflicting fleet proposals reported.',
1442
+ },
1338
1443
  {
1339
1444
  label: 'Risky actions waiting for proof',
1340
1445
  value: String(snapshot.proof_waiting),
@@ -1356,9 +1461,13 @@ function renderFleetTui(state) {
1356
1461
  hint: 'Latest fleet decision signal returned by Marrow.',
1357
1462
  },
1358
1463
  {
1359
- label: 'Degraded hooks',
1360
- value: degraded,
1361
- hint: degraded === 'none' ? 'No missing hooks reported.' : 'Repair hooks before trusting passive coverage.',
1464
+ label: 'Passive activation / coverage',
1465
+ value: coverageValue,
1466
+ hint: activation.drift
1467
+ ? `Configuration drift detected. ${activation.exact_fix || fixCommand}`
1468
+ : degraded === 'none'
1469
+ ? `Capability=${activation.capability_level}; no missing hooks reported.`
1470
+ : `Degraded hooks: ${degraded}. Repair before trusting passive coverage.`,
1362
1471
  },
1363
1472
  {
1364
1473
  label: 'Deploy/publish/merge gates',
@@ -1651,31 +1760,35 @@ async function runFleetInteractive(options, input = process.stdin, output = proc
1651
1760
  state.status = 'Agent selection changed.';
1652
1761
  render();
1653
1762
  } else if (key.name === 'return') {
1654
- if (state.cursor === 9) {
1763
+ if (state.cursor === 10) {
1655
1764
  cleanup();
1656
1765
  output.write(`\n${state.snapshot.fix_commands[0] || 'npx @getmarrow/install doctor'}\n`);
1657
1766
  resolve();
1658
1767
  return;
1659
1768
  }
1660
- if (state.cursor === 10) {
1769
+ if (state.cursor === 11) {
1661
1770
  cleanup();
1662
1771
  resolve();
1663
1772
  return;
1664
1773
  }
1665
1774
  const selectedAgent = state.snapshot.agents[state.agentIndex];
1666
- if (state.cursor === 0 || state.cursor === 8) {
1775
+ if (state.cursor === 0 || state.cursor === 9) {
1667
1776
  state.lastResult = selectedAgent
1668
1777
  ? `Agent ${selectedAgent.id}: status=${selectedAgent.status}${selectedAgent.role ? ` role=${selectedAgent.role}` : ''}${selectedAgent.last_seen_at ? ` last_seen=${selectedAgent.last_seen_at}` : ''}`
1669
1778
  : 'No live agent roster returned yet. Check API key scope or wait for agents to log activity.';
1670
- } else if (state.cursor === 5) {
1779
+ } else if (state.cursor === 2) {
1780
+ const receipts = state.snapshot.arbitrations.receipts;
1781
+ state.lastResult = receipts.length
1782
+ ? receipts.map((receipt, index) => `${index + 1}. ${receipt.id}: ${receipt.resolution}${receipt.decision_id ? `; decision=${receipt.decision_id}` : ''}${receipt.selected_proposal_id ? `; selected=${receipt.selected_proposal_id}` : ''}; ${receipt.exact_next_action || receipt.conflict_type}${receipt.owner_approval_required ? ' Owner approval must be issued from an authenticated Marrow dashboard session.' : ''}`).join(' ')
1783
+ : 'No agent disagreements or arbitration receipts returned yet.';
1784
+ } else if (state.cursor === 6) {
1671
1785
  state.lastResult = state.snapshot.recent_decisions.length
1672
1786
  ? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
1673
1787
  : 'No recent decisions returned yet.';
1674
- } else if (state.cursor === 6) {
1675
- state.lastResult = state.snapshot.degraded_hooks.length
1676
- ? `Degraded hooks: ${state.snapshot.degraded_hooks.join(', ')}. Fix: ${state.snapshot.fix_commands[0]}`
1677
- : 'No degraded hooks reported.';
1678
1788
  } else if (state.cursor === 7) {
1789
+ const coverage = state.snapshot.activation_coverage;
1790
+ 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]}` : ''}`;
1791
+ } else if (state.cursor === 8) {
1679
1792
  state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
1680
1793
  } else {
1681
1794
  state.lastResult = fleetPanel(state.snapshot).replace(/\n/g, ' ');
package/src/installer.js CHANGED
@@ -6,7 +6,38 @@ const crypto = require('node:crypto');
6
6
  const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
7
7
  const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
8
8
  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']);
9
+ const INSTALLER_ADAPTER_VERSION = '0.1.34';
10
+ const MCP_ADAPTER_VERSION = '3.9.50';
11
+ const SDK_ADAPTER_VERSION = '3.7.49';
12
+ const SDK_ADAPTER_INTEGRITY = 'sha512-9a6pWWACubWTmulG7TwwgMNP/iSzoEh4wCH2oWUyO8XHeD2Eokj1GXrkCyX86md3bV7vGX6kyllfswZDlGj6WA==';
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',
@@ -422,38 +454,245 @@ function upsertBlock(content, block) {
422
454
  return `${content}${separator}${block}\n`;
423
455
  }
424
456
 
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'));
457
+ function exactHookConfigured(settings, eventName, command, matcher) {
458
+ const entries = settings?.hooks?.[eventName];
459
+ if (!Array.isArray(entries)) return false;
460
+ return entries.some((entry) => {
461
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
462
+ if (matcher != null && entry.matcher !== matcher) return false;
463
+ if (!Array.isArray(entry.hooks)) return false;
464
+ return entry.hooks.some((hook) => (
465
+ hook
466
+ && typeof hook === 'object'
467
+ && !Array.isArray(hook)
468
+ && hook.type === 'command'
469
+ && typeof hook.command === 'string'
470
+ && hook.command.trim() === command
471
+ ));
472
+ });
473
+ }
435
474
 
436
- if (!hasPost) {
437
- postToolUse.push({
438
- matcher: 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*',
439
- hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp hook' }],
475
+ function exactHookDescriptors(settings, eventName, command, matcher) {
476
+ const entries = settings?.hooks?.[eventName];
477
+ if (!Array.isArray(entries)) return [];
478
+ return entries.flatMap((entry) => {
479
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
480
+ if (matcher != null && entry.matcher !== matcher) return [];
481
+ if (!Array.isArray(entry.hooks)) return [];
482
+ return entry.hooks.flatMap((hook) => {
483
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
484
+ || hook.type !== 'command' || typeof hook.command !== 'string'
485
+ || hook.command.trim() !== command) return [];
486
+ return [{
487
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
488
+ command,
489
+ timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout)
490
+ ? hook.timeout
491
+ : null,
492
+ }];
440
493
  });
494
+ });
495
+ }
496
+
497
+ function marrowHookSubcommand(command) {
498
+ if (typeof command !== 'string') return null;
499
+ const match = command.trim().match(
500
+ /^npx\s+(?:-y\s+)?@getmarrow\/mcp(?:@[^\s]+)?\s+(context-hook|pre-action-hook|hook|session-hook)$/,
501
+ );
502
+ return match?.[1] || null;
503
+ }
504
+
505
+ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
506
+ const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
507
+ let preferredHandler = null;
508
+ const retained = [];
509
+ for (const entry of original) {
510
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) {
511
+ retained.push(entry);
512
+ continue;
513
+ }
514
+ const remaining = [];
515
+ for (const hook of entry.hooks) {
516
+ const detected = hook && typeof hook === 'object' && !Array.isArray(hook)
517
+ && hook.type === 'command' ? marrowHookSubcommand(hook.command) : null;
518
+ if (detected) {
519
+ const exactMatcher = matcher == null ? entry.matcher === undefined : entry.matcher === matcher;
520
+ if (detected === subcommand && (!preferredHandler || (hook.command === command && exactMatcher))) {
521
+ preferredHandler = hook;
522
+ }
523
+ continue;
524
+ }
525
+ remaining.push(hook);
526
+ }
527
+ if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
441
528
  }
442
- if (!hasPrompt) {
443
- userPromptSubmit.push({
444
- hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp context-hook' }],
529
+ const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
530
+ if (matcher != null) canonical.matcher = matcher;
531
+ retained.push(canonical);
532
+ return retained;
533
+ }
534
+
535
+ function marrowHookDescriptors(settings, eventName, subcommand) {
536
+ const entries = settings?.hooks?.[eventName];
537
+ if (!Array.isArray(entries)) return [];
538
+ return entries.flatMap((entry) => {
539
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !Array.isArray(entry.hooks)) return [];
540
+ return entry.hooks.flatMap((hook) => {
541
+ if (!hook || typeof hook !== 'object' || Array.isArray(hook)
542
+ || hook.type !== 'command') return [];
543
+ const detected = marrowHookSubcommand(hook.command);
544
+ if (!detected || (subcommand && detected !== subcommand)) return [];
545
+ return [{
546
+ matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
547
+ command: hook.command.trim(),
548
+ timeout: typeof hook.timeout === 'number' && Number.isFinite(hook.timeout) ? hook.timeout : null,
549
+ }];
445
550
  });
551
+ });
552
+ }
553
+
554
+ function safeJsonObject(filePath) {
555
+ try {
556
+ return parseJsonObject(filePath);
557
+ } catch {
558
+ return {};
446
559
  }
560
+ }
561
+
562
+ function claudeNativeHookFingerprint(settings) {
563
+ const contract = {
564
+ schema: 'marrow-claude-native-hooks.v3',
565
+ adapter_version: MCP_ADAPTER_VERSION,
566
+ expected_hooks: NATIVE_EXPECTED_HOOKS,
567
+ configured: {
568
+ prompt: exactHookConfigured(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
569
+ pre_action: exactHookConfigured(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
570
+ action_result_success: exactHookConfigured(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
571
+ action_result_failure: exactHookConfigured(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
572
+ session_end: exactHookConfigured(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
573
+ },
574
+ descriptors: {
575
+ prompt: exactHookDescriptors(settings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND),
576
+ pre_action: exactHookDescriptors(settings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
577
+ action_result_success: exactHookDescriptors(settings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
578
+ action_result_failure: exactHookDescriptors(settings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER),
579
+ session_end: exactHookDescriptors(settings, 'Stop', MCP_SESSION_END_HOOK_COMMAND),
580
+ },
581
+ active_marrow_handlers: {
582
+ prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
583
+ pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
584
+ action_result_success: marrowHookDescriptors(settings, 'PostToolUse'),
585
+ action_result_failure: marrowHookDescriptors(settings, 'PostToolUseFailure'),
586
+ session_end: marrowHookDescriptors(settings, 'Stop'),
587
+ },
588
+ };
589
+ return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
590
+ }
591
+
592
+ function upsertClaudeHooks(settingsPath) {
593
+ const settings = parseJsonObject(settingsPath);
594
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
595
+ ? settings.hooks
596
+ : {};
597
+ const postToolUse = reconcileMarrowCommandHook(
598
+ settings, 'PostToolUse', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
599
+ );
600
+ const postToolUseFailure = reconcileMarrowCommandHook(
601
+ settings, 'PostToolUseFailure', 'hook', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
602
+ );
603
+ const preToolUse = reconcileMarrowCommandHook(
604
+ settings, 'PreToolUse', 'pre-action-hook', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER,
605
+ );
606
+ const userPromptSubmit = reconcileMarrowCommandHook(
607
+ settings, 'UserPromptSubmit', 'context-hook', MCP_CONTEXT_HOOK_COMMAND,
608
+ );
609
+ const stop = reconcileMarrowCommandHook(
610
+ settings, 'Stop', 'session-hook', MCP_SESSION_END_HOOK_COMMAND,
611
+ );
447
612
 
448
613
  settings.hooks = {
449
614
  ...hooks,
615
+ PreToolUse: preToolUse,
450
616
  PostToolUse: postToolUse,
617
+ PostToolUseFailure: postToolUseFailure,
451
618
  UserPromptSubmit: userPromptSubmit,
619
+ Stop: stop,
452
620
  };
453
621
 
454
622
  return JSON.stringify(settings, null, 2) + '\n';
455
623
  }
456
624
 
625
+ function activationProfile(detection, plan, changes, client) {
626
+ const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
627
+ || HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
628
+ const sdkDependency = inspectSdkDependency(detection);
629
+ const capabilityLevel = client === 'custom' && (plan.mode === 'sdk' || plan.mode === 'both')
630
+ ? 'sdk_passive_runtime'
631
+ : registry.capability_level;
632
+ const expectedHooks = capabilityLevel === 'sdk_passive_runtime'
633
+ ? ['pre_action', 'action_result', 'outcome_closure']
634
+ : [...registry.automatic];
635
+ const adapterVersion = capabilityLevel === 'native_hooks' || capabilityLevel === 'mcp'
636
+ ? MCP_ADAPTER_VERSION
637
+ : capabilityLevel === 'sdk_passive_runtime'
638
+ ? SDK_ADAPTER_VERSION
639
+ : INSTALLER_ADAPTER_VERSION;
640
+ const observedHooks = [];
641
+ const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
642
+ if (capabilityLevel === 'native_hooks'
643
+ && exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
644
+ if (capabilityLevel === 'native_hooks'
645
+ && exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
646
+ if (capabilityLevel === 'native_hooks'
647
+ && exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
648
+ && exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
649
+ if (capabilityLevel === 'native_hooks'
650
+ && exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
651
+ const passiveRuntime = safeRead(detection.paths.passiveRuntime);
652
+ if (capabilityLevel === 'sdk_passive_runtime'
653
+ && sdkDependency.present
654
+ && /await import\('@getmarrow\/sdk'\)/.test(passiveRuntime)
655
+ && /runtime\.install\(\)/.test(passiveRuntime)) {
656
+ for (const hook of ['pre_action', 'action_result', 'outcome_closure']) {
657
+ if (!observedHooks.includes(hook)) observedHooks.push(hook);
658
+ }
659
+ }
660
+ const mcpConfigs = [detection.paths.mcpJson, detection.paths.cursorMcp]
661
+ .map((filePath) => safeJsonObject(filePath));
662
+ if (capabilityLevel === 'mcp' && mcpConfigs.some((config) => (
663
+ config?.mcpServers?.marrow?.command === 'npx'
664
+ && Array.isArray(config.mcpServers.marrow.args)
665
+ && config.mcpServers.marrow.args.join(' ') === `-y ${MCP_PACKAGE_SPEC}`
666
+ ))) observedHooks.push('mcp_tool_calls');
667
+ const fingerprintMaterial = changes
668
+ .filter((change) => change.applied || change.already_present)
669
+ .map((change) => `${change.label}:${crypto.createHash('sha256').update(safeRead(change.path)).digest('hex')}`)
670
+ .sort()
671
+ .join('|');
672
+ const configFingerprint = capabilityLevel === 'native_hooks'
673
+ ? claudeNativeHookFingerprint(claudeSettings)
674
+ : crypto.createHash('sha256')
675
+ .update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
676
+ .digest('hex');
677
+ const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
678
+ const exactFix = complete
679
+ ? null
680
+ : capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
681
+ ? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
682
+ : capabilityLevel === 'governed_wrapper'
683
+ ? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
684
+ : 'npx @getmarrow/install --repair';
685
+ return {
686
+ adapter_version: adapterVersion,
687
+ capability_level: capabilityLevel,
688
+ config_fingerprint: configFingerprint,
689
+ expected_hooks: expectedHooks,
690
+ observed_hooks: observedHooks,
691
+ complete,
692
+ exact_fix: exactFix,
693
+ };
694
+ }
695
+
457
696
  function upsertMcpServerConfig(filePath) {
458
697
  const config = parseJsonObject(filePath);
459
698
  const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
@@ -461,7 +700,7 @@ function upsertMcpServerConfig(filePath) {
461
700
  : {};
462
701
  servers.marrow = {
463
702
  command: 'npx',
464
- args: ['-y', '@getmarrow/mcp'],
703
+ args: ['-y', MCP_PACKAGE_SPEC],
465
704
  env: {
466
705
  MARROW_API_KEY: '${MARROW_API_KEY}',
467
706
  MARROW_BASE_URL: '${MARROW_BASE_URL}',
@@ -485,7 +724,7 @@ function inspectSdkDependency(detection) {
485
724
  return {
486
725
  required: true,
487
726
  present: false,
488
- install_command: 'npm install @getmarrow/sdk',
727
+ install_command: `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
489
728
  warning: 'package.json could not be parsed; verify @getmarrow/sdk manually.',
490
729
  };
491
730
  }
@@ -496,11 +735,72 @@ function inspectSdkDependency(detection) {
496
735
  packageJson.optionalDependencies,
497
736
  packageJson.peerDependencies,
498
737
  ];
499
- const present = dependencyBlocks.some((deps) => deps && Object.prototype.hasOwnProperty.call(deps, '@getmarrow/sdk'));
738
+ const declaredSpec = dependencyBlocks
739
+ .map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
740
+ .find(Boolean) || null;
741
+ const objectTargetsSdk = (value) => {
742
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
743
+ return Object.entries(value).some(([key, nested]) => (
744
+ key.includes('@getmarrow/sdk') || objectTargetsSdk(nested)
745
+ ));
746
+ };
747
+ const overrideDetected = objectTargetsSdk(packageJson.overrides)
748
+ || objectTargetsSdk(packageJson.resolutions)
749
+ || objectTargetsSdk(packageJson.pnpm?.overrides);
750
+ let lockVerified = false;
751
+ try {
752
+ const lock = JSON.parse(safeRead(path.join(detection.root, 'package-lock.json')) || '{}');
753
+ const rootLock = lock?.packages?.[''];
754
+ const lockedSdk = lock?.packages?.['node_modules/@getmarrow/sdk'];
755
+ const lockedDeclaration = [
756
+ rootLock?.dependencies,
757
+ rootLock?.devDependencies,
758
+ rootLock?.optionalDependencies,
759
+ rootLock?.peerDependencies,
760
+ ].map((deps) => deps && typeof deps['@getmarrow/sdk'] === 'string' ? deps['@getmarrow/sdk'] : null)
761
+ .find(Boolean) || null;
762
+ lockVerified = [2, 3].includes(lock?.lockfileVersion)
763
+ && lockedDeclaration === declaredSpec
764
+ && lockedSdk?.version === SDK_ADAPTER_VERSION
765
+ && lockedSdk?.resolved === SDK_ADAPTER_TARBALL
766
+ && lockedSdk?.integrity === SDK_ADAPTER_INTEGRITY;
767
+ } catch {
768
+ lockVerified = false;
769
+ }
770
+ const installedPackagePath = findUp(
771
+ detection.root,
772
+ [path.join('node_modules', '@getmarrow', 'sdk', 'package.json')],
773
+ );
774
+ let installedVersion = null;
775
+ let installedName = null;
776
+ try {
777
+ const installedPackage = installedPackagePath ? JSON.parse(safeRead(installedPackagePath)) : null;
778
+ installedVersion = typeof installedPackage?.version === 'string' ? installedPackage.version : null;
779
+ installedName = typeof installedPackage?.name === 'string' ? installedPackage.name : null;
780
+ } catch {
781
+ installedVersion = null;
782
+ installedName = null;
783
+ }
784
+ const declarationTrusted = typeof declaredSpec === 'string'
785
+ && declaredSpec.trim().length > 0
786
+ && /^[v0-9xX*<>=~^|.\s-]+$/.test(declaredSpec.trim());
787
+ const present = declarationTrusted
788
+ && !overrideDetected
789
+ && lockVerified
790
+ && installedName === '@getmarrow/sdk'
791
+ && installedVersion === SDK_ADAPTER_VERSION;
500
792
  return {
501
793
  required: true,
502
794
  present,
503
- install_command: present ? null : 'npm install @getmarrow/sdk',
795
+ declared: declaredSpec != null,
796
+ declared_spec: declaredSpec,
797
+ declaration_trusted: declarationTrusted,
798
+ override_detected: overrideDetected,
799
+ lock_verified: lockVerified,
800
+ installed_name: installedName,
801
+ installed_version: installedVersion,
802
+ expected_version: SDK_ADAPTER_VERSION,
803
+ install_command: present ? null : `npm install @getmarrow/sdk@${SDK_ADAPTER_VERSION}`,
504
804
  };
505
805
  }
506
806
 
@@ -569,8 +869,7 @@ function buildPlan(detection, options) {
569
869
  }
570
870
 
571
871
  function applyPlan(plan, options) {
572
- const changes = [];
573
- for (const write of plan.writes) {
872
+ const prepared = plan.writes.map((write) => {
574
873
  const before = safeRead(write.path);
575
874
  let after;
576
875
  if (write.type === 'file') {
@@ -587,6 +886,11 @@ function applyPlan(plan, options) {
587
886
  throw new Error(`Unknown write type: ${write.type}`);
588
887
  }
589
888
 
889
+ return { write, before, after };
890
+ });
891
+
892
+ const changes = [];
893
+ for (const { write, before, after } of prepared) {
590
894
  const changed = before !== after;
591
895
  const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor);
592
896
  changes.push({
@@ -628,7 +932,6 @@ function isCanonicalTimestamp(value) {
628
932
 
629
933
  function runtimeGateVerified(runtime) {
630
934
  if (!runtime || typeof runtime !== 'object') return false;
631
- if (runtime.ok === true) return true;
632
935
  const gate = runtime.risk_gate;
633
936
  if (!gate || typeof gate !== 'object') return false;
634
937
  if (typeof gate.allow === 'boolean' || typeof gate.allowed === 'boolean') return true;
@@ -733,7 +1036,19 @@ async function runSelfTest(options) {
733
1036
  const installValueMoment = buildInstallValueMoment(firstValueSignal, status, runtime, performance, firstValue, tokenValueProof);
734
1037
  let activationReceipt = null;
735
1038
  let activationVerified = false;
1039
+ let activationProfileReceipt = null;
736
1040
  if (options.activation) {
1041
+ const activationAdapterVersion = options.activation.adapter_version || INSTALLER_ADAPTER_VERSION;
1042
+ const activationCapabilityLevel = options.activation.capability_level || 'event_contract';
1043
+ const activationExpectedHooks = Array.isArray(options.activation.expected_hooks) ? options.activation.expected_hooks : [];
1044
+ const activationConfigFingerprint = options.activation.config_fingerprint || crypto.createHash('sha256')
1045
+ .update(JSON.stringify({
1046
+ harness: options.activation.harness || options.client || 'custom',
1047
+ install_surface: options.activation.install_surface || 'unknown',
1048
+ adapter_version: activationAdapterVersion,
1049
+ expected_hooks: activationExpectedHooks,
1050
+ }))
1051
+ .digest('hex');
737
1052
  activationReceipt = firstValue && firstValue.activation_receipt;
738
1053
  const receiptValid = activationReceipt
739
1054
  && typeof activationReceipt === 'object'
@@ -750,7 +1065,58 @@ async function runSelfTest(options) {
750
1065
  if (!receiptValid) {
751
1066
  throw new Error('activation receipt did not verify the exact self-test decision, agent, runtime gate, and closed successful outcome');
752
1067
  }
753
- activationVerified = Boolean(firstValue.active && runtimeGateVerified(runtime) && (status.enabled ?? status.ok));
1068
+ activationProfileReceipt = await requestJson(`${baseUrl}/v1/agent/integrations/events`, {
1069
+ method: 'POST',
1070
+ headers,
1071
+ body: JSON.stringify({
1072
+ event_id: `activation-${activationConfigFingerprint.slice(0, 32)}`,
1073
+ event_type: 'activation_profile_registered',
1074
+ harness: options.activation.harness,
1075
+ agent_id: options.agentId,
1076
+ session_id: headers['x-marrow-session-id'],
1077
+ adapter_version: activationAdapterVersion,
1078
+ capability_level: activationCapabilityLevel,
1079
+ config_fingerprint: activationConfigFingerprint,
1080
+ expected_hooks: activationExpectedHooks,
1081
+ action: 'passive integration activation profile registered',
1082
+ occurred_at: new Date().toISOString(),
1083
+ }),
1084
+ });
1085
+ const profileCoverage = activationProfileReceipt?.activation_coverage;
1086
+ const profileReceipt = profileCoverage?.profile_receipt;
1087
+ const accountBindingId = profileCoverage?.account_binding_id;
1088
+ const expectedConfigurationBindingId = typeof accountBindingId === 'string'
1089
+ ? crypto.createHash('sha256')
1090
+ .update(`agent-config-receipt:v2:${accountBindingId}:${activationConfigFingerprint}`)
1091
+ .digest('hex')
1092
+ : null;
1093
+ const expectedHooksMatch = Array.isArray(profileCoverage?.capture_coverage?.expected_hooks)
1094
+ && [...profileCoverage.capture_coverage.expected_hooks].sort().join(',') === [...activationExpectedHooks].sort().join(',');
1095
+ const profileBound = Boolean(
1096
+ activationProfileReceipt?.accepted === true
1097
+ && profileCoverage?.agent_id === options.agentId
1098
+ && profileCoverage?.harness === options.activation.harness
1099
+ && profileCoverage?.activation?.adapter_version === activationAdapterVersion
1100
+ && profileCoverage?.activation?.capability_level === activationCapabilityLevel
1101
+ && profileReceipt?.agent_id === options.agentId
1102
+ && profileReceipt?.harness === options.activation.harness
1103
+ && profileReceipt?.adapter_version === activationAdapterVersion
1104
+ && profileReceipt?.capability_level === activationCapabilityLevel
1105
+ && typeof accountBindingId === 'string'
1106
+ && accountBindingId.length === 64
1107
+ && profileReceipt?.account_binding_id === accountBindingId
1108
+ && profileReceipt?.config_fingerprint_verified === true
1109
+ && profileReceipt?.configuration_binding_id === expectedConfigurationBindingId
1110
+ && profileReceipt?.expected_hooks_verified === true
1111
+ && expectedHooksMatch
1112
+ );
1113
+ activationVerified = Boolean(
1114
+ firstValue.active
1115
+ && runtimeGateVerified(runtime)
1116
+ && (status.enabled ?? status.ok)
1117
+ && options.activation.complete === true
1118
+ && profileBound,
1119
+ );
754
1120
  if (!activationVerified) {
755
1121
  throw new Error('activation prerequisites were not all verified by the server');
756
1122
  }
@@ -769,6 +1135,8 @@ async function runSelfTest(options) {
769
1135
  runtime_before_you_act: runtime.before_you_act || null,
770
1136
  activation_verified: activationVerified,
771
1137
  activation_receipt: activationReceipt,
1138
+ activation_profile_receipt: activationProfileReceipt,
1139
+ activation_coverage: status.activation_coverage || firstValue.activation_coverage || null,
772
1140
  first_value: firstValue && firstValue.ok !== false ? firstValue : null,
773
1141
  first_value_signal: firstValueSignal,
774
1142
  install_value_moment: installValueMoment,
@@ -916,7 +1284,7 @@ function printReport(report) {
916
1284
 
917
1285
  process.stdout.write('\nPlanned changes:\n');
918
1286
  for (const change of report.changes) {
919
- const marker = change.changed ? (report.writeMode === 'write' ? 'wrote' : 'would write') : 'unchanged';
1287
+ const marker = change.applied ? 'wrote' : change.changed ? 'would write' : 'unchanged';
920
1288
  process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
921
1289
  }
922
1290
 
@@ -1025,6 +1393,9 @@ async function install(options) {
1025
1393
  if (options.activate && options.selfTest === false) {
1026
1394
  throw new Error('activate requires the server self-test');
1027
1395
  }
1396
+ if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
1397
+ throw new Error('repair requires explicit write authorization (--yes)');
1398
+ }
1028
1399
  const detection = detectEnvironment(options.cwd);
1029
1400
  const plan = buildPlan(detection, options);
1030
1401
  const writeMode = options.doctor ? 'doctor' : options.dryRun ? 'dry-run' : options.repair ? 'repair' : options.yes ? 'write' : 'dry-run';
@@ -1032,6 +1403,7 @@ async function install(options) {
1032
1403
  const client = detectedClient(detection);
1033
1404
  options.client = client;
1034
1405
  if (!options.agentId) options.agentId = stableAgentId(detection.root, client);
1406
+ const profile = activationProfile(detection, plan, changes, client);
1035
1407
  options.activation = options.activate ? {
1036
1408
  harness: client,
1037
1409
  agent_id: options.agentId,
@@ -1041,14 +1413,20 @@ async function install(options) {
1041
1413
  .filter((change) => change.changed && change.applied && /hook|runtime|rule|instruction|config/i.test(change.label))
1042
1414
  .map((change) => change.label)
1043
1415
  .slice(0, 20),
1044
- capture_verified: changes.every((change) => change.applied),
1416
+ capture_verified: changes.every((change) => change.applied || change.already_present),
1417
+ adapter_version: profile.adapter_version,
1418
+ capability_level: profile.capability_level,
1419
+ config_fingerprint: profile.config_fingerprint,
1420
+ expected_hooks: profile.expected_hooks,
1421
+ observed_hooks: profile.observed_hooks,
1422
+ complete: profile.complete,
1045
1423
  intervention_verified: false,
1046
1424
  closure_verified: false,
1047
1425
  } : null;
1048
1426
  const configInspection = inspectNpmTokenConfig();
1049
1427
  const sdkDependency = inspectSdkDependency(detection);
1050
1428
  const configDiagnostics = configInspection.safe;
1051
- const configRepairs = options.repair && !options.dryRun && !options.doctor
1429
+ const configRepairs = options.repair && options.yes && !options.dryRun && !options.doctor
1052
1430
  ? repairConfigDiagnostics(configDiagnostics)
1053
1431
  : [];
1054
1432
  const envHints = options.apiKey ? [] : findLikelyEnvFiles(detection);
@@ -1063,7 +1441,7 @@ async function install(options) {
1063
1441
  if (options.activate && !selfTest.activation_verified) {
1064
1442
  throw new Error('Marrow activation failed: server confirmation was not returned');
1065
1443
  }
1066
- const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
1444
+ const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
1067
1445
  const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
1068
1446
  const remediation = options.repair
1069
1447
  ? {
@@ -1098,6 +1476,7 @@ async function install(options) {
1098
1476
  agent_id: options.agentId,
1099
1477
  server_confirmed: Boolean(selfTest.activation_verified),
1100
1478
  receipt: selfTest.activation_receipt || null,
1479
+ profile,
1101
1480
  },
1102
1481
  changes,
1103
1482
  doctor: {
@@ -1150,4 +1529,8 @@ module.exports = {
1150
1529
  buildInstallValueMoment,
1151
1530
  buildTokenValueProof,
1152
1531
  stableAgentId,
1532
+ activationProfile,
1533
+ claudeNativeHookFingerprint,
1534
+ printReport,
1535
+ HARNESS_CAPABILITY_REGISTRY,
1153
1536
  };