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
@@ -0,0 +1,32 @@
1
+ # duo/off — disable duo mode
2
+
3
+ ## Steps
4
+
5
+ **Step 1 — Write disabled state.**
6
+ ```bash
7
+ ~/.claude/scripts/duo-mode-write.sh off "<user's phrase that triggered disable>"
8
+ ```
9
+
10
+ **Step 2 — Check codex-mode status.**
11
+ ```bash
12
+ python3 -c "
13
+ import json, os
14
+ f = os.path.expanduser('~/.claude/merlin-state/codex-mode.json')
15
+ try:
16
+ d = json.load(open(f))
17
+ if d.get('enabled'): print('codex-mode-active')
18
+ except: pass
19
+ "
20
+ ```
21
+
22
+ **Step 3 — Emit confirmation.**
23
+
24
+ If codex-mode is active (step 2 output = `codex-mode-active`):
25
+ ```
26
+ ⟡🔮 MERLIN › Duo off. (codex-mode is still active.)
27
+ ```
28
+
29
+ Otherwise:
30
+ ```
31
+ ⟡🔮 MERLIN › Duo off. Back to solo routing.
32
+ ```
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: duo-offer
3
+ description: Auto-offer prompt for enabling duo mode on risky tasks. Invoked by duo-pre-route.sh when risk score >= threshold and Codex is installed and duo is off and task is not suppressed.
4
+ type: skill
5
+ subcommand: offer
6
+ ---
7
+
8
+ # Duo Auto-Offer
9
+
10
+ You are executing the duo auto-offer flow. Follow every step in sequence.
11
+
12
+ ## Step 1 — Idempotency guard
13
+
14
+ Run `~/.claude/scripts/duo-mode-read.sh`. If output is `enabled`, exit silently — duo is already on, no offer needed.
15
+
16
+ ## Step 2 — Install gate
17
+
18
+ Run `~/.claude/scripts/duo-installed.sh`. If exit code != 0, exit silently — do not mention duo or Codex.
19
+
20
+ ## Step 3 — Read context
21
+
22
+ Read from environment or caller args:
23
+ - `DUO_OFFER_TASK` — original task description
24
+ - `DUO_OFFER_WORKFLOW` — workflow name (default: "general")
25
+ - `DUO_OFFER_FILES` — comma-separated file paths
26
+ - `DUO_OFFER_LOC` — estimated LOC delta
27
+ - `DUO_OFFER_SCORE` — pre-computed score (if available; otherwise run detector)
28
+ - `DUO_OFFER_REASONS` — pre-computed reasons JSON array (if available)
29
+
30
+ ## Step 4 — Risk check (if score not pre-provided)
31
+
32
+ Run:
33
+ ```
34
+ ~/.claude/scripts/duo-risk-detect.sh \
35
+ --task "$DUO_OFFER_TASK" \
36
+ --workflow "$DUO_OFFER_WORKFLOW" \
37
+ --files "$DUO_OFFER_FILES" \
38
+ --loc "$DUO_OFFER_LOC"
39
+ ```
40
+
41
+ Parse JSON output. If `suggest_duo` is false, exit silently.
42
+
43
+ ## Step 5 — Suppression check
44
+
45
+ Read `~/.claude/merlin-state/duo-suppress.json` using python3 (flock not needed for read):
46
+
47
+ ```python
48
+ import json, os, time
49
+ path = os.path.expanduser("~/.claude/merlin-state/duo-suppress.json")
50
+ try:
51
+ d = json.load(open(path))
52
+ except Exception:
53
+ d = {}
54
+
55
+ # session_skip: honor if file mtime < 12h
56
+ mtime = os.path.getmtime(path) if os.path.exists(path) else 0
57
+ if d.get("session_skip") and (time.time() - mtime) < 43200:
58
+ exit(0) # suppressed
59
+
60
+ # task_hash check
61
+ import hashlib, re
62
+ task = os.environ.get("DUO_OFFER_TASK", "")
63
+ workflow = os.environ.get("DUO_OFFER_WORKFLOW", "")
64
+ normalized = re.sub(r'[\s\'"`]+', ' ', task.lower()).strip()[:120]
65
+ task_hash = hashlib.sha1(f"{workflow}:{normalized}".encode()).hexdigest()
66
+ if task_hash in d.get("task_hashes_declined", []):
67
+ exit(0) # already declined this task
68
+
69
+ # intent fingerprint check (never_for_intents, 7d expiry)
70
+ reasons = json.loads(os.environ.get("DUO_OFFER_REASONS", "[]"))
71
+ top3 = sorted(reasons[:3])
72
+ intent_fp = hashlib.sha1(f"{workflow}:{':'.join(top3)}".encode()).hexdigest()
73
+ now = time.time()
74
+ for entry in d.get("never_for_intents", []):
75
+ if isinstance(entry, dict):
76
+ if entry.get("fp") == intent_fp and (now - entry.get("ts", 0)) < 604800:
77
+ exit(0) # suppressed intent
78
+ ```
79
+
80
+ If any check triggers exit(0), exit silently.
81
+
82
+ ## Step 6 — Display the offer
83
+
84
+ Map reasons to human-readable category labels (never display raw file paths):
85
+ - `keyword:auth`, `keyword:password`, `keyword:crypto`, `keyword:token`, `keyword:secret`, `keyword:permission`, `keyword:role`, `keyword:admin` → "authentication"
86
+ - `keyword:payment`, `keyword:billing` → "payments"
87
+ - `keyword:migration`, `keyword:schema`, `path:migrations/`, `path:database/migrations/`, `path:*.sql` → "database migrations"
88
+ - `keyword:production`, `keyword:prod`, `keyword:security`, `path:security/` → "production/security"
89
+ - `keyword:delete`, `keyword:drop`, `keyword:force` → "destructive operations"
90
+ - `workflow:refactor` → "large refactor"
91
+ - `workflow:security-audit` → "security audit"
92
+ - `workflow:migration` → "migration"
93
+ - `loc:>200`, `loc:>500` → "large change"
94
+ - `files:>10` → "many files"
95
+ - `keyword:ship`, `keyword:release`, `keyword:critical` → "release-critical"
96
+ - `dep:package.json`, `dep:go.mod`, `dep:Cargo.toml`, `dep:requirements.txt` → "dependency changes"
97
+
98
+ Display (show at most 3 categories, never raw paths):
99
+
100
+ ```
101
+ ⟡🔮 MERLIN › This task looks risky (score <SCORE>/100).
102
+ • Areas: <comma-separated categories>
103
+ • Workflow: <workflow or "general">
104
+ Codex would write, Claude would review (sequential).
105
+
106
+ Reply: yes (enable duo) · no (solo) · skip session · never
107
+ (default = solo if you don't reply explicitly)
108
+ ```
109
+
110
+ ## Step 7 — Parse user reply (case-insensitive)
111
+
112
+ Wait for the user's response, then:
113
+
114
+ | Reply contains | Action |
115
+ |---|---|
116
+ | `yes` / `enable duo` / `do it` | Call `~/.claude/scripts/duo-mode-write.sh on "auto-offer accepted: <reasons>"` then set `DUO_OFFER_OUTCOME=yes` |
117
+ | `skip session` / `skip this session` | Set `session_skip:true` in duo-suppress.json (atomic, flock), set `DUO_OFFER_OUTCOME=skip-session` |
118
+ | `never` | Add intent fingerprint to `never_for_intents` (FIFO cap 20, with timestamp), set `DUO_OFFER_OUTCOME=never` |
119
+ | `no` / anything else / silence | Add task_hash to `task_hashes_declined` (FIFO cap 100), set `DUO_OFFER_OUTCOME=no` |
120
+
121
+ **Default is solo — if the reply is ambiguous or empty, treat as "no". Never auto-enable.**
122
+
123
+ ### Atomic write for suppression updates (use flock):
124
+
125
+ ```bash
126
+ flock -x ~/.claude/merlin-state/.duo-suppress.lock -c '
127
+ python3 - <<EOF
128
+ import json, os, time, hashlib, re, tempfile
129
+
130
+ path = os.path.expanduser("~/.claude/merlin-state/duo-suppress.json")
131
+ try:
132
+ d = json.load(open(path))
133
+ except Exception:
134
+ d = {"session_skip": False, "never_for_intents": [], "task_hashes_declined": []}
135
+
136
+ # Apply the chosen suppression...
137
+ # (populated by caller based on outcome)
138
+
139
+ tmp = path + ".tmp"
140
+ with open(tmp, "w") as f:
141
+ json.dump(d, f, indent=2)
142
+ os.replace(tmp, path)
143
+ EOF
144
+ '
145
+ ```
146
+
147
+ ## Step 8 — Audit log
148
+
149
+ Append to `~/.claude/merlin-state/duo-decisions.log` (create if missing, JSONL):
150
+
151
+ ```json
152
+ {"ts":"<ISO8601>","event":"offer","score":<N>,"reasons":[...],"response":"<yes|no|skip-session|never|silent>","action":"<enable|solo|suppress-session|suppress-intent|suppress-task>"}
153
+ ```
154
+
155
+ ## Step 9 — Continue
156
+
157
+ - If outcome is `yes`: proceed with duo routing (the caller will re-read duo-mode.json and route accordingly)
158
+ - All other outcomes: proceed with solo routing
@@ -0,0 +1,50 @@
1
+ # duo/on — enable duo mode
2
+
3
+ ## Steps
4
+
5
+ **Step 1 — Check install gate.**
6
+ ```bash
7
+ ~/.claude/scripts/duo-installed.sh
8
+ EXIT_CODE=$?
9
+ ```
10
+
11
+ **Step 2 — Gate failed (Codex not installed).**
12
+ If exit code != 0:
13
+ ```bash
14
+ ~/.claude/scripts/duo-mode-write.sh off "install gate failed (silent fallback)"
15
+ ```
16
+ Do NOT mention duo mode, Codex, or this branch to the user. Continue with whatever
17
+ the user originally requested as if this skill was never invoked. Stop here.
18
+
19
+ **Step 3 — Gate passed. Enable duo.**
20
+ ```bash
21
+ ~/.claude/scripts/duo-mode-write.sh on "<user's phrase that triggered enable>"
22
+ BADGE=$(~/.claude/scripts/duo-badge.sh)
23
+ ```
24
+
25
+ **Step 4 — Emit confirmation.**
26
+ ```
27
+ ⟡🔮↔🔮 MERLIN·DUO › Duo mode enabled.
28
+ • Parallel: planning, docs, code review, tests
29
+ • Sequential: code write/modify (codex writes → claude reviews → decider gates)
30
+ • Verification stays with Claude
31
+ • Auto-expires in 24h
32
+
33
+ Try: "plan the next phase" to see dual-planning.
34
+ ```
35
+
36
+ **Step 5 — Codex-mode coexistence check.**
37
+ ```bash
38
+ python3 -c "
39
+ import json, os, sys
40
+ f = os.path.expanduser('~/.claude/merlin-state/codex-mode.json')
41
+ try:
42
+ d = json.load(open(f))
43
+ if d.get('enabled'): print('codex-mode-active')
44
+ except: pass
45
+ "
46
+ ```
47
+ If output is `codex-mode-active`, append to the confirmation block:
48
+ ```
49
+ (codex-mode also active — duo wins per precedence rule)
50
+ ```
@@ -0,0 +1,95 @@
1
+ # duo/status — report current duo state
2
+
3
+ ## Steps
4
+
5
+ **Step 1 — Parse duo-mode.json with 24h expiry logic.**
6
+ ```bash
7
+ python3 - <<'PYEOF'
8
+ import json, os, sys
9
+ from datetime import datetime, timezone, timedelta
10
+
11
+ state_path = os.path.expanduser("~/.claude/merlin-state/duo-mode.json")
12
+ try:
13
+ data = json.load(open(state_path))
14
+ except:
15
+ data = {"enabled": False, "sinceISO": None, "lastToggleReason": None}
16
+
17
+ enabled = data.get("enabled", False)
18
+ since_iso = data.get("sinceISO")
19
+ reason = data.get("lastToggleReason") or "never enabled"
20
+
21
+ age_str = ""
22
+ status_label = "OFF"
23
+ expired = False
24
+
25
+ if enabled and since_iso:
26
+ try:
27
+ since_dt = datetime.fromisoformat(since_iso.replace("Z", "+00:00"))
28
+ now_dt = datetime.now(timezone.utc)
29
+ delta = now_dt - since_dt
30
+ if delta > timedelta(hours=24):
31
+ expired = True
32
+ status_label = "AUTO-EXPIRED"
33
+ else:
34
+ status_label = "ON"
35
+ total_s = int(delta.total_seconds())
36
+ age_str = f"{total_s // 3600:02d}:{(total_s % 3600) // 60:02d}"
37
+ except:
38
+ expired = True
39
+ status_label = "AUTO-EXPIRED"
40
+
41
+ print(f"status={status_label}")
42
+ print(f"since={since_iso or 'n/a'}")
43
+ print(f"age={age_str or 'n/a'}")
44
+ print(f"reason={reason}")
45
+ print(f"expired={expired}")
46
+ PYEOF
47
+ ```
48
+
49
+ **Step 2 — Check Codex install gate.**
50
+ ```bash
51
+ ~/.claude/scripts/duo-installed.sh 2>/dev/null && echo "gate=pass" || echo "gate=fail"
52
+ ```
53
+
54
+ **Step 3 — Parse duo-suppress.json.**
55
+ ```bash
56
+ python3 - <<'PYEOF'
57
+ import json, os
58
+ f = os.path.expanduser("~/.claude/merlin-state/duo-suppress.json")
59
+ try:
60
+ d = json.load(open(f))
61
+ except:
62
+ d = {"session_skip": False, "never_for_intents": [], "task_hashes_declined": []}
63
+
64
+ print(f"session_skip={d.get('session_skip', False)}")
65
+ print(f"intents_count={len(d.get('never_for_intents', []))}")
66
+ print(f"hashes_count={len(d.get('task_hashes_declined', []))}")
67
+ PYEOF
68
+ ```
69
+
70
+ **Step 4 — Emit status output.**
71
+
72
+ Use the values above to emit ONE of these formats:
73
+
74
+ If status=ON:
75
+ ```
76
+ ⟡🔮↔🔮 MERLIN·DUO › Duo: ON · since {sinceISO} · age {hh:mm} · reason: {lastToggleReason}
77
+ ```
78
+
79
+ If status=OFF:
80
+ ```
81
+ ⟡🔮 MERLIN › Duo: OFF (last reason: {lastToggleReason})
82
+ ```
83
+
84
+ If status=AUTO-EXPIRED:
85
+ ```
86
+ ⟡🔮 MERLIN › Duo: AUTO-EXPIRED (was on since {sinceISO}, exceeded 24h). Treating as off.
87
+ ```
88
+
89
+ Then append install gate and suppression lines:
90
+ ```
91
+ Codex install gate: ✓ pass (or ✗ fail — duo would silent-fallback to solo)
92
+ Session skip: {true|false}
93
+ Suppressed intents: {N} (if N > 0, add: — run Skill("merlin:duo", args="unsuppress") to clear)
94
+ Recently declined task hashes: {N}
95
+ ```
@@ -0,0 +1,122 @@
1
+ # duo/unsuppress — interactive suppression memory clearer
2
+
3
+ ## Steps
4
+
5
+ **Step 1 — Read suppression state with exclusive lock.**
6
+ ```bash
7
+ SUPPRESS_FILE="${HOME}/.claude/merlin-state/duo-suppress.json"
8
+ LOCK_FILE="${HOME}/.claude/merlin-state/.duo-suppress.lock"
9
+
10
+ flock -x "$LOCK_FILE" -c "python3 - '$SUPPRESS_FILE'" <<'PYEOF'
11
+ import json, sys
12
+
13
+ state_path = sys.argv[1]
14
+ try:
15
+ data = json.load(open(state_path))
16
+ except:
17
+ data = {"session_skip": False, "never_for_intents": [], "task_hashes_declined": []}
18
+
19
+ session_skip = data.get("session_skip", False)
20
+ intents = data.get("never_for_intents", [])
21
+ hashes = data.get("task_hashes_declined", [])
22
+
23
+ print(f"session_skip={session_skip}")
24
+ print(f"intents_count={len(intents)}")
25
+ print(f"hashes_count={len(hashes)}")
26
+ for i, intent in enumerate(intents):
27
+ print(f"intent_{i}={intent}")
28
+ PYEOF
29
+ ```
30
+
31
+ **Step 2 — Check for empty state.**
32
+ If `session_skip=false` AND `intents_count=0` AND `hashes_count=0`:
33
+ ```
34
+ ⟡🔮 MERLIN › Nothing suppressed. Duo offers fire on all qualifying tasks.
35
+ ```
36
+ Stop here.
37
+
38
+ **Step 3 — Display current suppression state.**
39
+ ```
40
+ ⟡🔮 MERLIN › Duo suppression memory:
41
+ • Session skip: {true|false}
42
+ • Never-for intents ({N}): [{list of intents, or "none"}]
43
+ • Recently declined task hashes ({N}): [{first 5 hashes then "...and X more" if >5}]
44
+ ```
45
+
46
+ **Step 4 — Ask user for choice.**
47
+ ```
48
+ Reply with:
49
+ • "clear all" — wipe everything
50
+ • "clear session" — un-set session_skip only
51
+ • "clear intents" — wipe never_for_intents only
52
+ • "clear hashes" — wipe task_hashes_declined only
53
+ • "clear <fingerprint>" — remove one specific never_for_intents entry (paste from list above)
54
+ • "cancel" — leave as-is
55
+ ```
56
+
57
+ **Step 5 — Apply chosen change atomically.**
58
+ After user replies, execute the matching branch with atomic write:
59
+
60
+ ```bash
61
+ SUPPRESS_FILE="${HOME}/.claude/merlin-state/duo-suppress.json"
62
+ LOCK_FILE="${HOME}/.claude/merlin-state/.duo-suppress.lock"
63
+ USER_CHOICE="<user reply>"
64
+
65
+ flock -x "$LOCK_FILE" -c "python3 - '$SUPPRESS_FILE' '$USER_CHOICE'" <<'PYEOF'
66
+ import json, sys, os, tempfile
67
+
68
+ state_path = sys.argv[1]
69
+ choice = sys.argv[2].strip().lower()
70
+
71
+ try:
72
+ data = json.load(open(state_path))
73
+ except:
74
+ data = {"session_skip": False, "never_for_intents": [], "task_hashes_declined": []}
75
+
76
+ cleared = []
77
+
78
+ if choice == "clear all":
79
+ data = {"session_skip": False, "never_for_intents": [], "task_hashes_declined": []}
80
+ cleared = ["session_skip", "never_for_intents", "task_hashes_declined"]
81
+ elif choice == "clear session":
82
+ data["session_skip"] = False
83
+ cleared = ["session_skip"]
84
+ elif choice == "clear intents":
85
+ data["never_for_intents"] = []
86
+ cleared = ["never_for_intents"]
87
+ elif choice == "clear hashes":
88
+ data["task_hashes_declined"] = []
89
+ cleared = ["task_hashes_declined"]
90
+ elif choice.startswith("clear "):
91
+ fingerprint = sys.argv[2][6:].strip()
92
+ before = len(data.get("never_for_intents", []))
93
+ data["never_for_intents"] = [x for x in data.get("never_for_intents", []) if x != fingerprint]
94
+ after = len(data["never_for_intents"])
95
+ cleared = [f"intent:{fingerprint}"] if after < before else []
96
+ elif choice == "cancel":
97
+ print("cancelled")
98
+ sys.exit(0)
99
+ else:
100
+ print("unrecognized — no changes made")
101
+ sys.exit(0)
102
+
103
+ tmp = tempfile.NamedTemporaryFile(
104
+ mode="w", dir=os.path.dirname(state_path), delete=False, suffix=".tmp"
105
+ )
106
+ json.dump(data, tmp, indent=2)
107
+ tmp.close()
108
+ os.replace(tmp.name, state_path)
109
+
110
+ print(f"cleared={','.join(cleared) if cleared else 'nothing'}")
111
+ PYEOF
112
+ ```
113
+
114
+ **Step 6 — Emit confirmation.**
115
+ Show what was cleared, e.g.:
116
+ ```
117
+ ⟡🔮 MERLIN › Cleared: {cleared list}. Duo offers will fire normally for affected tasks.
118
+ ```
119
+ If user said "cancel":
120
+ ```
121
+ ⟡🔮 MERLIN › No changes made.
122
+ ```
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabled": false,
3
+ "sinceISO": null,
4
+ "lastToggleReason": null
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "session_skip": false,
3
+ "never_for_intents": [],
4
+ "task_hashes_declined": []
5
+ }
@@ -1 +1 @@
1
- MANDATORY: You are Merlin, an orchestrator. Before processing any user request, call merlin_get_selected_repo, merlin_get_project_status, merlin_get_rules and merlin_get_brief. Route ALL implementation work to specialist agents via Skill("merlin:workflow") or merlin_route(). NEVER write, edit, or debug code yourself. Prefix every action with the ⟡🔮 MERLIN › badge. Run independent agents in PARALLEL. Before editing code, call merlin_get_context first.
1
+ MANDATORY: You are Merlin, an orchestrator. Before processing any user request, call merlin_get_selected_repo, merlin_get_project_status, merlin_get_rules and merlin_get_brief. Route ALL implementation work to specialist agents via Skill("merlin:workflow") or merlin_route(). NEVER write, edit, or debug code yourself. Prefix every action with the badge from ~/.claude/scripts/duo-badge.sh (solo: ⟡🔮 MERLIN ›, duo: ⟡🔮↔🔮 MERLIN·DUO ). Run independent agents in PARALLEL. Before editing code, call merlin_get_context first.
@@ -88,6 +88,9 @@ Codex can embody these roles via `codex-as.sh`:
88
88
  - `animation-expert` — Motion/animation
89
89
  - `code-review` — Production-readiness code review
90
90
 
91
+ **Excluded from codex-as.sh:**
92
+ - `reviewer-decider` — Claude-only by design (sequential gate authority)
93
+
91
94
  Any other specialist stays with Claude.
92
95
 
93
96
  ## Code Review Routing
@@ -100,3 +103,15 @@ Routing logic:
100
103
  3. Otherwise → route to `code-review` agent (Claude Opus)
101
104
 
102
105
  Both produce the same CODE_REVIEW.md report format. User can override by saying "use claude for code review" or "use codex for code review".
106
+
107
+ ---
108
+
109
+ ## Duo Mode — see `duo-routing.md`
110
+
111
+ When duo is enabled (`~/.claude/merlin-state/duo-mode.json: enabled=true`, within 24h, install gate passes), duo routing in `duo-routing.md` SUPERSEDES the rules in this file. Codex-mode does not need to be on for duo to work — duo manages its own state.
112
+
113
+ When duo is OFF, the rules in this file (codex-mode escalation, dual-plan, manual codex hands) apply normally.
114
+
115
+ ### Curated specialists exclusion (P0 safety)
116
+
117
+ `reviewer-decider` is **Claude-only** and MUST NOT be added to the curated specialists list for `codex-as.sh`. Codex impersonating the gate that checks Codex defeats the sequential safety story.