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.
- package/README.md +19 -0
- package/bin/install.cjs +71 -16
- package/files/CLAUDE.md +25 -3
- package/files/agents/merlin.md +3 -2
- package/files/agents/reviewer-decider.md +124 -0
- package/files/commands/merlin/challenge.md +2 -0
- package/files/hooks/config-change.sh +3 -2
- package/files/hooks/notify-desktop.sh +1 -1
- package/files/hooks/notify-webhook.sh +2 -1
- package/files/hooks/orchestrator-guard.sh +3 -2
- package/files/hooks/pre-edit-sights-check.sh +3 -2
- package/files/hooks/task-completed-verify.sh +2 -2
- package/files/hooks/user-prompt-router.sh +6 -5
- package/files/hooks/worktree-create.sh +1 -1
- package/files/hooks/worktree-remove.sh +1 -1
- package/files/merlin/skills/duo/SKILL.md +48 -0
- package/files/merlin/skills/duo/off.md +32 -0
- package/files/merlin/skills/duo/offer.md +158 -0
- package/files/merlin/skills/duo/on.md +50 -0
- package/files/merlin/skills/duo/status.md +95 -0
- package/files/merlin/skills/duo/unsuppress.md +122 -0
- package/files/merlin-state/duo-mode.json +5 -0
- package/files/merlin-state/duo-suppress.json +5 -0
- package/files/merlin-system-prompt.txt +1 -1
- package/files/rules/codex-routing.md +15 -0
- package/files/rules/duo-routing.md +203 -0
- package/files/rules/merlin-routing.md +6 -0
- package/files/scripts/duo-badge.sh +39 -0
- package/files/scripts/duo-codex-call.sh +83 -0
- package/files/scripts/duo-installed.sh +8 -0
- package/files/scripts/duo-mode-read.sh +51 -0
- package/files/scripts/duo-mode-write.sh +66 -0
- package/files/scripts/duo-pre-route.sh +124 -0
- package/files/scripts/duo-risk-detect.sh +157 -0
- 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": "
|
|
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",
|