create-merlin-brain 3.10.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/bin/install.cjs +146 -22
  2. package/bin/runtime-adapters.cjs +396 -0
  3. package/dist/server/cost/tracker.d.ts +38 -2
  4. package/dist/server/cost/tracker.d.ts.map +1 -1
  5. package/dist/server/cost/tracker.js +87 -15
  6. package/dist/server/cost/tracker.js.map +1 -1
  7. package/dist/server/server.d.ts.map +1 -1
  8. package/dist/server/server.js +74 -30
  9. package/dist/server/server.js.map +1 -1
  10. package/dist/server/tools/adaptive.js +1 -1
  11. package/dist/server/tools/adaptive.js.map +1 -1
  12. package/dist/server/tools/agents-index.js +3 -3
  13. package/dist/server/tools/agents-index.js.map +1 -1
  14. package/dist/server/tools/agents.js +5 -5
  15. package/dist/server/tools/agents.js.map +1 -1
  16. package/dist/server/tools/behaviors.js +4 -4
  17. package/dist/server/tools/behaviors.js.map +1 -1
  18. package/dist/server/tools/context.js +7 -7
  19. package/dist/server/tools/context.js.map +1 -1
  20. package/dist/server/tools/cost.d.ts +3 -1
  21. package/dist/server/tools/cost.d.ts.map +1 -1
  22. package/dist/server/tools/cost.js +66 -13
  23. package/dist/server/tools/cost.js.map +1 -1
  24. package/dist/server/tools/discoveries.js +6 -6
  25. package/dist/server/tools/discoveries.js.map +1 -1
  26. package/dist/server/tools/index.d.ts +4 -0
  27. package/dist/server/tools/index.d.ts.map +1 -1
  28. package/dist/server/tools/index.js +4 -0
  29. package/dist/server/tools/index.js.map +1 -1
  30. package/dist/server/tools/learning.d.ts +12 -0
  31. package/dist/server/tools/learning.d.ts.map +1 -0
  32. package/dist/server/tools/learning.js +269 -0
  33. package/dist/server/tools/learning.js.map +1 -0
  34. package/dist/server/tools/project.js +7 -7
  35. package/dist/server/tools/project.js.map +1 -1
  36. package/dist/server/tools/promote.d.ts +11 -0
  37. package/dist/server/tools/promote.d.ts.map +1 -0
  38. package/dist/server/tools/promote.js +315 -0
  39. package/dist/server/tools/promote.js.map +1 -0
  40. package/dist/server/tools/route.d.ts.map +1 -1
  41. package/dist/server/tools/route.js +65 -24
  42. package/dist/server/tools/route.js.map +1 -1
  43. package/dist/server/tools/session-restore.d.ts +18 -0
  44. package/dist/server/tools/session-restore.d.ts.map +1 -0
  45. package/dist/server/tools/session-restore.js +154 -0
  46. package/dist/server/tools/session-restore.js.map +1 -0
  47. package/dist/server/tools/session-search.d.ts +16 -0
  48. package/dist/server/tools/session-search.d.ts.map +1 -0
  49. package/dist/server/tools/session-search.js +240 -0
  50. package/dist/server/tools/session-search.js.map +1 -0
  51. package/dist/server/tools/sights-index.js +2 -2
  52. package/dist/server/tools/sights-index.js.map +1 -1
  53. package/dist/server/tools/smart-route.d.ts.map +1 -1
  54. package/dist/server/tools/smart-route.js +4 -5
  55. package/dist/server/tools/smart-route.js.map +1 -1
  56. package/dist/server/tools/verification.js +1 -1
  57. package/dist/server/tools/verification.js.map +1 -1
  58. package/files/agents/code-organization-supervisor.md +9 -0
  59. package/files/agents/context-guardian.md +9 -0
  60. package/files/agents/docs-keeper.md +11 -1
  61. package/files/agents/dry-refactor.md +12 -1
  62. package/files/agents/elite-code-refactorer.md +10 -0
  63. package/files/agents/hardening-guard.md +13 -1
  64. package/files/agents/implementation-dev.md +12 -1
  65. package/files/agents/merlin-access-control-reviewer.md +248 -0
  66. package/files/agents/merlin-api-designer.md +9 -0
  67. package/files/agents/merlin-codebase-mapper.md +9 -1
  68. package/files/agents/merlin-debugger.md +10 -0
  69. package/files/agents/merlin-dependency-auditor.md +216 -0
  70. package/files/agents/merlin-executor.md +12 -1
  71. package/files/agents/merlin-frontend.md +9 -0
  72. package/files/agents/merlin-input-validator.md +247 -0
  73. package/files/agents/merlin-integration-checker.md +9 -1
  74. package/files/agents/merlin-migrator.md +9 -0
  75. package/files/agents/merlin-milestone-auditor.md +8 -0
  76. package/files/agents/merlin-performance.md +8 -0
  77. package/files/agents/merlin-planner.md +10 -0
  78. package/files/agents/merlin-researcher.md +10 -0
  79. package/files/agents/merlin-reviewer.md +42 -7
  80. package/files/agents/merlin-sast-reviewer.md +182 -0
  81. package/files/agents/merlin-secret-scanner.md +203 -0
  82. package/files/agents/merlin-security.md +9 -0
  83. package/files/agents/merlin-verifier.md +9 -0
  84. package/files/agents/merlin-work-verifier.md +9 -0
  85. package/files/agents/merlin.md +10 -0
  86. package/files/agents/ops-railway.md +11 -1
  87. package/files/agents/orchestrator-retrofit.md +9 -1
  88. package/files/agents/product-spec.md +11 -1
  89. package/files/agents/remotion.md +8 -0
  90. package/files/agents/system-architect.md +11 -1
  91. package/files/agents/tests-qa.md +12 -1
  92. package/files/commands/merlin/course-correct.md +219 -0
  93. package/files/commands/merlin/debug.md +2 -2
  94. package/files/commands/merlin/execute-phase.md +96 -199
  95. package/files/commands/merlin/execute-plan.md +118 -182
  96. package/files/commands/merlin/health.md +385 -0
  97. package/files/commands/merlin/loop-recipes.md +93 -36
  98. package/files/commands/merlin/map-codebase.md +4 -4
  99. package/files/commands/merlin/next.md +240 -0
  100. package/files/commands/merlin/optimize-prompts.md +158 -0
  101. package/files/commands/merlin/plan-phase.md +1 -1
  102. package/files/commands/merlin/profiles.md +215 -0
  103. package/files/commands/merlin/promote.md +176 -0
  104. package/files/commands/merlin/quick.md +229 -0
  105. package/files/commands/merlin/readiness-gate.md +208 -0
  106. package/files/commands/merlin/research-phase.md +2 -2
  107. package/files/commands/merlin/research-project.md +4 -4
  108. package/files/commands/merlin/resume-work.md +27 -1
  109. package/files/commands/merlin/route.md +43 -1
  110. package/files/commands/merlin/sandbox.md +359 -0
  111. package/files/commands/merlin/usage.md +55 -0
  112. package/files/commands/merlin/verify-work.md +1 -1
  113. package/files/docker/Dockerfile.merlin +20 -0
  114. package/files/docker/docker-compose.merlin.yml +23 -0
  115. package/files/hook-templates/auto-commit.sh +64 -0
  116. package/files/hook-templates/auto-format.sh +95 -0
  117. package/files/hook-templates/auto-test.sh +117 -0
  118. package/files/hook-templates/branch-protection.sh +72 -0
  119. package/files/hook-templates/changelog-reminder.sh +76 -0
  120. package/files/hook-templates/complexity-check.sh +112 -0
  121. package/files/hook-templates/import-audit.sh +83 -0
  122. package/files/hook-templates/license-header.sh +84 -0
  123. package/files/hook-templates/pr-description.sh +100 -0
  124. package/files/hook-templates/todo-tracker.sh +80 -0
  125. package/files/hooks/check-file-size.sh +17 -4
  126. package/files/hooks/config-change.sh +44 -16
  127. package/files/hooks/instructions-loaded.sh +22 -5
  128. package/files/hooks/notify-desktop.sh +157 -0
  129. package/files/hooks/notify-webhook.sh +141 -0
  130. package/files/hooks/pre-edit-sights-check.sh +76 -9
  131. package/files/hooks/security-scanner.sh +153 -0
  132. package/files/hooks/session-end-memory-sync.sh +97 -0
  133. package/files/hooks/session-end.sh +274 -1
  134. package/files/hooks/session-start.sh +19 -6
  135. package/files/hooks/smart-approve.sh +270 -0
  136. package/files/hooks/teammate-idle-verify.sh +87 -12
  137. package/files/hooks/worktree-create.sh +20 -3
  138. package/files/hooks/worktree-remove.sh +21 -3
  139. package/files/merlin/references/plan-format.md +37 -9
  140. package/files/merlin/sandbox.json +9 -0
  141. package/files/merlin/security.json +11 -0
  142. package/files/merlin/templates/ci/docs-update.yml +81 -0
  143. package/files/merlin/templates/ci/pr-review.yml +50 -0
  144. package/files/merlin/templates/ci/security-audit.yml +74 -0
  145. package/files/merlin/templates/config.json +9 -1
  146. package/files/rules/api-rules.md +30 -0
  147. package/files/rules/frontend-rules.md +25 -0
  148. package/files/rules/hooks-rules.md +36 -0
  149. package/files/rules/mcp-rules.md +30 -0
  150. package/files/rules/worker-rules.md +29 -0
  151. package/package.json +1 -1
package/bin/install.cjs CHANGED
@@ -11,6 +11,11 @@
11
11
  const isServeMode = process.argv.includes('serve');
12
12
  const isInjectMode = process.argv.includes('inject-headers');
13
13
 
14
+ // --runtime flag: 'all' | 'claude' | 'codex' | 'opencode' | 'gemini'
15
+ // Default: 'all' (configure all detected runtimes)
16
+ const runtimeFlagIdx = process.argv.indexOf('--runtime');
17
+ const RUNTIME_FLAG = runtimeFlagIdx !== -1 ? (process.argv[runtimeFlagIdx + 1] || 'all') : 'all';
18
+
14
19
  // =============================================================================
15
20
  // MODE: inject-headers - Add Merlin reminder to CLAUDE.md files
16
21
  // =============================================================================
@@ -101,12 +106,14 @@ const fs = require('fs');
101
106
  const path = require('path');
102
107
  const os = require('os');
103
108
  const readline = require('readline');
109
+ const { detectRuntimes, configureRuntimes } = require('./runtime-adapters.cjs');
104
110
 
105
111
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
106
112
  const MERLIN_DIR = path.join(CLAUDE_DIR, 'merlin');
107
113
  const AGENTS_DIR = path.join(CLAUDE_DIR, 'agents');
108
114
  const COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands', 'merlin');
109
115
  const LOOP_DIR = path.join(CLAUDE_DIR, 'loop');
116
+ const RULES_DIR = path.join(CLAUDE_DIR, 'rules');
110
117
 
111
118
  const colors = {
112
119
  reset: '\x1b[0m',
@@ -838,7 +845,7 @@ async function install() {
838
845
  }
839
846
 
840
847
  // Step 0: Clean up legacy GSD/ccwiki artifacts
841
- logStep('0/11', 'Cleaning up legacy installations...');
848
+ logStep('0/13', 'Cleaning up legacy installations...');
842
849
  const cleaned = cleanupLegacy();
843
850
  if (cleaned.length > 0) {
844
851
  for (const item of cleaned) {
@@ -849,11 +856,24 @@ async function install() {
849
856
  }
850
857
 
851
858
  // Step 1: Ensure Claude Code is installed and up to date
852
- logStep('1/11', 'Checking Claude Code...');
859
+ logStep('1/13', 'Checking Claude Code...');
853
860
  const claudeCheck = ensureClaudeCode();
854
861
 
855
- // Step 2: Install globally for instant startup across all terminals
856
- logStep('2/11', 'Installing globally (fast startup for all terminals)...');
862
+ // Step 2: Detect runtimes
863
+ logStep('2/13', 'Detecting runtimes...');
864
+ const detectedRuntimes = detectRuntimes();
865
+ log(` ${colors.green}✅${colors.reset} Claude Code (primary)`);
866
+ for (const rt of detectedRuntimes) {
867
+ const icon = rt.found ? `${colors.green}✅${colors.reset}` : `${colors.yellow}⬚${colors.reset}`;
868
+ const suffix = rt.found ? '' : ' (not found)';
869
+ log(` ${icon} ${rt.label}${suffix}`);
870
+ }
871
+ if (RUNTIME_FLAG === 'claude') {
872
+ log(` ${colors.yellow}--runtime claude:${colors.reset} other runtimes will not be configured`);
873
+ }
874
+
875
+ // Step 3: Install globally for instant startup across all terminals
876
+ logStep('3/13', 'Installing globally (fast startup for all terminals)...');
857
877
  try {
858
878
  const { execSync } = require('child_process');
859
879
  // Check if already installed globally and up-to-date
@@ -893,16 +913,16 @@ async function install() {
893
913
  useGlobalBinary = false;
894
914
  }
895
915
 
896
- // Step 3: Create directories
897
- logStep('3/11', 'Creating directories...');
916
+ // Step 4: Create directories
917
+ logStep('4/13', 'Creating directories...');
898
918
  ensureDir(CLAUDE_DIR);
899
919
  ensureDir(MERLIN_DIR);
900
920
  ensureDir(AGENTS_DIR);
901
921
  ensureDir(COMMANDS_DIR);
902
922
  logSuccess('Directories created');
903
923
 
904
- // Step 4: Install Merlin core (workflows, references, templates)
905
- logStep('4/11', 'Installing Merlin workflows...');
924
+ // Step 5: Install Merlin core (workflows, references, templates)
925
+ logStep('5/13', 'Installing Merlin workflows...');
906
926
  const merlinSrc = path.join(filesDir, 'merlin');
907
927
  if (fs.existsSync(merlinSrc)) {
908
928
  const count = copyDirRecursive(merlinSrc, MERLIN_DIR);
@@ -914,8 +934,8 @@ async function install() {
914
934
  logWarn('Merlin workflows not found in package');
915
935
  }
916
936
 
917
- // Step 5: Install agents (tiered)
918
- logStep('5/11', 'Installing Merlin agents...');
937
+ // Step 6: Install agents (tiered)
938
+ logStep('6/13', 'Installing Merlin agents...');
919
939
  const agentsSrc = path.join(filesDir, 'agents');
920
940
  if (fs.existsSync(agentsSrc)) {
921
941
  // Load agent manifest for tiered display
@@ -942,8 +962,32 @@ async function install() {
942
962
  logWarn('Agents not found in package');
943
963
  }
944
964
 
945
- // Step 6: Install commands
946
- logStep('6/11', 'Installing /merlin:* commands...');
965
+ // Step 7: Install path-scoped rules
966
+ logStep('7/13', 'Installing path-scoped rules...');
967
+ const rulesSrc = path.join(filesDir, 'rules');
968
+ if (fs.existsSync(rulesSrc)) {
969
+ ensureDir(RULES_DIR);
970
+ const ruleFiles = fs.readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
971
+ let installedCount = 0;
972
+ let skippedCount = 0;
973
+ for (const ruleFile of ruleFiles) {
974
+ const destPath = path.join(RULES_DIR, ruleFile);
975
+ // Never overwrite existing user rules — they may have been customized
976
+ if (fs.existsSync(destPath)) {
977
+ skippedCount++;
978
+ } else {
979
+ fs.copyFileSync(path.join(rulesSrc, ruleFile), destPath);
980
+ installedCount++;
981
+ }
982
+ }
983
+ if (installedCount > 0) logSuccess(`Installed ${installedCount} path-scoped rule files`);
984
+ if (skippedCount > 0) logSuccess(`Skipped ${skippedCount} existing rule files (user customizations preserved)`);
985
+ } else {
986
+ logWarn('Rules not found in package');
987
+ }
988
+
989
+ // Step 8: Install commands
990
+ logStep('8/13', 'Installing /merlin:* commands...');
947
991
  const commandsSrc = path.join(filesDir, 'commands', 'merlin');
948
992
  if (fs.existsSync(commandsSrc)) {
949
993
  const count = copyDirRecursive(commandsSrc, COMMANDS_DIR);
@@ -952,8 +996,8 @@ async function install() {
952
996
  logWarn('Commands not found in package');
953
997
  }
954
998
 
955
- // Step 7: Install CLAUDE.md
956
- logStep('7/11', 'Configuring Claude Code...');
999
+ // Step 9: Install CLAUDE.md
1000
+ logStep('9/13', 'Configuring Claude Code...');
957
1001
  const claudeMdSrc = path.join(filesDir, 'CLAUDE.md');
958
1002
  const claudeMdDest = path.join(CLAUDE_DIR, 'CLAUDE.md');
959
1003
 
@@ -964,12 +1008,12 @@ async function install() {
964
1008
  logSuccess('Installed CLAUDE.md (Merlin instructions)');
965
1009
  }
966
1010
 
967
- // Step 8: Install Merlin Loop (legacy — kept for backward compatibility)
1011
+ // Step 10: Install Merlin Loop (legacy — kept for backward compatibility)
968
1012
  // NOTE: Claude Code now has a native /loop command that replaces these scripts.
969
1013
  // Use /merlin:loop-recipes in Claude Code for pre-built loop patterns.
970
1014
  // These scripts are still copied so existing users and terminal workflows
971
1015
  // (merlin-loop, merlin session) continue to work without interruption.
972
- logStep('8/11', 'Installing Merlin Loop (legacy scripts)...');
1016
+ logStep('10/13', 'Installing Merlin Loop (legacy scripts)...');
973
1017
  const loopSrc = path.join(filesDir, 'loop');
974
1018
  if (fs.existsSync(loopSrc)) {
975
1019
  ensureDir(LOOP_DIR);
@@ -996,12 +1040,13 @@ async function install() {
996
1040
  logSuccess(`Installed ${count} loop files`);
997
1041
  logSuccess(`Added 'merlin-loop' command`);
998
1042
  logSuccess(`Added 'merlin session' command (interactive orchestrator)`);
1043
+ logWarn(`Note: Custom loop scripts are deprecated. Use /loop with /merlin:loop-recipes instead.`);
999
1044
  } else {
1000
1045
  logWarn('Merlin Loop not found in package');
1001
1046
  }
1002
1047
 
1003
- // Step 9: Install Claude Code hooks
1004
- logStep('9/11', 'Installing Claude Code hooks...');
1048
+ // Step 11: Install Claude Code hooks
1049
+ logStep('11/13', 'Installing Claude Code hooks...');
1005
1050
  const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
1006
1051
  const hooksSrc = path.join(filesDir, 'hooks');
1007
1052
  if (fs.existsSync(hooksSrc)) {
@@ -1100,6 +1145,27 @@ async function install() {
1100
1145
  hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end.sh' }]
1101
1146
  });
1102
1147
 
1148
+ // Stop hook: desktop notification (fires after session-end analytics)
1149
+ addHookIfMissing(settings.hooks.Stop, {
1150
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-desktop.sh' }]
1151
+ });
1152
+
1153
+ // Stop hook: webhook notification (Slack / Discord, fire-and-forget)
1154
+ addHookIfMissing(settings.hooks.Stop, {
1155
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-webhook.sh' }]
1156
+ });
1157
+
1158
+ // Stop hook: auto-memory sync (extracts session decisions → Sights cloud)
1159
+ addHookIfMissing(settings.hooks.Stop, {
1160
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end-memory-sync.sh' }]
1161
+ });
1162
+
1163
+ // Notification hook: desktop alert when Claude needs input
1164
+ settings.hooks.Notification = settings.hooks.Notification || [];
1165
+ addHookIfMissing(settings.hooks.Notification, {
1166
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-desktop.sh' }]
1167
+ });
1168
+
1103
1169
  // TeammateIdle hook (Agent Teams quality gate — only fires when Teams active)
1104
1170
  settings.hooks.TeammateIdle = settings.hooks.TeammateIdle || [];
1105
1171
  addHookIfMissing(settings.hooks.TeammateIdle, {
@@ -1124,6 +1190,48 @@ async function install() {
1124
1190
  hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/task-completed-verify.sh' }]
1125
1191
  });
1126
1192
 
1193
+ // InstructionsLoaded hook (cold-start Sights pre-warm)
1194
+ settings.hooks.InstructionsLoaded = settings.hooks.InstructionsLoaded || [];
1195
+ addHookIfMissing(settings.hooks.InstructionsLoaded, {
1196
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/instructions-loaded.sh' }]
1197
+ });
1198
+
1199
+ // WorktreeCreate hook (propagate Merlin config into new worktrees)
1200
+ settings.hooks.WorktreeCreate = settings.hooks.WorktreeCreate || [];
1201
+ addHookIfMissing(settings.hooks.WorktreeCreate, {
1202
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/worktree-create.sh' }]
1203
+ });
1204
+
1205
+ // WorktreeRemove hook (cleanup + lifecycle analytics)
1206
+ settings.hooks.WorktreeRemove = settings.hooks.WorktreeRemove || [];
1207
+ addHookIfMissing(settings.hooks.WorktreeRemove, {
1208
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/worktree-remove.sh' }]
1209
+ });
1210
+
1211
+ // ConfigChange hook (validate API key + audit trail)
1212
+ settings.hooks.ConfigChange = settings.hooks.ConfigChange || [];
1213
+ addHookIfMissing(settings.hooks.ConfigChange, {
1214
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/config-change.sh' }]
1215
+ });
1216
+
1217
+ // PostToolUse: file size enforcement (implementation agents only)
1218
+ addHookIfMissing(settings.hooks.PostToolUse, {
1219
+ matcher: 'Edit|Write',
1220
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/check-file-size.sh' }]
1221
+ });
1222
+
1223
+ // PreToolUse: security scanner (prompt injection, secrets, data exfiltration)
1224
+ addHookIfMissing(settings.hooks.PreToolUse, {
1225
+ matcher: 'Write|Edit|Bash|NotebookEdit',
1226
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/security-scanner.sh' }]
1227
+ });
1228
+
1229
+ // PreToolUse: smart bash auto-approval (read-only safe, dangerous blocked, unknown pass-through)
1230
+ addHookIfMissing(settings.hooks.PreToolUse, {
1231
+ matcher: 'Bash',
1232
+ hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/smart-approve.sh' }]
1233
+ });
1234
+
1127
1235
  // --- Prompt-based hooks cleanup & registration ---
1128
1236
  // Remove ALL prompt-type hooks from Merlin (identified by type: 'prompt').
1129
1237
  // This prevents duplicates across version upgrades and removes broken hooks.
@@ -1144,6 +1252,10 @@ async function install() {
1144
1252
  settings.hooks.Notification = removeAllPromptHooks(settings.hooks.Notification);
1145
1253
  settings.hooks.TaskCompleted = settings.hooks.TaskCompleted || [];
1146
1254
  settings.hooks.TaskCompleted = removeAllPromptHooks(settings.hooks.TaskCompleted);
1255
+ settings.hooks.InstructionsLoaded = removeAllPromptHooks(settings.hooks.InstructionsLoaded || []);
1256
+ settings.hooks.WorktreeCreate = removeAllPromptHooks(settings.hooks.WorktreeCreate || []);
1257
+ settings.hooks.WorktreeRemove = removeAllPromptHooks(settings.hooks.WorktreeRemove || []);
1258
+ settings.hooks.ConfigChange = removeAllPromptHooks(settings.hooks.ConfigChange || []);
1147
1259
 
1148
1260
  // NOTE: The PreToolUse prompt hook has been REMOVED. It caused an infinite
1149
1261
  // rejection loop because the evaluator model cannot see conversation history
@@ -1189,8 +1301,8 @@ async function install() {
1189
1301
  return cfg;
1190
1302
  }
1191
1303
 
1192
- // Step 10: Optional Merlin Sights configuration
1193
- logStep('10/11', 'Merlin Sights configuration...');
1304
+ // Step 12: Optional Merlin Sights configuration
1305
+ logStep('12/13', 'Merlin Sights configuration...');
1194
1306
 
1195
1307
  // Check if API key is already configured (skip prompt on updates)
1196
1308
  let existingApiKey = '';
@@ -1321,8 +1433,20 @@ async function install() {
1321
1433
  logWarn('Skipped Merlin Sights (you can configure it later)');
1322
1434
  }
1323
1435
 
1324
- // Step 11: Set up shell integration
1325
- logStep('11/11', 'Setting up shell integration...');
1436
+ // Step 13: Configure non-Claude-Code runtimes
1437
+ logStep('13/13', 'Configuring additional runtimes...');
1438
+ configureRuntimes({
1439
+ runtimeFlag: RUNTIME_FLAG,
1440
+ useGlobalBinary,
1441
+ apiKey,
1442
+ logSuccess,
1443
+ logWarn,
1444
+ logInfo: (msg) => log(msg),
1445
+ quiet: true, // step 2 already printed the detection summary
1446
+ });
1447
+
1448
+ // Shell integration (runs after runtime adapters)
1449
+ log(`\n${colors.cyan}[shell]${colors.reset} Setting up shell integration...`);
1326
1450
  const shellConfigured = setupShellIntegration();
1327
1451
  if (shellConfigured) {
1328
1452
  log(`\n ${colors.yellow}IMPORTANT:${colors.reset} Run ${colors.cyan}source ~/.zshrc${colors.reset} or ${colors.bright}restart your terminal${colors.reset}`);
@@ -0,0 +1,396 @@
1
+ // MERLIN RUNTIME ADAPTERS
2
+ // Detects and configures Merlin for non-Claude-Code runtimes.
3
+ // Claude Code is always handled by the main installer (install.cjs).
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const { execSync } = require('child_process');
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Runtime definitions
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const RUNTIMES = [
17
+ {
18
+ id: 'codex',
19
+ label: 'Codex CLI',
20
+ configDir: path.join(os.homedir(), '.codex'),
21
+ binary: 'codex',
22
+ },
23
+ {
24
+ id: 'opencode',
25
+ label: 'OpenCode',
26
+ configDir: path.join(os.homedir(), '.opencode'),
27
+ binary: 'opencode',
28
+ },
29
+ {
30
+ id: 'gemini',
31
+ label: 'Gemini CLI',
32
+ configDir: path.join(os.homedir(), '.gemini'),
33
+ binary: 'gemini',
34
+ },
35
+ ];
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Detection helpers
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function inPath(binary) {
42
+ try {
43
+ execSync(`which ${binary} 2>/dev/null`, { stdio: 'pipe' });
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ function detectRuntimes() {
51
+ return RUNTIMES.map((rt) => ({
52
+ ...rt,
53
+ found: fs.existsSync(rt.configDir) || inPath(rt.binary),
54
+ }));
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // MCP server command helper (mirrors logic in install.cjs)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function buildMcpCommand(useGlobalBinary) {
62
+ if (useGlobalBinary) {
63
+ return { command: 'merlin-brain', args: undefined };
64
+ }
65
+ return { command: 'node', args: [path.join(__dirname, 'serve.js')] };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // AGENTS.md content for instruction-file runtimes
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function buildAgentsMd() {
73
+ return `# Merlin Brain — AI Development System
74
+
75
+ > Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
76
+ >
77
+ > Note: Tool names in these instructions reflect Claude Code conventions (Read, Write, Edit,
78
+ > Bash, Grep, Glob). Names may differ in your runtime — adapt as needed.
79
+
80
+ ## Boot Sequence (run at session start)
81
+
82
+ 1. Call \`merlin_get_selected_repo\` — connect Merlin Sights to this project.
83
+ 2. Call \`merlin_get_project_status\` — load current project state and active tasks.
84
+ 3. Show a brief status summary, then handle the user's request.
85
+
86
+ Do NOT skip these steps. Do NOT start working without Merlin context.
87
+
88
+ ## Before Every File Edit
89
+
90
+ Call \`merlin_get_context("your task")\` before writing or modifying any code.
91
+
92
+ ## Routing Specialist Work
93
+
94
+ Route tasks to the right specialist agent:
95
+ - Architecture decisions → system-architect
96
+ - Implementation → implementation-dev
97
+ - Testing → tests-qa
98
+ - Security review → merlin-security
99
+ - Documentation → docs-keeper
100
+
101
+ ## MCP Tools Available
102
+
103
+ The Merlin MCP server exposes these tools:
104
+ - \`merlin_get_selected_repo\` — identify current repository
105
+ - \`merlin_get_project_status\` — load tasks and project state
106
+ - \`merlin_get_context(task)\` — fetch relevant code context for a task
107
+ - \`merlin_find_files(query)\` — locate files by description
108
+ - \`merlin_search(query)\` — semantic code search
109
+
110
+ ## Core Principles
111
+
112
+ - Always search before writing — reuse existing functions and patterns.
113
+ - Keep files under 400 lines — split into sub-modules when approaching the limit.
114
+ - Graceful failure — if Merlin Sights is unavailable, continue with file exploration.
115
+ `;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Codex CLI adapter
120
+ // Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function configureCodex(rt, useGlobalBinary, apiKey) {
124
+ const results = [];
125
+
126
+ // Ensure config dir exists
127
+ if (!fs.existsSync(rt.configDir)) {
128
+ fs.mkdirSync(rt.configDir, { recursive: true });
129
+ }
130
+
131
+ // Write AGENTS.md
132
+ const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
133
+ const agentsMd = buildAgentsMd();
134
+ const existingAgentsMd = fs.existsSync(agentsMdPath)
135
+ ? fs.readFileSync(agentsMdPath, 'utf8')
136
+ : '';
137
+
138
+ if (existingAgentsMd.includes('Merlin Brain')) {
139
+ results.push('AGENTS.md already has Merlin (skipped)');
140
+ } else {
141
+ const combined = existingAgentsMd
142
+ ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + agentsMd
143
+ : agentsMd;
144
+ fs.writeFileSync(agentsMdPath, combined);
145
+ results.push('Wrote ~/.codex/AGENTS.md');
146
+ }
147
+
148
+ // Write / update config.toml with MCP section
149
+ const configTomlPath = path.join(rt.configDir, 'config.toml');
150
+ const { command, args } = buildMcpCommand(useGlobalBinary);
151
+ const argsToml = args
152
+ ? `args = [${args.map((a) => `"${a}"`).join(', ')}]`
153
+ : '';
154
+ const envToml = apiKey ? `\n MERLIN_API_KEY = "${apiKey}"` : '';
155
+ const mcpToml = `
156
+ [mcp.merlin]
157
+ command = "${command}"
158
+ ${argsToml ? argsToml + '\n' : ''}${apiKey ? `[mcp.merlin.env]${envToml}\n` : ''}`;
159
+
160
+ let tomlContent = fs.existsSync(configTomlPath)
161
+ ? fs.readFileSync(configTomlPath, 'utf8')
162
+ : '';
163
+
164
+ if (tomlContent.includes('[mcp.merlin]')) {
165
+ results.push('config.toml already has Merlin MCP (skipped)');
166
+ } else {
167
+ fs.appendFileSync(configTomlPath, mcpToml);
168
+ results.push('Added Merlin MCP to ~/.codex/config.toml');
169
+ }
170
+
171
+ return results;
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // OpenCode adapter
176
+ // Writes: ~/.opencode/config.json (mcpServers section) + ~/.opencode/AGENTS.md
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function configureOpenCode(rt, useGlobalBinary, apiKey) {
180
+ const results = [];
181
+
182
+ if (!fs.existsSync(rt.configDir)) {
183
+ fs.mkdirSync(rt.configDir, { recursive: true });
184
+ }
185
+
186
+ // MCP config JSON
187
+ const configJsonPath = path.join(rt.configDir, 'config.json');
188
+ let config = {};
189
+ if (fs.existsSync(configJsonPath)) {
190
+ try {
191
+ config = JSON.parse(fs.readFileSync(configJsonPath, 'utf8'));
192
+ } catch {
193
+ config = {};
194
+ }
195
+ }
196
+
197
+ config.mcpServers = config.mcpServers || {};
198
+ if (config.mcpServers.merlin) {
199
+ results.push('config.json already has Merlin MCP (skipped)');
200
+ } else {
201
+ const { command, args } = buildMcpCommand(useGlobalBinary);
202
+ const mcpEntry = { command, type: 'stdio' };
203
+ if (args) mcpEntry.args = args;
204
+ if (apiKey) mcpEntry.env = { MERLIN_API_KEY: apiKey };
205
+ config.mcpServers.merlin = mcpEntry;
206
+ fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2));
207
+ results.push('Added Merlin MCP to ~/.opencode/config.json');
208
+ }
209
+
210
+ // Instructions file
211
+ const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
212
+ const existingAgentsMd = fs.existsSync(agentsMdPath)
213
+ ? fs.readFileSync(agentsMdPath, 'utf8')
214
+ : '';
215
+ if (existingAgentsMd.includes('Merlin Brain')) {
216
+ results.push('AGENTS.md already has Merlin (skipped)');
217
+ } else {
218
+ const combined = existingAgentsMd
219
+ ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
220
+ : buildAgentsMd();
221
+ fs.writeFileSync(agentsMdPath, combined);
222
+ results.push('Wrote ~/.opencode/AGENTS.md');
223
+ }
224
+
225
+ return results;
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Gemini CLI adapter
230
+ // Writes: ~/.gemini/GEMINI.md + ~/.gemini/settings.json (mcpServers section)
231
+ // ---------------------------------------------------------------------------
232
+
233
+ function configureGemini(rt, useGlobalBinary, apiKey) {
234
+ const results = [];
235
+
236
+ if (!fs.existsSync(rt.configDir)) {
237
+ fs.mkdirSync(rt.configDir, { recursive: true });
238
+ }
239
+
240
+ // GEMINI.md instructions file
241
+ const geminiMdPath = path.join(rt.configDir, 'GEMINI.md');
242
+ const existingGeminiMd = fs.existsSync(geminiMdPath)
243
+ ? fs.readFileSync(geminiMdPath, 'utf8')
244
+ : '';
245
+ if (existingGeminiMd.includes('Merlin Brain')) {
246
+ results.push('GEMINI.md already has Merlin (skipped)');
247
+ } else {
248
+ const combined = existingGeminiMd
249
+ ? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
250
+ : buildAgentsMd();
251
+ fs.writeFileSync(geminiMdPath, combined);
252
+ results.push('Wrote ~/.gemini/GEMINI.md');
253
+ }
254
+
255
+ // settings.json MCP config
256
+ const settingsPath = path.join(rt.configDir, 'settings.json');
257
+ let settings = {};
258
+ if (fs.existsSync(settingsPath)) {
259
+ try {
260
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
261
+ } catch {
262
+ settings = {};
263
+ }
264
+ }
265
+
266
+ settings.mcpServers = settings.mcpServers || {};
267
+ if (settings.mcpServers.merlin) {
268
+ results.push('settings.json already has Merlin MCP (skipped)');
269
+ } else {
270
+ const { command, args } = buildMcpCommand(useGlobalBinary);
271
+ const mcpEntry = { command };
272
+ if (args) mcpEntry.args = args;
273
+ if (apiKey) mcpEntry.env = { MERLIN_API_KEY: apiKey };
274
+ settings.mcpServers.merlin = mcpEntry;
275
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
276
+ results.push('Added Merlin MCP to ~/.gemini/settings.json');
277
+ }
278
+
279
+ return results;
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Per-project runtime files
284
+ // Writes AGENTS.md alongside CLAUDE.md when initializing a project.
285
+ // ---------------------------------------------------------------------------
286
+
287
+ /**
288
+ * Generate per-project runtime instruction files next to a CLAUDE.md.
289
+ * Call this whenever Merlin creates or updates a project's CLAUDE.md.
290
+ *
291
+ * @param {string} projectDir - Absolute path to the project directory
292
+ * @param {string[]} runtimeIds - Which runtime files to generate ('codex'|'opencode'|'gemini'|'all')
293
+ */
294
+ function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
295
+ const results = [];
296
+ const targets = runtimeIds.includes('all')
297
+ ? ['codex', 'opencode', 'gemini']
298
+ : runtimeIds;
299
+
300
+ const agentsMd = buildAgentsMd();
301
+
302
+ // AGENTS.md — used by Codex and OpenCode
303
+ if (targets.includes('codex') || targets.includes('opencode')) {
304
+ const agentsMdPath = path.join(projectDir, 'AGENTS.md');
305
+ if (!fs.existsSync(agentsMdPath)) {
306
+ fs.writeFileSync(agentsMdPath, agentsMd);
307
+ results.push(`Created ${agentsMdPath}`);
308
+ }
309
+ }
310
+
311
+ // GEMINI.md — used by Gemini CLI
312
+ if (targets.includes('gemini')) {
313
+ const geminiMdPath = path.join(projectDir, 'GEMINI.md');
314
+ if (!fs.existsSync(geminiMdPath)) {
315
+ fs.writeFileSync(geminiMdPath, agentsMd);
316
+ results.push(`Created ${geminiMdPath}`);
317
+ }
318
+ }
319
+
320
+ return results;
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Main entry point called from install.cjs
325
+ // ---------------------------------------------------------------------------
326
+
327
+ /**
328
+ * Detect and configure all non-Claude-Code runtimes.
329
+ *
330
+ * @param {object} opts
331
+ * @param {string} opts.runtimeFlag - Value of --runtime flag ('all'|'claude'|runtime-id)
332
+ * @param {boolean} opts.useGlobalBinary - Whether merlin-brain global binary is available
333
+ * @param {string} opts.apiKey - Merlin Sights API key (may be empty)
334
+ * @param {Function} opts.logSuccess - Logging helper
335
+ * @param {Function} opts.logWarn - Logging helper
336
+ * @param {Function} opts.logInfo - Plain log helper (console.log compatible)
337
+ * @param {boolean} [opts.quiet] - If true, skip printing the detection summary
338
+ * @returns {{ detected: object[], configured: string[] }}
339
+ */
340
+ function configureRuntimes({ runtimeFlag, useGlobalBinary, apiKey, logSuccess, logWarn, logInfo, quiet }) {
341
+ const detected = detectRuntimes();
342
+
343
+ // Print detection summary (skipped when caller already printed it)
344
+ if (!quiet) {
345
+ for (const rt of detected) {
346
+ const icon = rt.found ? '\u2705' : '\u2B1A';
347
+ const suffix = rt.found ? '' : ' (not found)';
348
+ logInfo(` ${icon} ${rt.label}${suffix}`);
349
+ }
350
+ }
351
+
352
+ // Determine which runtimes to configure
353
+ const skip = runtimeFlag === 'claude'; // --runtime claude → Claude Code only
354
+ if (skip) {
355
+ if (!quiet) logInfo(' Skipping other runtimes (--runtime claude)');
356
+ return { detected, configured: [] };
357
+ }
358
+
359
+ const filterToOne = !['all', 'claude', undefined, ''].includes(runtimeFlag);
360
+ const targetRuntimes = filterToOne
361
+ ? detected.filter((rt) => rt.found && rt.id === runtimeFlag)
362
+ : detected.filter((rt) => rt.found);
363
+
364
+ const configured = [];
365
+
366
+ for (const rt of targetRuntimes) {
367
+ let results = [];
368
+ try {
369
+ if (rt.id === 'codex') {
370
+ results = configureCodex(rt, useGlobalBinary, apiKey);
371
+ } else if (rt.id === 'opencode') {
372
+ results = configureOpenCode(rt, useGlobalBinary, apiKey);
373
+ } else if (rt.id === 'gemini') {
374
+ results = configureGemini(rt, useGlobalBinary, apiKey);
375
+ } else {
376
+ logWarn(` Unknown runtime "${rt.id}" — skipped`);
377
+ continue;
378
+ }
379
+ for (const msg of results) {
380
+ logSuccess(msg);
381
+ configured.push(msg);
382
+ }
383
+ } catch (err) {
384
+ logWarn(` Failed to configure ${rt.label}: ${err.message}`);
385
+ }
386
+ }
387
+
388
+ return { detected, configured };
389
+ }
390
+
391
+ module.exports = {
392
+ detectRuntimes,
393
+ configureRuntimes,
394
+ generateProjectRuntimeFiles,
395
+ buildAgentsMd,
396
+ };