@getmarrow/install 0.1.51 → 0.1.53
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 +17 -6
- package/bin/marrow-install.js +2 -1
- package/package.json +1 -1
- package/src/control-state.js +116 -0
- package/src/first-hour.js +71 -7
- package/src/governed-runner.js +92 -11
- package/src/installer.js +869 -42
- package/src/usage-telemetry.js +235 -0
package/src/installer.js
CHANGED
|
@@ -5,12 +5,13 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const { version: INSTALLER_ADAPTER_VERSION } = require('../package.json');
|
|
6
6
|
const { controllerStatus, controllerSupportedPlatform, ensureGovernanceController } = require('./controller-manager');
|
|
7
7
|
const { firstCapturePath, harnessReloadPlan } = require('./first-hour');
|
|
8
|
+
const { evidence: localControlEvidence } = require('./control-state');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
10
11
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
11
12
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
12
|
-
const MCP_ADAPTER_VERSION = '3.9.
|
|
13
|
-
const MCP_ADAPTER_SOURCE_SHA = '
|
|
13
|
+
const MCP_ADAPTER_VERSION = '3.9.75';
|
|
14
|
+
const MCP_ADAPTER_SOURCE_SHA = '7548a7ae2d7e95bb16c62470da41af06cb62c5c1';
|
|
14
15
|
const SDK_ADAPTER_VERSION = '3.7.62';
|
|
15
16
|
const SDK_ADAPTER_INTEGRITY = 'sha512-n1i6Be09TpAQ9BPNRKY7aCvA2iSUPpJfw8djw2MELwpNbBCtKiZ29Jji77BK/6EFLUpSIcTW/Gmdf/ccf0JRYQ==';
|
|
16
17
|
const SDK_ADAPTER_TARBALL = `https://registry.npmjs.org/@getmarrow/sdk/-/sdk-${SDK_ADAPTER_VERSION}.tgz`;
|
|
@@ -33,21 +34,75 @@ const MCP_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mc
|
|
|
33
34
|
const MCP_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp pre-action-hook`;
|
|
34
35
|
const MCP_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp hook`;
|
|
35
36
|
const MCP_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp session-hook`;
|
|
37
|
+
const CODEX_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-context-hook`;
|
|
38
|
+
const CODEX_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-pre-action-hook`;
|
|
39
|
+
const CODEX_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-hook`;
|
|
40
|
+
const CODEX_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp codex-session-hook`;
|
|
41
|
+
const CURSOR_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-pre-action-hook`;
|
|
42
|
+
const CURSOR_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-hook`;
|
|
43
|
+
const CURSOR_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cursor-session-hook`;
|
|
44
|
+
const CLINE_PRE_ACTION_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook`;
|
|
45
|
+
const CLINE_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-hook`;
|
|
46
|
+
const CLINE_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-session-hook`;
|
|
47
|
+
const WINDSURF_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-pre-action-hook`;
|
|
48
|
+
const WINDSURF_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-hook`;
|
|
49
|
+
const WINDSURF_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp windsurf-session-hook`;
|
|
50
|
+
const WINDSURF_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
51
|
+
const WINDSURF_PRE_ACTION_HOOK_COMMAND = `sh -c 'stderr="$(${WINDSURF_PRE_ACTION_ENTRYPOINT} 2>&1 >/dev/null)"; status=$?; if [ "$status" -eq 0 ]; then exit 0; fi; if [ "$status" -eq 2 ]; then printf "%s\\n" "$stderr" >&2; exit 2; fi; printf "%s\\n" "${WINDSURF_LAUNCH_FAILURE}" >&2; exit 2'`;
|
|
52
|
+
const WINDSURF_ACTION_RESULT_HOOK_COMMAND = `sh -c '${WINDSURF_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
|
|
53
|
+
const WINDSURF_SESSION_END_HOOK_COMMAND = `sh -c '${WINDSURF_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; exit 0'`;
|
|
54
|
+
const GEMINI_PRE_ACTION_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-pre-action-hook`;
|
|
55
|
+
const GEMINI_ACTION_RESULT_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-hook`;
|
|
56
|
+
const GEMINI_SESSION_END_ENTRYPOINT = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp gemini-session-hook`;
|
|
57
|
+
const GEMINI_FIXED_DENIAL = 'Marrow blocked this action because required governance approval or proof is unavailable.';
|
|
58
|
+
const GEMINI_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
59
|
+
const GEMINI_PRE_ACTION_HOOK_COMMAND = `sh -c 'output="$(${GEMINI_PRE_ACTION_ENTRYPOINT} 2>/dev/null)" && { case "$output" in "{\\"decision\\":\\"allow\\"}"|"{\\"decision\\":\\"deny\\",\\"reason\\":\\"${GEMINI_FIXED_DENIAL}\\"}") printf "%s\\n" "$output"; exit 0 ;; esac; }; printf "%s\\n" "${GEMINI_LAUNCH_FAILURE}" >&2; exit 2'`;
|
|
60
|
+
const GEMINI_ACTION_RESULT_HOOK_COMMAND = `sh -c '${GEMINI_ACTION_RESULT_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
|
|
61
|
+
const GEMINI_SESSION_END_HOOK_COMMAND = `sh -c '${GEMINI_SESSION_END_ENTRYPOINT} >/dev/null 2>&1 || :; printf "%s\\n" "{}"; exit 0'`;
|
|
62
|
+
const GROK_CONTEXT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-context-hook`;
|
|
63
|
+
const GROK_ACTION_RESULT_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-hook`;
|
|
64
|
+
const GROK_SESSION_END_HOOK_COMMAND = `npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp grok-session-hook`;
|
|
65
|
+
const GROK_FIXED_DENIAL = 'Marrow blocked this protected action.';
|
|
66
|
+
const GROK_LAUNCH_FAILURE = 'Marrow governance adapter was unavailable; this action is blocked.';
|
|
67
|
+
const GROK_PRE_ACTION_GUARD_SOURCE = [
|
|
68
|
+
'const {spawn}=require("node:child_process");',
|
|
69
|
+
`const valid=new Set([${JSON.stringify('{"decision":"allow"}')},${JSON.stringify(`{"decision":"deny","reason":"${GROK_FIXED_DENIAL}"}`)}]);`,
|
|
70
|
+
'let child=null,timer=null,done=false,input=[],inputBytes=0,output="",outputBytes=0;',
|
|
71
|
+
`const fail=()=>{if(done)return;done=true;if(timer)clearTimeout(timer);if(child&&!child.killed)child.kill("SIGKILL");process.stderr.write(${JSON.stringify(`${GROK_LAUNCH_FAILURE}\n`)});process.exitCode=2;process.stdin.destroy();};`,
|
|
72
|
+
'process.stdin.on("error",fail);',
|
|
73
|
+
'process.stdin.on("data",chunk=>{const value=Buffer.from(chunk);inputBytes+=value.length;if(inputBytes>65536){fail();return;}input.push(value);});',
|
|
74
|
+
'process.stdin.on("end",()=>{if(done)return;try{',
|
|
75
|
+
`child=spawn(process.platform==="win32"?"npx.cmd":"npx",${JSON.stringify(['-y', `--package=${MCP_PACKAGE_SPEC}`, 'marrow-mcp', 'grok-pre-action-hook'])},{stdio:["pipe","pipe","ignore"]});`,
|
|
76
|
+
'timer=setTimeout(fail,5000);',
|
|
77
|
+
'child.stdout.on("data",chunk=>{if(done)return;outputBytes+=chunk.length;if(outputBytes>512){fail();return;}output+=chunk.toString("utf8");});',
|
|
78
|
+
'child.on("error",fail);child.stdin.on("error",fail);',
|
|
79
|
+
'child.on("close",code=>{if(done)return;if(code!==0||!valid.has(output)){fail();return;}done=true;if(timer)clearTimeout(timer);process.stdout.write(output);});',
|
|
80
|
+
'child.stdin.end(Buffer.concat(input));}catch{fail();}});',
|
|
81
|
+
].join('');
|
|
82
|
+
const GROK_PRE_ACTION_HOOK_COMMAND = `node -e '${GROK_PRE_ACTION_GUARD_SOURCE}'`;
|
|
36
83
|
const NATIVE_HOOK_MATCHER = 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*';
|
|
84
|
+
const CODEX_NATIVE_HOOK_MATCHER = 'Bash|apply_patch|Edit|Write|MultiEdit|mcp__(?!marrow__marrow_).*|functions\\.(?!marrow_).*';
|
|
85
|
+
const CURSOR_NATIVE_HOOK_MATCHER = 'Shell|Write|Delete|Task|MCP:(?!marrow(?:_.*|:marrow_.*)$).*';
|
|
86
|
+
const GEMINI_NATIVE_HOOK_MATCHER = '^(?:run_shell_command|write_file|replace|edit_file|delete_file|mcp_(?!marrow_marrow_)[A-Za-z0-9_]{1,192})$';
|
|
87
|
+
const GROK_NATIVE_HOOK_MATCHER = 'run_terminal_command|search_replace|write|spawn_subagent|use_tool|workflow|image_gen|image_edit|image_to_video|reference_to_video';
|
|
88
|
+
const CODEX_HOOK_TIMEOUT_SECONDS = 5;
|
|
89
|
+
const CODEX_SESSION_TIMEOUT_SECONDS = 3;
|
|
90
|
+
const GEMINI_HOOK_TIMEOUT_MS = 5000;
|
|
91
|
+
const GEMINI_CLOSEOUT_TIMEOUT_MS = 3000;
|
|
37
92
|
const NATIVE_EXPECTED_HOOKS = ['prompt', 'pre_action', 'action_result', 'session_end'];
|
|
38
93
|
const SOURCE_CLIENTS = new Set(['claude-code', 'cursor', 'composer', 'windsurf', 'openclaw', 'codex', 'gemini', 'grok', 'deepseek', 'qwen', 'kimi', 'minimax', 'cline', 'opencode', 'hermes', 'glm', 'mcp', 'ci', 'custom', 'unknown']);
|
|
39
94
|
const HARNESS_CAPABILITY_REGISTRY = Object.freeze([
|
|
40
95
|
{ client: 'claude-code', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
41
|
-
{ client: 'cursor', capability_level: '
|
|
42
|
-
{ client: 'composer', capability_level: '
|
|
43
|
-
{ client: 'cline', capability_level: '
|
|
44
|
-
{ client: 'windsurf', capability_level: '
|
|
45
|
-
{ client: 'codex', capability_level: '
|
|
96
|
+
{ client: 'cursor', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
|
|
97
|
+
{ client: 'composer', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'mcp' },
|
|
98
|
+
{ client: 'cline', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'cancel_closeout'], install_surface: 'mcp' },
|
|
99
|
+
{ client: 'windsurf', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'response_closeout'], install_surface: 'mcp' },
|
|
100
|
+
{ client: 'codex', capability_level: 'native_hooks', automatic: ['prompt', 'pre_action', 'action_result', 'session_end'], install_surface: 'mcp' },
|
|
46
101
|
{ client: 'opencode', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
47
102
|
{ client: 'hermes', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
48
103
|
{ client: 'openclaw', capability_level: 'event_contract', automatic: [], install_surface: 'addon' },
|
|
49
|
-
{ client: 'gemini', capability_level: '
|
|
50
|
-
{ client: 'grok', capability_level: '
|
|
104
|
+
{ client: 'gemini', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
|
|
105
|
+
{ client: 'grok', capability_level: 'native_hooks', automatic: ['pre_action', 'action_result', 'turn_closeout'], install_surface: 'mcp' },
|
|
51
106
|
{ client: 'deepseek', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
52
107
|
{ client: 'qwen', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
53
108
|
{ client: 'kimi', capability_level: 'governed_wrapper', automatic: ['pre_action', 'action_result', 'outcome_closure'], install_surface: 'runner' },
|
|
@@ -269,41 +324,69 @@ function parseArgs(argv) {
|
|
|
269
324
|
activate: false,
|
|
270
325
|
controller: true,
|
|
271
326
|
};
|
|
327
|
+
let explicitOperation = false;
|
|
272
328
|
|
|
273
329
|
for (let i = 0; i < argv.length; i += 1) {
|
|
274
330
|
const arg = argv[i];
|
|
275
331
|
if (arg === 'activate' || arg === '--activate') {
|
|
332
|
+
explicitOperation = true;
|
|
276
333
|
options.activate = true;
|
|
277
334
|
options.yes = true;
|
|
278
335
|
options.selfTest = true;
|
|
279
336
|
}
|
|
280
|
-
else if (arg === '--yes' || arg === '-y')
|
|
337
|
+
else if (arg === '--yes' || arg === '-y') {
|
|
338
|
+
explicitOperation = true;
|
|
339
|
+
options.yes = true;
|
|
340
|
+
}
|
|
281
341
|
else if (arg === '--repair' || arg === 'repair' || arg === 'update' || arg === '--update') {
|
|
342
|
+
explicitOperation = true;
|
|
282
343
|
options.repair = true;
|
|
283
344
|
options.yes = true;
|
|
284
345
|
options.update = true;
|
|
285
346
|
}
|
|
286
|
-
else if (arg === '--dry-run')
|
|
287
|
-
|
|
347
|
+
else if (arg === '--dry-run') {
|
|
348
|
+
explicitOperation = true;
|
|
349
|
+
options.dryRun = true;
|
|
350
|
+
}
|
|
351
|
+
else if (arg === '--doctor' || arg === 'doctor' || arg === 'check') {
|
|
352
|
+
explicitOperation = true;
|
|
353
|
+
options.doctor = true;
|
|
354
|
+
}
|
|
288
355
|
else if (arg === '--json') options.json = true;
|
|
289
356
|
else if (arg === '--no-self-test') {
|
|
357
|
+
explicitOperation = true;
|
|
290
358
|
options.selfTest = false;
|
|
291
359
|
options.selfTestExplicitlyDisabled = true;
|
|
292
360
|
}
|
|
293
361
|
else if (arg === '--no-controller') options.controller = false;
|
|
294
362
|
else if (arg === '--self-test') options.selfTest = true;
|
|
295
363
|
else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
|
|
296
|
-
else if (arg === '--mode')
|
|
364
|
+
else if (arg === '--mode') {
|
|
365
|
+
explicitOperation = true;
|
|
366
|
+
options.mode = argv[++i] || options.mode;
|
|
367
|
+
}
|
|
297
368
|
else if (arg === '--key') {
|
|
298
369
|
options.apiKey = argv[++i] || options.apiKey;
|
|
299
370
|
options.keyFromArg = true;
|
|
300
371
|
}
|
|
301
372
|
else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
|
|
302
373
|
else if (arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
|
|
303
|
-
else if (arg === '--mcp')
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
374
|
+
else if (arg === '--mcp') {
|
|
375
|
+
explicitOperation = true;
|
|
376
|
+
options.mode = 'mcp';
|
|
377
|
+
}
|
|
378
|
+
else if (arg === '--sdk') {
|
|
379
|
+
explicitOperation = true;
|
|
380
|
+
options.mode = 'sdk';
|
|
381
|
+
}
|
|
382
|
+
else if (arg === '--md' || arg === '--instructions') {
|
|
383
|
+
explicitOperation = true;
|
|
384
|
+
options.mode = 'md';
|
|
385
|
+
}
|
|
386
|
+
else if (arg === '--both') {
|
|
387
|
+
explicitOperation = true;
|
|
388
|
+
options.mode = 'both';
|
|
389
|
+
}
|
|
307
390
|
else if (arg === '--help' || arg === '-h') {
|
|
308
391
|
options.help = true;
|
|
309
392
|
} else {
|
|
@@ -314,6 +397,12 @@ function parseArgs(argv) {
|
|
|
314
397
|
if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
|
|
315
398
|
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
316
399
|
}
|
|
400
|
+
if (!explicitOperation) {
|
|
401
|
+
options.activate = true;
|
|
402
|
+
options.yes = true;
|
|
403
|
+
options.selfTest = true;
|
|
404
|
+
}
|
|
405
|
+
if (options.dryRun) options.selfTest = false;
|
|
317
406
|
if (options.activate && options.selfTestExplicitlyDisabled) {
|
|
318
407
|
throw new Error('activate cannot be combined with --no-self-test because server verification is required');
|
|
319
408
|
}
|
|
@@ -326,6 +415,7 @@ function parseArgs(argv) {
|
|
|
326
415
|
|
|
327
416
|
function usage() {
|
|
328
417
|
return `Usage:
|
|
418
|
+
npx @getmarrow/install
|
|
329
419
|
npx @getmarrow/install --dry-run
|
|
330
420
|
npx @getmarrow/install activate
|
|
331
421
|
npx @getmarrow/install --yes
|
|
@@ -336,6 +426,7 @@ function usage() {
|
|
|
336
426
|
npx @getmarrow/install --sdk --yes
|
|
337
427
|
|
|
338
428
|
Options:
|
|
429
|
+
(no command) Detect, install, self-test, and start the supported persistent controller
|
|
339
430
|
activate Detect, install, self-test, and return a server-confirmed activation receipt
|
|
340
431
|
--dry-run Print planned changes without writing
|
|
341
432
|
--doctor Check install health without writing
|
|
@@ -361,6 +452,9 @@ function detectedClient(detection) {
|
|
|
361
452
|
if (detection.openclaw) return 'openclaw';
|
|
362
453
|
if (detection.claudeCode) return 'claude-code';
|
|
363
454
|
if (detection.cursor) return 'cursor';
|
|
455
|
+
if (detection.cline) return 'cline';
|
|
456
|
+
if (detection.windsurf) return 'windsurf';
|
|
457
|
+
if (detection.gemini) return 'gemini';
|
|
364
458
|
if (detection.codex) return 'codex';
|
|
365
459
|
return 'custom';
|
|
366
460
|
}
|
|
@@ -402,6 +496,14 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
|
402
496
|
requirements: path.join(root, 'requirements.txt'),
|
|
403
497
|
setupPy: path.join(root, 'setup.py'),
|
|
404
498
|
claudeSettings: path.join(root, '.claude', 'settings.json'),
|
|
499
|
+
codexHooks: path.join(root, '.codex', 'hooks.json'),
|
|
500
|
+
cursorHooks: path.join(root, '.cursor', 'hooks.json'),
|
|
501
|
+
windsurfHooks: path.join(root, '.windsurf', 'hooks.json'),
|
|
502
|
+
geminiSettings: path.join(root, '.gemini', 'settings.json'),
|
|
503
|
+
grokHooks: path.join(home, '.grok', 'hooks', 'marrow.json'),
|
|
504
|
+
clinePreToolUseHook: path.join(root, '.clinerules', 'hooks', 'PreToolUse'),
|
|
505
|
+
clinePostToolUseHook: path.join(root, '.clinerules', 'hooks', 'PostToolUse'),
|
|
506
|
+
clineTaskCancelHook: path.join(root, '.clinerules', 'hooks', 'TaskCancel'),
|
|
405
507
|
claudeMd: path.join(root, 'CLAUDE.md'),
|
|
406
508
|
agentsMd: path.join(root, 'AGENTS.md'),
|
|
407
509
|
cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
|
|
@@ -420,6 +522,10 @@ function detectEnvironment(cwd = process.cwd(), env = process.env) {
|
|
|
420
522
|
python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
|
|
421
523
|
claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
|
|
422
524
|
cursor: exists(path.join(root, '.cursor')),
|
|
525
|
+
cline: exists(path.join(root, '.clinerules')),
|
|
526
|
+
windsurf: exists(path.join(root, '.windsurf')),
|
|
527
|
+
gemini: exists(path.join(root, '.gemini')),
|
|
528
|
+
grok: exists(path.join(home, '.grok')) || exists(path.join(root, '.grok')),
|
|
423
529
|
codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
|
|
424
530
|
mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
|
|
425
531
|
openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
|
|
@@ -622,12 +728,12 @@ Marrow should run passively after install:
|
|
|
622
728
|
|
|
623
729
|
- Use MCP plus these instructions in every workspace: \`npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup\`.
|
|
624
730
|
- Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
|
|
625
|
-
- Native Claude hooks install only when \`.claude\` is present. Cursor
|
|
731
|
+
- Native Claude hooks install only when \`.claude\` is present. Codex native hooks install into \`.codex/hooks.json\`; Cursor and Composer use \`.cursor/hooks.json\`; Cline uses non-overwriting executable files under \`.clinerules/hooks/\`; Windsurf uses \`.windsurf/hooks.json\`; Gemini CLI uses \`.gemini/settings.json\`; Grok uses trusted global \`~/.grok/hooks/marrow.json\`. Restart the host, enable/review hooks, disable Windsurf Restricted Mode where native hooks are required, and trust the workspace before claiming runtime coverage. The governed wrapper remains an explicit bounded fallback. Hermes, OpenClaw, and custom hosts need a bounded event adapter.
|
|
626
732
|
- Keep passive token/model usage proof enabled. Empty savings stay zero until observed model usage lands. Do not invent token, cost, or time savings.
|
|
627
733
|
- Before risky work, use Marrow's decision brief or passive prompt hook.
|
|
628
734
|
- After activate writes MCP or hooks, restart this harness, then run \`npx @getmarrow/install@latest doctor --self-test\`. Do not claim Marrow is live in a process that has not reloaded.
|
|
629
735
|
- First turn: tell the owner the gate is live and empty savings are honest. The next deploy, merge, or publish must go through Marrow.
|
|
630
|
-
- Cursor
|
|
736
|
+
- Cursor and Composer use configured native hooks after restart and \`/hooks\` trust review. Cline uses its configured PreToolUse, PostToolUse, and TaskCancel hooks only after Enable Hooks, restart, executable trust, and workspace trust; TaskComplete is not claimed as current coverage. Windsurf uses configured native pre-action, success-result, and response-closeout hooks only after restart, trust review, and leaving Restricted Mode. Gemini CLI uses configured BeforeTool, AfterTool, and AfterAgent hooks only after restart and project fingerprint review and approval in \`/hooks panel\`; explicit user disablement is preserved. Grok uses global native PreToolUse, PostToolUse/PostToolUseFailure, and one nonblocking Stop closeout only after restart and \`/hooks\` inspection; Grok hooks remain user-toggleable. MCP tools remain on demand. Codex uses configured native hooks after restart and \`/hooks\` trust review. The governed wrapper remains an explicit bounded fallback.
|
|
631
737
|
- Before the session ends, close open work with session-end auto-commit or \`marrow_commit\`. Record model usage only when the host response includes counts.
|
|
632
738
|
- After meaningful work, record the outcome so future agents learn from it.
|
|
633
739
|
- After Marrow blocks, warns, or requires review, use the decision trace receipt to tell the operator what changed and which recorded workflow or proof is required. Stay quiet for routine low-risk work.
|
|
@@ -757,12 +863,12 @@ function exactHookDescriptors(settings, eventName, command, matcher) {
|
|
|
757
863
|
function marrowHookSubcommand(command) {
|
|
758
864
|
if (typeof command !== 'string') return null;
|
|
759
865
|
const match = command.trim().match(
|
|
760
|
-
/^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
866
|
+
/^npx\s+(?:-y\s+)?(?:--package=@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp|@getmarrow\/mcp(?:@[^\s]+)?)\s+(?:(?:claude|codex|cursor|grok)-)?(context-hook|pre-action-hook|hook|session-hook)$/,
|
|
761
867
|
);
|
|
762
868
|
return match?.[1] || null;
|
|
763
869
|
}
|
|
764
870
|
|
|
765
|
-
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher) {
|
|
871
|
+
function reconcileMarrowCommandHook(settings, eventName, subcommand, command, matcher, handlerOptions = {}) {
|
|
766
872
|
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
767
873
|
let preferredHandler = null;
|
|
768
874
|
const retained = [];
|
|
@@ -786,7 +892,7 @@ function reconcileMarrowCommandHook(settings, eventName, subcommand, command, ma
|
|
|
786
892
|
}
|
|
787
893
|
if (remaining.length > 0) retained.push({ ...entry, hooks: remaining });
|
|
788
894
|
}
|
|
789
|
-
const canonical = { hooks: [{ ...(preferredHandler || {}), type: 'command', command }] };
|
|
895
|
+
const canonical = { hooks: [{ ...(preferredHandler || {}), ...handlerOptions, type: 'command', command }] };
|
|
790
896
|
if (matcher != null) canonical.matcher = matcher;
|
|
791
897
|
retained.push(canonical);
|
|
792
898
|
return retained;
|
|
@@ -882,6 +988,439 @@ function upsertClaudeHooks(settingsPath) {
|
|
|
882
988
|
return JSON.stringify(settings, null, 2) + '\n';
|
|
883
989
|
}
|
|
884
990
|
|
|
991
|
+
function codexNativeHookFingerprint(settings) {
|
|
992
|
+
const contract = {
|
|
993
|
+
schema: 'marrow-codex-native-hooks.v1',
|
|
994
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
995
|
+
expected_hooks: NATIVE_EXPECTED_HOOKS,
|
|
996
|
+
configured: {
|
|
997
|
+
prompt: exactHookConfigured(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
|
|
998
|
+
pre_action: exactHookConfigured(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
999
|
+
action_result: exactHookConfigured(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1000
|
+
session_end: exactHookConfigured(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
|
|
1001
|
+
},
|
|
1002
|
+
descriptors: {
|
|
1003
|
+
prompt: exactHookDescriptors(settings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND),
|
|
1004
|
+
pre_action: exactHookDescriptors(settings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1005
|
+
action_result: exactHookDescriptors(settings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER),
|
|
1006
|
+
session_end: exactHookDescriptors(settings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND),
|
|
1007
|
+
},
|
|
1008
|
+
active_marrow_handlers: {
|
|
1009
|
+
prompt: marrowHookDescriptors(settings, 'UserPromptSubmit'),
|
|
1010
|
+
pre_action: marrowHookDescriptors(settings, 'PreToolUse'),
|
|
1011
|
+
action_result: marrowHookDescriptors(settings, 'PostToolUse'),
|
|
1012
|
+
session_end: marrowHookDescriptors(settings, 'SessionEnd'),
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function upsertCodexHooks(hooksPath) {
|
|
1019
|
+
const settings = parseJsonObject(hooksPath);
|
|
1020
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1021
|
+
? settings.hooks
|
|
1022
|
+
: {};
|
|
1023
|
+
settings.hooks = {
|
|
1024
|
+
...hooks,
|
|
1025
|
+
UserPromptSubmit: reconcileMarrowCommandHook(
|
|
1026
|
+
settings, 'UserPromptSubmit', 'context-hook', CODEX_CONTEXT_HOOK_COMMAND, undefined,
|
|
1027
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS },
|
|
1028
|
+
),
|
|
1029
|
+
PreToolUse: reconcileMarrowCommandHook(
|
|
1030
|
+
settings, 'PreToolUse', 'pre-action-hook', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
|
|
1031
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS, async: false },
|
|
1032
|
+
),
|
|
1033
|
+
PostToolUse: reconcileMarrowCommandHook(
|
|
1034
|
+
settings, 'PostToolUse', 'hook', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER,
|
|
1035
|
+
{ timeout: CODEX_HOOK_TIMEOUT_SECONDS },
|
|
1036
|
+
),
|
|
1037
|
+
SessionEnd: reconcileMarrowCommandHook(
|
|
1038
|
+
settings, 'SessionEnd', 'session-hook', CODEX_SESSION_END_HOOK_COMMAND, undefined,
|
|
1039
|
+
{ timeout: CODEX_SESSION_TIMEOUT_SECONDS },
|
|
1040
|
+
),
|
|
1041
|
+
};
|
|
1042
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function reconcileCursorHook(settings, eventName, subcommand, canonical) {
|
|
1046
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1047
|
+
const retained = original.filter((entry) => !(
|
|
1048
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1049
|
+
&& marrowHookSubcommand(entry.command)
|
|
1050
|
+
));
|
|
1051
|
+
return [...retained, canonical];
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function exactCursorHookConfigured(settings, eventName, command, matcher, required = {}) {
|
|
1055
|
+
const entries = settings?.hooks?.[eventName];
|
|
1056
|
+
if (!Array.isArray(entries)) return false;
|
|
1057
|
+
return entries.some((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1058
|
+
&& entry.command === command
|
|
1059
|
+
&& (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
|
|
1060
|
+
&& Object.entries(required).every(([key, value]) => entry[key] === value));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function cursorHookDescriptors(settings, eventName) {
|
|
1064
|
+
const entries = settings?.hooks?.[eventName];
|
|
1065
|
+
if (!Array.isArray(entries)) return [];
|
|
1066
|
+
return entries.flatMap((entry) => {
|
|
1067
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !marrowHookSubcommand(entry.command)) return [];
|
|
1068
|
+
return [{
|
|
1069
|
+
matcher: typeof entry.matcher === 'string' ? entry.matcher : null,
|
|
1070
|
+
command: String(entry.command).trim(),
|
|
1071
|
+
timeout: typeof entry.timeout === 'number' && Number.isFinite(entry.timeout) ? entry.timeout : null,
|
|
1072
|
+
failClosed: entry.failClosed === true,
|
|
1073
|
+
async: entry.async === false ? false : null,
|
|
1074
|
+
}];
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function cursorNativeHookFingerprint(settings) {
|
|
1079
|
+
const contract = {
|
|
1080
|
+
schema: 'marrow-cursor-native-hooks.v1',
|
|
1081
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1082
|
+
expected_hooks: ['pre_action', 'action_result', 'outcome_closure'],
|
|
1083
|
+
configured: {
|
|
1084
|
+
pre_action: exactCursorHookConfigured(settings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1085
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1086
|
+
failClosed: true,
|
|
1087
|
+
async: false,
|
|
1088
|
+
}),
|
|
1089
|
+
action_result_success: exactCursorHookConfigured(settings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1090
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1091
|
+
}),
|
|
1092
|
+
action_result_failure: exactCursorHookConfigured(settings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1093
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1094
|
+
}),
|
|
1095
|
+
outcome_closure: exactCursorHookConfigured(settings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1096
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1097
|
+
}),
|
|
1098
|
+
},
|
|
1099
|
+
descriptors: {
|
|
1100
|
+
pre_action: cursorHookDescriptors(settings, 'preToolUse'),
|
|
1101
|
+
action_result_success: cursorHookDescriptors(settings, 'postToolUse'),
|
|
1102
|
+
action_result_failure: cursorHookDescriptors(settings, 'postToolUseFailure'),
|
|
1103
|
+
outcome_closure: cursorHookDescriptors(settings, 'stop'),
|
|
1104
|
+
},
|
|
1105
|
+
};
|
|
1106
|
+
return crypto.createHash('sha256').update(JSON.stringify(contract)).digest('hex');
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function upsertCursorHooks(hooksPath) {
|
|
1110
|
+
const settings = parseJsonObject(hooksPath);
|
|
1111
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1112
|
+
? settings.hooks
|
|
1113
|
+
: {};
|
|
1114
|
+
settings.version = 1;
|
|
1115
|
+
settings.hooks = {
|
|
1116
|
+
...hooks,
|
|
1117
|
+
preToolUse: reconcileCursorHook(settings, 'preToolUse', 'pre-action-hook', {
|
|
1118
|
+
command: CURSOR_PRE_ACTION_HOOK_COMMAND,
|
|
1119
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1120
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1121
|
+
failClosed: true,
|
|
1122
|
+
async: false,
|
|
1123
|
+
}),
|
|
1124
|
+
postToolUse: reconcileCursorHook(settings, 'postToolUse', 'hook', {
|
|
1125
|
+
command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
|
|
1126
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1127
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1128
|
+
}),
|
|
1129
|
+
postToolUseFailure: reconcileCursorHook(settings, 'postToolUseFailure', 'hook', {
|
|
1130
|
+
command: CURSOR_ACTION_RESULT_HOOK_COMMAND,
|
|
1131
|
+
matcher: CURSOR_NATIVE_HOOK_MATCHER,
|
|
1132
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1133
|
+
}),
|
|
1134
|
+
stop: reconcileCursorHook(settings, 'stop', 'session-hook', {
|
|
1135
|
+
command: CURSOR_SESSION_END_HOOK_COMMAND,
|
|
1136
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1137
|
+
}),
|
|
1138
|
+
};
|
|
1139
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const WINDSURF_PRE_EVENTS = ['pre_write_code', 'pre_run_command', 'pre_mcp_tool_use'];
|
|
1143
|
+
const WINDSURF_POST_EVENTS = ['post_write_code', 'post_run_command', 'post_mcp_tool_use'];
|
|
1144
|
+
|
|
1145
|
+
function windsurfMarrowHookEntrypoint(command) {
|
|
1146
|
+
if (typeof command !== 'string') return null;
|
|
1147
|
+
const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+windsurf-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
|
|
1148
|
+
return match?.[1] || null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function reconcileWindsurfHook(settings, eventName, command) {
|
|
1152
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1153
|
+
const retained = original.filter((entry) => !(
|
|
1154
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1155
|
+
&& windsurfMarrowHookEntrypoint(entry.command)
|
|
1156
|
+
));
|
|
1157
|
+
return [...retained, { command, show_output: false }];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function exactWindsurfHookConfigured(settings, eventName, command) {
|
|
1161
|
+
const entries = settings?.hooks?.[eventName];
|
|
1162
|
+
return Array.isArray(entries) && entries.some((entry) => (
|
|
1163
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1164
|
+
&& entry.command === command
|
|
1165
|
+
&& entry.show_output === false
|
|
1166
|
+
));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function windsurfNativeHookFingerprint(settings) {
|
|
1170
|
+
const configured = Object.fromEntries([
|
|
1171
|
+
...WINDSURF_PRE_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND)]),
|
|
1172
|
+
...WINDSURF_POST_EVENTS.map((event) => [event, exactWindsurfHookConfigured(settings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND)]),
|
|
1173
|
+
['post_cascade_response', exactWindsurfHookConfigured(settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND)],
|
|
1174
|
+
]);
|
|
1175
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1176
|
+
schema: 'marrow-windsurf-native-hooks.v1',
|
|
1177
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1178
|
+
expected_hooks: ['pre_action', 'action_result', 'response_closeout'],
|
|
1179
|
+
restricted_mode_disables_hooks: true,
|
|
1180
|
+
configured,
|
|
1181
|
+
})).digest('hex');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function upsertWindsurfHooks(hooksPath) {
|
|
1185
|
+
const settings = parseJsonObject(hooksPath);
|
|
1186
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1187
|
+
? settings.hooks
|
|
1188
|
+
: {};
|
|
1189
|
+
settings.hooks = { ...hooks };
|
|
1190
|
+
for (const eventName of WINDSURF_PRE_EVENTS) {
|
|
1191
|
+
settings.hooks[eventName] = reconcileWindsurfHook(
|
|
1192
|
+
settings, eventName, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
for (const eventName of WINDSURF_POST_EVENTS) {
|
|
1196
|
+
settings.hooks[eventName] = reconcileWindsurfHook(
|
|
1197
|
+
settings, eventName, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
settings.hooks.post_cascade_response = reconcileWindsurfHook(
|
|
1201
|
+
settings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1202
|
+
);
|
|
1203
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function geminiMarrowHookEntrypoint(command) {
|
|
1207
|
+
if (typeof command !== 'string') return null;
|
|
1208
|
+
const match = command.match(/@getmarrow\/mcp(?:@[^\s]+)?\s+marrow-mcp\s+gemini-(pre-action-hook|hook|session-hook)(?:\s|['"]|$)/);
|
|
1209
|
+
return match?.[1] || null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function reconcileGeminiHook(settings, eventName, canonical) {
|
|
1213
|
+
const original = Array.isArray(settings?.hooks?.[eventName]) ? settings.hooks[eventName] : [];
|
|
1214
|
+
const retained = [];
|
|
1215
|
+
for (const group of original) {
|
|
1216
|
+
if (!group || typeof group !== 'object' || Array.isArray(group) || !Array.isArray(group.hooks)) {
|
|
1217
|
+
retained.push(group);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const hooks = group.hooks.filter((handler) => !(
|
|
1221
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1222
|
+
&& (String(handler.name || '').startsWith('marrow-') || geminiMarrowHookEntrypoint(handler.command))
|
|
1223
|
+
));
|
|
1224
|
+
if (hooks.length > 0) retained.push({ ...group, hooks });
|
|
1225
|
+
}
|
|
1226
|
+
return [...retained, canonical];
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function exactGeminiHookConfigured(settings, eventName, name, command, matcher, timeout) {
|
|
1230
|
+
const groups = settings?.hooks?.[eventName];
|
|
1231
|
+
if (!Array.isArray(groups)) return false;
|
|
1232
|
+
return groups.some((group) => (
|
|
1233
|
+
group && typeof group === 'object' && !Array.isArray(group)
|
|
1234
|
+
&& (matcher === undefined ? group.matcher === undefined : group.matcher === matcher)
|
|
1235
|
+
&& Array.isArray(group.hooks)
|
|
1236
|
+
&& group.hooks.some((handler) => (
|
|
1237
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1238
|
+
&& handler.name === name
|
|
1239
|
+
&& handler.type === 'command'
|
|
1240
|
+
&& handler.command === command
|
|
1241
|
+
&& handler.timeout === timeout
|
|
1242
|
+
))
|
|
1243
|
+
));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function geminiHooksExplicitlyDisabled(settings) {
|
|
1247
|
+
return settings?.hooksConfig?.enabled === false;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function geminiNativeHookFingerprint(settings) {
|
|
1251
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1252
|
+
schema: 'marrow-gemini-native-hooks.v1',
|
|
1253
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1254
|
+
expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
|
|
1255
|
+
explicitly_enabled: settings?.hooksConfig?.enabled === true,
|
|
1256
|
+
explicitly_disabled: geminiHooksExplicitlyDisabled(settings),
|
|
1257
|
+
configured: {
|
|
1258
|
+
pre_action: exactGeminiHookConfigured(
|
|
1259
|
+
settings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1260
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1261
|
+
),
|
|
1262
|
+
action_result: exactGeminiHookConfigured(
|
|
1263
|
+
settings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1264
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1265
|
+
),
|
|
1266
|
+
turn_closeout: exactGeminiHookConfigured(
|
|
1267
|
+
settings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1268
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1269
|
+
),
|
|
1270
|
+
},
|
|
1271
|
+
session_end_claimed: false,
|
|
1272
|
+
})).digest('hex');
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function exactGrokHookConfigured(settings, eventName, command, matcher, timeout) {
|
|
1276
|
+
const entries = settings?.hooks?.[eventName];
|
|
1277
|
+
if (!Array.isArray(entries)) return false;
|
|
1278
|
+
return entries.some((entry) => (
|
|
1279
|
+
entry && typeof entry === 'object' && !Array.isArray(entry)
|
|
1280
|
+
&& (matcher === undefined ? entry.matcher === undefined : entry.matcher === matcher)
|
|
1281
|
+
&& Array.isArray(entry.hooks)
|
|
1282
|
+
&& entry.hooks.some((handler) => (
|
|
1283
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1284
|
+
&& handler.type === 'command'
|
|
1285
|
+
&& handler.command === command
|
|
1286
|
+
&& handler.timeout === timeout
|
|
1287
|
+
))
|
|
1288
|
+
));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function grokHasDuplicateSessionEnd(settings) {
|
|
1292
|
+
const entries = settings?.hooks?.SessionEnd;
|
|
1293
|
+
if (!Array.isArray(entries)) return false;
|
|
1294
|
+
return entries.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((handler) => (
|
|
1295
|
+
handler && typeof handler === 'object' && !Array.isArray(handler)
|
|
1296
|
+
&& typeof handler.command === 'string'
|
|
1297
|
+
&& /marrow-mcp\s+grok-session-hook(?:\s|['"]|$)/.test(handler.command)
|
|
1298
|
+
)));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function grokNativeHookFingerprint(settings) {
|
|
1302
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1303
|
+
schema: 'marrow-grok-native-hooks.v1',
|
|
1304
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1305
|
+
expected_hooks: ['pre_action', 'action_result', 'turn_closeout'],
|
|
1306
|
+
configured: {
|
|
1307
|
+
context: exactGrokHookConfigured(settings, 'UserPromptSubmit', GROK_CONTEXT_HOOK_COMMAND, undefined, 5),
|
|
1308
|
+
pre_action: exactGrokHookConfigured(settings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7),
|
|
1309
|
+
action_result_success: exactGrokHookConfigured(settings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
|
|
1310
|
+
action_result_failure: exactGrokHookConfigured(settings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5),
|
|
1311
|
+
turn_closeout: exactGrokHookConfigured(settings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3),
|
|
1312
|
+
duplicate_session_end: grokHasDuplicateSessionEnd(settings),
|
|
1313
|
+
},
|
|
1314
|
+
})).digest('hex');
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function upsertGeminiHooks(settingsPath) {
|
|
1318
|
+
const settings = parseJsonObject(settingsPath);
|
|
1319
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
|
|
1320
|
+
? settings.hooks
|
|
1321
|
+
: {};
|
|
1322
|
+
settings.hooks = {
|
|
1323
|
+
...hooks,
|
|
1324
|
+
BeforeTool: reconcileGeminiHook(settings, 'BeforeTool', {
|
|
1325
|
+
matcher: GEMINI_NATIVE_HOOK_MATCHER,
|
|
1326
|
+
hooks: [{
|
|
1327
|
+
name: 'marrow-before-tool',
|
|
1328
|
+
type: 'command',
|
|
1329
|
+
command: GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1330
|
+
timeout: GEMINI_HOOK_TIMEOUT_MS,
|
|
1331
|
+
}],
|
|
1332
|
+
}),
|
|
1333
|
+
AfterTool: reconcileGeminiHook(settings, 'AfterTool', {
|
|
1334
|
+
matcher: GEMINI_NATIVE_HOOK_MATCHER,
|
|
1335
|
+
hooks: [{
|
|
1336
|
+
name: 'marrow-after-tool',
|
|
1337
|
+
type: 'command',
|
|
1338
|
+
command: GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1339
|
+
timeout: GEMINI_HOOK_TIMEOUT_MS,
|
|
1340
|
+
}],
|
|
1341
|
+
}),
|
|
1342
|
+
AfterAgent: reconcileGeminiHook(settings, 'AfterAgent', {
|
|
1343
|
+
hooks: [{
|
|
1344
|
+
name: 'marrow-after-agent',
|
|
1345
|
+
type: 'command',
|
|
1346
|
+
command: GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1347
|
+
timeout: GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1348
|
+
}],
|
|
1349
|
+
}),
|
|
1350
|
+
};
|
|
1351
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function clinePreToolUseHookSource() {
|
|
1355
|
+
return `#!/bin/sh
|
|
1356
|
+
output="$(npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp cline-pre-action-hook 2>/dev/null)" || output=""
|
|
1357
|
+
if [ -n "$output" ]; then
|
|
1358
|
+
validated="$(printf '%s' "$output" | NODE_OPTIONS= node -e 'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",c=>s+=c);process.stdin.on("end",()=>{try{const v=JSON.parse(s);const keys=Object.keys(v).sort();const allow=v.cancel===false&&keys.length===1&&keys[0]==="cancel";const deny=v.cancel===true&&typeof v.errorMessage==="string"&&v.errorMessage.length>0&&v.errorMessage.length<=500&&keys.length===2&&keys[0]==="cancel"&&keys[1]==="errorMessage";if(!allow&&!deny)process.exit(1);process.stdout.write(JSON.stringify(v));}catch{process.exit(1);}});' 2>/dev/null)" || validated=""
|
|
1359
|
+
if [ -n "$validated" ]; then
|
|
1360
|
+
printf '%s\n' "$validated"
|
|
1361
|
+
exit 0
|
|
1362
|
+
fi
|
|
1363
|
+
fi
|
|
1364
|
+
printf '%s\n' '{"cancel":true,"errorMessage":"Marrow governance did not return a valid decision. Restore trusted configuration and retry."}'
|
|
1365
|
+
exit 0
|
|
1366
|
+
`;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function clineTelemetryHookSource(entrypoint) {
|
|
1370
|
+
return `#!/bin/sh
|
|
1371
|
+
npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp ${entrypoint} >/dev/null 2>&1 || :
|
|
1372
|
+
exit 0
|
|
1373
|
+
`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function clineHookContract(detection) {
|
|
1377
|
+
return [
|
|
1378
|
+
{
|
|
1379
|
+
stage: 'pre_action',
|
|
1380
|
+
path: detection.paths.clinePreToolUseHook,
|
|
1381
|
+
label: 'Cline PreToolUse native hook',
|
|
1382
|
+
content: clinePreToolUseHookSource(),
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
stage: 'action_result',
|
|
1386
|
+
path: detection.paths.clinePostToolUseHook,
|
|
1387
|
+
label: 'Cline PostToolUse native hook',
|
|
1388
|
+
content: clineTelemetryHookSource('cline-hook'),
|
|
1389
|
+
},
|
|
1390
|
+
{
|
|
1391
|
+
stage: 'cancel_closeout',
|
|
1392
|
+
path: detection.paths.clineTaskCancelHook,
|
|
1393
|
+
label: 'Cline TaskCancel native hook',
|
|
1394
|
+
content: clineTelemetryHookSource('cline-session-hook'),
|
|
1395
|
+
},
|
|
1396
|
+
];
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function exactExecutableFile(filePath, content) {
|
|
1400
|
+
try {
|
|
1401
|
+
const stat = fs.lstatSync(filePath);
|
|
1402
|
+
return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o111) !== 0 && safeRead(filePath) === content;
|
|
1403
|
+
} catch {
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function clineNativeHookFingerprint(detection) {
|
|
1409
|
+
const hooks = clineHookContract(detection).map((hook) => ({
|
|
1410
|
+
stage: hook.stage,
|
|
1411
|
+
configured: exactExecutableFile(hook.path, hook.content),
|
|
1412
|
+
content_sha256: exactExecutableFile(hook.path, hook.content)
|
|
1413
|
+
? crypto.createHash('sha256').update(hook.content).digest('hex')
|
|
1414
|
+
: null,
|
|
1415
|
+
}));
|
|
1416
|
+
return crypto.createHash('sha256').update(JSON.stringify({
|
|
1417
|
+
schema: 'marrow-cline-native-hooks.v1',
|
|
1418
|
+
adapter_version: MCP_ADAPTER_VERSION,
|
|
1419
|
+
task_complete_support: 'coming_soon_not_configured',
|
|
1420
|
+
hooks,
|
|
1421
|
+
})).digest('hex');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
885
1424
|
function activationProfile(detection, plan, changes, client) {
|
|
886
1425
|
const registry = HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === client)
|
|
887
1426
|
|| HARNESS_CAPABILITY_REGISTRY.find((entry) => entry.client === 'custom');
|
|
@@ -899,15 +1438,78 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
899
1438
|
: INSTALLER_ADAPTER_VERSION;
|
|
900
1439
|
const observedHooks = [];
|
|
901
1440
|
const claudeSettings = safeJsonObject(detection.paths.claudeSettings);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1441
|
+
const codexSettings = safeJsonObject(detection.paths.codexHooks);
|
|
1442
|
+
const cursorSettings = safeJsonObject(detection.paths.cursorHooks);
|
|
1443
|
+
const windsurfSettings = safeJsonObject(detection.paths.windsurfHooks);
|
|
1444
|
+
const geminiSettings = safeJsonObject(detection.paths.geminiSettings);
|
|
1445
|
+
const grokSettings = safeJsonObject(detection.paths.grokHooks);
|
|
1446
|
+
if (client === 'codex') {
|
|
1447
|
+
if (exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1448
|
+
if (exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1449
|
+
if (exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1450
|
+
if (exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1451
|
+
} else if (client === 'cursor' || client === 'composer') {
|
|
1452
|
+
if (exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1453
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
|
|
1454
|
+
})) observedHooks.push('pre_action');
|
|
1455
|
+
if (exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1456
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1457
|
+
}) && exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1458
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1459
|
+
})) observedHooks.push('action_result');
|
|
1460
|
+
if (exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1461
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1462
|
+
})) observedHooks.push('outcome_closure');
|
|
1463
|
+
} else if (client === 'cline') {
|
|
1464
|
+
for (const hook of clineHookContract(detection)) {
|
|
1465
|
+
if (exactExecutableFile(hook.path, hook.content)) observedHooks.push(hook.stage);
|
|
1466
|
+
}
|
|
1467
|
+
} else if (client === 'windsurf') {
|
|
1468
|
+
if (WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1469
|
+
windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1470
|
+
))) observedHooks.push('pre_action');
|
|
1471
|
+
if (WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1472
|
+
windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1473
|
+
))) observedHooks.push('action_result');
|
|
1474
|
+
if (exactWindsurfHookConfigured(
|
|
1475
|
+
windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1476
|
+
)) observedHooks.push('response_closeout');
|
|
1477
|
+
} else if (client === 'gemini' && !geminiHooksExplicitlyDisabled(geminiSettings)) {
|
|
1478
|
+
if (exactGeminiHookConfigured(
|
|
1479
|
+
geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1480
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1481
|
+
)) observedHooks.push('pre_action');
|
|
1482
|
+
if (exactGeminiHookConfigured(
|
|
1483
|
+
geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1484
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1485
|
+
)) observedHooks.push('action_result');
|
|
1486
|
+
if (exactGeminiHookConfigured(
|
|
1487
|
+
geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1488
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1489
|
+
)) observedHooks.push('turn_closeout');
|
|
1490
|
+
} else if (client === 'grok') {
|
|
1491
|
+
if (exactGrokHookConfigured(
|
|
1492
|
+
grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1493
|
+
)) observedHooks.push('pre_action');
|
|
1494
|
+
if (exactGrokHookConfigured(
|
|
1495
|
+
grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1496
|
+
) && exactGrokHookConfigured(
|
|
1497
|
+
grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1498
|
+
)) observedHooks.push('action_result');
|
|
1499
|
+
if (exactGrokHookConfigured(
|
|
1500
|
+
grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
|
|
1501
|
+
) && !grokHasDuplicateSessionEnd(grokSettings)) observedHooks.push('turn_closeout');
|
|
1502
|
+
} else {
|
|
1503
|
+
if (capabilityLevel === 'native_hooks'
|
|
1504
|
+
&& exactHookConfigured(claudeSettings, 'UserPromptSubmit', MCP_CONTEXT_HOOK_COMMAND)) observedHooks.push('prompt');
|
|
1505
|
+
if (capabilityLevel === 'native_hooks'
|
|
1506
|
+
&& exactHookConfigured(claudeSettings, 'PreToolUse', MCP_PRE_ACTION_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('pre_action');
|
|
1507
|
+
if (capabilityLevel === 'native_hooks'
|
|
1508
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUse', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)
|
|
1509
|
+
&& exactHookConfigured(claudeSettings, 'PostToolUseFailure', MCP_ACTION_RESULT_HOOK_COMMAND, NATIVE_HOOK_MATCHER)) observedHooks.push('action_result');
|
|
1510
|
+
if (capabilityLevel === 'native_hooks'
|
|
1511
|
+
&& exactHookConfigured(claudeSettings, 'Stop', MCP_SESSION_END_HOOK_COMMAND)) observedHooks.push('session_end');
|
|
1512
|
+
}
|
|
911
1513
|
const passiveRuntime = safeRead(detection.paths.passiveRuntime);
|
|
912
1514
|
if (capabilityLevel === 'sdk_passive_runtime'
|
|
913
1515
|
&& sdkDependency.present
|
|
@@ -930,17 +1532,40 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
930
1532
|
.sort()
|
|
931
1533
|
.join('|');
|
|
932
1534
|
const configFingerprint = capabilityLevel === 'native_hooks'
|
|
933
|
-
?
|
|
1535
|
+
? client === 'codex' ? codexNativeHookFingerprint(codexSettings)
|
|
1536
|
+
: client === 'cursor' || client === 'composer' ? cursorNativeHookFingerprint(cursorSettings)
|
|
1537
|
+
: client === 'cline' ? clineNativeHookFingerprint(detection)
|
|
1538
|
+
: client === 'windsurf' ? windsurfNativeHookFingerprint(windsurfSettings)
|
|
1539
|
+
: client === 'gemini' ? geminiNativeHookFingerprint(geminiSettings)
|
|
1540
|
+
: client === 'grok' ? grokNativeHookFingerprint(grokSettings)
|
|
1541
|
+
: claudeNativeHookFingerprint(claudeSettings)
|
|
934
1542
|
: crypto.createHash('sha256')
|
|
935
1543
|
.update(`${client}:${capabilityLevel}:${expectedHooks.join(',')}:${fingerprintMaterial}`)
|
|
936
1544
|
.digest('hex');
|
|
937
1545
|
const complete = expectedHooks.length > 0 && expectedHooks.every((hook) => observedHooks.includes(hook));
|
|
1546
|
+
const clineConflicts = changes
|
|
1547
|
+
.filter((change) => change.hook_conflict)
|
|
1548
|
+
.map((change) => change.label);
|
|
938
1549
|
const exactFix = complete
|
|
939
|
-
?
|
|
1550
|
+
? client === 'cline'
|
|
1551
|
+
? 'Enable Hooks in Cline, trust the project hook executables and workspace, then restart Cline. TaskComplete remains unverified and is not configured.'
|
|
1552
|
+
: client === 'windsurf'
|
|
1553
|
+
? 'Restart Windsurf, trust the workspace hook configuration, and leave Restricted Mode before expecting hooks to run. MCP tools remain on demand.'
|
|
1554
|
+
: client === 'gemini'
|
|
1555
|
+
? 'Restart Gemini CLI, open /hooks panel, and review and approve the project hook fingerprints. MCP tools remain on demand.'
|
|
1556
|
+
: client === 'grok'
|
|
1557
|
+
? 'Restart Grok, inspect the installed global hooks with /hooks, and confirm they are enabled. Configuration remains client-self-reported and does not verify observed coverage.'
|
|
1558
|
+
: null
|
|
940
1559
|
: capabilityLevel === 'sdk_passive_runtime' && !sdkDependency.present
|
|
941
1560
|
? `${sdkDependency.install_command} && npx @getmarrow/install --repair`
|
|
942
1561
|
: capabilityLevel === 'governed_wrapper'
|
|
943
1562
|
? `npx @getmarrow/install run --agent <agent-id> -- ${client}`
|
|
1563
|
+
: client === 'cline' && clineConflicts.length > 0
|
|
1564
|
+
? 'Move or remove the conflicting owner-managed Cline hook file after owner review, then run npx @getmarrow/install --repair. Marrow will never overwrite or compose it.'
|
|
1565
|
+
: client === 'gemini' && geminiHooksExplicitlyDisabled(geminiSettings)
|
|
1566
|
+
? 'Hooks are explicitly disabled. After owner review, run /hooks enable-all, open /hooks panel, review and approve the project hook fingerprints, then restart Gemini CLI.'
|
|
1567
|
+
: client === 'grok'
|
|
1568
|
+
? `Run npx -y --package=${MCP_PACKAGE_SPEC} marrow-mcp setup, restart Grok, then inspect /hooks and confirm the global hooks are enabled.`
|
|
944
1569
|
: 'npx @getmarrow/install --repair';
|
|
945
1570
|
return {
|
|
946
1571
|
adapter_version: adapterVersion,
|
|
@@ -950,9 +1575,42 @@ function activationProfile(detection, plan, changes, client) {
|
|
|
950
1575
|
observed_hooks: observedHooks,
|
|
951
1576
|
evidence_authority: 'client_self_reported',
|
|
952
1577
|
coverage_verified: false,
|
|
1578
|
+
passive_live: false,
|
|
953
1579
|
configuration_complete: complete,
|
|
954
1580
|
complete,
|
|
955
1581
|
exact_fix: exactFix,
|
|
1582
|
+
...(client === 'cline' ? {
|
|
1583
|
+
hook_conflicts: clineConflicts,
|
|
1584
|
+
task_complete_support: 'coming_soon_not_configured',
|
|
1585
|
+
task_completion_closure_verified: false,
|
|
1586
|
+
enable_hooks_required: true,
|
|
1587
|
+
executable_trust_required: true,
|
|
1588
|
+
} : {}),
|
|
1589
|
+
...(client === 'windsurf' ? {
|
|
1590
|
+
restricted_mode_disables_hooks: true,
|
|
1591
|
+
restart_required: true,
|
|
1592
|
+
workspace_trust_required: true,
|
|
1593
|
+
mcp_tools: 'on_demand',
|
|
1594
|
+
} : {}),
|
|
1595
|
+
...(client === 'gemini' ? {
|
|
1596
|
+
hooks_enabled: !geminiHooksExplicitlyDisabled(geminiSettings),
|
|
1597
|
+
explicit_disable_preserved: geminiHooksExplicitlyDisabled(geminiSettings),
|
|
1598
|
+
trust_review_required: true,
|
|
1599
|
+
restart_required: true,
|
|
1600
|
+
mcp_tools: 'on_demand',
|
|
1601
|
+
session_end_delivery_claimed: false,
|
|
1602
|
+
deterministic_closeout: 'AfterAgent',
|
|
1603
|
+
} : {}),
|
|
1604
|
+
...(client === 'grok' ? {
|
|
1605
|
+
hooks_user_toggleable: true,
|
|
1606
|
+
hook_review_required: true,
|
|
1607
|
+
restart_required: true,
|
|
1608
|
+
global_hook_path: detection.paths.grokHooks,
|
|
1609
|
+
mcp_tools: 'on_demand',
|
|
1610
|
+
duplicate_session_end_configured: grokHasDuplicateSessionEnd(grokSettings),
|
|
1611
|
+
deterministic_closeout: 'Stop',
|
|
1612
|
+
governed_wrapper_fallback: 'explicit_bounded_only',
|
|
1613
|
+
} : {}),
|
|
956
1614
|
};
|
|
957
1615
|
}
|
|
958
1616
|
|
|
@@ -1075,7 +1733,74 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
|
|
|
1075
1733
|
const detected = detectedClient(detection) === entry.client
|
|
1076
1734
|
|| (entry.client === 'claude-code' && detection.claudeCode)
|
|
1077
1735
|
|| (entry.client === 'cursor' && detection.cursor)
|
|
1736
|
+
|| (entry.client === 'cline' && detection.cline)
|
|
1737
|
+
|| (entry.client === 'windsurf' && detection.windsurf)
|
|
1738
|
+
|| (entry.client === 'gemini' && detection.gemini)
|
|
1739
|
+
|| (entry.client === 'grok' && detection.grok)
|
|
1078
1740
|
|| (entry.client === 'codex' && detection.codex);
|
|
1741
|
+
const codexSettings = entry.client === 'codex' ? safeJsonObject(detection.paths.codexHooks) : null;
|
|
1742
|
+
const codexConfigured = Boolean(codexSettings
|
|
1743
|
+
&& exactHookConfigured(codexSettings, 'UserPromptSubmit', CODEX_CONTEXT_HOOK_COMMAND)
|
|
1744
|
+
&& exactHookConfigured(codexSettings, 'PreToolUse', CODEX_PRE_ACTION_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
|
|
1745
|
+
&& exactHookConfigured(codexSettings, 'PostToolUse', CODEX_ACTION_RESULT_HOOK_COMMAND, CODEX_NATIVE_HOOK_MATCHER)
|
|
1746
|
+
&& exactHookConfigured(codexSettings, 'SessionEnd', CODEX_SESSION_END_HOOK_COMMAND));
|
|
1747
|
+
const cursorSettings = ['cursor', 'composer'].includes(entry.client) ? safeJsonObject(detection.paths.cursorHooks) : null;
|
|
1748
|
+
const cursorConfigured = Boolean(cursorSettings
|
|
1749
|
+
&& exactCursorHookConfigured(cursorSettings, 'preToolUse', CURSOR_PRE_ACTION_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1750
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS, failClosed: true, async: false,
|
|
1751
|
+
})
|
|
1752
|
+
&& exactCursorHookConfigured(cursorSettings, 'postToolUse', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1753
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1754
|
+
})
|
|
1755
|
+
&& exactCursorHookConfigured(cursorSettings, 'postToolUseFailure', CURSOR_ACTION_RESULT_HOOK_COMMAND, CURSOR_NATIVE_HOOK_MATCHER, {
|
|
1756
|
+
timeout: CODEX_HOOK_TIMEOUT_SECONDS,
|
|
1757
|
+
})
|
|
1758
|
+
&& exactCursorHookConfigured(cursorSettings, 'stop', CURSOR_SESSION_END_HOOK_COMMAND, undefined, {
|
|
1759
|
+
timeout: CODEX_SESSION_TIMEOUT_SECONDS,
|
|
1760
|
+
}));
|
|
1761
|
+
const clineConfigured = entry.client === 'cline'
|
|
1762
|
+
&& clineHookContract(detection).every((hook) => exactExecutableFile(hook.path, hook.content));
|
|
1763
|
+
const windsurfSettings = entry.client === 'windsurf' ? safeJsonObject(detection.paths.windsurfHooks) : null;
|
|
1764
|
+
const windsurfConfigured = Boolean(windsurfSettings
|
|
1765
|
+
&& WINDSURF_PRE_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1766
|
+
windsurfSettings, event, WINDSURF_PRE_ACTION_HOOK_COMMAND,
|
|
1767
|
+
))
|
|
1768
|
+
&& WINDSURF_POST_EVENTS.every((event) => exactWindsurfHookConfigured(
|
|
1769
|
+
windsurfSettings, event, WINDSURF_ACTION_RESULT_HOOK_COMMAND,
|
|
1770
|
+
))
|
|
1771
|
+
&& exactWindsurfHookConfigured(
|
|
1772
|
+
windsurfSettings, 'post_cascade_response', WINDSURF_SESSION_END_HOOK_COMMAND,
|
|
1773
|
+
));
|
|
1774
|
+
const geminiSettings = entry.client === 'gemini' ? safeJsonObject(detection.paths.geminiSettings) : null;
|
|
1775
|
+
const geminiConfigured = Boolean(geminiSettings
|
|
1776
|
+
&& !geminiHooksExplicitlyDisabled(geminiSettings)
|
|
1777
|
+
&& exactGeminiHookConfigured(
|
|
1778
|
+
geminiSettings, 'BeforeTool', 'marrow-before-tool', GEMINI_PRE_ACTION_HOOK_COMMAND,
|
|
1779
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1780
|
+
)
|
|
1781
|
+
&& exactGeminiHookConfigured(
|
|
1782
|
+
geminiSettings, 'AfterTool', 'marrow-after-tool', GEMINI_ACTION_RESULT_HOOK_COMMAND,
|
|
1783
|
+
GEMINI_NATIVE_HOOK_MATCHER, GEMINI_HOOK_TIMEOUT_MS,
|
|
1784
|
+
)
|
|
1785
|
+
&& exactGeminiHookConfigured(
|
|
1786
|
+
geminiSettings, 'AfterAgent', 'marrow-after-agent', GEMINI_SESSION_END_HOOK_COMMAND,
|
|
1787
|
+
undefined, GEMINI_CLOSEOUT_TIMEOUT_MS,
|
|
1788
|
+
));
|
|
1789
|
+
const grokSettings = entry.client === 'grok' ? safeJsonObject(detection.paths.grokHooks) : null;
|
|
1790
|
+
const grokConfigured = Boolean(grokSettings
|
|
1791
|
+
&& exactGrokHookConfigured(
|
|
1792
|
+
grokSettings, 'PreToolUse', GROK_PRE_ACTION_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 7,
|
|
1793
|
+
)
|
|
1794
|
+
&& exactGrokHookConfigured(
|
|
1795
|
+
grokSettings, 'PostToolUse', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1796
|
+
)
|
|
1797
|
+
&& exactGrokHookConfigured(
|
|
1798
|
+
grokSettings, 'PostToolUseFailure', GROK_ACTION_RESULT_HOOK_COMMAND, GROK_NATIVE_HOOK_MATCHER, 5,
|
|
1799
|
+
)
|
|
1800
|
+
&& exactGrokHookConfigured(
|
|
1801
|
+
grokSettings, 'Stop', GROK_SESSION_END_HOOK_COMMAND, undefined, 3,
|
|
1802
|
+
)
|
|
1803
|
+
&& !grokHasDuplicateSessionEnd(grokSettings));
|
|
1079
1804
|
return {
|
|
1080
1805
|
client: entry.client,
|
|
1081
1806
|
capability_level: entry.capability_level,
|
|
@@ -1085,12 +1810,35 @@ function defaultHarnessInstallMatrix(detection = detectEnvironment(process.cwd()
|
|
|
1085
1810
|
mcp: true,
|
|
1086
1811
|
instructions: true,
|
|
1087
1812
|
sdk_passive_runtime: node,
|
|
1088
|
-
native_hooks: entry.capability_level === 'native_hooks' && Boolean(
|
|
1813
|
+
native_hooks: entry.capability_level === 'native_hooks' && Boolean(
|
|
1814
|
+
entry.client === 'claude-code' ? detection.claudeCode
|
|
1815
|
+
: entry.client === 'codex' ? detection.codex
|
|
1816
|
+
: ['cursor', 'composer'].includes(entry.client) ? detection.cursor
|
|
1817
|
+
: entry.client === 'cline' ? detection.cline
|
|
1818
|
+
: entry.client === 'windsurf' ? detection.windsurf
|
|
1819
|
+
: entry.client === 'gemini' ? detection.gemini
|
|
1820
|
+
: entry.client === 'grok' ? detection.grok
|
|
1821
|
+
: false,
|
|
1822
|
+
),
|
|
1089
1823
|
governed_wrapper: entry.capability_level === 'governed_wrapper',
|
|
1090
1824
|
},
|
|
1091
|
-
configured_locally:
|
|
1825
|
+
configured_locally: entry.client === 'codex' ? codexConfigured
|
|
1826
|
+
: ['cursor', 'composer'].includes(entry.client) ? cursorConfigured
|
|
1827
|
+
: entry.client === 'cline' ? clineConfigured
|
|
1828
|
+
: entry.client === 'windsurf' ? windsurfConfigured
|
|
1829
|
+
: entry.client === 'gemini' ? geminiConfigured
|
|
1830
|
+
: entry.client === 'grok' ? grokConfigured
|
|
1831
|
+
: detected && entry.automatic.length > 0,
|
|
1092
1832
|
verified_passive: false,
|
|
1093
|
-
unsupported_claim: entry.
|
|
1833
|
+
unsupported_claim: entry.client === 'cline'
|
|
1834
|
+
? 'TaskComplete is documented as coming soon and is not configured or counted as observed coverage.'
|
|
1835
|
+
: entry.client === 'windsurf'
|
|
1836
|
+
? 'Restricted Mode disables hooks; configuration requires restart and trust review and never verifies passive coverage.'
|
|
1837
|
+
: entry.client === 'gemini'
|
|
1838
|
+
? 'Project hooks require restart and project fingerprint review and approval in /hooks panel; explicit hooksConfig.enabled=false is preserved and SessionEnd delivery is not claimed.'
|
|
1839
|
+
: entry.client === 'grok'
|
|
1840
|
+
? 'Global hooks are user-toggleable; restart and /hooks inspection are required, configuration remains client-self-reported, and duplicate SessionEnd closeout is forbidden.'
|
|
1841
|
+
: entry.capability_level === 'event_contract'
|
|
1094
1842
|
? 'Needs a bounded event adapter. MCP tools remain on demand.'
|
|
1095
1843
|
: null,
|
|
1096
1844
|
};
|
|
@@ -1131,6 +1879,42 @@ function buildPlan(detection, options) {
|
|
|
1131
1879
|
transform: upsertClaudeHooks,
|
|
1132
1880
|
});
|
|
1133
1881
|
}
|
|
1882
|
+
if (detection.codex) {
|
|
1883
|
+
writes.push({
|
|
1884
|
+
type: 'json-transform',
|
|
1885
|
+
path: detection.paths.codexHooks,
|
|
1886
|
+
label: 'Codex native hooks',
|
|
1887
|
+
transform: upsertCodexHooks,
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
if (detection.cline) {
|
|
1891
|
+
for (const hook of clineHookContract(detection)) {
|
|
1892
|
+
writes.push({
|
|
1893
|
+
type: 'owned-executable',
|
|
1894
|
+
path: hook.path,
|
|
1895
|
+
label: hook.label,
|
|
1896
|
+
content: hook.content,
|
|
1897
|
+
mode: 0o755,
|
|
1898
|
+
conflict_fix: 'Move or remove the existing owner-managed Cline hook after owner review, then run npx @getmarrow/install --repair.',
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (detection.windsurf) {
|
|
1903
|
+
writes.push({
|
|
1904
|
+
type: 'json-transform',
|
|
1905
|
+
path: detection.paths.windsurfHooks,
|
|
1906
|
+
label: 'Windsurf native hooks',
|
|
1907
|
+
transform: upsertWindsurfHooks,
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
if (detection.gemini) {
|
|
1911
|
+
writes.push({
|
|
1912
|
+
type: 'json-transform',
|
|
1913
|
+
path: detection.paths.geminiSettings,
|
|
1914
|
+
label: 'Gemini CLI native hooks',
|
|
1915
|
+
transform: upsertGeminiHooks,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1134
1918
|
writes.push({
|
|
1135
1919
|
type: 'json-transform',
|
|
1136
1920
|
path: detection.paths.mcpJson,
|
|
@@ -1138,6 +1922,12 @@ function buildPlan(detection, options) {
|
|
|
1138
1922
|
transform: (filePath) => upsertMcpServerConfig(filePath, { agentId, baseUrl }),
|
|
1139
1923
|
});
|
|
1140
1924
|
if (detection.cursor) {
|
|
1925
|
+
writes.push({
|
|
1926
|
+
type: 'json-transform',
|
|
1927
|
+
path: detection.paths.cursorHooks,
|
|
1928
|
+
label: 'Cursor native hooks',
|
|
1929
|
+
transform: upsertCursorHooks,
|
|
1930
|
+
});
|
|
1141
1931
|
writes.push({
|
|
1142
1932
|
type: 'json-transform',
|
|
1143
1933
|
path: detection.paths.cursorMcp,
|
|
@@ -1229,8 +2019,10 @@ function applyPlan(plan, options) {
|
|
|
1229
2019
|
const root = path.resolve(plan.root || path.dirname(plan.writes[0].path));
|
|
1230
2020
|
for (const write of plan.writes) assertContainedManagedTarget(root, write.path);
|
|
1231
2021
|
const prepared = plan.writes.map((write) => {
|
|
2022
|
+
const fileExists = exists(write.path);
|
|
1232
2023
|
const before = safeRead(write.path);
|
|
1233
2024
|
let after;
|
|
2025
|
+
let hookConflict = false;
|
|
1234
2026
|
if (write.type === 'file') {
|
|
1235
2027
|
if (write.overwrite === false && before) {
|
|
1236
2028
|
after = before;
|
|
@@ -1241,27 +2033,43 @@ function applyPlan(plan, options) {
|
|
|
1241
2033
|
after = upsertBlock(before, write.block);
|
|
1242
2034
|
} else if (write.type === 'json-transform') {
|
|
1243
2035
|
after = write.transform(write.path);
|
|
2036
|
+
} else if (write.type === 'owned-executable') {
|
|
2037
|
+
if (fileExists && before !== write.content) {
|
|
2038
|
+
after = before;
|
|
2039
|
+
hookConflict = true;
|
|
2040
|
+
} else {
|
|
2041
|
+
after = write.content;
|
|
2042
|
+
}
|
|
1244
2043
|
} else {
|
|
1245
2044
|
throw new Error(`Unknown write type: ${write.type}`);
|
|
1246
2045
|
}
|
|
1247
2046
|
|
|
1248
|
-
|
|
2047
|
+
const beforeMode = fileExists ? fs.lstatSync(write.path).mode & 0o777 : null;
|
|
2048
|
+
const modeChanged = !hookConflict && typeof write.mode === 'number' && beforeMode !== write.mode;
|
|
2049
|
+
return { write, before, after, hookConflict, modeChanged };
|
|
1249
2050
|
});
|
|
1250
2051
|
|
|
1251
2052
|
const changes = [];
|
|
1252
|
-
for (const { write, before, after } of prepared) {
|
|
1253
|
-
const
|
|
1254
|
-
const
|
|
2053
|
+
for (const { write, before, after, hookConflict, modeChanged } of prepared) {
|
|
2054
|
+
const contentChanged = before !== after;
|
|
2055
|
+
const changed = !hookConflict && (contentChanged || modeChanged);
|
|
2056
|
+
const writeApplied = Boolean(options.yes && !options.dryRun && !options.doctor && !hookConflict);
|
|
1255
2057
|
changes.push({
|
|
1256
2058
|
path: write.path,
|
|
1257
2059
|
label: write.label,
|
|
1258
2060
|
changed,
|
|
1259
2061
|
applied: changed && writeApplied,
|
|
1260
|
-
already_present: !changed,
|
|
2062
|
+
already_present: !changed && !hookConflict,
|
|
2063
|
+
hook_conflict: hookConflict,
|
|
2064
|
+
...(hookConflict ? { exact_fix: write.conflict_fix } : {}),
|
|
1261
2065
|
});
|
|
1262
|
-
if (
|
|
2066
|
+
if (contentChanged && writeApplied) {
|
|
1263
2067
|
atomicWriteManagedFile(root, write.path, after);
|
|
1264
2068
|
}
|
|
2069
|
+
if (modeChanged && writeApplied) {
|
|
2070
|
+
assertContainedManagedTarget(root, write.path);
|
|
2071
|
+
fs.chmodSync(write.path, write.mode);
|
|
2072
|
+
}
|
|
1265
2073
|
}
|
|
1266
2074
|
return changes;
|
|
1267
2075
|
}
|
|
@@ -1836,6 +2644,9 @@ async function install(options) {
|
|
|
1836
2644
|
if (options.activate && options.selfTest === false) {
|
|
1837
2645
|
throw new Error('activate requires the server self-test');
|
|
1838
2646
|
}
|
|
2647
|
+
if (options.activate && !String(options.apiKey || '').trim()) {
|
|
2648
|
+
throw new Error('activation requires MARROW_API_KEY from the process environment or trusted secret storage');
|
|
2649
|
+
}
|
|
1839
2650
|
if (options.repair && options.yes !== true && !options.dryRun && !options.doctor) {
|
|
1840
2651
|
throw new Error('repair requires explicit write authorization (--yes)');
|
|
1841
2652
|
}
|
|
@@ -1860,6 +2671,7 @@ async function install(options) {
|
|
|
1860
2671
|
configuration_complete: changes.every((change) => change.applied || change.already_present),
|
|
1861
2672
|
evidence_authority: 'client_self_reported',
|
|
1862
2673
|
coverage_verified: false,
|
|
2674
|
+
passive_live: false,
|
|
1863
2675
|
adapter_version: profile.adapter_version,
|
|
1864
2676
|
capability_level: profile.capability_level,
|
|
1865
2677
|
config_fingerprint: profile.config_fingerprint,
|
|
@@ -1892,12 +2704,14 @@ async function install(options) {
|
|
|
1892
2704
|
const changedConfig = changes.some((change) => change.applied) || configRepairs.some((repair) => repair.changed);
|
|
1893
2705
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1894
2706
|
const controllerPlatform = options.controllerPlatform || process.platform;
|
|
2707
|
+
const localControl = localControlEvidence({ apiKey: options.apiKey, home: options.controlHome });
|
|
1895
2708
|
let controller = await controllerStatus({
|
|
1896
2709
|
root: detection.root,
|
|
1897
2710
|
agentId: options.agentId,
|
|
1898
2711
|
platform: controllerPlatform,
|
|
1899
2712
|
});
|
|
1900
2713
|
const shouldEnsureController = options.controller !== false
|
|
2714
|
+
&& localControl.enabled
|
|
1901
2715
|
&& controllerSupportedPlatform(controllerPlatform)
|
|
1902
2716
|
&& Boolean(options.apiKey)
|
|
1903
2717
|
&& selfTestPassed
|
|
@@ -1953,6 +2767,7 @@ async function install(options) {
|
|
|
1953
2767
|
python: detection.python,
|
|
1954
2768
|
claudeCode: detection.claudeCode,
|
|
1955
2769
|
cursor: detection.cursor,
|
|
2770
|
+
grok: detection.grok,
|
|
1956
2771
|
codex: detection.codex,
|
|
1957
2772
|
openclaw: detection.openclaw,
|
|
1958
2773
|
mcpConfig: detection.mcpConfig,
|
|
@@ -1991,6 +2806,7 @@ async function install(options) {
|
|
|
1991
2806
|
configRepairs,
|
|
1992
2807
|
sdkDependency,
|
|
1993
2808
|
controller,
|
|
2809
|
+
local_control: localControl,
|
|
1994
2810
|
selfTest,
|
|
1995
2811
|
warnings: [
|
|
1996
2812
|
...(options.keyFromArg
|
|
@@ -2038,6 +2854,17 @@ module.exports = {
|
|
|
2038
2854
|
stableAgentId,
|
|
2039
2855
|
activationProfile,
|
|
2040
2856
|
claudeNativeHookFingerprint,
|
|
2857
|
+
codexNativeHookFingerprint,
|
|
2858
|
+
cursorNativeHookFingerprint,
|
|
2859
|
+
clineNativeHookFingerprint,
|
|
2860
|
+
windsurfNativeHookFingerprint,
|
|
2861
|
+
geminiNativeHookFingerprint,
|
|
2862
|
+
grokNativeHookFingerprint,
|
|
2863
|
+
GROK_CONTEXT_HOOK_COMMAND,
|
|
2864
|
+
GROK_PRE_ACTION_HOOK_COMMAND,
|
|
2865
|
+
GROK_ACTION_RESULT_HOOK_COMMAND,
|
|
2866
|
+
GROK_SESSION_END_HOOK_COMMAND,
|
|
2867
|
+
GROK_NATIVE_HOOK_MATCHER,
|
|
2041
2868
|
printReport,
|
|
2042
2869
|
ADAPTER_PROVENANCE,
|
|
2043
2870
|
HARNESS_CAPABILITY_REGISTRY,
|