create-merlin-brain 3.22.0 → 3.23.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 (76) hide show
  1. package/README.md +22 -4
  2. package/bin/merlin-ask.cjs +111 -0
  3. package/bin/merlin-cli.cjs +22 -0
  4. package/bin/runtime-adapters.cjs +678 -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 +45 -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/hud.d.ts +13 -0
  25. package/dist/server/tools/hud.d.ts.map +1 -0
  26. package/dist/server/tools/hud.js +295 -0
  27. package/dist/server/tools/hud.js.map +1 -0
  28. package/dist/server/tools/index.d.ts +4 -0
  29. package/dist/server/tools/index.d.ts.map +1 -1
  30. package/dist/server/tools/index.js +4 -0
  31. package/dist/server/tools/index.js.map +1 -1
  32. package/dist/server/tools/provider-ask.d.ts +10 -0
  33. package/dist/server/tools/provider-ask.d.ts.map +1 -0
  34. package/dist/server/tools/provider-ask.js +234 -0
  35. package/dist/server/tools/provider-ask.js.map +1 -0
  36. package/dist/server/tools/rate-limit.d.ts +8 -0
  37. package/dist/server/tools/rate-limit.d.ts.map +1 -0
  38. package/dist/server/tools/rate-limit.js +184 -0
  39. package/dist/server/tools/rate-limit.js.map +1 -0
  40. package/dist/server/tools/skills.d.ts +16 -0
  41. package/dist/server/tools/skills.d.ts.map +1 -0
  42. package/dist/server/tools/skills.js +326 -0
  43. package/dist/server/tools/skills.js.map +1 -0
  44. package/dist/server/tools/team-workers.d.ts +7 -0
  45. package/dist/server/tools/team-workers.d.ts.map +1 -0
  46. package/dist/server/tools/team-workers.js +271 -0
  47. package/dist/server/tools/team-workers.js.map +1 -0
  48. package/dist/server/utils/merlin-manifest.d.ts +6 -1
  49. package/dist/server/utils/merlin-manifest.d.ts.map +1 -1
  50. package/dist/server/utils/merlin-manifest.js +34 -1
  51. package/dist/server/utils/merlin-manifest.js.map +1 -1
  52. package/files/CLAUDE.md +22 -0
  53. package/files/hooks/rate-limit-watch.sh +120 -0
  54. package/files/hooks/statusline.sh +148 -0
  55. package/files/merlin/skills/SKILLS-INDEX.md +82 -0
  56. package/files/merlin/skills/automation/payments.md +14 -0
  57. package/files/merlin/skills/automation/webhooks.md +14 -0
  58. package/files/merlin/skills/coding/accessibility.md +14 -0
  59. package/files/merlin/skills/coding/api-design.md +14 -0
  60. package/files/merlin/skills/coding/debug-mode.md +14 -0
  61. package/files/merlin/skills/coding/focus-mode.md +14 -0
  62. package/files/merlin/skills/coding/loop.md +14 -0
  63. package/files/merlin/skills/coding/performance.md +14 -0
  64. package/files/merlin/skills/coding/react-patterns.md +51 -0
  65. package/files/merlin/skills/coding/security-hardening.md +56 -0
  66. package/files/merlin/skills/coding/verify.md +14 -0
  67. package/files/merlin/skills/communication/dispatcher.md +40 -0
  68. package/files/merlin/skills/communication/email-gmail.md +31 -0
  69. package/files/merlin/skills/communication/telegram.md +50 -0
  70. package/files/merlin/skills/communication/whatsapp.md +47 -0
  71. package/files/merlin/skills/data/google-sheets.md +14 -0
  72. package/files/merlin/skills/design/animation.md +14 -0
  73. package/files/merlin/skills/devops/docker-containers.md +14 -0
  74. package/files/merlin/skills/research/brainstorm.md +14 -0
  75. package/files/merlin/skills/testing/tdd-workflow.md +58 -0
  76. 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,165 @@ 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
+ ## Codex + Merlin Workflow
189
+
190
+ - For codebase questions, start with \`merlin_search("query")\` or \`merlin_get_context("task")\`.
191
+ - Before every code edit, call \`merlin_get_context("your task")\`.
192
+ - When local file search is still needed, prefer fast repo-native tools such as \`rg\`.
193
+ - Use Codex's normal execution style: inspect first, edit surgically, verify after changes.
194
+ - Keep progress updates concise and factual while work is in flight.
195
+ - Prefer Merlin skills and custom agents installed in this repo when the task matches them.
196
+
197
+ ## Editing Discipline
198
+
199
+ - Prefer \`apply_patch\` for manual edits.
200
+ - Avoid broad rewrites when a narrow patch is sufficient.
201
+ - Preserve user changes you did not make.
202
+ - Verify behavior after implementation work before claiming completion.
203
+
204
+ ## Delegation
205
+
206
+ - Use Merlin tools first for routing and context.
207
+ - Use Codex sub-agents only for clearly bounded side tasks that can run in parallel.
208
+ - Keep the critical path local when the next step depends on immediate code understanding.
209
+
210
+ ## MCP Tools Available
211
+
212
+ - \`merlin_get_selected_repo\` — identify the active repository
213
+ - \`merlin_get_project_status\` — load project state and active tasks
214
+ - \`merlin_get_context(task)\` — fetch targeted implementation context
215
+ - \`merlin_find_files(query)\` — locate files by purpose
216
+ - \`merlin_search(query)\` — semantic code search
217
+
218
+ ## Defaults
219
+
220
+ - Search before writing.
221
+ - Reuse existing patterns before introducing new ones.
222
+ - If Merlin Sights is unavailable, continue with disciplined local exploration instead of blocking.
223
+ `;
224
+ }
225
+
73
226
  return `# Merlin Brain — AI Development System
74
227
 
75
228
  > Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
@@ -115,6 +268,425 @@ The Merlin MCP server exposes these tools:
115
268
  `;
116
269
  }
117
270
 
271
+ function buildCodexHooksJson(hooksRoot) {
272
+ return JSON.stringify({
273
+ hooks: {
274
+ SessionStart: [
275
+ {
276
+ matcher: 'startup|resume',
277
+ hooks: [
278
+ {
279
+ type: 'command',
280
+ command: `bash "${path.join(hooksRoot, 'session-start.sh')}"`,
281
+ statusMessage: 'Merlin booting',
282
+ },
283
+ ],
284
+ },
285
+ ],
286
+ UserPromptSubmit: [
287
+ {
288
+ hooks: [
289
+ {
290
+ type: 'command',
291
+ command: `bash "${path.join(hooksRoot, 'user-prompt-router.sh')}"`,
292
+ statusMessage: 'Merlin routing',
293
+ },
294
+ ],
295
+ },
296
+ ],
297
+ PreToolUse: [
298
+ {
299
+ matcher: 'Bash',
300
+ hooks: [
301
+ {
302
+ type: 'command',
303
+ command: `bash "${path.join(hooksRoot, 'bash-pre-tool.sh')}"`,
304
+ statusMessage: 'Merlin command guard',
305
+ },
306
+ ],
307
+ },
308
+ ],
309
+ PostToolUse: [
310
+ {
311
+ matcher: 'Bash',
312
+ hooks: [
313
+ {
314
+ type: 'command',
315
+ command: `bash "${path.join(hooksRoot, 'bash-post-tool.sh')}"`,
316
+ statusMessage: 'Merlin command review',
317
+ },
318
+ ],
319
+ },
320
+ ],
321
+ Stop: [
322
+ {
323
+ hooks: [
324
+ {
325
+ type: 'command',
326
+ command: `bash "${path.join(hooksRoot, 'stop.sh')}"`,
327
+ statusMessage: 'Merlin wrap-up',
328
+ },
329
+ ],
330
+ },
331
+ ],
332
+ },
333
+ }, null, 2) + '\n';
334
+ }
335
+
336
+ function buildCodexSessionStartHook() {
337
+ return `#!/usr/bin/env bash
338
+ set -euo pipefail
339
+ trap 'echo "{}"; exit 0' ERR
340
+
341
+ MERLIN_HOME="\${HOME}/.codex/merlin"
342
+ mkdir -p "\${MERLIN_HOME}/analytics" "\${MERLIN_HOME}/sessions" 2>/dev/null || true
343
+
344
+ _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."
345
+ _context="\${_context} Use Merlin context before edits: call merlin_get_context(task)."
346
+ _context="\${_context} Prefer Merlin skills and custom agents installed in this repository when they match the task."
347
+ _context="\${_context} Keep the Codex experience pragmatic: inspect first, patch surgically, verify before claiming completion."
348
+ _context="\${_context} Prefix visible progress updates with the Merlin badge when practical: ⟡🔮 MERLIN ›"
349
+
350
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\\n' "\${_context//\"/\\\\\"}"
351
+ `;
352
+ }
353
+
354
+ function buildCodexUserPromptRouterHook() {
355
+ return `#!/usr/bin/env bash
356
+ set -euo pipefail
357
+ trap 'echo "{}"; exit 0' ERR
358
+
359
+ input=""
360
+ if [ ! -t 0 ]; then
361
+ input=$(cat 2>/dev/null || true)
362
+ fi
363
+
364
+ [ -z "$input" ] && echo "{}" && exit 0
365
+
366
+ prompt=""
367
+ if command -v jq >/dev/null 2>&1; then
368
+ prompt=$(echo "$input" | jq -r '.prompt // .userPrompt // empty' 2>/dev/null || true)
369
+ else
370
+ prompt=$(echo "$input" | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\\(.*\\)".*/\\1/' 2>/dev/null || true)
371
+ fi
372
+
373
+ [ -z "$prompt" ] && echo "{}" && exit 0
374
+
375
+ clean=$(printf '%s' "$prompt" | sed -E -e 's/<[^>]+>//g' -e 's|https?://[^ ]*||g' -e 's|/[a-zA-Z0-9._/-]+||g' -e 's/\`[^\`]*\`//g')
376
+
377
+ suggestion=""
378
+ if echo "$clean" | grep -qiE "resume|pick up|continue|where were we"; then
379
+ suggestion='Merlin routing: resume context and use the merlin-resume skill.'
380
+ elif echo "$clean" | grep -qiE "progress|status|where are we|how far"; then
381
+ suggestion='Merlin routing: use the merlin-progress skill.'
382
+ elif echo "$clean" | grep -qiE "map codebase|understand this repo|learn this codebase|explore the architecture"; then
383
+ suggestion='Merlin routing: use the merlin-map-codebase skill before implementation.'
384
+ elif echo "$clean" | grep -qiE "bug|crash|error|not working|fix|failing|exception"; then
385
+ suggestion='Merlin routing: debug first, then route bounded work to implementation and verification agents.'
386
+ elif echo "$clean" | grep -qiE "refactor|cleanup|clean up|dry|restructure"; then
387
+ suggestion='Merlin routing: keep scope narrow, preserve behavior, and use implementation plus verification agents.'
388
+ elif echo "$clean" | grep -qiE "build|add|create|implement|new feature|develop"; then
389
+ suggestion='Merlin routing: gather Merlin context first, then execute with implementation-focused agents.'
390
+ fi
391
+
392
+ [ -z "$suggestion" ] && echo "{}" && exit 0
393
+
394
+ printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\\n' "\${suggestion//\"/\\\\\"}"
395
+ `;
396
+ }
397
+
398
+ function buildCodexBashPreToolHook() {
399
+ return `#!/usr/bin/env bash
400
+ set -euo pipefail
401
+ trap 'echo "{}"; exit 0' ERR
402
+
403
+ input=""
404
+ if [ ! -t 0 ]; then
405
+ input=$(cat 2>/dev/null || true)
406
+ fi
407
+
408
+ [ -z "$input" ] && echo "{}" && exit 0
409
+
410
+ command_str=""
411
+ if command -v jq >/dev/null 2>&1; then
412
+ command_str=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
413
+ else
414
+ command_str=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
415
+ fi
416
+
417
+ [ -z "$command_str" ] && echo "{}" && exit 0
418
+
419
+ block() {
420
+ printf '{"decision":"block","reason":"%s"}\\n' "$1"
421
+ exit 2
422
+ }
423
+
424
+ if echo "$command_str" | grep -qE '(curl|wget)\\s[^|]*\\|\\s*(bash|sh|zsh|python|ruby|perl)\\b' 2>/dev/null; then
425
+ block "Merlin blocked pipe-to-shell execution."
426
+ fi
427
+ if echo "$command_str" | grep -qE '(^|\\s|;|&&|\\|\\|)(sudo|su)\\s' 2>/dev/null; then
428
+ block "Merlin blocked privilege escalation."
429
+ fi
430
+ 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
431
+ block "Merlin blocked a destructive git command."
432
+ fi
433
+ if echo "$command_str" | grep -qE '\\brm\\s+(-[a-z]*r[a-z]*f|-rf|-fr|--recursive)' 2>/dev/null; then
434
+ block "Merlin blocked a destructive rm command."
435
+ fi
436
+ 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
437
+ block "Merlin blocked a command containing a secret or private key."
438
+ fi
439
+
440
+ echo '{}'
441
+ `;
442
+ }
443
+
444
+ function buildCodexBashPostToolHook() {
445
+ return `#!/usr/bin/env bash
446
+ set -euo pipefail
447
+ trap 'echo "{}"; exit 0' ERR
448
+
449
+ input=""
450
+ if [ ! -t 0 ]; then
451
+ input=$(cat 2>/dev/null || true)
452
+ fi
453
+
454
+ [ -z "$input" ] && echo "{}" && exit 0
455
+
456
+ if command -v jq >/dev/null 2>&1; then
457
+ cmd=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
458
+ else
459
+ cmd=""
460
+ fi
461
+
462
+ if echo "$cmd" | grep -qiE 'npm test|pnpm test|yarn test|vitest|pytest|cargo test|go test'; then
463
+ printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"Merlin note: summarize whether verification passed before moving on."}}\\n'
464
+ exit 0
465
+ fi
466
+
467
+ echo '{}'
468
+ `;
469
+ }
470
+
471
+ function buildCodexStopHook() {
472
+ return `#!/usr/bin/env bash
473
+ set -euo pipefail
474
+ trap 'echo "{}"; exit 0' ERR
475
+
476
+ MERLIN_HOME="\${HOME}/.codex/merlin"
477
+ mkdir -p "\${MERLIN_HOME}/sessions" 2>/dev/null || true
478
+
479
+ if command -v jq >/dev/null 2>&1; then
480
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
481
+ jq -n --arg ts "$ts" '{ timestamp: $ts }' > "\${MERLIN_HOME}/sessions/session-$(date +%s).json" 2>/dev/null || true
482
+ fi
483
+
484
+ echo '{}'
485
+ `;
486
+ }
487
+
488
+ function installCodexHookScripts(codexDir) {
489
+ const hooksRoot = path.join(codexDir, 'merlin', 'hooks');
490
+ const scripts = {
491
+ 'session-start.sh': buildCodexSessionStartHook(),
492
+ 'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
493
+ 'bash-pre-tool.sh': buildCodexBashPreToolHook(),
494
+ 'bash-post-tool.sh': buildCodexBashPostToolHook(),
495
+ 'stop.sh': buildCodexStopHook(),
496
+ };
497
+
498
+ ensureDir(hooksRoot);
499
+ for (const [name, content] of Object.entries(scripts)) {
500
+ const target = path.join(hooksRoot, name);
501
+ writeFile(target, content);
502
+ fs.chmodSync(target, '755');
503
+ }
504
+
505
+ return hooksRoot;
506
+ }
507
+
508
+ function buildCodexAgentSpecs() {
509
+ return [
510
+ {
511
+ filename: 'implementation-dev.toml',
512
+ name: 'implementation_dev',
513
+ description: 'Implementation specialist for bounded code changes with verification.',
514
+ model: 'gpt-5.4',
515
+ effort: 'medium',
516
+ sandbox: 'workspace-write',
517
+ nicknames: ['Forge', 'Patch', 'Build'],
518
+ instructions: 'Implement the requested change with minimal surface area. Inspect existing patterns first, preserve unrelated user edits, and verify behavior before handing back results.',
519
+ },
520
+ {
521
+ filename: 'tests-qa.toml',
522
+ name: 'tests_qa',
523
+ description: 'Testing specialist focused on regressions, missing cases, and verification.',
524
+ model: 'gpt-5.4-mini',
525
+ effort: 'medium',
526
+ sandbox: 'workspace-write',
527
+ nicknames: ['Spec', 'Check', 'Proof'],
528
+ instructions: 'Focus on tests, reproducibility, and verification evidence. Add or improve tests when justified, and report concrete coverage gaps when testing is blocked.',
529
+ },
530
+ {
531
+ filename: 'system-architect.toml',
532
+ name: 'system_architect',
533
+ description: 'Architecture specialist for design tradeoffs and decomposition.',
534
+ model: 'gpt-5.4',
535
+ effort: 'high',
536
+ sandbox: 'read-only',
537
+ nicknames: ['Northstar', 'Grid', 'Frame'],
538
+ 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.',
539
+ },
540
+ {
541
+ filename: 'docs-keeper.toml',
542
+ name: 'docs_keeper',
543
+ description: 'Documentation specialist for READMEs, usage guides, and project docs.',
544
+ model: 'gpt-5.4-mini',
545
+ effort: 'medium',
546
+ sandbox: 'workspace-write',
547
+ nicknames: ['Ledger', 'Script', 'Index'],
548
+ instructions: 'Update documentation to match the actual implementation. Prefer concise, accurate edits tied to real behavior and verified paths.',
549
+ },
550
+ {
551
+ filename: 'hardening-guard.toml',
552
+ name: 'hardening_guard',
553
+ description: 'Security and resilience reviewer for edge cases and failure handling.',
554
+ model: 'gpt-5.4',
555
+ effort: 'high',
556
+ sandbox: 'read-only',
557
+ nicknames: ['Shield', 'Gate', 'Sentinel'],
558
+ instructions: 'Review for security issues, data loss, unsafe command usage, auth gaps, and weak failure handling. Lead with concrete risks and mitigations.',
559
+ },
560
+ {
561
+ filename: 'merlin-codebase-mapper.toml',
562
+ name: 'merlin_codebase_mapper',
563
+ description: 'Read-only codebase mapper for architecture, flows, and key files.',
564
+ model: 'gpt-5.4-mini',
565
+ effort: 'medium',
566
+ sandbox: 'read-only',
567
+ nicknames: ['Scout', 'Atlas', 'Survey'],
568
+ instructions: 'Map the relevant code paths, file ownership, and architectural seams before implementation. Prefer precise citations over broad summaries.',
569
+ },
570
+ {
571
+ filename: 'merlin-researcher.toml',
572
+ name: 'merlin_researcher',
573
+ description: 'Research specialist for external docs and fast-moving technical questions.',
574
+ model: 'gpt-5.4-mini',
575
+ effort: 'medium',
576
+ sandbox: 'read-only',
577
+ nicknames: ['Query', 'Lens', 'Signal'],
578
+ instructions: 'Use primary sources when research is needed. Distinguish verified facts from inference and return concise findings with citations.',
579
+ },
580
+ {
581
+ filename: 'merlin-verifier.toml',
582
+ name: 'merlin_verifier',
583
+ description: 'Verification specialist for validation, behavior checks, and release confidence.',
584
+ model: 'gpt-5.4-mini',
585
+ effort: 'medium',
586
+ sandbox: 'workspace-write',
587
+ nicknames: ['Audit', 'Pulse', 'Trace'],
588
+ 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.',
589
+ },
590
+ ];
591
+ }
592
+
593
+ function installCodexAgents(projectDir) {
594
+ const agentsDir = path.join(projectDir, '.codex', 'agents');
595
+ ensureDir(agentsDir);
596
+ const created = [];
597
+
598
+ for (const spec of buildCodexAgentSpecs()) {
599
+ const content = [
600
+ `name = "${spec.name}"`,
601
+ `description = "${spec.description}"`,
602
+ `model = "${spec.model}"`,
603
+ `model_reasoning_effort = "${spec.effort}"`,
604
+ `sandbox_mode = "${spec.sandbox}"`,
605
+ `nickname_candidates = [${spec.nicknames.map((n) => `"${n}"`).join(', ')}]`,
606
+ `developer_instructions = ${tomlMultiline(spec.instructions)}`,
607
+ '',
608
+ ].join('\n');
609
+ writeFile(path.join(agentsDir, spec.filename), content);
610
+ created.push(path.join(agentsDir, spec.filename));
611
+ }
612
+
613
+ return created;
614
+ }
615
+
616
+ function buildCodexSkillSpecs() {
617
+ return [
618
+ {
619
+ dir: 'merlin-map-codebase',
620
+ content: `---
621
+ name: merlin-map-codebase
622
+ description: Use when the user wants to understand a repository, onboard to a codebase, or map architecture before implementing changes.
623
+ ---
624
+
625
+ Before making architectural claims, call \`merlin_get_selected_repo\` and \`merlin_get_project_status\`.
626
+ Then use \`merlin_search\` and \`merlin_get_context\` to identify the core files, flows, and conventions relevant to the request.
627
+ When the task is large, prefer the \`merlin_codebase_mapper\` custom agent for bounded exploration.`,
628
+ },
629
+ {
630
+ dir: 'merlin-progress',
631
+ content: `---
632
+ name: merlin-progress
633
+ description: Use when the user asks for current status, progress, next steps, or where work left off in the repository.
634
+ ---
635
+
636
+ Call \`merlin_get_project_status\` first.
637
+ Summarize the current state, active tasks, and recommended next step.
638
+ If local planning files exist, reconcile them with Merlin status instead of trusting either source blindly.`,
639
+ },
640
+ {
641
+ dir: 'merlin-resume',
642
+ content: `---
643
+ name: merlin-resume
644
+ description: Use when the user is returning to a project and wants to continue, resume, or recover context quickly.
645
+ ---
646
+
647
+ Boot with Merlin tools first, then reconstruct the current state from project status, local planning files, and recent git changes.
648
+ Prefer concise orientation: what was in progress, what is blocked, and the most reasonable next action.`,
649
+ },
650
+ {
651
+ dir: 'merlin-workflow',
652
+ content: `---
653
+ name: merlin-workflow
654
+ description: Use when the task should be decomposed into architecture, implementation, testing, docs, or verification specialists instead of one monolithic pass.
655
+ ---
656
+
657
+ Use Merlin context before decomposition.
658
+ For bounded parallel work, use the custom Codex agents installed in \`.codex/agents\`.
659
+ Keep the critical path local when a side task would block the very next action.`,
660
+ },
661
+ {
662
+ dir: 'merlin-verify',
663
+ content: `---
664
+ name: merlin-verify
665
+ description: Use when implementation work is complete and you need focused validation, test review, or release confidence.
666
+ ---
667
+
668
+ Prefer direct evidence over narration.
669
+ Run or inspect the relevant verification commands, summarize what passed, what failed, and any residual risk.
670
+ Use the \`merlin_verifier\` or \`tests_qa\` agent for bounded validation passes when parallel work helps.`,
671
+ },
672
+ ];
673
+ }
674
+
675
+ function installCodexSkills(projectDir) {
676
+ const skillsRoot = path.join(projectDir, '.agents', 'skills');
677
+ ensureDir(skillsRoot);
678
+ const created = [];
679
+
680
+ for (const spec of buildCodexSkillSpecs()) {
681
+ const skillDir = path.join(skillsRoot, spec.dir);
682
+ ensureDir(skillDir);
683
+ writeFile(path.join(skillDir, 'SKILL.md'), spec.content.trim() + '\n');
684
+ created.push(path.join(skillDir, 'SKILL.md'));
685
+ }
686
+
687
+ return created;
688
+ }
689
+
118
690
  // ---------------------------------------------------------------------------
119
691
  // Codex CLI adapter
120
692
  // Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
@@ -130,7 +702,7 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
130
702
 
131
703
  // Write AGENTS.md
132
704
  const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
133
- const agentsMd = buildAgentsMd();
705
+ const agentsMd = buildInstructionMd('codex');
134
706
  const existingAgentsMd = fs.existsSync(agentsMdPath)
135
707
  ? fs.readFileSync(agentsMdPath, 'utf8')
136
708
  : '';
@@ -145,29 +717,62 @@ function configureCodex(rt, useGlobalBinary, apiKey) {
145
717
  results.push('Wrote ~/.codex/AGENTS.md');
146
718
  }
147
719
 
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(', ')}]`
720
+ // Install Codex hook scripts + hooks.json
721
+ const hooksRoot = installCodexHookScripts(rt.configDir);
722
+ const hooksJsonPath = path.join(rt.configDir, 'hooks.json');
723
+ const existingHooks = fs.existsSync(hooksJsonPath)
724
+ ? fs.readFileSync(hooksJsonPath, 'utf8')
153
725
  : '';
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` : ''}`;
726
+ writeFile(hooksJsonPath, mergeHooksJson(existingHooks, hooksRoot));
727
+ results.push('Installed ~/.codex/hooks.json');
159
728
 
729
+ // Write / update config.toml with MCP section and Codex features
730
+ const configTomlPath = path.join(rt.configDir, 'config.toml');
731
+ const { command, args } = buildMcpCommand(useGlobalBinary);
160
732
  let tomlContent = fs.existsSync(configTomlPath)
161
733
  ? fs.readFileSync(configTomlPath, 'utf8')
162
734
  : '';
163
735
 
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');
736
+ tomlContent = removeTomlSection(removeTomlSection(tomlContent, 'mcp.merlin'), 'mcp_servers.merlin');
737
+ tomlContent = removeTomlSection(tomlContent, 'features');
738
+ tomlContent = removeTomlSection(tomlContent, 'tui');
739
+ tomlContent = removeTomlSection(tomlContent, 'agents');
740
+ tomlContent = removeTomlSection(tomlContent, 'shell_environment_policy');
741
+ tomlContent = tomlContent
742
+ .replace(/^project_doc_fallback_filenames = .*$/gm, '')
743
+ .replace(/^codex_hooks = .*$/gm, '')
744
+ .replace(/^notifications = .*$/gm, '')
745
+ .replace(/^max_threads = .*$/gm, '')
746
+ .replace(/^max_depth = .*$/gm, '')
747
+ .replace(/^inherit = .*$/gm, '')
748
+ .replace(/^startup_timeout_sec = .*$/gm, '')
749
+ .replace(/^tool_timeout_sec = .*$/gm, '')
750
+ .replace(/^command = "merlin-brain"\n?/m, '')
751
+ .replace(/^args = \[.*create-merlin-brain.*\]\n?/m, '')
752
+ .replace(/^args = \[.*packages\/create-merlin-brain\/bin\/serve\.js.*\]\n?/m, '')
753
+ .trimEnd();
754
+ tomlContent = ensureTomlSectionLine(tomlContent, 'features', 'codex_hooks = true');
755
+ tomlContent = ensureTomlSectionLine(tomlContent, 'tui', 'notifications = true');
756
+ tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_threads = 8');
757
+ tomlContent = ensureTomlSectionLine(tomlContent, 'agents', 'max_depth = 2');
758
+ tomlContent = ensureTomlSectionLine(tomlContent, 'shell_environment_policy', 'inherit = "all"');
759
+ tomlContent = ensureTomlTopLevelLine(tomlContent, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
760
+
761
+ const mcpLines = [
762
+ `command = "${command}"`,
763
+ args ? `args = [${args.map((a) => `"${a}"`).join(', ')}]` : null,
764
+ 'startup_timeout_sec = 20',
765
+ 'tool_timeout_sec = 120',
766
+ ].filter(Boolean).join('\n');
767
+
768
+ tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin', mcpLines);
769
+ if (apiKey) {
770
+ tomlContent = upsertTomlSection(tomlContent, 'mcp_servers.merlin.env', `MERLIN_API_KEY = "${apiKey}"`);
169
771
  }
170
772
 
773
+ writeFile(configTomlPath, tomlContent.trimEnd() + '\n');
774
+ results.push('Configured Merlin MCP and Codex features in ~/.codex/config.toml');
775
+
171
776
  return results;
172
777
  }
173
778
 
@@ -216,8 +821,8 @@ function configureOpenCode(rt, useGlobalBinary, apiKey) {
216
821
  results.push('AGENTS.md already has Merlin (skipped)');
217
822
  } else {
218
823
  const combined = existingAgentsMd
219
- ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
220
- : buildAgentsMd();
824
+ ? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('opencode')
825
+ : buildInstructionMd('opencode');
221
826
  fs.writeFileSync(agentsMdPath, combined);
222
827
  results.push('Wrote ~/.opencode/AGENTS.md');
223
828
  }
@@ -246,8 +851,8 @@ function configureGemini(rt, useGlobalBinary, apiKey) {
246
851
  results.push('GEMINI.md already has Merlin (skipped)');
247
852
  } else {
248
853
  const combined = existingGeminiMd
249
- ? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
250
- : buildAgentsMd();
854
+ ? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildInstructionMd('gemini')
855
+ : buildInstructionMd('gemini');
251
856
  fs.writeFileSync(geminiMdPath, combined);
252
857
  results.push('Wrote ~/.gemini/GEMINI.md');
253
858
  }
@@ -297,22 +902,67 @@ function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
297
902
  ? ['codex', 'opencode', 'gemini']
298
903
  : runtimeIds;
299
904
 
300
- const agentsMd = buildAgentsMd();
301
-
302
905
  // AGENTS.md — used by Codex and OpenCode
303
906
  if (targets.includes('codex') || targets.includes('opencode')) {
304
907
  const agentsMdPath = path.join(projectDir, 'AGENTS.md');
305
908
  if (!fs.existsSync(agentsMdPath)) {
306
- fs.writeFileSync(agentsMdPath, agentsMd);
909
+ const instructionMd = targets.includes('codex')
910
+ ? buildInstructionMd('codex')
911
+ : buildInstructionMd('opencode');
912
+ fs.writeFileSync(agentsMdPath, instructionMd);
307
913
  results.push(`Created ${agentsMdPath}`);
308
914
  }
309
915
  }
310
916
 
917
+ if (targets.includes('codex')) {
918
+ const projectConfigPath = path.join(projectDir, '.codex', 'config.toml');
919
+ let projectConfig = fs.existsSync(projectConfigPath)
920
+ ? fs.readFileSync(projectConfigPath, 'utf8')
921
+ : '';
922
+ projectConfig = ensureTomlTopLevelLine(projectConfig, 'project_doc_fallback_filenames = ["CLAUDE.md", "GEMINI.md"]');
923
+ projectConfig = ensureTomlSectionLine(projectConfig, 'features', 'codex_hooks = true');
924
+ projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_threads = 8');
925
+ projectConfig = ensureTomlSectionLine(projectConfig, 'agents', 'max_depth = 2');
926
+ writeFile(projectConfigPath, projectConfig.trimEnd() + '\n');
927
+ results.push(`Created ${projectConfigPath}`);
928
+
929
+ const projectHooksRoot = path.join(projectDir, '.codex', 'merlin', 'hooks');
930
+ ensureDir(projectHooksRoot);
931
+ const hookSpecs = {
932
+ 'session-start.sh': buildCodexSessionStartHook(),
933
+ 'user-prompt-router.sh': buildCodexUserPromptRouterHook(),
934
+ 'bash-pre-tool.sh': buildCodexBashPreToolHook(),
935
+ 'bash-post-tool.sh': buildCodexBashPostToolHook(),
936
+ 'stop.sh': buildCodexStopHook(),
937
+ };
938
+ for (const [name, content] of Object.entries(hookSpecs)) {
939
+ const hookPath = path.join(projectHooksRoot, name);
940
+ writeFile(hookPath, content);
941
+ fs.chmodSync(hookPath, '755');
942
+ }
943
+ const projectHooksPath = path.join(projectDir, '.codex', 'hooks.json');
944
+ const existingProjectHooks = fs.existsSync(projectHooksPath)
945
+ ? fs.readFileSync(projectHooksPath, 'utf8')
946
+ : '';
947
+ writeFile(projectHooksPath, mergeHooksJson(existingProjectHooks, projectHooksRoot));
948
+ results.push(`Created ${path.join(projectDir, '.codex', 'hooks.json')}`);
949
+
950
+ const createdAgents = installCodexAgents(projectDir);
951
+ if (createdAgents.length) {
952
+ results.push(`Installed ${createdAgents.length} Codex agents`);
953
+ }
954
+
955
+ const createdSkills = installCodexSkills(projectDir);
956
+ if (createdSkills.length) {
957
+ results.push(`Installed ${createdSkills.length} Codex skills`);
958
+ }
959
+ }
960
+
311
961
  // GEMINI.md — used by Gemini CLI
312
962
  if (targets.includes('gemini')) {
313
963
  const geminiMdPath = path.join(projectDir, 'GEMINI.md');
314
964
  if (!fs.existsSync(geminiMdPath)) {
315
- fs.writeFileSync(geminiMdPath, agentsMd);
965
+ fs.writeFileSync(geminiMdPath, buildInstructionMd('gemini'));
316
966
  results.push(`Created ${geminiMdPath}`);
317
967
  }
318
968
  }
@@ -392,5 +1042,5 @@ module.exports = {
392
1042
  detectRuntimes,
393
1043
  configureRuntimes,
394
1044
  generateProjectRuntimeFiles,
395
- buildAgentsMd,
1045
+ buildInstructionMd,
396
1046
  };