create-merlin-brain 3.4.7 → 3.4.9

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/bin/install.cjs CHANGED
@@ -176,14 +176,18 @@ function ensureDir(dir) {
176
176
  }
177
177
  }
178
178
 
179
- async function promptApiKey() {
179
+ async function promptApiKey(hasExisting) {
180
180
  const rl = readline.createInterface({
181
181
  input: process.stdin,
182
182
  output: process.stdout,
183
183
  });
184
184
 
185
+ const prompt = hasExisting
186
+ ? `\n${colors.cyan}?${colors.reset} Merlin Sights API key (Enter to keep existing, or paste new): `
187
+ : `\n${colors.cyan}?${colors.reset} Merlin Sights API key (get one at ${colors.cyan}merlin.build${colors.reset}, Enter to skip): `;
188
+
185
189
  return new Promise((resolve) => {
186
- rl.question(`\n${colors.cyan}?${colors.reset} Merlin Sights API key (press Enter to skip): `, (answer) => {
190
+ rl.question(prompt, (answer) => {
187
191
  rl.close();
188
192
  resolve(answer.trim());
189
193
  });
@@ -272,9 +276,9 @@ merlin-loop() {
272
276
  if (rcContent.includes('# Merlin Brain - The AI Brain') ||
273
277
  rcContent.includes('# Merlin Brain - Makes Claude Code') ||
274
278
  rcContent.includes('# Merlin Brain -')) {
275
- // Remove old blocks
276
- rcContent = rcContent.replace(/\n# ═+\n# Merlin Brain[\s\S]*?alias cc="claude"\n?/g, '');
277
- rcContent = rcContent.replace(/\n# Merlin Brain - Makes Claude Code[\s\S]*?alias cc='claude'\n?/g, '');
279
+ // Remove old blocks (capture full block including trailing merlin-loop if present)
280
+ rcContent = rcContent.replace(/\n# ═+\n# Merlin Brain[\s\S]*?alias cc="claude"\n?([\s\S]*?merlin-loop\(\) \{[\s\S]*?\n\}\n)?/g, '');
281
+ rcContent = rcContent.replace(/\n# Merlin Brain - Makes Claude Code[\s\S]*?alias cc='claude'\n?([\s\S]*?merlin-loop\(\) \{[\s\S]*?\n\}\n)?/g, '');
278
282
  rcContent = rcContent.replace(/\n# Merlin - Claude Code with[\s\S]*?alias cc=.*\n?/g, '');
279
283
  fs.writeFileSync(rcFile, rcContent);
280
284
  logSuccess('Updating existing Merlin shell integration');
@@ -302,7 +306,7 @@ function cleanupLegacy() {
302
306
  const content = fs.readFileSync(claudeMdPath, 'utf8');
303
307
  if (content.includes('ccwiki_') || content.includes('briefed')) {
304
308
  fs.unlinkSync(claudeMdPath);
305
- cleaned.push('~/.claude/CLAUDE.md (had old naming)');
309
+ cleaned.push('Removed ~/.claude/CLAUDE.md (had old naming)');
306
310
  }
307
311
  }
308
312
 
@@ -324,20 +328,27 @@ function cleanupLegacy() {
324
328
  let settings = JSON.parse(content);
325
329
 
326
330
  // Remove broken hooks (contain $HOME, gsd-, or old paths)
331
+ // Iterate ALL hook types, handle both old and new format
327
332
  if (settings.hooks) {
328
- const hookTypes = ['PreToolUse', 'PostToolUse', 'Stop', 'Start'];
329
- for (const hookType of hookTypes) {
333
+ for (const hookType of Object.keys(settings.hooks)) {
330
334
  if (Array.isArray(settings.hooks[hookType])) {
331
335
  const originalLength = settings.hooks[hookType].length;
332
- settings.hooks[hookType] = settings.hooks[hookType].filter(hook => {
333
- // Remove hooks with $HOME (doesn't work on Windows)
334
- // Remove hooks with gsd- (old Get Shit Done)
335
- // Remove hooks referencing non-existent files
336
- const hookPath = typeof hook === 'string' ? hook : hook?.command || hook?.path || '';
337
- if (hookPath.includes('$HOME') ||
338
- hookPath.includes('gsd-') ||
339
- hookPath.includes('ccwiki') ||
340
- hookPath.includes('briefed')) {
336
+ settings.hooks[hookType] = settings.hooks[hookType].filter(entry => {
337
+ // Extract command paths from both old and new format
338
+ let hookPaths = '';
339
+ if (entry.command) {
340
+ // Old format: { type, command, ... }
341
+ hookPaths = entry.command;
342
+ } else if (entry.hooks && Array.isArray(entry.hooks)) {
343
+ // New format: { matcher?, hooks: [{ type, command }] }
344
+ hookPaths = entry.hooks.map(h => h.command || h.path || '').join(' ');
345
+ } else if (typeof entry === 'string') {
346
+ hookPaths = entry;
347
+ }
348
+ if (hookPaths.includes('$HOME') ||
349
+ hookPaths.includes('gsd-') ||
350
+ hookPaths.includes('ccwiki') ||
351
+ hookPaths.includes('briefed')) {
341
352
  return false;
342
353
  }
343
354
  return true;
@@ -359,13 +370,13 @@ function cleanupLegacy() {
359
370
 
360
371
  if (modified) {
361
372
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
362
- cleaned.push('~/.claude/settings.local.json (removed old tools and broken hooks)');
373
+ cleaned.push('Cleaned ~/.claude/settings.local.json (old tools and broken hooks)');
363
374
  }
364
375
  } catch (jsonErr) {
365
376
  // If JSON parsing fails, just do string replacement
366
377
  if (modified) {
367
378
  fs.writeFileSync(settingsPath, content);
368
- cleaned.push('~/.claude/settings.local.json (removed old tools)');
379
+ cleaned.push('Cleaned ~/.claude/settings.local.json (old tools)');
369
380
  }
370
381
  }
371
382
  } catch (e) { /* ignore */ }
@@ -379,7 +390,7 @@ function cleanupLegacy() {
379
390
  // Remove old gsd-* hook files
380
391
  if (file.startsWith('gsd-') || file.includes('ccwiki') || file.includes('briefed')) {
381
392
  removeFile(path.join(hooksDir, file));
382
- cleaned.push(`~/.claude/hooks/${file}`);
393
+ cleaned.push(`Removed ~/.claude/hooks/${file}`);
383
394
  }
384
395
  }
385
396
  }
@@ -424,7 +435,7 @@ function cleanupLegacy() {
424
435
 
425
436
  if (modified) {
426
437
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
427
- cleaned.push('~/.claude/config.json (migrated to global binary)');
438
+ cleaned.push('Migrated ~/.claude/config.json (to global binary)');
428
439
  }
429
440
  } catch (e) { /* ignore */ }
430
441
  }
@@ -470,7 +481,7 @@ function cleanupLegacy() {
470
481
 
471
482
  if (modified) {
472
483
  fs.writeFileSync(desktopConfigPath, JSON.stringify(config, null, 2));
473
- cleaned.push('~/.claude/claude_desktop_config.json (migrated to global binary)');
484
+ cleaned.push('Migrated ~/.claude/claude_desktop_config.json (to global binary)');
474
485
  }
475
486
  } catch (e) { /* ignore */ }
476
487
  }
@@ -513,7 +524,7 @@ function cleanupLegacy() {
513
524
 
514
525
  for (const dir of dirsToRemove) {
515
526
  if (removeDirRecursive(dir)) {
516
- cleaned.push(dir.replace(os.homedir(), '~'));
527
+ cleaned.push('Removed ' + dir.replace(os.homedir(), '~'));
517
528
  }
518
529
  }
519
530
 
@@ -543,7 +554,7 @@ function cleanupLegacy() {
543
554
 
544
555
  for (const file of filesToRemove) {
545
556
  if (removeFile(file)) {
546
- cleaned.push(file.replace(os.homedir(), '~'));
557
+ cleaned.push('Removed ' + file.replace(os.homedir(), '~'));
547
558
  }
548
559
  }
549
560
 
@@ -565,7 +576,7 @@ function cleanupLegacy() {
565
576
  try {
566
577
  // Reset to empty object so Merlin auto-detects from git remote
567
578
  fs.writeFileSync(merlinConfigPath, '{}');
568
- cleaned.push('~/.merlin/config.json (reset for auto-detect)');
579
+ cleaned.push('Reset ~/.merlin/config.json (for auto-detect)');
569
580
  } catch (e) { /* ignore */ }
570
581
  }
571
582
 
@@ -586,7 +597,7 @@ function cleanupLegacy() {
586
597
  claudeJson.mcpServers.merlin.command = 'merlin-brain';
587
598
  delete claudeJson.mcpServers.merlin.args;
588
599
  modified = true;
589
- cleaned.push('~/.claude.json (migrated to global binary)');
600
+ cleaned.push('Migrated ~/.claude.json (to global binary)');
590
601
  }
591
602
  }
592
603
 
@@ -594,7 +605,7 @@ function cleanupLegacy() {
594
605
  if (claudeJson.mcpServers?.briefed) {
595
606
  delete claudeJson.mcpServers.briefed;
596
607
  modified = true;
597
- cleaned.push('~/.claude.json (removed deprecated briefed server)');
608
+ cleaned.push('Removed deprecated briefed server from ~/.claude.json');
598
609
  }
599
610
 
600
611
  if (modified) {
@@ -622,7 +633,7 @@ function cleanupLegacy() {
622
633
  const pkgJsonPath2 = path.join(entryPath, 'node_modules', 'create-merlin-pro', 'package.json');
623
634
  if (fs.existsSync(pkgJsonPath) || fs.existsSync(pkgJsonPath2)) {
624
635
  removeDirRecursive(entryPath);
625
- cleaned.push('~/.npm/_npx/' + entry.name + ' (old npx cache)');
636
+ cleaned.push('Removed ~/.npm/_npx/' + entry.name + ' (old npx cache)');
626
637
  }
627
638
  }
628
639
  }
@@ -639,12 +650,12 @@ function cleanupLegacy() {
639
650
  // Remove old gsd-*.md and ccwiki-*.md agents
640
651
  if ((file.startsWith('gsd-') || file.startsWith('ccwiki-')) && file.endsWith('.md')) {
641
652
  removeFile(path.join(agentsDir, file));
642
- cleaned.push(`~/.claude/agents/${file}`);
653
+ cleaned.push(`Removed ~/.claude/agents/${file}`);
643
654
  }
644
655
  // Remove backup files in agents
645
656
  if (file.endsWith('.backup') || file.endsWith('.bak') || file.endsWith('.old') || file.endsWith('~')) {
646
657
  removeFile(path.join(agentsDir, file));
647
- cleaned.push(`~/.claude/agents/${file}`);
658
+ cleaned.push(`Removed ~/.claude/agents/${file}`);
648
659
  }
649
660
  }
650
661
  }
@@ -709,7 +720,7 @@ function cleanupLegacy() {
709
720
 
710
721
  if (modified) {
711
722
  fs.writeFileSync(rcFile, content);
712
- cleaned.push(rcFile.replace(os.homedir(), '~') + ' (removed old integrations)');
723
+ cleaned.push('Updated ' + rcFile.replace(os.homedir(), '~') + ' (removed old integrations)');
713
724
  }
714
725
  }
715
726
  }
@@ -734,7 +745,7 @@ async function install() {
734
745
  const cleaned = cleanupLegacy();
735
746
  if (cleaned.length > 0) {
736
747
  for (const item of cleaned) {
737
- logSuccess(`Removed ${item}`);
748
+ logSuccess(item);
738
749
  }
739
750
  } else {
740
751
  logSuccess('No legacy artifacts found');
@@ -890,60 +901,83 @@ async function install() {
890
901
  }
891
902
  settings.hooks = settings.hooks || {};
892
903
 
893
- const addHookIfMissing = (hookArray, hookConfig) => {
894
- const exists = hookArray.some(h => h.command === hookConfig.command);
895
- if (!exists) hookArray.push(hookConfig);
904
+ // ═══════════════════════════════════════════════════════════════
905
+ // MIGRATE old-format hooks to new format (matcher + hooks array)
906
+ // Old: { type, command, matcher?, prompt? }
907
+ // New: { matcher?, hooks: [{ type, command, prompt? }] }
908
+ // ═══════════════════════════════════════════════════════════════
909
+ for (const [eventName, entries] of Object.entries(settings.hooks)) {
910
+ if (!Array.isArray(entries)) continue;
911
+ settings.hooks[eventName] = entries.map(entry => {
912
+ // Already in new format (has hooks array)
913
+ if (entry.hooks && Array.isArray(entry.hooks)) return entry;
914
+ // Old format → convert to new format
915
+ const handler = {};
916
+ if (entry.type) handler.type = entry.type;
917
+ if (entry.command) handler.command = entry.command;
918
+ if (entry.prompt) handler.prompt = entry.prompt;
919
+ if (entry.timeout !== undefined) handler.timeout = entry.timeout;
920
+ if (entry.async !== undefined) handler.async = entry.async;
921
+ if (entry.statusMessage) handler.statusMessage = entry.statusMessage;
922
+ const newEntry = {};
923
+ if (entry.matcher) newEntry.matcher = entry.matcher;
924
+ newEntry.hooks = [handler];
925
+ return newEntry;
926
+ });
927
+ }
928
+
929
+ // Helper: check if a command hook already exists in an array (new format)
930
+ const addHookIfMissing = (hookArray, matcherGroup) => {
931
+ const cmd = matcherGroup.hooks?.[0]?.command;
932
+ if (!cmd) { hookArray.push(matcherGroup); return hookArray; }
933
+ const exists = hookArray.some(entry =>
934
+ entry.hooks?.some(h => h.command === cmd)
935
+ );
936
+ if (!exists) hookArray.push(matcherGroup);
896
937
  return hookArray;
897
938
  };
898
939
 
899
- // SessionStart hook
940
+ // SessionStart hooks
900
941
  settings.hooks.SessionStart = settings.hooks.SessionStart || [];
901
942
  addHookIfMissing(settings.hooks.SessionStart, {
902
- type: 'command',
903
- command: 'bash ~/.claude/hooks/session-start.sh'
943
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-start.sh' }]
904
944
  });
905
945
 
906
946
  // SessionStart hook: Agent sync (background, runs at most once/hour)
907
947
  addHookIfMissing(settings.hooks.SessionStart, {
908
- type: 'command',
909
- command: 'bash ~/.claude/hooks/agent-sync.sh'
948
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/agent-sync.sh' }]
910
949
  });
911
950
 
912
951
  // PreToolUse hook (Edit/Write only)
913
952
  settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
914
953
  addHookIfMissing(settings.hooks.PreToolUse, {
915
- type: 'command',
916
- command: 'bash ~/.claude/hooks/pre-edit-sights-check.sh',
917
- matcher: 'Edit|Write'
954
+ matcher: 'Edit|Write',
955
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/pre-edit-sights-check.sh' }]
918
956
  });
919
957
 
920
958
  // PostToolUse hook (Edit/Write only)
921
959
  settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
922
960
  addHookIfMissing(settings.hooks.PostToolUse, {
923
- type: 'command',
924
- command: 'bash ~/.claude/hooks/post-edit-logger.sh',
925
- matcher: 'Edit|Write'
961
+ matcher: 'Edit|Write',
962
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/post-edit-logger.sh' }]
926
963
  });
927
964
 
928
965
  // Stop hook (session end)
929
966
  settings.hooks.Stop = settings.hooks.Stop || [];
930
967
  addHookIfMissing(settings.hooks.Stop, {
931
- type: 'command',
932
- command: 'bash ~/.claude/hooks/session-end.sh'
968
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end.sh' }]
933
969
  });
934
970
 
935
971
  // TeammateIdle hook (Agent Teams quality gate — only fires when Teams active)
936
972
  settings.hooks.TeammateIdle = settings.hooks.TeammateIdle || [];
937
973
  addHookIfMissing(settings.hooks.TeammateIdle, {
938
- type: 'command',
939
- command: 'bash ~/.claude/hooks/teammate-idle-verify.sh'
974
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/teammate-idle-verify.sh' }]
940
975
  });
941
976
 
942
977
  // SubagentStart hook (Agent Teams context injection — only fires when Teams active)
943
978
  settings.hooks.SubagentStart = settings.hooks.SubagentStart || [];
944
979
  addHookIfMissing(settings.hooks.SubagentStart, {
945
- type: 'command',
946
- command: 'bash ~/.claude/hooks/subagent-context.sh'
980
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/subagent-context.sh' }]
947
981
  });
948
982
 
949
983
  // --- Prompt-based hooks (LLM evaluates .md content) ---
@@ -951,11 +985,15 @@ async function install() {
951
985
  // first (identified by containing 'merlin' or 'Merlin' in their prompt text),
952
986
  // then re-add fresh ones. This prevents duplicate hooks across version upgrades.
953
987
  const removeMerlinPromptHooks = (hookArray) => {
954
- return hookArray.filter(h => {
955
- if (h.type !== 'prompt') return true; // keep non-prompt hooks
956
- const prompt = (h.prompt || '').toLowerCase();
957
- // Merlin prompt hooks contain these markers
958
- return !(prompt.includes('merlin') || prompt.includes('sights'));
988
+ return hookArray.filter(entry => {
989
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return true;
990
+ // Remove entry if ALL its hooks are Merlin prompt hooks
991
+ const allMerlinPrompts = entry.hooks.every(h => {
992
+ if (h.type !== 'prompt') return false;
993
+ const prompt = (h.prompt || '').toLowerCase();
994
+ return prompt.includes('merlin') || prompt.includes('sights');
995
+ });
996
+ return !allMerlinPrompts;
959
997
  });
960
998
  };
961
999
 
@@ -964,34 +1002,31 @@ async function install() {
964
1002
  settings.hooks.Stop = removeMerlinPromptHooks(settings.hooks.Stop);
965
1003
  settings.hooks.Notification = settings.hooks.Notification || [];
966
1004
  settings.hooks.Notification = removeMerlinPromptHooks(settings.hooks.Notification);
1005
+ settings.hooks.TaskCompleted = settings.hooks.TaskCompleted || [];
1006
+ settings.hooks.TaskCompleted = removeMerlinPromptHooks(settings.hooks.TaskCompleted);
967
1007
 
968
- // Now add fresh prompt hooks
969
- // Prompt-based SessionStart hook: FULL Merlin boot sequence
970
- // This is the key hook that makes every session feel like Loop
971
- settings.hooks.SessionStart.push({
972
- type: 'prompt',
973
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'session-start-boot.md'), 'utf8')
1008
+ // Now add fresh prompt/command hooks in new format
1009
+ // NOTE: SessionStart does NOT support prompt hooks (only command hooks).
1010
+ // The Merlin boot sequence is handled via CLAUDE.md instructions instead.
1011
+ // We use a command hook to inject additionalContext at session start.
1012
+ addHookIfMissing(settings.hooks.SessionStart, {
1013
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-start-context.sh' }]
974
1014
  });
975
1015
 
976
- // Prompt-based PreToolUse hook: enforce Sights check before edits
977
- // This is the Sights guarantee — no edit without context
1016
+ // Prompt-based PreToolUse hook: evaluate Sights check before edits
978
1017
  settings.hooks.PreToolUse.push({
979
- type: 'prompt',
980
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8'),
981
- matcher: 'Edit|Write'
1018
+ matcher: 'Edit|Write',
1019
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'pre-edit-sights-enforce.md'), 'utf8') }]
982
1020
  });
983
1021
 
984
- // Prompt-based Stop hook: quality gate before session ends
1022
+ // Prompt-based Stop hook: evaluate whether stopping is appropriate
985
1023
  settings.hooks.Stop.push({
986
- type: 'prompt',
987
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8')
1024
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'stop-check.md'), 'utf8') }]
988
1025
  });
989
1026
 
990
- // Prompt-based Notification hook: verify task completion quality
991
- settings.hooks.Notification.push({
992
- type: 'prompt',
993
- prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8'),
994
- matcher: 'task_completed'
1027
+ // Task completion verification hook (using dedicated TaskCompleted event)
1028
+ settings.hooks.TaskCompleted.push({
1029
+ hooks: [{ type: 'prompt', prompt: fs.readFileSync(path.join(HOOKS_DIR, 'task-completed-verify.md'), 'utf8') }]
995
1030
  });
996
1031
 
997
1032
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
@@ -1031,11 +1066,15 @@ async function install() {
1031
1066
 
1032
1067
  let apiKey = existingApiKey;
1033
1068
  if (existingApiKey) {
1034
- logSuccess(`Sights API key already configured (skipping prompt)`);
1069
+ const masked = existingApiKey.slice(0, 8) + '...' + existingApiKey.slice(-4);
1070
+ console.log(` Existing API key found: ${colors.cyan}${masked}${colors.reset}`);
1035
1071
  } else {
1036
- console.log(`Merlin Sights provides instant codebase context.`);
1037
- console.log(`Get your API key at: ${colors.cyan}https://merlin.build${colors.reset}`);
1038
- apiKey = await promptApiKey();
1072
+ console.log(` Merlin Sights provides instant codebase context.`);
1073
+ console.log(` Get your API key at: ${colors.cyan}https://merlin.build${colors.reset}`);
1074
+ }
1075
+ const inputKey = await promptApiKey(!!existingApiKey);
1076
+ if (inputKey) {
1077
+ apiKey = inputKey;
1039
1078
  }
1040
1079
 
1041
1080
  if (apiKey) {
@@ -2,19 +2,19 @@
2
2
  # Merlin Hook: Agent Sync (SessionStart, background)
3
3
  # Checks installed agents freshness and updates from cloud. Max once/hour.
4
4
  set -euo pipefail
5
- trap 'exit 0' ERR
5
+ trap 'echo "{}"; exit 0' ERR
6
6
 
7
7
  AGENTS_DIR="${HOME}/.claude/agents"
8
8
  MERLIN_DIR="${HOME}/.claude/merlin"
9
9
  LAST_SYNC="${MERLIN_DIR}/.last-agent-sync"
10
10
  API_URL="${MERLIN_API_URL:-https://api.merlin.build}"
11
11
 
12
- [ -d "${AGENTS_DIR}" ] || exit 0
12
+ [ -d "${AGENTS_DIR}" ] || { echo '{}'; exit 0; }
13
13
 
14
14
  # Skip if synced within the last hour
15
15
  if [ -f "${LAST_SYNC}" ]; then
16
16
  last=$(cat "${LAST_SYNC}" 2>/dev/null || echo "0")
17
- [ $(($(date +%s) - last)) -lt 3600 ] && exit 0
17
+ [ $(($(date +%s) - last)) -lt 3600 ] && { echo '{}'; exit 0; }
18
18
  fi
19
19
 
20
20
  sync_agents() {
@@ -41,4 +41,7 @@ sync_agents() {
41
41
  }
42
42
 
43
43
  sync_agents &
44
+
45
+ # Claude Code command hooks must output valid JSON to stdout
46
+ echo '{}'
44
47
  exit 0
@@ -5,7 +5,7 @@
5
5
  # Always exits 0 — never blocks Claude Code.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -30,4 +30,6 @@ fi
30
30
  # Log file change event
31
31
  log_event "file_changed" "$(printf '{"file":"%s","tool":"%s"}' "${file_path:-unknown}" "${tool_name:-unknown}")"
32
32
 
33
+ # Claude Code command hooks must output valid JSON to stdout
34
+ echo '{}'
33
35
  exit 0
@@ -5,7 +5,7 @@
5
5
  # Advisory only — always exits 0, never blocks edits.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -35,4 +35,6 @@ fi
35
35
  # Log the pre-edit event
36
36
  log_event "pre_edit" "$(printf '{"file":"%s"}' "${file_path:-unknown}")"
37
37
 
38
+ # Claude Code command hooks must output valid JSON to stdout
39
+ echo '{}'
38
40
  exit 0
@@ -1,25 +1,12 @@
1
- # Merlin Sights Enforcement Before Every Edit
1
+ You are evaluating whether a file edit should proceed. Context: $ARGUMENTS
2
2
 
3
- STOP. You are about to modify a file. Merlin requires a Sights check first.
3
+ Check: Has the assistant recently called merlin_get_context, merlin_find_files, or merlin_search for this file's area in the current conversation? Look at recent tool calls in the transcript.
4
4
 
5
- ## Required: Context Check
6
- If you have NOT called `merlin_get_context` for this file's area in the last 2 minutes:
7
- 1. Call `merlin_get_context("brief description of what you're changing and why")`
8
- 2. Review what Sights returns existing patterns, related files, conventions
9
- 3. Check if similar code already exists (don't duplicate)
10
- 4. THEN proceed with the edit
5
+ Rules:
6
+ - If Sights was consulted recently (within the last few messages) for this area: allow the edit
7
+ - If this is a minor/trivial edit (formatting, typo fix, small change): allow the edit
8
+ - If this is a significant edit and Sights was NOT consulted: suggest checking first
11
9
 
12
- ## Required: Convention Check
13
- Follow ALL rules from Sights:
14
- - Max file size (usually 400 lines) if approaching, split the file
15
- - Naming conventions — match existing patterns
16
- - Testing requirements — note if tests are needed
17
-
18
- ## Required: Show Your Work
19
- After the Sights check, show a brief line so the user sees Merlin is involved:
20
- ```
21
- Merlin | Checked [area] | [what you found or "clear to edit"]
22
- ```
23
-
24
- ## Never Skip This
25
- Every edit should be informed by codebase knowledge. This is what makes Merlin sessions better than raw Claude sessions. The user chose Merlin for a reason — deliver on it.
10
+ Respond with ONLY valid JSON, no other text:
11
+ - Allow the edit: {"ok": true}
12
+ - Suggest Sights check first: {"ok": false, "reason": "Call merlin_get_context before editing this file to check for existing patterns and conventions"}
@@ -5,7 +5,7 @@
5
5
  # Always exits 0 — never blocks Claude Code shutdown.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -24,4 +24,6 @@ if command -v merlin >/dev/null 2>&1; then
24
24
  merlin save-checkpoint "Session ended" >/dev/null 2>&1 &
25
25
  fi
26
26
 
27
+ # Claude Code command hooks must output valid JSON to stdout
28
+ echo '{}'
27
29
  exit 0
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Merlin Hook: SessionStart (context injection)
4
+ # Injects Merlin identity context into the session via additionalContext.
5
+ # Uses the SessionStart command hook JSON output format.
6
+ # Always exits 0 — never blocks Claude Code startup.
7
+ #
8
+ set -euo pipefail
9
+ trap 'echo "{}"; exit 0' ERR
10
+
11
+ # Output additionalContext JSON for Claude to see at session start.
12
+ # This reminds Claude it's a Merlin-powered session.
13
+ # Full boot instructions are in CLAUDE.md — this is a lightweight nudge.
14
+ cat <<'CONTEXT_JSON'
15
+ {
16
+ "hookSpecificOutput": {
17
+ "hookEventName": "SessionStart",
18
+ "additionalContext": "You are a Merlin-powered session. Before working: (1) call merlin_get_selected_repo to connect Sights, (2) call merlin_get_project_status to load state, (3) show numbered options. Check Sights before every edit. Route complex tasks to specialists via /merlin:route. Save checkpoints before stopping."
19
+ }
20
+ }
21
+ CONTEXT_JSON
22
+
23
+ exit 0
@@ -5,7 +5,7 @@
5
5
  # Always exits 0 — never blocks Claude Code startup.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -28,4 +28,6 @@ if command -v merlin >/dev/null 2>&1; then
28
28
  record_sights_call
29
29
  fi
30
30
 
31
+ # Claude Code command hooks must output valid JSON to stdout
32
+ echo '{}'
31
33
  exit 0
@@ -1,36 +1,13 @@
1
- # Merlin Session End Protocol Before Stopping
1
+ You are evaluating whether Claude should stop working. Analyze the conversation context: $ARGUMENTS
2
2
 
3
- Before this session ends, complete this checklist:
3
+ Check these criteria:
4
+ 1. Did the user's most recent request get fully addressed?
5
+ 2. Are there any obvious errors or broken code that should be fixed before stopping?
6
+ 3. Did Claude mention follow-up tasks it intended to complete but hasn't done yet?
4
7
 
5
- ## 1. Uncommitted Work
6
- Check `git status`. If there are uncommitted changes:
7
- - Stage and commit with a clear message
8
- - Run `merlin_run_verification` before committing
8
+ IMPORTANT: This is NOT about saving checkpoints or showing summaries — those are handled elsewhere.
9
+ Only evaluate whether the core work is done.
9
10
 
10
- ## 2. Save Checkpoint
11
- Call `merlin_save_checkpoint` with:
12
- - What was accomplished this session
13
- - What's left to do
14
- - Any blockers or decisions made
15
- This lets the next session (or another agent) pick up exactly where you left off.
16
-
17
- ## 3. Session Summary
18
- Show the user a brief summary:
19
- ```
20
- Merlin | Session Complete
21
- - [X] files changed
22
- - [Y] Sights queries made
23
- - [Z] tasks completed
24
- - Checkpoint saved: [yes/no]
25
- ```
26
-
27
- ## 4. Next Steps
28
- Always end with what should happen next:
29
- ```
30
- Next session:
31
- [1] Continue with [next task]
32
- [2] Review what was built
33
- [3] [other relevant option]
34
- ```
35
-
36
- Do NOT stop without saving checkpoint. The user trusts Merlin to maintain continuity across sessions.
11
+ Respond with ONLY valid JSON, no other text:
12
+ - If stopping is appropriate: {"ok": true}
13
+ - If Claude should continue: {"ok": false, "reason": "brief explanation of what's unfinished"}
@@ -5,7 +5,7 @@
5
5
  # Advisory only — always exits 0, never blocks subagent startup.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -28,9 +28,21 @@ fi
28
28
  # Log subagent start event
29
29
  log_event "subagent_start" "$(printf '{"task":"%s"}' "${task_desc:-unknown}")"
30
30
 
31
- # If Merlin CLI available, fetch and output context for the task
31
+ # If Merlin CLI available, fetch context for the task and output as JSON
32
32
  if [ -n "$task_desc" ] && command -v merlin >/dev/null 2>&1; then
33
- merlin context "$task_desc" 2>/dev/null || true
33
+ context_text=$(merlin context "$task_desc" 2>/dev/null || true)
34
+ if [ -n "$context_text" ]; then
35
+ # Output as proper JSON additionalContext for the subagent
36
+ # Use jq if available, otherwise output empty JSON (context is best-effort)
37
+ if command -v jq >/dev/null 2>&1; then
38
+ jq -n --arg ctx "$context_text" '{hookSpecificOutput:{hookEventName:"SubagentStart",additionalContext:$ctx}}'
39
+ else
40
+ echo '{}'
41
+ fi
42
+ exit 0
43
+ fi
34
44
  fi
35
45
 
46
+ # Claude Code command hooks must output valid JSON to stdout
47
+ echo '{}'
36
48
  exit 0
@@ -1,14 +1,13 @@
1
- # Task Completion Verification - Merlin Quality Gate
1
+ You are evaluating whether a task should be marked as completed. Context: $ARGUMENTS
2
2
 
3
- Before marking this task as complete, verify:
3
+ Check these criteria:
4
+ 1. Was the task's stated objective accomplished based on the conversation?
5
+ 2. Were there any errors or test failures mentioned that haven't been resolved?
6
+ 3. Is the implementation reasonably complete (not a half-finished skeleton)?
4
7
 
5
- 1. **Code Quality**: Does the implementation follow the project's coding conventions? Check with merlin_get_conventions if unsure.
8
+ Be lenient don't block completion for minor issues like missing docs or style nits.
9
+ Only block if the core objective clearly wasn't met or there are unresolved errors.
6
10
 
7
- 2. **Sights Alignment**: Is the code consistent with existing patterns? Verify with merlin_get_context for the modified areas.
8
-
9
- 3. **No Regressions**: Run the project's test suite if it exists. Check build passes.
10
-
11
- 4. **Documentation**: If the task added new APIs or features, are they documented?
12
-
13
- If all verifications pass, allow task completion.
14
- If issues are found, list them and suggest fixes before completing.
11
+ Respond with ONLY valid JSON, no other text:
12
+ - Task is complete: {"ok": true}
13
+ - Task is not complete: {"ok": false, "reason": "brief explanation of what's unfinished"}
@@ -5,7 +5,7 @@
5
5
  # Informational only — always exits 0 for v1.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -43,4 +43,6 @@ if [ -f "tsconfig.json" ] && command -v npx >/dev/null 2>&1; then
43
43
  fi
44
44
  fi
45
45
 
46
+ # Claude Code command hooks must output valid JSON to stdout
47
+ echo '{}'
46
48
  exit 0
@@ -5,7 +5,7 @@
5
5
  # Advisory only — always exits 0, never blocks teammates.
6
6
  #
7
7
  set -euo pipefail
8
- trap 'exit 0' ERR
8
+ trap 'echo "{}"; exit 0' ERR
9
9
 
10
10
  HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
 
@@ -38,4 +38,6 @@ if command -v npm >/dev/null 2>&1; then
38
38
  npm run build --if-present >/dev/null 2>&1 || true
39
39
  fi
40
40
 
41
+ # Claude Code command hooks must output valid JSON to stdout
42
+ echo '{}'
41
43
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-merlin-brain",
3
- "version": "3.4.7",
3
+ "version": "3.4.9",
4
4
  "description": "Merlin - The Ultimate AI Brain for Claude Code. One install: workflows, agents, loop, and Sights MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",