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,157 @@
1
+ #!/usr/bin/env bash
2
+ # duo-risk-detect.sh — heuristic risk scorer for duo auto-offer
3
+ # Usage: duo-risk-detect.sh --task "<text>" [--workflow <name>] [--files <comma,sep>] [--loc <int>]
4
+ # Output: single JSON line {"score":<0-100>,"reasons":[...],"suggest_duo":<bool>}
5
+ # Always exits 0 — never blocks routing. Times out at 500ms.
6
+
7
+ set -euo pipefail
8
+
9
+ TASK=""
10
+ WORKFLOW=""
11
+ FILES=""
12
+ LOC=""
13
+
14
+ while [[ $# -gt 0 ]]; do
15
+ case "$1" in
16
+ --task) TASK="${2:-}"; shift 2 ;;
17
+ --workflow) WORKFLOW="${2:-}"; shift 2 ;;
18
+ --files) FILES="${2:-}"; shift 2 ;;
19
+ --loc) LOC="${2:-}"; shift 2 ;;
20
+ *) shift ;;
21
+ esac
22
+ done
23
+
24
+ THRESHOLD="${MERLIN_DUO_OFFER_THRESHOLD:-50}"
25
+
26
+ # Write the scorer to a temp file so timeout can exec it cleanly (no eval of user input)
27
+ TMPSCRIPT=$(mktemp /tmp/duo-risk-score.XXXXXX.py)
28
+ trap 'rm -f "$TMPSCRIPT"' EXIT
29
+
30
+ cat > "$TMPSCRIPT" << 'PYEOF'
31
+ import os, sys, json, re
32
+
33
+ task = os.environ.get("_DUO_TASK", "")
34
+ workflow = os.environ.get("_DUO_WORKFLOW", "")
35
+ files = os.environ.get("_DUO_FILES", "")
36
+ loc_raw = os.environ.get("_DUO_LOC", "")
37
+ threshold = int(os.environ.get("_DUO_THRESHOLD", "50"))
38
+
39
+ score = 0
40
+ reasons = []
41
+
42
+ # Keyword scoring: +20 each, cap +40
43
+ KEYWORDS = [
44
+ "auth", "password", "payment", "billing", "migration", "schema",
45
+ "production", "prod", "security", "crypto", "token", "secret",
46
+ "permission", "role", "admin", "delete", "drop", "force"
47
+ ]
48
+ task_lower = task.lower()
49
+ kw_score = 0
50
+ for kw in KEYWORDS:
51
+ if re.search(r"\b" + re.escape(kw) + r"\b", task_lower):
52
+ if kw_score < 40:
53
+ add = min(20, 40 - kw_score)
54
+ kw_score += add
55
+ reasons.append(f"keyword:{kw}")
56
+ score += kw_score
57
+
58
+ # File path scoring: +25 each, cap +50
59
+ PATH_PATTERNS = [
60
+ ("migrations/", "path:migrations/"),
61
+ ("auth/", "path:auth/"),
62
+ ("payment/", "path:payment/"),
63
+ ("billing/", "path:billing/"),
64
+ ("security/", "path:security/"),
65
+ ("database/migrations/", "path:database/migrations/"),
66
+ ("infra/", "path:infra/"),
67
+ (".github/workflows/", "path:.github/workflows/"),
68
+ ("Dockerfile", "path:Dockerfile"),
69
+ (".sql", "path:*.sql"),
70
+ ]
71
+ path_score = 0
72
+ for pattern, tag in PATH_PATTERNS:
73
+ if pattern in files:
74
+ if path_score < 50:
75
+ add = min(25, 50 - path_score)
76
+ path_score += add
77
+ reasons.append(tag)
78
+ score += path_score
79
+
80
+ # Workflow scoring: +30
81
+ HIGH_RISK_WORKFLOWS = {"security-audit", "refactor", "migration"}
82
+ if workflow.lower() in HIGH_RISK_WORKFLOWS:
83
+ score += 30
84
+ reasons.append(f"workflow:{workflow.lower()}")
85
+
86
+ # LOC delta scoring (>500 replaces >200)
87
+ try:
88
+ loc = int(loc_raw.strip()) if loc_raw.strip() else 0
89
+ except ValueError:
90
+ loc = 0
91
+
92
+ if loc > 500:
93
+ score += 35
94
+ reasons.append("loc:>500")
95
+ elif loc > 200:
96
+ score += 20
97
+ reasons.append("loc:>200")
98
+
99
+ # Files count: +15
100
+ if files.strip():
101
+ file_list = [f for f in files.split(",") if f.strip()]
102
+ if len(file_list) > 10:
103
+ score += 15
104
+ reasons.append("files:>10")
105
+
106
+ # Production/critical keywords: +25 (first match only)
107
+ PROD_WORDS = ["production", "ship", "release", "critical"]
108
+ for word in PROD_WORDS:
109
+ if re.search(r"\b" + re.escape(word) + r"\b", task_lower):
110
+ score += 25
111
+ reasons.append(f"keyword:{word}")
112
+ break
113
+
114
+ # Dep-manifest files: +15 (first match only)
115
+ DEP_FILES = ["package.json", "go.mod", "Cargo.toml", "requirements.txt"]
116
+ for dep in DEP_FILES:
117
+ if dep in files:
118
+ score += 15
119
+ reasons.append(f"dep:{dep}")
120
+ break
121
+
122
+ # Cap at 100, keep top 5 reasons
123
+ score = min(score, 100)
124
+ reasons = reasons[:5]
125
+
126
+ print(json.dumps({"score": score, "reasons": reasons, "suggest_duo": score >= threshold}))
127
+ PYEOF
128
+
129
+ # Export all inputs via env — never interpolated into code
130
+ export _DUO_TASK="$TASK"
131
+ export _DUO_WORKFLOW="$WORKFLOW"
132
+ export _DUO_FILES="$FILES"
133
+ export _DUO_LOC="$LOC"
134
+ export _DUO_THRESHOLD="$THRESHOLD"
135
+
136
+ # Run with 500ms timeout — try gtimeout (macOS Homebrew coreutils), then timeout, then perl alarm
137
+ _TIMEOUT_CMD=""
138
+ if command -v gtimeout >/dev/null 2>&1; then
139
+ _TIMEOUT_CMD="gtimeout 0.5"
140
+ elif timeout --version >/dev/null 2>&1; then
141
+ _TIMEOUT_CMD="timeout 0.5"
142
+ fi
143
+
144
+ if [[ -n "$_TIMEOUT_CMD" ]]; then
145
+ OUT=$($_TIMEOUT_CMD python3 "$TMPSCRIPT" 2>/dev/null) || OUT=""
146
+ else
147
+ OUT=$(perl -e 'alarm 1; exec @ARGV' -- python3 "$TMPSCRIPT" 2>/dev/null) || OUT=""
148
+ fi
149
+
150
+ # Validate JSON; fall back to safe default
151
+ if python3 -c "import json,sys; json.loads(sys.stdin.read())" <<< "$OUT" 2>/dev/null; then
152
+ echo "$OUT"
153
+ else
154
+ echo '{"score":0,"reasons":["timeout"],"suggest_duo":false}'
155
+ fi
156
+
157
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-merlin-brain",
3
- "version": "4.2.0",
3
+ "version": "5.0.1",
4
4
  "description": "Merlin - The Ultimate AI Brain for Claude Code, Codex, and other AI CLIs. One install: workflows, agents, loop, and Sights MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",