create-merlin-brain 3.22.0 → 4.0.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 (80) hide show
  1. package/README.md +38 -4
  2. package/bin/merlin-ask.cjs +111 -0
  3. package/bin/merlin-cli.cjs +22 -0
  4. package/bin/runtime-adapters.cjs +709 -28
  5. package/dist/server/api/client.d.ts +2 -0
  6. package/dist/server/api/client.d.ts.map +1 -1
  7. package/dist/server/api/client.js +4 -0
  8. package/dist/server/api/client.js.map +1 -1
  9. package/dist/server/server.d.ts.map +1 -1
  10. package/dist/server/server.js +56 -4
  11. package/dist/server/server.js.map +1 -1
  12. package/dist/server/tools/auto-mode.d.ts +9 -0
  13. package/dist/server/tools/auto-mode.d.ts.map +1 -0
  14. package/dist/server/tools/auto-mode.js +231 -0
  15. package/dist/server/tools/auto-mode.js.map +1 -0
  16. package/dist/server/tools/computer-use.d.ts +8 -0
  17. package/dist/server/tools/computer-use.d.ts.map +1 -0
  18. package/dist/server/tools/computer-use.js +355 -0
  19. package/dist/server/tools/computer-use.js.map +1 -0
  20. package/dist/server/tools/dream.d.ts +9 -0
  21. package/dist/server/tools/dream.d.ts.map +1 -0
  22. package/dist/server/tools/dream.js +246 -0
  23. package/dist/server/tools/dream.js.map +1 -0
  24. package/dist/server/tools/help.d.ts +3 -0
  25. package/dist/server/tools/help.d.ts.map +1 -0
  26. package/dist/server/tools/help.js +110 -0
  27. package/dist/server/tools/help.js.map +1 -0
  28. package/dist/server/tools/hud.d.ts +13 -0
  29. package/dist/server/tools/hud.d.ts.map +1 -0
  30. package/dist/server/tools/hud.js +295 -0
  31. package/dist/server/tools/hud.js.map +1 -0
  32. package/dist/server/tools/index.d.ts +5 -0
  33. package/dist/server/tools/index.d.ts.map +1 -1
  34. package/dist/server/tools/index.js +5 -0
  35. package/dist/server/tools/index.js.map +1 -1
  36. package/dist/server/tools/provider-ask.d.ts +10 -0
  37. package/dist/server/tools/provider-ask.d.ts.map +1 -0
  38. package/dist/server/tools/provider-ask.js +234 -0
  39. package/dist/server/tools/provider-ask.js.map +1 -0
  40. package/dist/server/tools/rate-limit.d.ts +8 -0
  41. package/dist/server/tools/rate-limit.d.ts.map +1 -0
  42. package/dist/server/tools/rate-limit.js +184 -0
  43. package/dist/server/tools/rate-limit.js.map +1 -0
  44. package/dist/server/tools/skills.d.ts +16 -0
  45. package/dist/server/tools/skills.d.ts.map +1 -0
  46. package/dist/server/tools/skills.js +326 -0
  47. package/dist/server/tools/skills.js.map +1 -0
  48. package/dist/server/tools/team-workers.d.ts +7 -0
  49. package/dist/server/tools/team-workers.d.ts.map +1 -0
  50. package/dist/server/tools/team-workers.js +271 -0
  51. package/dist/server/tools/team-workers.js.map +1 -0
  52. package/dist/server/utils/merlin-manifest.d.ts +6 -1
  53. package/dist/server/utils/merlin-manifest.d.ts.map +1 -1
  54. package/dist/server/utils/merlin-manifest.js +34 -1
  55. package/dist/server/utils/merlin-manifest.js.map +1 -1
  56. package/files/CLAUDE.md +22 -0
  57. package/files/hooks/rate-limit-watch.sh +120 -0
  58. package/files/hooks/statusline.sh +148 -0
  59. package/files/merlin/skills/SKILLS-INDEX.md +82 -0
  60. package/files/merlin/skills/automation/payments.md +14 -0
  61. package/files/merlin/skills/automation/webhooks.md +14 -0
  62. package/files/merlin/skills/coding/accessibility.md +14 -0
  63. package/files/merlin/skills/coding/api-design.md +14 -0
  64. package/files/merlin/skills/coding/debug-mode.md +14 -0
  65. package/files/merlin/skills/coding/focus-mode.md +14 -0
  66. package/files/merlin/skills/coding/loop.md +14 -0
  67. package/files/merlin/skills/coding/performance.md +14 -0
  68. package/files/merlin/skills/coding/react-patterns.md +51 -0
  69. package/files/merlin/skills/coding/security-hardening.md +56 -0
  70. package/files/merlin/skills/coding/verify.md +14 -0
  71. package/files/merlin/skills/communication/dispatcher.md +40 -0
  72. package/files/merlin/skills/communication/email-gmail.md +31 -0
  73. package/files/merlin/skills/communication/telegram.md +50 -0
  74. package/files/merlin/skills/communication/whatsapp.md +47 -0
  75. package/files/merlin/skills/data/google-sheets.md +14 -0
  76. package/files/merlin/skills/design/animation.md +14 -0
  77. package/files/merlin/skills/devops/docker-containers.md +14 -0
  78. package/files/merlin/skills/research/brainstorm.md +14 -0
  79. package/files/merlin/skills/testing/tdd-workflow.md +58 -0
  80. package/package.json +4 -2
@@ -8,7 +8,6 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
  const { execSync } = require('child_process');
11
-
12
11
  // ---------------------------------------------------------------------------
13
12
  // Runtime definitions
14
13
  // ---------------------------------------------------------------------------
@@ -65,11 +64,178 @@ function buildMcpCommand(useGlobalBinary) {
65
64
  return { command: 'node', args: [path.join(__dirname, 'serve.js')] };
66
65
  }
67
66
 
67
+ function ensureDir(dir) {
68
+ if (!fs.existsSync(dir)) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ }
71
+ }
72
+
73
+ function writeFile(filePath, content) {
74
+ ensureDir(path.dirname(filePath));
75
+ fs.writeFileSync(filePath, content);
76
+ }
77
+
78
+ function tomlMultiline(text) {
79
+ return "'''\n" + text.replace(/'''/g, "'''\" + \"'\" + '''") + "\n'''";
80
+ }
81
+
82
+ function escapeRegex(value) {
83
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
84
+ }
85
+
86
+ function ensureTomlSectionLine(content, sectionName, line) {
87
+ const key = line.split('=')[0].trim();
88
+ const sectionRegex = new RegExp(`(^\\[${escapeRegex(sectionName)}\\]\\n)([\\s\\S]*?)(?=^\\[|$)`, 'm');
89
+ if (sectionRegex.test(content)) {
90
+ return content.replace(sectionRegex, (match, start, body) => {
91
+ const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=.*$`, 'm');
92
+ let nextBody = body.replace(keyRegex, line);
93
+ if (!keyRegex.test(body)) {
94
+ const trimmedBody = body.replace(/\s+$/, '');
95
+ nextBody = `${trimmedBody ? `${trimmedBody}\n` : ''}${line}\n`;
96
+ }
97
+ const trimmedBody = nextBody.replace(/\s+$/, '');
98
+ return `${start}${trimmedBody ? `${trimmedBody}\n` : ''}`;
99
+ });
100
+ }
101
+
102
+ const prefix = content.trimEnd();
103
+ return `${prefix}${prefix ? '\n\n' : ''}[${sectionName}]\n${line}\n`;
104
+ }
105
+
106
+ function upsertTomlSection(content, sectionName, sectionBody) {
107
+ const sectionRegex = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|$)`, 'm');
108
+ const next = content.replace(sectionRegex, '').trimEnd();
109
+ return `${next}${next ? '\n\n' : ''}[${sectionName}]\n${sectionBody.trim()}\n`;
110
+ }
111
+
112
+ function removeTomlSection(content, sectionName) {
113
+ const sectionRegex = new RegExp(`^\\[${escapeRegex(sectionName)}\\]\\n[\\s\\S]*?(?=^\\[|$)`, 'm');
114
+ return content.replace(sectionRegex, '').trimEnd();
115
+ }
116
+
117
+ function ensureTomlTopLevelLine(content, line) {
118
+ const key = line.split('=')[0].trim();
119
+ const lines = content.split('\n');
120
+ const keyRegex = new RegExp(`^${escapeRegex(key)}\\s*=.*$`);
121
+ const existingIndex = lines.findIndex((current) => keyRegex.test(current.trim()));
122
+ if (existingIndex !== -1) {
123
+ lines[existingIndex] = line;
124
+ return lines.join('\n');
125
+ }
126
+
127
+ let insertAt = 0;
128
+ while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;
129
+ if (insertAt < lines.length && !lines[insertAt].startsWith('[')) {
130
+ while (insertAt < lines.length && lines[insertAt].trim() !== '') insertAt++;
131
+ }
132
+
133
+ const next = [...lines];
134
+ next.splice(insertAt, 0, line);
135
+ return next.join('\n');
136
+ }
137
+
138
+ function mergeHooksJson(existingContent, hooksRoot) {
139
+ const nextConfig = JSON.parse(buildCodexHooksJson(hooksRoot));
140
+ let base = { hooks: {} };
141
+
142
+ if (existingContent && existingContent.trim()) {
143
+ try {
144
+ base = JSON.parse(existingContent);
145
+ } catch {
146
+ base = { hooks: {} };
147
+ }
148
+ }
149
+
150
+ base.hooks = base.hooks || {};
151
+ for (const [eventName, entries] of Object.entries(nextConfig.hooks)) {
152
+ base.hooks[eventName] = base.hooks[eventName] || [];
153
+ for (const entry of entries) {
154
+ const cmd = entry.hooks?.[0]?.command;
155
+ const exists = base.hooks[eventName].some((existing) =>
156
+ existing?.hooks?.some((hook) => hook.command === cmd)
157
+ );
158
+ if (!exists) {
159
+ base.hooks[eventName].push(entry);
160
+ }
161
+ }
162
+ }
163
+
164
+ return JSON.stringify(base, null, 2) + '\n';
165
+ }
166
+
68
167
  // ---------------------------------------------------------------------------
69
- // AGENTS.md content for instruction-file runtimes
168
+ // Instruction file content for non-Claude runtimes
70
169
  // ---------------------------------------------------------------------------
71
170
 
72
- function buildAgentsMd() {
171
+ function buildInstructionMd(runtime = 'generic') {
172
+ if (runtime === 'codex') {
173
+ return `# Merlin Brain — Codex Operating Layer
174
+
175
+ > Installed by Merlin (https://merlin.build). Keep this file so Codex loads Merlin context automatically.
176
+
177
+ ## Session Boot
178
+
179
+ Before doing real work in a repository:
180
+
181
+ 1. Call \`merlin_get_selected_repo\`.
182
+ 2. Call \`merlin_get_project_status\`.
183
+ 3. Call \`merlin_get_rules\` and \`merlin_get_brief\`.
184
+ 4. Summarize the current state, then proceed.
185
+
186
+ Do not skip boot. Do not start editing code without Merlin context.
187
+
188
+ ## Discoverability
189
+
190
+ - If the user asks what Merlin can do, call \`merlin_help\`.
191
+ - If the best Merlin path is unclear, call \`merlin_help(task)\`.
192
+ - For new features or integrations, call \`merlin_recommend_for_task(task)\` before building from scratch.
193
+ - For agent selection, call \`merlin_smart_route(task)\`.
194
+ - For reusable prompt packs, call \`merlin_find_skill(query)\`.
195
+
196
+ ## Codex + Merlin Workflow
197
+
198
+ - For codebase questions, start with \`merlin_search("query")\` or \`merlin_get_context("task")\`.
199
+ - Before every code edit, call \`merlin_get_context("your task")\`.
200
+ - Let Merlin choose the route first when helpful: tool vs skill vs custom agent vs direct execution.
201
+ - When local file search is still needed, prefer fast repo-native tools such as \`rg\`.
202
+ - Use Codex's normal execution style: inspect first, edit surgically, verify after changes.
203
+ - Keep progress updates concise and factual while work is in flight.
204
+ - Prefer Merlin skills and custom agents installed in this repo when the task matches them.
205
+
206
+ ## Editing Discipline
207
+
208
+ - Prefer \`apply_patch\` for manual edits.
209
+ - Avoid broad rewrites when a narrow patch is sufficient.
210
+ - Preserve user changes you did not make.
211
+ - Verify behavior after implementation work before claiming completion.
212
+
213
+ ## Delegation
214
+
215
+ - Use Merlin tools first for routing and context.
216
+ - Use Codex sub-agents only for clearly bounded side tasks that can run in parallel.
217
+ - Keep the critical path local when the next step depends on immediate code understanding.
218
+
219
+ ## MCP Tools Available
220
+
221
+ - \`merlin_get_selected_repo\` — identify the active repository
222
+ - \`merlin_get_project_status\` — load project state and active tasks
223
+ - \`merlin_help(task?)\` — explain Merlin capabilities and recommend the best route
224
+ - \`merlin_get_context(task)\` — fetch targeted implementation context
225
+ - \`merlin_find_files(query)\` — locate files by purpose
226
+ - \`merlin_search(query)\` — semantic code search
227
+ - \`merlin_find_skill(query)\` — find reusable Merlin skills
228
+ - \`merlin_smart_route(task)\` — choose the best specialist agent
229
+ - \`merlin_recommend_for_task(task)\` — find agents and reference codebases before building
230
+
231
+ ## Defaults
232
+
233
+ - Search before writing.
234
+ - Reuse existing patterns before introducing new ones.
235
+ - If Merlin Sights is unavailable, continue with disciplined local exploration instead of blocking.
236
+ `;
237
+ }
238
+
73
239
  return `# Merlin Brain — AI Development System
74
240
 
75
241
  > Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
@@ -115,6 +281,443 @@ The Merlin MCP server exposes these tools:
115
281
  `;
116
282
  }
117
283
 
284
+ function buildCodexHooksJson(hooksRoot) {
285
+ return JSON.stringify({
286
+ hooks: {
287
+ SessionStart: [
288
+ {
289
+ matcher: 'startup|resume',
290
+ hooks: [
291
+ {
292
+ type: 'command',
293
+ command: `bash "${path.join(hooksRoot, 'session-start.sh')}"`,
294
+ statusMessage: 'Merlin booting',
295
+ },
296
+ ],
297
+ },
298
+ ],
299
+ UserPromptSubmit: [
300
+ {
301
+ hooks: [
302
+ {
303
+ type: 'command',
304
+ command: `bash "${path.join(hooksRoot, 'user-prompt-router.sh')}"`,
305
+ statusMessage: 'Merlin routing',
306
+ },
307
+ ],
308
+ },
309
+ ],
310
+ PreToolUse: [
311
+ {
312
+ matcher: 'Bash',
313
+ hooks: [
314
+ {
315
+ type: 'command',
316
+ command: `bash "${path.join(hooksRoot, 'bash-pre-tool.sh')}"`,
317
+ statusMessage: 'Merlin command guard',
318
+ },
319
+ ],
320
+ },
321
+ ],
322
+ PostToolUse: [
323
+ {
324
+ matcher: 'Bash',
325
+ hooks: [
326
+ {
327
+ type: 'command',
328
+ command: `bash "${path.join(hooksRoot, 'bash-post-tool.sh')}"`,
329
+ statusMessage: 'Merlin command review',
330
+ },
331
+ ],
332
+ },
333
+ ],
334
+ Stop: [
335
+ {
336
+ hooks: [
337
+ {
338
+ type: 'command',
339
+ command: `bash "${path.join(hooksRoot, 'stop.sh')}"`,
340
+ statusMessage: 'Merlin wrap-up',
341
+ },
342
+ ],
343
+ },
344
+ ],
345
+ },
346
+ }, null, 2) + '\n';
347
+ }
348
+
349
+ function buildCodexSessionStartHook() {
350
+ return `#!/usr/bin/env bash
351
+ set -euo pipefail
352
+ trap 'echo "{}"; exit 0' ERR
353
+
354
+ MERLIN_HOME="\${HOME}/.codex/merlin"
355
+ mkdir -p "\${MERLIN_HOME}/analytics" "\${MERLIN_HOME}/sessions" 2>/dev/null || true
356
+
357
+ _context="MERLIN MODE ACTIVE. Before handling the user request, call merlin_get_selected_repo, then merlin_get_project_status, then merlin_get_rules and merlin_get_brief."
358
+ _context="\${_context} Use Merlin context before edits: call merlin_get_context(task)."
359
+ _context="\${_context} If the best Merlin path is unclear, call merlin_help(task) before acting."
360
+ _context="\${_context} For task routing, prefer merlin_smart_route(task), merlin_find_skill(query), and merlin_recommend_for_task(task) over improvising."
361
+ _context="\${_context} Prefer Merlin skills and custom agents installed in this repository when they match the task."
362
+ _context="\${_context} Keep the Codex experience pragmatic: inspect first, patch surgically, verify before claiming completion."
363
+ _context="\${_context} Prefix visible progress updates with the Merlin badge when practical: ⟡🔮 MERLIN ›"
364
+
365
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "\${_context//\"/\\\\\"}"
366
+ `;
367
+ }
368
+
369
+ function buildCodexUserPromptRouterHook() {
370
+ return `#!/usr/bin/env bash
371
+ set -euo pipefail
372
+ trap 'echo "{}"; exit 0' ERR
373
+
374
+ input=""
375
+ if [ ! -t 0 ]; then
376
+ input=$(cat 2>/dev/null || true)
377
+ fi
378
+
379
+ [ -z "$input" ] && echo "{}" && exit 0
380
+
381
+ prompt=""
382
+ if command -v jq >/dev/null 2>&1; then
383
+ prompt=$(echo "$input" | jq -r '.prompt // .userPrompt // empty' 2>/dev/null || true)
384
+ else
385
+ prompt=$(echo "$input" | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/' 2>/dev/null || true)
386
+ fi
387
+
388
+ [ -z "$prompt" ] && echo "{}" && exit 0
389
+
390
+ clean=$(printf '%s' "$prompt" | sed -E -e 's/<[^>]+>//g' -e 's|https?://[^ ]*||g' -e 's|/[a-zA-Z0-9._/-]+||g' -e 's/\`[^\`]*\`//g')
391
+
392
+ suggestion=""
393
+ if echo "$clean" | grep -qiE "what can merlin do|how do i use merlin|what is available|available skills|available agents|available tools|help me use merlin"; then
394
+ suggestion='Merlin routing: call merlin_help first, then pick the recommended tool, skill, or agent path.'
395
+ elif echo "$clean" | grep -qiE "resume|pick up|continue|where were we"; then
396
+ suggestion='Merlin routing: call merlin_get_project_status, then use the merlin-resume skill.'
397
+ elif echo "$clean" | grep -qiE "progress|status|where are we|how far"; then
398
+ suggestion='Merlin routing: call merlin_get_project_status, then use the merlin-progress skill.'
399
+ elif echo "$clean" | grep -qiE "map codebase|understand this repo|learn this codebase|explore the architecture"; then
400
+ suggestion='Merlin routing: call merlin_search and merlin_get_context, then use the merlin-map-codebase skill before implementation.'
401
+ elif echo "$clean" | grep -qiE "bug|crash|error|not working|fix|failing|exception"; then
402
+ suggestion='Merlin routing: call merlin_get_context for the bug, then merlin_smart_route(task), then use merlin-verify after the fix.'
403
+ elif echo "$clean" | grep -qiE "refactor|cleanup|clean up|dry|restructure"; then
404
+ suggestion='Merlin routing: call merlin_get_context first, keep scope narrow, and use merlin-workflow plus merlin-verify.'
405
+ elif echo "$clean" | grep -qiE "oauth|auth|stripe|prisma|sdk|integration|api|graphql|railway|deploy"; then
406
+ suggestion='Merlin routing: call merlin_recommend_for_task(task), merlin_find_skill(query), and merlin_smart_route(task) before building.'
407
+ elif echo "$clean" | grep -qiE "build|add|create|implement|new feature|develop"; then
408
+ suggestion='Merlin routing: call merlin_help(task) or merlin_smart_route(task), gather Merlin context, then execute with implementation-focused agents.'
409
+ fi
410
+
411
+ [ -z "$suggestion" ] && echo "{}" && exit 0
412
+
413
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\\n' "\${suggestion//\"/\\\\\"}"
414
+ `;
415
+ }
416
+
417
+ function buildCodexBashPreToolHook() {
418
+ return `#!/usr/bin/env bash
419
+ set -euo pipefail
420
+ trap 'echo "{}"; exit 0' ERR
421
+
422
+ input=""
423
+ if [ ! -t 0 ]; then
424
+ input=$(cat 2>/dev/null || true)
425
+ fi
426
+
427
+ [ -z "$input" ] && echo "{}" && exit 0
428
+
429
+ command_str=""
430
+ if command -v jq >/dev/null 2>&1; then
431
+ command_str=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
432
+ else
433
+ command_str=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
434
+ fi
435
+
436
+ [ -z "$command_str" ] && echo "{}" && exit 0
437
+
438
+ block() {
439
+ printf '{"decision":"block","reason":"%s"}\\n' "$1"
440
+ exit 2
441
+ }
442
+
443
+ if echo "$command_str" | grep -qE '(curl|wget)\\s[^|]*\\|\\s*(bash|sh|zsh|python|ruby|perl)\\b' 2>/dev/null; then
444
+ block "Merlin blocked pipe-to-shell execution."
445
+ fi
446
+ if echo "$command_str" | grep -qE '(^|\\s|;|&&|\\|\\|)(sudo|su)\\s' 2>/dev/null; then
447
+ block "Merlin blocked privilege escalation."
448
+ fi
449
+ if echo "$command_str" | grep -qE 'git\\s+reset\\s+.*--hard|git\\s+clean\\s+.*-[a-z]*f|git\\s+push\\s+.*--force' 2>/dev/null; then
450
+ block "Merlin blocked a destructive git command."
451
+ fi
452
+ if echo "$command_str" | grep -qE '\\brm\\s+(-[a-z]*r[a-z]*f|-rf|-fr|--recursive)' 2>/dev/null; then
453
+ block "Merlin blocked a destructive rm command."
454
+ fi
455
+ if echo "$command_str" | grep -qE 'AKIA[0-9A-Z]{16}|sk-[A-Za-z0-9_-]{40,}|-----BEGIN (RSA|EC|DSA|OPENSSH|PRIVATE) KEY-----' 2>/dev/null; then
456
+ block "Merlin blocked a command containing a secret or private key."
457
+ fi
458
+
459
+ echo '{}'
460
+ `;
461
+ }
462
+
463
+ function buildCodexBashPostToolHook() {
464
+ return `#!/usr/bin/env bash
465
+ set -euo pipefail
466
+ trap 'echo "{}"; exit 0' ERR
467
+
468
+ input=""
469
+ if [ ! -t 0 ]; then
470
+ input=$(cat 2>/dev/null || true)
471
+ fi
472
+
473
+ [ -z "$input" ] && echo "{}" && exit 0
474
+
475
+ if command -v jq >/dev/null 2>&1; then
476
+ cmd=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
477
+ else
478
+ cmd=""
479
+ fi
480
+
481
+ if echo "$cmd" | grep -qiE 'npm test|pnpm test|yarn test|vitest|pytest|cargo test|go test'; then
482
+ printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Merlin note: summarize whether verification passed before moving on."}}\\n'
483
+ exit 0
484
+ fi
485
+
486
+ echo '{}'
487
+ `;
488
+ }
489
+
490
+ function buildCodexStopHook() {
491
+ return `#!/usr/bin/env bash
492
+ set -euo pipefail
493
+ trap 'echo "{}"; exit 0' ERR
494
+
495
+ MERLIN_HOME="\${HOME}/.codex/merlin"
496
+ mkdir -p "\${MERLIN_HOME}/sessions" 2>/dev/null || true
497
+
498
+ if command -v jq >/dev/null 2>&1; then
499
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
500
+ jq -n --arg ts "$ts" '{ timestamp: $ts }' > "\${MERLIN_HOME}/sessions/session-$(date +%s).json" 2>/dev/null || true
501
+ fi
502
+
503
+ echo '{}'
504
+ `;
505
+ }
506
+
507
+ function installCodexHookScripts(codexDir) {
508
+ const hooksRoot = path.join(codexDir, 'merlin', 'hooks');
509
+ const scripts = {
510
+ 'session-start.sh': buildCodexSessionStartHook(),
511
+ 'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
512
+ 'bash-pre-tool.sh': buildCodexBashPreToolHook(),
513
+ 'bash-post-tool.sh': buildCodexBashPostToolHook(),
514
+ 'stop.sh': buildCodexStopHook(),
515
+ };
516
+
517
+ ensureDir(hooksRoot);
518
+ for (const [name, content] of Object.entries(scripts)) {
519
+ const target = path.join(hooksRoot, name);
520
+ writeFile(target, content);
521
+ fs.chmodSync(target, '755');
522
+ }
523
+
524
+ return hooksRoot;
525
+ }
526
+
527
+ function buildCodexAgentSpecs() {
528
+ return [
529
+ {
530
+ filename: 'implementation-dev.toml',
531
+ name: 'implementation_dev',
532
+ description: 'Implementation specialist for bounded code changes with verification.',
533
+ model: 'gpt-5.4',
534
+ effort: 'medium',
535
+ sandbox: 'workspace-write',
536
+ nicknames: ['Forge', 'Patch', 'Build'],
537
+ instructions: 'Implement the requested change with minimal surface area. Inspect existing patterns first, preserve unrelated user edits, and verify behavior before handing back results.',
538
+ },
539
+ {
540
+ filename: 'tests-qa.toml',
541
+ name: 'tests_qa',
542
+ description: 'Testing specialist focused on regressions, missing cases, and verification.',
543
+ model: 'gpt-5.4-mini',
544
+ effort: 'medium',
545
+ sandbox: 'workspace-write',
546
+ nicknames: ['Spec', 'Check', 'Proof'],
547
+ instructions: 'Focus on tests, reproducibility, and verification evidence. Add or improve tests when justified, and report concrete coverage gaps when testing is blocked.',
548
+ },
549
+ {
550
+ filename: 'system-architect.toml',
551
+ name: 'system_architect',
552
+ description: 'Architecture specialist for design tradeoffs and decomposition.',
553
+ model: 'gpt-5.4',
554
+ effort: 'high',
555
+ sandbox: 'read-only',
556
+ nicknames: ['Northstar', 'Grid', 'Frame'],
557
+ instructions: 'Stay at the design layer unless explicitly asked to patch code. Map dependencies, tradeoffs, and sequencing clearly. Prefer the simplest architecture that satisfies the real constraints.',
558
+ },
559
+ {
560
+ filename: 'docs-keeper.toml',
561
+ name: 'docs_keeper',
562
+ description: 'Documentation specialist for READMEs, usage guides, and project docs.',
563
+ model: 'gpt-5.4-mini',
564
+ effort: 'medium',
565
+ sandbox: 'workspace-write',
566
+ nicknames: ['Ledger', 'Script', 'Index'],
567
+ instructions: 'Update documentation to match the actual implementation. Prefer concise, accurate edits tied to real behavior and verified paths.',
568
+ },
569
+ {
570
+ filename: 'hardening-guard.toml',
571
+ name: 'hardening_guard',
572
+ description: 'Security and resilience reviewer for edge cases and failure handling.',
573
+ model: 'gpt-5.4',
574
+ effort: 'high',
575
+ sandbox: 'read-only',
576
+ nicknames: ['Shield', 'Gate', 'Sentinel'],
577
+ instructions: 'Review for security issues, data loss, unsafe command usage, auth gaps, and weak failure handling. Lead with concrete risks and mitigations.',
578
+ },
579
+ {
580
+ filename: 'merlin-codebase-mapper.toml',
581
+ name: 'merlin_codebase_mapper',
582
+ description: 'Read-only codebase mapper for architecture, flows, and key files.',
583
+ model: 'gpt-5.4-mini',
584
+ effort: 'medium',
585
+ sandbox: 'read-only',
586
+ nicknames: ['Scout', 'Atlas', 'Survey'],
587
+ instructions: 'Map the relevant code paths, file ownership, and architectural seams before implementation. Prefer precise citations over broad summaries.',
588
+ },
589
+ {
590
+ filename: 'merlin-researcher.toml',
591
+ name: 'merlin_researcher',
592
+ description: 'Research specialist for external docs and fast-moving technical questions.',
593
+ model: 'gpt-5.4-mini',
594
+ effort: 'medium',
595
+ sandbox: 'read-only',
596
+ nicknames: ['Query', 'Lens', 'Signal'],
597
+ instructions: 'Use primary sources when research is needed. Distinguish verified facts from inference and return concise findings with citations.',
598
+ },
599
+ {
600
+ filename: 'merlin-verifier.toml',
601
+ name: 'merlin_verifier',
602
+ description: 'Verification specialist for validation, behavior checks, and release confidence.',
603
+ model: 'gpt-5.4-mini',
604
+ effort: 'medium',
605
+ sandbox: 'workspace-write',
606
+ nicknames: ['Audit', 'Pulse', 'Trace'],
607
+ instructions: 'Verify the requested outcome rather than merely restating changes. Prefer direct evidence from tests, builds, or code paths, and call out residual risks clearly.',
608
+ },
609
+ ];
610
+ }
611
+
612
+ function installCodexAgents(projectDir) {
613
+ const agentsDir = path.join(projectDir, '.codex', 'agents');
614
+ ensureDir(agentsDir);
615
+ const created = [];
616
+
617
+ for (const spec of buildCodexAgentSpecs()) {
618
+ const content = [
619
+ `name = "${spec.name}"`,
620
+ `description = "${spec.description}"`,
621
+ `model = "${spec.model}"`,
622
+ `model_reasoning_effort = "${spec.effort}"`,
623
+ `sandbox_mode = "${spec.sandbox}"`,
624
+ `nickname_candidates = [${spec.nicknames.map((n) => `"${n}"`).join(', ')}]`,
625
+ `developer_instructions = ${tomlMultiline(spec.instructions)}`,
626
+ '',
627
+ ].join('\n');
628
+ writeFile(path.join(agentsDir, spec.filename), content);
629
+ created.push(path.join(agentsDir, spec.filename));
630
+ }
631
+
632
+ return created;
633
+ }
634
+
635
+ function buildCodexSkillSpecs() {
636
+ return [
637
+ {
638
+ dir: 'merlin-discover',
639
+ content: `---
640
+ name: merlin-discover
641
+ description: Use when the user asks what Merlin can do, which Merlin features are available, or which Merlin path should be used for a task in Codex.
642
+ ---
643
+
644
+ Start with \`merlin_help\`.
645
+ If the user also has a concrete task, call \`merlin_help(task)\`.
646
+ Then choose the recommended route: direct Merlin tools, a Merlin skill, a custom Codex agent, or local execution.
647
+ Do not assume the user already knows internal Merlin names.`,
648
+ },
649
+ {
650
+ dir: 'merlin-map-codebase',
651
+ content: `---
652
+ name: merlin-map-codebase
653
+ description: Use when the user wants to understand a repository, onboard to a codebase, or map architecture before implementing changes.
654
+ ---
655
+
656
+ Before making architectural claims, call \`merlin_get_selected_repo\` and \`merlin_get_project_status\`.
657
+ Then use \`merlin_search\` and \`merlin_get_context\` to identify the core files, flows, and conventions relevant to the request.
658
+ When the task is large, prefer the \`merlin_codebase_mapper\` custom agent for bounded exploration.`,
659
+ },
660
+ {
661
+ dir: 'merlin-progress',
662
+ content: `---
663
+ name: merlin-progress
664
+ description: Use when the user asks for current status, progress, next steps, or where work left off in the repository.
665
+ ---
666
+
667
+ Call \`merlin_get_project_status\` first.
668
+ Summarize the current state, active tasks, and recommended next step.
669
+ If local planning files exist, reconcile them with Merlin status instead of trusting either source blindly.`,
670
+ },
671
+ {
672
+ dir: 'merlin-resume',
673
+ content: `---
674
+ name: merlin-resume
675
+ description: Use when the user is returning to a project and wants to continue, resume, or recover context quickly.
676
+ ---
677
+
678
+ Boot with Merlin tools first, then reconstruct the current state from project status, local planning files, and recent git changes.
679
+ Prefer concise orientation: what was in progress, what is blocked, and the most reasonable next action.`,
680
+ },
681
+ {
682
+ dir: 'merlin-workflow',
683
+ content: `---
684
+ name: merlin-workflow
685
+ description: Use when the task should be decomposed into architecture, implementation, testing, docs, or verification specialists instead of one monolithic pass.
686
+ ---
687
+
688
+ Use Merlin context before decomposition.
689
+ For bounded parallel work, use the custom Codex agents installed in \`.codex/agents\`.
690
+ Keep the critical path local when a side task would block the very next action.`,
691
+ },
692
+ {
693
+ dir: 'merlin-verify',
694
+ content: `---
695
+ name: merlin-verify
696
+ description: Use when implementation work is complete and you need focused validation, test review, or release confidence.
697
+ ---
698
+
699
+ Prefer direct evidence over narration.
700
+ Run or inspect the relevant verification commands, summarize what passed, what failed, and any residual risk.
701
+ Use the \`merlin_verifier\` or \`tests_qa\` agent for bounded validation passes when parallel work helps.`,
702
+ },
703
+ ];
704
+ }
705
+
706
+ function installCodexSkills(projectDir) {
707
+ const skillsRoot = path.join(projectDir, '.agents', 'skills');
708
+ ensureDir(skillsRoot);
709
+ const created = [];
710
+
711
+ for (const spec of buildCodexSkillSpecs()) {
712
+ const skillDir = path.join(skillsRoot, spec.dir);
713
+ ensureDir(skillDir);
714
+ writeFile(path.join(skillDir, 'SKILL.md'), spec.content.trim() + '\n');
715
+ created.push(path.join(skillDir, 'SKILL.md'));
716
+ }
717
+
718
+ return created;
719
+ }
720
+
118
721
  // ---------------------------------------------------------------------------
119
722
  // Codex CLI adapter
120
723
  // Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
@@ -130,7 +733,7 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
130
733
 
131
734
  // Write AGENTS.md
132
735
  const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
133
- const agentsMd = buildAgentsMd();
736
+ const agentsMd = buildInstructionMd('codex');
134
737
  const existingAgentsMd = fs.existsSync(agentsMdPath)
135
738
  ? fs.readFileSync(agentsMdPath, 'utf8')
136
739
  : '';
@@ -145,29 +748,62 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
145
748
  results.push('Wrote ~/.codex/AGENTS.md');
146
749
  }
147
750
 
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(', ')}]`
751
+ // Install Codex hook scripts + hooks.json
752
+ const hooksRoot = installCodexHookScripts(rt.configDir);
753
+ const hooksJsonPath = path.join(rt.configDir, 'hooks.json');
754
+ const existingHooks = fs.existsSync(hooksJsonPath)
755
+ ? fs.readFileSync(hooksJsonPath, 'utf8')
153
756
  : '';
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` : ''}`;
757
+ writeFile(hooksJsonPath, mergeHooksJson(existingHooks, hooksRoot));
758
+ results.push('Installed ~/.codex/hooks.json');
159
759
 
760
+ // Write / update config.toml with MCP section and Codex features
761
+ const configTomlPath = path.join(rt.configDir, 'config.toml');
762
+ const { command, args } = buildMcpCommand(useGlobalBinary);
160
763
  let tomlContent = fs.existsSync(configTomlPath)
161
764
  ? fs.readFileSync(configTomlPath, 'utf8')
162
765
  : '';
163
766
 
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');
767
+ tomlContent = removeTomlSection(removeTomlSection(tomlContent, 'mcp.merlin'), 'mcp_servers.merlin');
768
+ tomlContent = removeTomlSection(tomlContent, 'features');
769
+ tomlContent = removeTomlSection(tomlContent, 'tui');
770
+ tomlContent = removeTomlSection(tomlContent, 'agents');
771
+ tomlContent = removeTomlSection(tomlContent, 'shell_environment_policy');
772
+ tomlContent = tomlContent
773
+ .replace(/^project_doc_fallback_filenames = .*$/gm, '')
774
+ .replace(/^codex_hooks = .*$/gm, '')
775
+ .replace(/^notifications = .*$/gm, '')
776
+ .replace(/^max_threads = .*$/gm, '')
777
+ .replace(/^max_depth = .*$/gm, '')
778
+ .replace(/^inherit = .*$/gm, '')
779
+ .replace(/^startup_timeout_sec = .*$/gm, '')
780
+ .replace(/^tool_timeout_sec = .*$/gm, '')
781
+ .replace(/^command = "merlin-brain"\n?/m, '')
782
+ .replace(/^args = \[.*create-merlin-brain.*\]\n?/m, '')
783
+ .replace(/^args = \[.*packages\/create-merlin-brain\/bin\/serve\.js.*\]\n?/m, '')
784
+ .trimEnd();
785
+ tomlContent = ensureTomlSectionLine(tomlContent, 'features', 'codex_hooks = true');
786
+ tomlContent = ensureTomlSectionLine(tomlContent, 'tui', 'notifications = true');
787
+ tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_threads = 8');
788
+ tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_depth = 2');
789
+ tomlContent = ensureTomlSectionLine(tomlContent, 'shell_environment_policy', 'inherit = "all"');
790
+ tomlContent = ensureTomlTopLevelLine(tomlContent, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
791
+
792
+ const mcpLines = [
793
+ `command = "${command}"`,
794
+ args ? `args = [${args.map((a) => `"${a}"`).join(', ')}]` : null,
795
+ 'startup_timeout_sec = 20',
796
+ 'tool_timeout_sec = 120',
797
+ ].filter(Boolean).join('\n');
798
+
799
+ tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin', mcpLines);
800
+ if (apiKey) {
801
+ tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin.env', `MERLIN_API_KEY = "${apiKey}"`);
169
802
  }
170
803
 
804
+ writeFile(configTomlPath, tomlContent.trimEnd() + '\n');
805
+ results.push('Configured Merlin MCP and Codex features in ~/.codex/config.toml');
806
+
171
807
  return results;
172
808
  }
173
809
 
@@ -216,8 +852,8 @@ function configureOpenCode(rt, useGlobalBinary, apiKey) {
216
852
  results.push('AGENTS.md already has Merlin (skipped)');
217
853
  } else {
218
854
  const combined = existingAgentsMd
219
- ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
220
- : buildAgentsMd();
855
+ ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('opencode')
856
+ : buildInstructionMd('opencode');
221
857
  fs.writeFileSync(agentsMdPath, combined);
222
858
  results.push('Wrote ~/.opencode/AGENTS.md');
223
859
  }
@@ -246,8 +882,8 @@ function configureGemini(rt, useGlobalBinary, apiKey) {
246
882
  results.push('GEMINI.md already has Merlin (skipped)');
247
883
  } else {
248
884
  const combined = existingGeminiMd
249
- ? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
250
- : buildAgentsMd();
885
+ ? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('gemini')
886
+ : buildInstructionMd('gemini');
251
887
  fs.writeFileSync(geminiMdPath, combined);
252
888
  results.push('Wrote ~/.gemini/GEMINI.md');
253
889
  }
@@ -297,22 +933,67 @@ function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
297
933
  ? ['codex', 'opencode', 'gemini']
298
934
  : runtimeIds;
299
935
 
300
- const agentsMd = buildAgentsMd();
301
-
302
936
  // AGENTS.md — used by Codex and OpenCode
303
937
  if (targets.includes('codex') || targets.includes('opencode')) {
304
938
  const agentsMdPath = path.join(projectDir, 'AGENTS.md');
305
939
  if (!fs.existsSync(agentsMdPath)) {
306
- fs.writeFileSync(agentsMdPath, agentsMd);
940
+ const instructionMd = targets.includes('codex')
941
+ ? buildInstructionMd('codex')
942
+ : buildInstructionMd('opencode');
943
+ fs.writeFileSync(agentsMdPath, instructionMd);
307
944
  results.push(`Created ${agentsMdPath}`);
308
945
  }
309
946
  }
310
947
 
948
+ if (targets.includes('codex')) {
949
+ const projectConfigPath = path.join(projectDir, '.codex', 'config.toml');
950
+ let projectConfig = fs.existsSync(projectConfigPath)
951
+ ? fs.readFileSync(projectConfigPath, 'utf8')
952
+ : '';
953
+ projectConfig = ensureTomlTopLevelLine(projectConfig, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
954
+ projectConfig = ensureTomlSectionLine(projectConfig, 'features', 'codex_hooks = true');
955
+ projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_threads = 8');
956
+ projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_depth = 2');
957
+ writeFile(projectConfigPath, projectConfig.trimEnd() + '\n');
958
+ results.push(`Created ${projectConfigPath}`);
959
+
960
+ const projectHooksRoot = path.join(projectDir, '.codex', 'merlin', 'hooks');
961
+ ensureDir(projectHooksRoot);
962
+ const hookSpecs = {
963
+ 'session-start.sh': buildCodexSessionStartHook(),
964
+ 'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
965
+ 'bash-pre-tool.sh': buildCodexBashPreToolHook(),
966
+ 'bash-post-tool.sh': buildCodexBashPostToolHook(),
967
+ 'stop.sh': buildCodexStopHook(),
968
+ };
969
+ for (const [name, content] of Object.entries(hookSpecs)) {
970
+ const hookPath = path.join(projectHooksRoot, name);
971
+ writeFile(hookPath, content);
972
+ fs.chmodSync(hookPath, '755');
973
+ }
974
+ const projectHooksPath = path.join(projectDir, '.codex', 'hooks.json');
975
+ const existingProjectHooks = fs.existsSync(projectHooksPath)
976
+ ? fs.readFileSync(projectHooksPath, 'utf8')
977
+ : '';
978
+ writeFile(projectHooksPath, mergeHooksJson(existingProjectHooks, projectHooksRoot));
979
+ results.push(`Created ${path.join(projectDir, '.codex', 'hooks.json')}`);
980
+
981
+ const createdAgents = installCodexAgents(projectDir);
982
+ if (createdAgents.length) {
983
+ results.push(`Installed ${createdAgents.length} Codex agents`);
984
+ }
985
+
986
+ const createdSkills = installCodexSkills(projectDir);
987
+ if (createdSkills.length) {
988
+ results.push(`Installed ${createdSkills.length} Codex skills`);
989
+ }
990
+ }
991
+
311
992
  // GEMINI.md — used by Gemini CLI
312
993
  if (targets.includes('gemini')) {
313
994
  const geminiMdPath = path.join(projectDir, 'GEMINI.md');
314
995
  if (!fs.existsSync(geminiMdPath)) {
315
- fs.writeFileSync(geminiMdPath, agentsMd);
996
+ fs.writeFileSync(geminiMdPath, buildInstructionMd('gemini'));
316
997
  results.push(`Created ${geminiMdPath}`);
317
998
  }
318
999
  }
@@ -392,5 +1073,5 @@ module.exports = {
392
1073
  detectRuntimes,
393
1074
  configureRuntimes,
394
1075
  generateProjectRuntimeFiles,
395
- buildAgentsMd,
1076
+ buildInstructionMd,
396
1077
  };