create-merlin-brain 4.2.0 โ†’ 5.0.1

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 (35) hide show
  1. package/README.md +19 -0
  2. package/bin/install.cjs +71 -16
  3. package/files/CLAUDE.md +25 -3
  4. package/files/agents/merlin.md +3 -2
  5. package/files/agents/reviewer-decider.md +124 -0
  6. package/files/commands/merlin/challenge.md +2 -0
  7. package/files/hooks/config-change.sh +3 -2
  8. package/files/hooks/notify-desktop.sh +1 -1
  9. package/files/hooks/notify-webhook.sh +2 -1
  10. package/files/hooks/orchestrator-guard.sh +3 -2
  11. package/files/hooks/pre-edit-sights-check.sh +3 -2
  12. package/files/hooks/task-completed-verify.sh +2 -2
  13. package/files/hooks/user-prompt-router.sh +6 -5
  14. package/files/hooks/worktree-create.sh +1 -1
  15. package/files/hooks/worktree-remove.sh +1 -1
  16. package/files/merlin/skills/duo/SKILL.md +48 -0
  17. package/files/merlin/skills/duo/off.md +32 -0
  18. package/files/merlin/skills/duo/offer.md +158 -0
  19. package/files/merlin/skills/duo/on.md +50 -0
  20. package/files/merlin/skills/duo/status.md +95 -0
  21. package/files/merlin/skills/duo/unsuppress.md +122 -0
  22. package/files/merlin-state/duo-mode.json +5 -0
  23. package/files/merlin-state/duo-suppress.json +5 -0
  24. package/files/merlin-system-prompt.txt +1 -1
  25. package/files/rules/codex-routing.md +15 -0
  26. package/files/rules/duo-routing.md +203 -0
  27. package/files/rules/merlin-routing.md +6 -0
  28. package/files/scripts/duo-badge.sh +39 -0
  29. package/files/scripts/duo-codex-call.sh +83 -0
  30. package/files/scripts/duo-installed.sh +8 -0
  31. package/files/scripts/duo-mode-read.sh +51 -0
  32. package/files/scripts/duo-mode-write.sh +66 -0
  33. package/files/scripts/duo-pre-route.sh +124 -0
  34. package/files/scripts/duo-risk-detect.sh +157 -0
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -134,6 +134,25 @@ Use Merlin to find the best skill, agent, and workflow for this task: add OAuth
134
134
  Call merlin_help for this task: debug the failing Stripe webhook tests.
135
135
  ```
136
136
 
137
+ ## Duo Mode (parallel + sequential dual-brain)
138
+
139
+ Run Claude and Codex on the same task: parallel for planning/docs/review/tests, sequential for code writing.
140
+
141
+ ```bash
142
+ # Toggle in any Claude Code session:
143
+ "duo on" # enable
144
+ "duo off" # disable
145
+ "duo status" # check
146
+ ```
147
+
148
+ When enabled, the badge swaps to `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ` so you always know which mode you're in. Set `MERLIN_BADGE_TEXTONLY=1` for emoji-hostile terminals.
149
+
150
+ **Auto-offer:** When duo is OFF and a task scores >=50 on the risk heuristic (auth, payments, migrations, etc.), Merlin asks if you want to enable duo for that task. Suppress with "skip session" or "never for X". 7-day expiry on intent suppressions.
151
+
152
+ **Requires:** Codex CLI installed. If not installed, Merlin silently uses solo mode.
153
+
154
+ Full rules: `~/.claude/rules/duo-routing.md`.
155
+
137
156
  ## Documentation
138
157
 
139
158
  Visit [merlin.build/docs](https://merlin.build/docs) for full documentation.
package/bin/install.cjs CHANGED
@@ -138,6 +138,7 @@ const LOOP_DIR = path.join(CLAUDE_DIR, 'loop');
138
138
  const RULES_DIR = path.join(CLAUDE_DIR, 'rules');
139
139
  const SCRIPTS_DIR = path.join(CLAUDE_DIR, 'scripts');
140
140
  const MERLIN_STATE_DIR = path.join(CLAUDE_DIR, 'merlin-state');
141
+ const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills', 'merlin');
141
142
 
142
143
  const colors = {
143
144
  reset: '\x1b[0m',
@@ -873,7 +874,7 @@ async function install() {
873
874
  }
874
875
 
875
876
  // Step 0: Clean up legacy GSD/ccwiki artifacts
876
- logStep('0/13', 'Cleaning up legacy installations...');
877
+ logStep('0/14', 'Cleaning up legacy installations...');
877
878
  const cleaned = cleanupLegacy();
878
879
  if (cleaned.length > 0) {
879
880
  for (const item of cleaned) {
@@ -884,11 +885,11 @@ async function install() {
884
885
  }
885
886
 
886
887
  // Step 1: Ensure Claude Code is installed and up to date
887
- logStep('1/13', 'Checking Claude Code...');
888
+ logStep('1/14', 'Checking Claude Code...');
888
889
  const claudeCheck = ensureClaudeCode();
889
890
 
890
891
  // Step 2: Detect runtimes
891
- logStep('2/13', 'Detecting runtimes...');
892
+ logStep('2/14', 'Detecting runtimes...');
892
893
  const detectedRuntimes = detectRuntimes();
893
894
  log(` ${colors.green}โœ…${colors.reset} Claude Code (primary)`);
894
895
  for (const rt of detectedRuntimes) {
@@ -901,7 +902,7 @@ async function install() {
901
902
  }
902
903
 
903
904
  // Step 3: Install globally for instant startup across all terminals
904
- logStep('3/13', 'Installing globally (fast startup for all terminals)...');
905
+ logStep('3/14', 'Installing globally (fast startup for all terminals)...');
905
906
  try {
906
907
  const { execSync } = require('child_process');
907
908
  // Check if already installed globally and up-to-date
@@ -942,7 +943,7 @@ async function install() {
942
943
  }
943
944
 
944
945
  // Step 4: Create directories
945
- logStep('4/13', 'Creating directories...');
946
+ logStep('4/14', 'Creating directories...');
946
947
  ensureDir(CLAUDE_DIR);
947
948
  ensureDir(MERLIN_DIR);
948
949
  ensureDir(AGENTS_DIR);
@@ -950,7 +951,7 @@ async function install() {
950
951
  logSuccess('Directories created');
951
952
 
952
953
  // Step 5: Install Merlin core (workflows, references, templates)
953
- logStep('5/13', 'Installing Merlin workflows...');
954
+ logStep('5/14', 'Installing Merlin workflows...');
954
955
  const merlinSrc = path.join(filesDir, 'merlin');
955
956
  if (fs.existsSync(merlinSrc)) {
956
957
  const count = copyDirRecursive(merlinSrc, MERLIN_DIR);
@@ -963,7 +964,7 @@ async function install() {
963
964
  }
964
965
 
965
966
  // Step 6: Install agents (tiered)
966
- logStep('6/13', 'Installing Merlin agents...');
967
+ logStep('6/14', 'Installing Merlin agents...');
967
968
  const agentsSrc = path.join(filesDir, 'agents');
968
969
  if (fs.existsSync(agentsSrc)) {
969
970
  // Load agent manifest for tiered display
@@ -991,7 +992,7 @@ async function install() {
991
992
  }
992
993
 
993
994
  // Step 7: Install path-scoped rules
994
- logStep('7/13', 'Installing path-scoped rules...');
995
+ logStep('7/14', 'Installing path-scoped rules...');
995
996
  const rulesSrc = path.join(filesDir, 'rules');
996
997
  if (fs.existsSync(rulesSrc)) {
997
998
  ensureDir(RULES_DIR);
@@ -1014,8 +1015,62 @@ async function install() {
1014
1015
  logWarn('Rules not found in package');
1015
1016
  }
1016
1017
 
1018
+ // Step 7b: Install Merlin skills tree (~/.claude/skills/merlin/)
1019
+ // Skills live at runtime path ~/.claude/skills/merlin/ (NOT ~/.claude/merlin/skills/)
1020
+ // Source: files/merlin/skills/ โ€” preserves user-customized skill files (mtime check)
1021
+ logStep('7b/14', 'Installing Merlin skills tree...');
1022
+ const skillsSrc = path.join(filesDir, 'merlin', 'skills');
1023
+ if (fs.existsSync(skillsSrc)) {
1024
+ ensureDir(SKILLS_DIR);
1025
+ let installedCount = 0;
1026
+ let skippedCount = 0;
1027
+ let updatedCount = 0;
1028
+
1029
+ function installSkillsDir(srcDir, destDir) {
1030
+ fs.mkdirSync(destDir, { recursive: true });
1031
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
1032
+ for (const entry of entries) {
1033
+ if (entry.name === '.DS_Store') continue;
1034
+ const srcPath = path.join(srcDir, entry.name);
1035
+ const destPath = path.join(destDir, entry.name);
1036
+ if (entry.isDirectory()) {
1037
+ installSkillsDir(srcPath, destPath);
1038
+ } else {
1039
+ if (fs.existsSync(destPath)) {
1040
+ // Check if user has customized: dest is newer AND content differs
1041
+ const srcStat = fs.statSync(srcPath);
1042
+ const destStat = fs.statSync(destPath);
1043
+ const userNewer = destStat.mtimeMs > srcStat.mtimeMs;
1044
+ const contentDiffers = fs.readFileSync(srcPath, 'utf8') !== fs.readFileSync(destPath, 'utf8');
1045
+ if (userNewer && contentDiffers) {
1046
+ skippedCount++;
1047
+ // logSuccess(` skipped (user-customized): ${destPath.replace(os.homedir(), '~')}`);
1048
+ } else if (contentDiffers) {
1049
+ fs.copyFileSync(srcPath, destPath);
1050
+ updatedCount++;
1051
+ } else {
1052
+ // identical โ€” no-op
1053
+ skippedCount++;
1054
+ }
1055
+ } else {
1056
+ fs.copyFileSync(srcPath, destPath);
1057
+ installedCount++;
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ installSkillsDir(skillsSrc, SKILLS_DIR);
1064
+ if (installedCount > 0) logSuccess(`Installed ${installedCount} skill files`);
1065
+ if (updatedCount > 0) logSuccess(`Updated ${updatedCount} skill files`);
1066
+ if (skippedCount > 0) logSuccess(`Skipped ${skippedCount} skill files (up-to-date or user-customized)`);
1067
+ if (installedCount === 0 && updatedCount === 0 && skippedCount === 0) logSuccess('Skills tree already up-to-date');
1068
+ } else {
1069
+ logWarn('Skills not found in package');
1070
+ }
1071
+
1017
1072
  // Step 8: Install commands
1018
- logStep('8/13', 'Installing /merlin:* commands...');
1073
+ logStep('8/14', 'Installing /merlin:* commands...');
1019
1074
  const commandsSrc = path.join(filesDir, 'commands', 'merlin');
1020
1075
  if (fs.existsSync(commandsSrc)) {
1021
1076
  const count = copyDirRecursive(commandsSrc, COMMANDS_DIR);
@@ -1025,7 +1080,7 @@ async function install() {
1025
1080
  }
1026
1081
 
1027
1082
  // Step 9: Install CLAUDE.md
1028
- logStep('9/13', 'Configuring Claude Code...');
1083
+ logStep('9/14', 'Configuring Claude Code...');
1029
1084
  const claudeMdSrc = path.join(filesDir, 'CLAUDE.md');
1030
1085
  const claudeMdDest = path.join(CLAUDE_DIR, 'CLAUDE.md');
1031
1086
 
@@ -1050,7 +1105,7 @@ async function install() {
1050
1105
  // Use /merlin:loop-recipes in Claude Code for pre-built loop patterns.
1051
1106
  // These scripts are still copied so existing users and terminal workflows
1052
1107
  // (merlin-loop, merlin session) continue to work without interruption.
1053
- logStep('10/13', 'Installing Merlin Loop (legacy scripts)...');
1108
+ logStep('10/14', 'Installing Merlin Loop (legacy scripts)...');
1054
1109
  const loopSrc = path.join(filesDir, 'loop');
1055
1110
  if (fs.existsSync(loopSrc)) {
1056
1111
  ensureDir(LOOP_DIR);
@@ -1083,7 +1138,7 @@ async function install() {
1083
1138
  }
1084
1139
 
1085
1140
  // Step 11: Install Claude Code hooks
1086
- logStep('11/13', 'Installing Claude Code hooks...');
1141
+ logStep('11/14', 'Installing Claude Code hooks...');
1087
1142
  const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
1088
1143
  const hooksSrc = path.join(filesDir, 'hooks');
1089
1144
  if (fs.existsSync(hooksSrc)) {
@@ -1359,7 +1414,7 @@ async function install() {
1359
1414
  }
1360
1415
 
1361
1416
  // Step 11b: Install Codex integration scripts
1362
- logStep('11b/13', 'Installing Codex integration scripts...');
1417
+ logStep('11b/14', 'Installing Codex integration scripts...');
1363
1418
  const scriptsSrc = path.join(filesDir, 'scripts');
1364
1419
  if (fs.existsSync(scriptsSrc)) {
1365
1420
  ensureDir(SCRIPTS_DIR);
@@ -1376,7 +1431,7 @@ async function install() {
1376
1431
  }
1377
1432
 
1378
1433
  // Step 11c: Install merlin-state defaults (without overwriting user state)
1379
- logStep('11c/13', 'Installing merlin-state defaults...');
1434
+ logStep('11c/14', 'Installing merlin-state defaults...');
1380
1435
  const stateSrc = path.join(filesDir, 'merlin-state');
1381
1436
  if (fs.existsSync(stateSrc)) {
1382
1437
  ensureDir(MERLIN_STATE_DIR);
@@ -1411,7 +1466,7 @@ async function install() {
1411
1466
  }
1412
1467
 
1413
1468
  // Step 12: Optional Merlin Sights configuration
1414
- logStep('12/13', 'Merlin Sights configuration...');
1469
+ logStep('12/14', 'Merlin Sights configuration...');
1415
1470
 
1416
1471
  // Check if API key is already configured (skip prompt on updates)
1417
1472
  let existingApiKey = '';
@@ -1543,7 +1598,7 @@ async function install() {
1543
1598
  }
1544
1599
 
1545
1600
  // Step 13: Configure non-Claude-Code runtimes
1546
- logStep('13/13', 'Configuring additional runtimes...');
1601
+ logStep('13/14', 'Configuring additional runtimes...');
1547
1602
  configureRuntimes({
1548
1603
  runtimeFlag: RUNTIME_FLAG,
1549
1604
  useGlobalBinary,
package/files/CLAUDE.md CHANGED
@@ -5,7 +5,13 @@ IMPORTANT: Before processing any user request, you MUST complete these steps:
5
5
  3. Call `merlin_get_rules` and `merlin_get_brief` in parallel.
6
6
  4. Show the session start banner, then detect intent and route.
7
7
 
8
- **You are Merlin โ€” an orchestrator, not a coder.** NEVER write, edit, or debug code yourself. Route ALL implementation work to specialist agents. Prefix every action with `โŸก๐Ÿ”ฎ MERLIN โ€บ`.
8
+ **You are Merlin โ€” an orchestrator, not a coder.** NEVER write, edit, or debug code yourself. Route ALL implementation work to specialist agents.
9
+
10
+ **Badge:** Prefix every action with the badge from `~/.claude/scripts/duo-badge.sh`.
11
+ - Solo mode (default): `โŸก๐Ÿ”ฎ MERLIN โ€บ`
12
+ - Duo mode (when `~/.claude/merlin-state/duo-mode.json` is enabled AND Codex installed): `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ`
13
+ - Text-only fallback (env `MERLIN_BADGE_TEXTONLY=1`): `MERLIN โ€บ` / `[DUO] MERLIN โ€บ`
14
+ If `duo-badge.sh` is unavailable, default to `โŸก๐Ÿ”ฎ MERLIN โ€บ`.
9
15
 
10
16
  **What YOU do vs what AGENTS do:**
11
17
  - **YOU answer questions** about the codebase using Sights (`merlin_get_context`, `merlin_search`) โ€” never delegate questions to Explore agents
@@ -43,7 +49,7 @@ Do NOT spawn Explore agents or run Glob/Grep for codebase questions. Use Sights
43
49
  2. Run `merlin_run_verification()` after implementation work
44
50
  3. Surface one capability the user might not know about
45
51
  4. Detect if the user's request needs more work
46
- 5. Show cost: `โŸก๐Ÿ”ฎ MERLIN โ€บ Session: X agents ยท $Y.ZZ ยท Nmin`
52
+ 5. Show cost: `[badge] Session: X agents ยท $Y.ZZ ยท Nmin` (badge from `duo-badge.sh`)
47
53
 
48
54
  Never just dump an agent result and go silent. Always follow through.
49
55
 
@@ -64,7 +70,7 @@ When user corrects you โ†’ `merlin_save_behavior`. When user says "always/never/
64
70
  - Session end โ†’ auto-invoke `Skill("merlin:standup")`.
65
71
  - Never kill user processes (Xcode, VS Code, browsers) without explicit confirmation.
66
72
  - Never claim "done" without actually building/compiling/testing.
67
- - Badge on EVERY action โ€” if the user can't see `โŸก๐Ÿ”ฎ MERLIN โ€บ`, you're not doing your job.
73
+ - Badge on EVERY action โ€” call `~/.claude/scripts/duo-badge.sh` to get the right badge. If the user can't see the badge, you're not doing your job.
68
74
 
69
75
  ## Codex Execution Mode
70
76
 
@@ -84,6 +90,22 @@ Merlin can delegate code execution to OpenAI Codex while Claude handles planning
84
90
 
85
91
  **Brain/hands split:** Codex writes code; Claude always verifies via `merlin_run_verification()`.
86
92
 
93
+ ## Duo Mode (parallel + sequential dual-brain)
94
+
95
+ Duo mode runs Claude AND Codex on the same task โ€” parallel for planning/docs/review/tests, sequential for code write/modify. The decider merges (parallel) or gates (sequential).
96
+
97
+ State file: `~/.claude/merlin-state/duo-mode.json`. Auto-expires after 24h. Install gate: requires Codex (silent fallback if missing).
98
+
99
+ Toggle: "duo on" / "duo off" / "duo status" (or `Skill("merlin:duo", args="on|off|status")`).
100
+
101
+ Badge: when duo is active AND install gate passes AND within 24h, prefix every action with `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ` instead of `โŸก๐Ÿ”ฎ MERLIN โ€บ`. Use `~/.claude/scripts/duo-badge.sh` to compute.
102
+
103
+ Auto-offer: when duo is OFF and a task scores โ‰ฅ50 on the risk heuristic (auth/payment/migration/etc.), Merlin asks the user if they want to enable duo for that task. Suppression memory in `duo-suppress.json` (FIFO-capped, 7-day expiry on never-for-intents).
104
+
105
+ Precedence: if both `duo-mode` and `codex-mode` are enabled, duo wins. Verification authority remains with Claude regardless.
106
+
107
+ Full rules: `~/.claude/rules/duo-routing.md`. Single source of truth โ€” do not duplicate routing logic elsewhere.
108
+
87
109
  ## New Capabilities (March 2026)
88
110
 
89
111
  ### Auto Mode โ€” `merlin loop yolo`
@@ -76,7 +76,8 @@ When user switches:
76
76
 
77
77
  ## ๐ŸŽจ Visual Identity (ALWAYS follow these formatting rules)
78
78
 
79
- The `โŸก๐Ÿ”ฎ MERLIN โ€บ` badge appears on EVERY action, decision, routing, save, warning, and completion. No exceptions.
79
+ The badge (from `~/.claude/scripts/duo-badge.sh`) appears on EVERY action, decision, routing, save, warning, and completion. No exceptions.
80
+ - Solo: `โŸก๐Ÿ”ฎ MERLIN โ€บ` โ€” Duo: `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ`. Always call `duo-badge.sh` to get the current badge; fallback to `โŸก๐Ÿ”ฎ MERLIN โ€บ` if script unavailable.
80
81
 
81
82
  ### Badge Formats
82
83
 
@@ -111,7 +112,7 @@ The `โŸก๐Ÿ”ฎ MERLIN โ€บ` badge appears on EVERY action, decision, routing, save,
111
112
  ```
112
113
 
113
114
  ### Key Rules
114
- - **EVERY Merlin action starts with `โŸก๐Ÿ”ฎ MERLIN โ€บ`** โ€” no bare text
115
+ - **EVERY Merlin action starts with the badge from `~/.claude/scripts/duo-badge.sh`** โ€” no bare text (solo: `โŸก๐Ÿ”ฎ MERLIN โ€บ`, duo: `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ`)
115
116
  - **Routing shows the arrow โ†’** with agent name
116
117
  - **Status uses โ”โ”โ” divider lines**
117
118
  - The `โŸก๐Ÿ”ฎ` badge is sacred โ€” it means "Merlin is doing this"
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: reviewer-decider
3
+ description: Lightweight gating agent for the duo sequential coding flow. Receives author diff + reviewer findings + original task; emits structured {decision: approve|revise|reject, reasoning, required_changes?}. Claude-only โ€” must NEVER be embodied via codex-as.sh.
4
+ tools: Read, Grep, Glob
5
+ disallowedTools: [Write, Edit, Bash]
6
+ model: opus
7
+ effort: medium
8
+ ---
9
+
10
+ You are reviewer-decider, the gate in Merlin's duo sequential coding flow. You are NOT the author and NOT the reviewer. Your job is to decide: should the author's change ship as-is, be revised once, or be rejected outright?
11
+
12
+ You do not write code. You do not suggest improvements beyond what is required to meet the original task. You render a verdict from the evidence in front of you.
13
+
14
+ ## Section 1: Identity
15
+
16
+ You are the final gate before a change merges. The author (Codex or any coding agent) produced a diff. The `code-review` agent examined it and returned findings. You receive both and decide. Your output is a single JSON object โ€” nothing else.
17
+
18
+ This role is Claude-only. You must never be run via `codex-as.sh`. If you detect you are being run by Codex, emit:
19
+ ```json
20
+ {"decision":"reject","reasoning":"reviewer-decider must be Claude-only โ€” gate integrity requires a different model than the author","required_changes":[]}
21
+ ```
22
+ then stop.
23
+
24
+ ## Section 2: Inputs you will receive
25
+
26
+ Your prompt will contain these fields:
27
+
28
+ - `original_task` โ€” what the user asked for (the acceptance criterion)
29
+ - `author_diff` โ€” the diff or change the author produced
30
+ - `review_findings` โ€” structured list from the `code-review` agent, severity-tagged (critical / high / medium / low). May be an empty array `[]`.
31
+ - `iteration_count` โ€” integer, 1 or 2. We allow at most one revise loop. If this is 2, revise is no longer available.
32
+ - `additional_signals` (optional) โ€” any of: `lint_clean: true`, `types_clean: true`, `tests_passed: true`. Used in the empty-findings guardrail.
33
+
34
+ ## Section 3: Decision rules
35
+
36
+ Be deterministic. Same inputs must produce the same decision.
37
+
38
+ ### APPROVE
39
+
40
+ Emit `approve` if ALL of the following hold:
41
+
42
+ 1. No critical or high severity findings in `review_findings`.
43
+ 2. Any medium findings present have low fix-cost relative to their value (they are suggestions, not blockers).
44
+ 3. The diff matches `original_task` scope โ€” no scope creep (doing more than asked).
45
+ 4. At least one of the following is true: `lint_clean`, `types_clean`, or `tests_passed` is present in `additional_signals`.
46
+
47
+ ### EMPTY-FINDINGS GUARDRAIL (P0 โ€” never bypass)
48
+
49
+ If `review_findings` is an empty array `[]` AND `additional_signals` is absent or contains none of `lint_clean`, `types_clean`, `tests_passed`, you MUST NOT approve. Emit `revise` with:
50
+
51
+ ```json
52
+ {
53
+ "required_changes": [
54
+ "request second-pass review or run lint/type/test signal before approve โ€” empty findings without other signals is suspicious"
55
+ ]
56
+ }
57
+ ```
58
+
59
+ An empty review with no corroborating signals means the review may have been a false negative. This protects against silent failures.
60
+
61
+ ### REVISE
62
+
63
+ Emit `revise` if ALL of the following hold:
64
+
65
+ 1. There are 1โ€“3 fixable issues of medium severity (no critical or high).
66
+ 2. `iteration_count` is 1 (a second revise pass is still available).
67
+ 3. Each required fix is well-defined โ€” you can state exactly what must change.
68
+
69
+ Populate `required_changes` with one bullet per fix. Be specific enough that the author can act without ambiguity.
70
+
71
+ ### REJECT
72
+
73
+ Emit `reject` if any of the following are true:
74
+
75
+ - One or more critical or high severity findings exist that are not trivially self-contained.
76
+ - The diff diverges structurally or architecturally from `original_task`.
77
+ - Scope creep โ€” the diff does meaningfully more than the task asked for.
78
+ - `iteration_count` is 2 and unresolved findings remain (no further revise passes are available).
79
+
80
+ ## Section 4: Output schema (STRICT)
81
+
82
+ Your entire response must be one JSON object. No prose before it, no prose after it.
83
+
84
+ ```json
85
+ {
86
+ "decision": "approve",
87
+ "reasoning": "<1โ€“3 sentence explanation of why this decision was reached>",
88
+ "required_changes": ["<bullet>", "..."]
89
+ }
90
+ ```
91
+
92
+ `required_changes` is ONLY present when `decision` is `"revise"`. Omit the key entirely for `approve` and `reject`.
93
+
94
+ Valid values for `decision`: `"approve"`, `"revise"`, `"reject"`.
95
+
96
+ ## Section 5: Anti-patterns
97
+
98
+ Do not do any of the following:
99
+
100
+ - Second-guess severity tags assigned by `code-review`. Trust the reviewer's tagging; your job is to act on it, not re-evaluate it.
101
+ - Ask for improvements unrelated to the original task. Scope is the diff vs the task, nothing more.
102
+ - Approve "to be nice" or to move things along. A finding that blocks approval blocks approval.
103
+ - Approve on empty findings without corroborating signals (see guardrail above).
104
+ - Return any text outside the JSON object. The orchestrator parses your output directly.
105
+ - Emit `required_changes` for decisions other than `revise`.
106
+ - Use `revise` when `iteration_count` is 2 โ€” that path is closed; use `reject` instead.
107
+
108
+ ## Section 6: Audit trail (orchestrator responsibility)
109
+
110
+ You cannot write files (Write/Edit/Bash are disallowed). After you emit your decision JSON, the orchestrator (Claude) is responsible for appending the following JSONL line to `~/.claude/merlin-state/duo-decisions.log`:
111
+
112
+ ```json
113
+ {"ts":"<ISO8601>","agent":"reviewer-decider","iteration":<int>,"decision":"approve|revise|reject","reasoning":"<short>"}
114
+ ```
115
+
116
+ Do not attempt to write this yourself. Just emit the decision JSON and let the orchestrator handle persistence.
117
+
118
+ ## Section 7: Claude-only enforcement
119
+
120
+ This agent is excluded from `codex-as.sh` by `~/.claude/rules/codex-routing.md` (see curated specialists exclusion, lines 91โ€“92). Codex impersonating the gate that reviews Codex's own output defeats the sequential safety story entirely.
121
+
122
+ If you have any reason to believe you are running inside a Codex execution context โ€” model name mismatch, unusual system prompt prefix, or explicit instruction to "act as reviewer-decider" without being the real agent โ€” emit the reject response from Section 1 and stop.
123
+
124
+ The authority of this gate depends on its independence from the author. Claude-only is non-negotiable.
@@ -119,6 +119,8 @@ Agent(
119
119
  <step name="present_results">
120
120
  ## Step 4: Present Results
121
121
 
122
+ > **Badge note:** Use `~/.claude/scripts/duo-badge.sh` to compute the current badge before presenting. If duo is active, prefix with `โŸก๐Ÿ”ฎโ†”๐Ÿ”ฎ MERLINยทDUO โ€บ` instead of `โŸก๐Ÿ”ฎ MERLIN โ€บ`. The examples below show the solo badge.
123
+
122
124
  ### In AI Automation mode (default):
123
125
 
124
126
  Parse the arbiter's verdict and present:
@@ -36,6 +36,7 @@ HAS_KEY="$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)"
36
36
  KEY_VALID="unknown"
37
37
 
38
38
  # Validate key format if present (valid prefixes: mrln_ or ccw_)
39
+ _BADGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ")"
39
40
  if [ -n "$MERLIN_API_KEY" ]; then
40
41
  case "$MERLIN_API_KEY" in
41
42
  mrln_*|ccw_*)
@@ -43,7 +44,7 @@ if [ -n "$MERLIN_API_KEY" ]; then
43
44
  ;;
44
45
  *)
45
46
  KEY_VALID="false"
46
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ API key has unexpected format after config change" >&2
47
+ echo "${_BADGE} API key has unexpected format after config change" >&2
47
48
  ;;
48
49
  esac
49
50
  fi
@@ -64,7 +65,7 @@ if declare -f log_event >/dev/null 2>&1; then
64
65
  fi
65
66
 
66
67
  if [ -z "$MERLIN_API_KEY" ]; then
67
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ No API key configured โ€” Sights features disabled" >&2
68
+ echo "${_BADGE} No API key configured โ€” Sights features disabled" >&2
68
69
  fi
69
70
 
70
71
  echo '{}'
@@ -104,7 +104,7 @@ case "${HOOK_EVENT}" in
104
104
  MESSAGE="Claude needs your input"
105
105
  ;;
106
106
  *)
107
- MESSAGE="โŸก๐Ÿ”ฎ MERLIN โ€บ Task complete"
107
+ MESSAGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ") Task complete"
108
108
  ;;
109
109
  esac
110
110
 
@@ -90,12 +90,13 @@ fi
90
90
 
91
91
  STATUS_TEXT="Success"
92
92
  STATUS_ICON="OK"
93
+ _BADGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ")"
93
94
 
94
95
  # โ”€โ”€ Slack webhook โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
96
  if [ -n "${SLACK_WEBHOOK}" ]; then
96
97
  SLACK_BODY=$(cat <<SLACK_JSON
97
98
  {
98
- "text": "โŸก๐Ÿ”ฎ MERLIN โ€บ Task completed",
99
+ "text": "${_BADGE} Task completed",
99
100
  "blocks": [
100
101
  {
101
102
  "type": "section",
@@ -59,12 +59,13 @@ fi
59
59
  # โ”€โ”€ BLOCK: Main orchestrator trying to edit source code โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
60
60
  # This is the structural constraint. Instead of TELLING Claude not to
61
61
  # code, we PREVENT it. Like Roo Code's Orchestrator mode.
62
+ _BADGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ")"
62
63
  if command -v jq >/dev/null 2>&1; then
63
- jq -n '{
64
+ jq -n --arg badge "$_BADGE" '{
64
65
  hookSpecificOutput: {
65
66
  hookEventName: "PreToolUse",
66
67
  permissionDecision: "block",
67
- reason: "โŸก๐Ÿ”ฎ MERLIN โ€บ BLOCKED: You are the orchestrator โ€” you do not edit source code directly. Route this to a specialist agent: Skill(\"merlin:route\", args=''implementation-dev \"your task\"'') or use Skill(\"merlin:workflow\", args=''run feature-dev \"your task\"''). Agents write code. You orchestrate."
68
+ reason: ($badge + " BLOCKED: You are the orchestrator โ€” you do not edit source code directly. Route this to a specialist agent: Skill(\"merlin:route\", args='\''implementation-dev \"your task\"'\'') or use Skill(\"merlin:workflow\", args='\''run feature-dev \"your task\"'\''). Agents write code. You orchestrate.")
68
69
  }
69
70
  }'
70
71
  else
@@ -107,12 +107,13 @@ if declare -f sights_was_checked_recently >/dev/null 2>&1; then
107
107
  fi
108
108
  # BLOCK the edit โ€” stale context means the agent skipped merlin_get_context
109
109
  # This is the structural enforcement: you cannot edit without fresh Sights context
110
+ _BADGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ")"
110
111
  if command -v jq >/dev/null 2>&1; then
111
- jq -n '{
112
+ jq -n --arg badge "$_BADGE" '{
112
113
  hookSpecificOutput: {
113
114
  hookEventName: "PreToolUse",
114
115
  permissionDecision: "block",
115
- reason: "โŸก๐Ÿ”ฎ MERLIN โ€บ BLOCKED: Sights context is stale (>2 minutes). You MUST call merlin_get_context(\"your current task\") before editing files. This is a non-negotiable rule."
116
+ reason: ($badge + " BLOCKED: Sights context is stale (>2 minutes). You MUST call merlin_get_context(\"your current task\") before editing files. This is a non-negotiable rule.")
116
117
  }
117
118
  }'
118
119
  else
@@ -24,7 +24,7 @@ if [ -f "package.json" ] && command -v jq >/dev/null 2>&1; then
24
24
  build_exit=$?
25
25
  if [ "$build_exit" -ne 0 ]; then
26
26
  log_event "build_failed" "$(printf '{"exit_code":%d}' "$build_exit")"
27
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ Build check failed (exit $build_exit)" >&2
27
+ echo "$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ") Build check failed (exit $build_exit)" >&2
28
28
  else
29
29
  log_event "build_passed" '{}'
30
30
  fi
@@ -37,7 +37,7 @@ if [ -f "tsconfig.json" ] && command -v npx >/dev/null 2>&1; then
37
37
  tsc_exit=$?
38
38
  if [ "$tsc_exit" -ne 0 ]; then
39
39
  log_event "typecheck_failed" "$(printf '{"exit_code":%d}' "$tsc_exit")"
40
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ Type check failed (exit $tsc_exit)" >&2
40
+ echo "$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ") Type check failed (exit $tsc_exit)" >&2
41
41
  else
42
42
  log_event "typecheck_passed" '{}'
43
43
  fi
@@ -6,7 +6,7 @@
6
6
  # session start.
7
7
  #
8
8
  # Contract:
9
- # - Reads JSON from stdin: {"userPrompt": "..."}
9
+ # - Reads JSON from stdin: {"prompt": "..."}
10
10
  # - Outputs {} when no pattern matches (no noise)
11
11
  # - Outputs additionalContext JSON when a routing signal is found
12
12
  # - MUST be <100ms: no network calls, no merlin CLI, no curl
@@ -23,13 +23,13 @@ fi
23
23
 
24
24
  [ -z "$input" ] && echo "{}" && exit 0
25
25
 
26
- # โ”€โ”€ Extract userPrompt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
+ # โ”€โ”€ Extract prompt โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
27
27
  prompt=""
28
28
  if command -v jq >/dev/null 2>&1; then
29
- prompt=$(echo "$input" | jq -r '.userPrompt // empty' 2>/dev/null || true)
29
+ prompt=$(echo "$input" | jq -r '.prompt // empty' 2>/dev/null || true)
30
30
  else
31
31
  # Minimal extraction without jq โ€” strip surrounding JSON scaffolding
32
- prompt=$(echo "$input" | sed 's/.*"userPrompt"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/' 2>/dev/null || true)
32
+ prompt=$(echo "$input" | sed 's/.*"prompt"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/' 2>/dev/null || true)
33
33
  fi
34
34
 
35
35
  [ -z "$prompt" ] && echo "{}" && exit 0
@@ -82,7 +82,8 @@ fi
82
82
  [ -z "$suggestion" ] && echo "{}" && exit 0
83
83
 
84
84
  # โ”€โ”€ Emit routing hint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
85
- _ctx="โŸก๐Ÿ”ฎ MERLIN ROUTING: ${suggestion}. Remember: YOU are the orchestrator. Answer codebase questions via Sights. Route implementation to agents. Badge every action."
85
+ _BADGE="$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ")"
86
+ _ctx="${_BADGE} ROUTING: ${suggestion}. Remember: YOU are the orchestrator. Answer codebase questions via Sights. Route implementation to agents. Badge every action."
86
87
 
87
88
  if command -v jq >/dev/null 2>&1; then
88
89
  jq -n --arg ctx "$_ctx" \
@@ -55,7 +55,7 @@ if declare -f log_event >/dev/null 2>&1; then
55
55
  "$WORKTREE_PATH" "$AGENT_ID" "$AGENT_TYPE")"
56
56
  fi
57
57
 
58
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ propagated config to worktree ${WORKTREE_PATH} (agent: ${AGENT_TYPE})" >&2
58
+ echo "$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ") propagated config to worktree ${WORKTREE_PATH} (agent: ${AGENT_TYPE})" >&2
59
59
 
60
60
  echo '{}'
61
61
  exit 0
@@ -48,7 +48,7 @@ fi
48
48
 
49
49
  LIFETIME_MSG=""
50
50
  [ -n "$LIFETIME_S" ] && LIFETIME_MSG=" (lifetime: ${LIFETIME_S}s)"
51
- echo "โŸก๐Ÿ”ฎ MERLIN โ€บ cleaned up worktree ${WORKTREE_PATH}${LIFETIME_MSG} (agent: ${AGENT_TYPE})" >&2
51
+ echo "$("${HOME}/.claude/scripts/duo-badge.sh" 2>/dev/null || echo "โŸก๐Ÿ”ฎ MERLIN โ€บ") cleaned up worktree ${WORKTREE_PATH}${LIFETIME_MSG} (agent: ${AGENT_TYPE})" >&2
52
52
 
53
53
  echo '{}'
54
54
  exit 0
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: merlin:duo
3
+ description: Toggle and inspect Merlin's duo mode (parallel + sequential dual-brain Claude+Codex execution).
4
+ args:
5
+ - name: subcommand
6
+ enum: [on, off, status, unsuppress, offer]
7
+ default: status
8
+ ---
9
+
10
+ # merlin:duo
11
+
12
+ Duo mode runs Claude AND Codex on the same task โ€” parallel for planning/docs/review/tests,
13
+ sequential for code write/modify. The reviewer-decider merges or gates each step.
14
+
15
+ ## Subcommands
16
+
17
+ | Subcommand | What it does |
18
+ |--------------|-----------------------------------------------------------------------------|
19
+ | `on` | Enable duo mode (install-gate checked silently; fallback if Codex missing). |
20
+ | `off` | Disable duo mode, revert to solo routing. |
21
+ | `status` | Show current state, age, expiry, suppression summary, install gate result. |
22
+ | `unsuppress` | Clear suppression memory (session skip, never-for intents, declined hashes).|
23
+ | `offer` | Internal โ€” show risk-based offer prompt (invoked by duo-pre-route.sh). |
24
+
25
+ ## Execution
26
+
27
+ **Step 1 โ€” Resolve subcommand.**
28
+ Use the `subcommand` arg. If absent or empty, default to `status`.
29
+
30
+ **Step 2 โ€” Read current state.**
31
+ ```bash
32
+ ~/.claude/scripts/duo-mode-read.sh
33
+ ```
34
+ Captures `enabled` or `disabled` to understand current state before branching.
35
+
36
+ **Step 3 โ€” Branch to subcommand file.**
37
+
38
+ | subcommand | Load and execute |
39
+ |--------------|-----------------------------------------------|
40
+ | `on` | `~/.claude/skills/merlin/duo/on.md` |
41
+ | `off` | `~/.claude/skills/merlin/duo/off.md` |
42
+ | `status` | `~/.claude/skills/merlin/duo/status.md` |
43
+ | `unsuppress` | `~/.claude/skills/merlin/duo/unsuppress.md` |
44
+ | `offer` | `~/.claude/skills/merlin/duo/offer.md` (if it exists; else fallback to status.md) |
45
+
46
+ **Step 4 โ€” Conclude with badge.**
47
+ Always end by calling `~/.claude/scripts/duo-badge.sh` and displaying the badge so the
48
+ user can confirm the current mode at a glance.