create-merlin-brain 5.0.1 → 5.3.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 +38 -4
- package/bin/install.cjs +20 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +75 -0
- package/dist/server/server.js.map +1 -1
- package/dist/server/tools/context.d.ts.map +1 -1
- package/dist/server/tools/context.js +60 -0
- package/dist/server/tools/context.js.map +1 -1
- package/dist/server/tools/project-picture.d.ts +17 -0
- package/dist/server/tools/project-picture.d.ts.map +1 -0
- package/dist/server/tools/project-picture.js +204 -0
- package/dist/server/tools/project-picture.js.map +1 -0
- package/dist/server/tools/types.d.ts +24 -0
- package/dist/server/tools/types.d.ts.map +1 -1
- package/files/CLAUDE.md +2 -0
- package/files/agents/challenger-academic.md +8 -0
- package/files/agents/challenger-arbiter.md +8 -0
- package/files/agents/challenger-insider.md +8 -0
- package/files/agents/code-organization-supervisor.md +8 -0
- package/files/agents/codex-planner.md +36 -9
- package/files/agents/context-guardian.md +8 -0
- package/files/agents/docs-keeper.md +8 -0
- package/files/agents/dry-refactor.md +8 -0
- package/files/agents/elite-code-refactorer.md +8 -0
- package/files/agents/hardening-guard.md +8 -0
- package/files/agents/implementation-dev.md +8 -0
- package/files/agents/merlin-access-control-reviewer.md +8 -0
- package/files/agents/merlin-api-designer.md +8 -0
- package/files/agents/merlin-codebase-mapper.md +8 -0
- package/files/agents/merlin-debugger.md +8 -0
- package/files/agents/merlin-dependency-auditor.md +8 -0
- package/files/agents/merlin-edge-case-hunter.md +8 -0
- package/files/agents/merlin-executor.md +8 -0
- package/files/agents/merlin-frontend.md +8 -0
- package/files/agents/merlin-input-validator.md +8 -0
- package/files/agents/merlin-integration-checker.md +8 -0
- package/files/agents/merlin-migrator.md +8 -0
- package/files/agents/merlin-milestone-auditor.md +8 -0
- package/files/agents/merlin-party-review.md +8 -0
- package/files/agents/merlin-performance.md +8 -0
- package/files/agents/merlin-planner.md +9 -1
- package/files/agents/merlin-researcher.md +8 -0
- package/files/agents/merlin-reviewer.md +8 -0
- package/files/agents/merlin-sast-reviewer.md +8 -0
- package/files/agents/merlin-secret-scanner.md +8 -0
- package/files/agents/merlin-security.md +8 -0
- package/files/agents/merlin-verifier.md +8 -0
- package/files/agents/merlin-work-verifier.md +8 -0
- package/files/agents/merlin.md +8 -0
- package/files/agents/ops-railway.md +8 -0
- package/files/agents/orchestrator-retrofit.md +8 -0
- package/files/agents/product-spec.md +8 -0
- package/files/agents/remotion.md +8 -0
- package/files/agents/system-architect.md +8 -0
- package/files/agents/tests-qa.md +8 -0
- package/files/commands/merlin/course-correct.md +8 -0
- package/files/commands/merlin/design-audit.md +92 -0
- package/files/commands/merlin/health.md +8 -0
- package/files/commands/merlin/next.md +8 -0
- package/files/commands/merlin/optimize.md +89 -0
- package/files/commands/merlin/polish.md +99 -0
- package/files/commands/merlin/quick.md +8 -0
- package/files/commands/merlin/readiness-gate.md +8 -0
- package/files/commands/merlin/redesign.md +108 -0
- package/files/hooks/session-start.sh +88 -2
- package/files/hooks/statusline.sh +9 -1
- package/files/loop/README.md +11 -0
- package/files/merlin/skills/SKILLS-INDEX.md +16 -1
- package/files/merlin/skills/TASK-OPTIMIZER.json +310 -0
- package/files/merlin/skills/coding/focus-mode.md +8 -0
- package/files/merlin/skills/design/emil-design-eng.md +31 -0
- package/files/merlin/skills/design/impeccable.md +36 -0
- package/files/merlin/skills/duo/offer.md +20 -7
- package/files/merlin/skills/duo/on.md +26 -17
- package/files/merlin/skills/duo/status.md +10 -3
- package/files/merlin/templates/DEBUG.md +11 -0
- package/files/merlin/templates/UAT.md +11 -0
- package/files/merlin/templates/phase-prompt.md +11 -0
- package/files/merlin/templates/project.md +11 -0
- package/files/merlin/templates/requirements.md +11 -0
- package/files/merlin/templates/roadmap.md +11 -0
- package/files/merlin/templates/state.md +11 -0
- package/files/merlin/templates/verification-report.md +11 -0
- package/files/merlin/workflows/execute-phase.md +11 -0
- package/files/merlin/workflows/plan-phase.md +11 -0
- package/files/merlin/workflows/progress.md +11 -0
- package/files/merlin/workflows/resume-project.md +11 -0
- package/files/merlin/workflows/verify-phase.md +11 -0
- package/files/merlin/workflows/verify-work.md +11 -0
- package/files/merlin-system-prompt.txt +35 -1
- package/files/rules/codex-routing.md +19 -0
- package/files/rules/duo-routing.md +109 -10
- package/files/rules/merlin-routing.md +40 -0
- package/files/scripts/codex-as.sh +5 -2
- package/files/scripts/design-intent-detect.sh +8 -0
- package/files/scripts/duo-badge.sh +3 -5
- package/files/scripts/duo-installed.sh +3 -3
- package/files/scripts/duo-mode-read.sh +30 -10
- package/files/scripts/duo-mode-write.sh +28 -3
- package/files/scripts/duo-pre-route.sh +2 -8
- package/files/scripts/install-design-skills.sh +86 -0
- package/files/scripts/merlin-codex.sh +84 -0
- package/files/scripts/task-optimize.sh +335 -0
- package/package.json +1 -1
|
@@ -30,13 +30,7 @@ if [[ "$DUO_STATE" == "enabled" ]]; then
|
|
|
30
30
|
exit 0
|
|
31
31
|
fi
|
|
32
32
|
|
|
33
|
-
# Branch 2:
|
|
34
|
-
if ! "${SCRIPT_DIR}/duo-installed.sh" 2>/dev/null; then
|
|
35
|
-
echo "mode=solo"
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
# Branch 3: risk-detect suggestion
|
|
33
|
+
# Branch 2: risk-detect suggestion (duo-installed.sh is always-pass; offer fires regardless of Codex)
|
|
40
34
|
RISK_DETECT="${SCRIPT_DIR}/duo-risk-detect.sh"
|
|
41
35
|
if [[ ! -x "$RISK_DETECT" ]]; then
|
|
42
36
|
echo "mode=solo"
|
|
@@ -62,7 +56,7 @@ if [[ "$SUGGEST_DUO" != "true" ]]; then
|
|
|
62
56
|
exit 0
|
|
63
57
|
fi
|
|
64
58
|
|
|
65
|
-
# Branch
|
|
59
|
+
# Branch 2a: suppression check — use a temp python script to avoid heredoc-in-subshell issues
|
|
66
60
|
REASONS_JSON=$(python3 -c "
|
|
67
61
|
import json, sys
|
|
68
62
|
try:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Merlin Design Skills Installer
|
|
5
|
+
# Installs and prioritizes emilkowalski/skill and pbakaus/impeccable
|
|
6
|
+
|
|
7
|
+
# Honor skip flag
|
|
8
|
+
if [ "${MERLIN_SKIP_DESIGN_SKILLS:-0}" = "1" ]; then
|
|
9
|
+
exit 0
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
echo "[merlin] Installing priority design skills…"
|
|
13
|
+
|
|
14
|
+
SKILLS_DIR="${HOME}/.claude/skills"
|
|
15
|
+
mkdir -p "$SKILLS_DIR"
|
|
16
|
+
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════
|
|
18
|
+
# Install emilkowalski/skill (install-only, no license vendoring)
|
|
19
|
+
# ═══════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
if [ ! -f "$SKILLS_DIR/emil-design-eng/SKILL.md" ]; then
|
|
22
|
+
if command -v npx >/dev/null 2>&1; then
|
|
23
|
+
# Attempt install via skills CLI
|
|
24
|
+
if npx --yes skills add emilkowalski/skill 2>/dev/null; then
|
|
25
|
+
: # Success
|
|
26
|
+
else
|
|
27
|
+
# Non-fatal — user can install manually
|
|
28
|
+
echo "[merlin] emil skill install hint: npx --yes skills add emilkowalski/skill"
|
|
29
|
+
fi
|
|
30
|
+
else
|
|
31
|
+
echo "[merlin] npx not found; skipping emil skill (user can install: npx --yes skills add emilkowalski/skill)"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ═══════════════════════════════════════════════════════════════
|
|
36
|
+
# Install pbakaus/impeccable (fetch + preserve Apache-2.0 attribution)
|
|
37
|
+
# ═══════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
if [ ! -d "$SKILLS_DIR/impeccable" ]; then
|
|
40
|
+
if command -v git >/dev/null 2>&1; then
|
|
41
|
+
tmp=$(mktemp -d)
|
|
42
|
+
trap "rm -rf '$tmp'" EXIT
|
|
43
|
+
|
|
44
|
+
if git clone --depth 1 https://github.com/pbakaus/impeccable.git "$tmp" 2>/dev/null; then
|
|
45
|
+
# Copy the skill subtree (contains SKILL.md and commands/)
|
|
46
|
+
if [ -d "$tmp/dist/claude-code/.claude/skills/impeccable" ]; then
|
|
47
|
+
mkdir -p "$SKILLS_DIR"
|
|
48
|
+
cp -R "$tmp/dist/claude-code/.claude/skills/impeccable" "$SKILLS_DIR/"
|
|
49
|
+
|
|
50
|
+
# Preserve license attribution
|
|
51
|
+
if [ -f "$tmp/LICENSE" ]; then
|
|
52
|
+
cp "$tmp/LICENSE" "$SKILLS_DIR/impeccable/LICENSE"
|
|
53
|
+
fi
|
|
54
|
+
if [ -f "$tmp/NOTICE.md" ]; then
|
|
55
|
+
cp "$tmp/NOTICE.md" "$SKILLS_DIR/impeccable/NOTICE.md"
|
|
56
|
+
fi
|
|
57
|
+
else
|
|
58
|
+
echo "[merlin] impeccable skill structure not found in cloned repo"
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
echo "[merlin] git clone failed for impeccable; user can install manually"
|
|
62
|
+
fi
|
|
63
|
+
else
|
|
64
|
+
echo "[merlin] git not found; skipping impeccable skill (user can install manually: git clone pbakaus/impeccable)"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ═══════════════════════════════════════════════════════════════
|
|
69
|
+
# Verification
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
has_impeccable=0
|
|
73
|
+
has_emil=0
|
|
74
|
+
|
|
75
|
+
[ -d "$SKILLS_DIR/impeccable" ] && has_impeccable=1
|
|
76
|
+
[ -f "$SKILLS_DIR/emil-design-eng/SKILL.md" ] && has_emil=1
|
|
77
|
+
|
|
78
|
+
if [ "$has_impeccable" = "1" ] || [ "$has_emil" = "1" ]; then
|
|
79
|
+
echo "[merlin] Design skills ready:"
|
|
80
|
+
[ "$has_impeccable" = "1" ] && echo " ✓ impeccable"
|
|
81
|
+
[ "$has_emil" = "1" ] && echo " ✓ emil-design-eng"
|
|
82
|
+
else
|
|
83
|
+
echo "[merlin] Design skills install: no changes (already present or install failed)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
exit 0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# merlin-codex.sh — Merlin-aware Codex CLI wrapper
|
|
4
|
+
#
|
|
5
|
+
# Prints the Merlin engagement banner to stderr and injects the Merlin
|
|
6
|
+
# orchestrator system prompt into every Codex invocation. Pass-through
|
|
7
|
+
# subcommands (login, logout, mcp, etc.) are forwarded unchanged.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# merlin-codex # interactive Codex shell with Merlin banner
|
|
11
|
+
# merlin-codex exec "list files" # exec with Merlin prompt prepended
|
|
12
|
+
# merlin-codex "fix the auth bug" # prompt with Merlin prompt prepended
|
|
13
|
+
# merlin-codex --help # forwarded directly to codex
|
|
14
|
+
# merlin-codex login # forwarded directly (no prompt injection)
|
|
15
|
+
#
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
export MERLIN_RUNTIME=codex
|
|
19
|
+
|
|
20
|
+
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
SYSTEM_PROMPT_FILE="${HOME}/.claude/merlin-system-prompt.txt"
|
|
22
|
+
FALLBACK_PROMPT="You are Merlin: orchestrator, not coder. Route implementation to specialists. Badge every action with ⟡🔮 MERLIN ›."
|
|
23
|
+
|
|
24
|
+
# ── Pass-through subcommands (no Merlin prompt injection) ──────
|
|
25
|
+
PASSTHROUGH_CMDS="login logout mcp mcp-server app app-server completion sandbox debug"
|
|
26
|
+
|
|
27
|
+
# ── Determine duo mode and pair for banner ─────────────────────
|
|
28
|
+
_mode_label="Solo"
|
|
29
|
+
_pair_label=""
|
|
30
|
+
if [ -x "${SCRIPTS_DIR}/duo-mode-read.sh" ]; then
|
|
31
|
+
_duo=$("${SCRIPTS_DIR}/duo-mode-read.sh" 2>/dev/null) || _duo="disabled"
|
|
32
|
+
if [ "${_duo}" = "enabled" ]; then
|
|
33
|
+
_mode_label="Duo"
|
|
34
|
+
_pair=$("${SCRIPTS_DIR}/duo-mode-read.sh" --pair 2>/dev/null) || _pair="none"
|
|
35
|
+
if [ "${_pair}" != "none" ]; then
|
|
36
|
+
_pair_label=" (${_pair})"
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# ── Print banner to stderr (always) ───────────────────────────
|
|
42
|
+
printf '⟡🔮 MERLIN · entering Codex via merlin-codex\n' >&2
|
|
43
|
+
printf '🎯 Mode: %s%s\n' "${_mode_label}" "${_pair_label}" >&2
|
|
44
|
+
printf '▶ Codex will inherit Merlin'"'"'s orchestrator instructions\n' >&2
|
|
45
|
+
|
|
46
|
+
# ── If no args, open interactive Codex shell ──────────────────
|
|
47
|
+
if [ $# -eq 0 ]; then
|
|
48
|
+
exec codex
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── Check for pass-through subcommand or flags ────────────────
|
|
52
|
+
FIRST_ARG="${1:-}"
|
|
53
|
+
for cmd in ${PASSTHROUGH_CMDS}; do
|
|
54
|
+
if [ "${FIRST_ARG}" = "${cmd}" ]; then
|
|
55
|
+
exec codex "$@"
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
# Also pass through if first arg is a flag like --help, --version
|
|
59
|
+
if [[ "${FIRST_ARG}" == --* ]] || [[ "${FIRST_ARG}" == -* ]]; then
|
|
60
|
+
exec codex "$@"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── Load Merlin system prompt ──────────────────────────────────
|
|
64
|
+
MERLIN_PROMPT="${FALLBACK_PROMPT}"
|
|
65
|
+
if [ -f "${SYSTEM_PROMPT_FILE}" ]; then
|
|
66
|
+
MERLIN_PROMPT=$(cat "${SYSTEM_PROMPT_FILE}" 2>/dev/null) || MERLIN_PROMPT="${FALLBACK_PROMPT}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ── Inject Merlin prompt into Codex invocation ─────────────────
|
|
70
|
+
# Try -c instructions= config override first (documented Codex flag).
|
|
71
|
+
# If the subcommand is "exec", inject into the prompt argument.
|
|
72
|
+
# Otherwise prepend to the full prompt string.
|
|
73
|
+
if [ "${FIRST_ARG}" = "exec" ]; then
|
|
74
|
+
shift
|
|
75
|
+
# $@ is now the prompt(s) passed to codex exec
|
|
76
|
+
ORIGINAL_PROMPT="${*:-}"
|
|
77
|
+
FULL_PROMPT="${MERLIN_PROMPT}"$'\n\n'"---USER---"$'\n\n'"${ORIGINAL_PROMPT}"
|
|
78
|
+
exec codex exec "${FULL_PROMPT}"
|
|
79
|
+
else
|
|
80
|
+
# Treat remaining args as a single prompt string
|
|
81
|
+
ORIGINAL_PROMPT="${*:-}"
|
|
82
|
+
FULL_PROMPT="${MERLIN_PROMPT}"$'\n\n'"---USER---"$'\n\n'"${ORIGINAL_PROMPT}"
|
|
83
|
+
exec codex "${FULL_PROMPT}"
|
|
84
|
+
fi
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task-optimize.sh — Universal Task Optimizer for Merlin v5.3.0
|
|
3
|
+
# Usage: task-optimize.sh --task "<text>" [--registry <path>] [--self-test]
|
|
4
|
+
# Output: single JSON line {"score":<0-100>,"intent":"<id>","skills":["<id1>"],"agent":"<agent>","matched_phrases":["..."]}
|
|
5
|
+
# Always exits 0 in normal usage. --self-test exits non-zero on failure.
|
|
6
|
+
# Times out at 500ms.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
TASK=""
|
|
11
|
+
REGISTRY=""
|
|
12
|
+
SELF_TEST=false
|
|
13
|
+
|
|
14
|
+
while [[ $# -gt 0 ]]; do
|
|
15
|
+
case "$1" in
|
|
16
|
+
--task) TASK="${2:-}"; shift 2 ;;
|
|
17
|
+
--registry) REGISTRY="${2:-}"; shift 2 ;;
|
|
18
|
+
--self-test) SELF_TEST=true; shift ;;
|
|
19
|
+
*) shift ;;
|
|
20
|
+
esac
|
|
21
|
+
done
|
|
22
|
+
|
|
23
|
+
# Default registry locations
|
|
24
|
+
if [[ -z "$REGISTRY" ]]; then
|
|
25
|
+
if [[ -f "$HOME/.claude/merlin/skills/TASK-OPTIMIZER.json" ]]; then
|
|
26
|
+
REGISTRY="$HOME/.claude/merlin/skills/TASK-OPTIMIZER.json"
|
|
27
|
+
elif [[ -f "$HOME/.claude/skills/merlin/TASK-OPTIMIZER.json" ]]; then
|
|
28
|
+
REGISTRY="$HOME/.claude/skills/merlin/TASK-OPTIMIZER.json"
|
|
29
|
+
else
|
|
30
|
+
# Fallback to package location (for testing during development)
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
REGISTRY="$SCRIPT_DIR/../merlin/skills/TASK-OPTIMIZER.json"
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════
|
|
37
|
+
# SELF-TEST MODE
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════
|
|
39
|
+
if [[ "$SELF_TEST" == "true" ]]; then
|
|
40
|
+
TMPTEST=$(mktemp /tmp/task-opt-test.XXXXXX.py)
|
|
41
|
+
trap 'rm -f "$TMPTEST"' EXIT
|
|
42
|
+
|
|
43
|
+
cat > "$TMPTEST" << 'PYTESTEOF'
|
|
44
|
+
import sys
|
|
45
|
+
import os
|
|
46
|
+
import json
|
|
47
|
+
import subprocess
|
|
48
|
+
|
|
49
|
+
# Test cases: (task, expected_intent, expected_agent, min_score)
|
|
50
|
+
TEST_CASES = [
|
|
51
|
+
("this looks ugly, redesign it", "design", "ui-builder", 25),
|
|
52
|
+
("make it pretty", "design", "ui-builder", 25),
|
|
53
|
+
("redesign the homepage", "design", "ui-builder", 25),
|
|
54
|
+
("design review of the new component", "design", "ui-builder", 25),
|
|
55
|
+
("WCAG compliance audit", "a11y", "hardening-guard", 25),
|
|
56
|
+
("accessibility fixes for the form", "a11y", "hardening-guard", 25),
|
|
57
|
+
("responsive design issue on mobile", "design", "ui-builder", 25),
|
|
58
|
+
("mobile broken on iPhone", "design", "ui-builder", 25),
|
|
59
|
+
("dark mode toggle implementation", "design", "ui-builder", 25),
|
|
60
|
+
("theme switching support", "design", "ui-builder", 25),
|
|
61
|
+
("match Stripe design quality", "design", "ui-builder", 25),
|
|
62
|
+
("loading state for the form", "design", "ui-builder", 25),
|
|
63
|
+
("UX writing for empty states", "design", "ui-builder", 25),
|
|
64
|
+
("error messages copy review", "design", "ui-builder", 25),
|
|
65
|
+
("animate this drawer", "animation-polish", "animation-expert", 25),
|
|
66
|
+
("transitions feel stiff", "animation-polish", "animation-expert", 25),
|
|
67
|
+
("add motion to the cards", "animation-polish", "animation-expert", 25),
|
|
68
|
+
("polish the micro-interactions", "animation-polish", "animation-expert", 25),
|
|
69
|
+
("add OAuth login flow", "security", "hardening-guard", 25),
|
|
70
|
+
("CSRF protection check", "security", "hardening-guard", 25),
|
|
71
|
+
("input validation hardening", "security", "hardening-guard", 25),
|
|
72
|
+
("write unit tests for the parser", "testing", "tests-qa", 25),
|
|
73
|
+
("TDD workflow for the API", "testing", "tests-qa", 25),
|
|
74
|
+
("Jest test coverage", "testing", "tests-qa", 25),
|
|
75
|
+
("REST API endpoint design", "api", "merlin-api-designer", 25),
|
|
76
|
+
("OpenAPI spec for the v2 routes", "api", "merlin-api-designer", 25),
|
|
77
|
+
("GraphQL schema design", "api", "merlin-api-designer", 25),
|
|
78
|
+
("Core Web Vitals optimization", "performance", "merlin-performance", 25),
|
|
79
|
+
("bundle size reduction", "performance", "merlin-performance", 25),
|
|
80
|
+
("lighthouse score improvements", "performance", "merlin-performance", 25),
|
|
81
|
+
("debug this null pointer crash", "debug", "merlin-debugger", 25),
|
|
82
|
+
("investigate the broken deploy", "debug", "merlin-debugger", 25),
|
|
83
|
+
("Docker containerization for the worker", "docker", "ops-railway", 25),
|
|
84
|
+
("multi-stage build for the API", "docker", "ops-railway", 25),
|
|
85
|
+
("Stripe payment integration", "payments", "implementation-dev", 25),
|
|
86
|
+
("subscription billing setup", "payments", "implementation-dev", 25),
|
|
87
|
+
("webhook signature verification", "webhooks", "implementation-dev", 25),
|
|
88
|
+
("send email via Gmail API", "email", "implementation-dev", 25),
|
|
89
|
+
("WhatsApp business API integration", "whatsapp", "implementation-dev", 25),
|
|
90
|
+
("Telegram bot for notifications", "telegram", "implementation-dev", 25),
|
|
91
|
+
("Google Sheets automation", "sheets", "implementation-dev", 25),
|
|
92
|
+
("brainstorm options for onboarding", "brainstorm", "merlin-researcher", 25),
|
|
93
|
+
("React component refactor", "react", "merlin-frontend", 25),
|
|
94
|
+
("useState hook pattern", "react", "merlin-frontend", 25),
|
|
95
|
+
("Next.js server component", "react", "merlin-frontend", 25),
|
|
96
|
+
("fix the database query", "none", "", 0),
|
|
97
|
+
("rename a variable", "none", "", 0),
|
|
98
|
+
("update README", "none", "", 0),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def run_test(script_path, registry_path, task, expected_intent, expected_agent, min_score):
|
|
102
|
+
"""Run the optimizer and check result."""
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
['bash', script_path, '--task', task, '--registry', registry_path],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=5
|
|
109
|
+
)
|
|
110
|
+
output = result.stdout.strip()
|
|
111
|
+
if not output:
|
|
112
|
+
return False, f"empty output (stderr: {result.stderr.strip()})"
|
|
113
|
+
|
|
114
|
+
data = json.loads(output)
|
|
115
|
+
actual_intent = data.get('intent', '')
|
|
116
|
+
actual_agent = data.get('agent', '')
|
|
117
|
+
actual_score = data.get('score', 0)
|
|
118
|
+
|
|
119
|
+
# Check conditions
|
|
120
|
+
if expected_intent == "none":
|
|
121
|
+
# For "none" cases, we expect score < min_score (which is 0, so score must be 0)
|
|
122
|
+
if actual_intent != "none" and actual_score >= 25:
|
|
123
|
+
return False, f"got intent={actual_intent}, agent={actual_agent}, score={actual_score} (expected none)"
|
|
124
|
+
return True, ""
|
|
125
|
+
else:
|
|
126
|
+
if actual_intent != expected_intent:
|
|
127
|
+
return False, f"intent={actual_intent} (expected {expected_intent})"
|
|
128
|
+
if actual_agent != expected_agent:
|
|
129
|
+
return False, f"agent={actual_agent} (expected {expected_agent})"
|
|
130
|
+
if actual_score < min_score:
|
|
131
|
+
return False, f"score={actual_score} (expected >= {min_score})"
|
|
132
|
+
return True, ""
|
|
133
|
+
except json.JSONDecodeError as e:
|
|
134
|
+
return False, f"invalid JSON: {e}"
|
|
135
|
+
except subprocess.TimeoutExpired:
|
|
136
|
+
return False, "timeout"
|
|
137
|
+
except Exception as e:
|
|
138
|
+
return False, str(e)
|
|
139
|
+
|
|
140
|
+
def main():
|
|
141
|
+
script_path = os.environ.get('_SCRIPT_PATH', '')
|
|
142
|
+
registry_path = os.environ.get('_REGISTRY_PATH', '')
|
|
143
|
+
|
|
144
|
+
if not script_path or not registry_path:
|
|
145
|
+
print("Error: _SCRIPT_PATH and _REGISTRY_PATH must be set", file=sys.stderr)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
print("\n" + "=" * 60)
|
|
149
|
+
print("Universal Task Optimizer — Self-Test Suite")
|
|
150
|
+
print("=" * 60 + "\n")
|
|
151
|
+
|
|
152
|
+
passed = 0
|
|
153
|
+
failed = 0
|
|
154
|
+
|
|
155
|
+
for task, expected_intent, expected_agent, min_score in TEST_CASES:
|
|
156
|
+
ok, reason = run_test(script_path, registry_path, task, expected_intent, expected_agent, min_score)
|
|
157
|
+
if ok:
|
|
158
|
+
print(f"PASS: '{task[:50]}...' => {expected_intent}")
|
|
159
|
+
passed += 1
|
|
160
|
+
else:
|
|
161
|
+
print(f"FAIL: '{task[:50]}...' => {reason}")
|
|
162
|
+
failed += 1
|
|
163
|
+
|
|
164
|
+
print("\n" + "=" * 60)
|
|
165
|
+
print(f"RESULTS: {passed} passed, {failed} failed")
|
|
166
|
+
print("=" * 60)
|
|
167
|
+
|
|
168
|
+
sys.exit(0 if failed == 0 else 1)
|
|
169
|
+
|
|
170
|
+
if __name__ == '__main__':
|
|
171
|
+
main()
|
|
172
|
+
PYTESTEOF
|
|
173
|
+
|
|
174
|
+
export _SCRIPT_PATH="${BASH_SOURCE[0]}"
|
|
175
|
+
export _REGISTRY_PATH="$REGISTRY"
|
|
176
|
+
python3 "$TMPTEST"
|
|
177
|
+
exit $?
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# ═══════════════════════════════════════════════════════════════
|
|
181
|
+
# MAIN SCORER
|
|
182
|
+
# ═══════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
THRESHOLD="${MERLIN_TASK_OPT_THRESHOLD:-25}"
|
|
185
|
+
|
|
186
|
+
# Write the scorer to a temp file so timeout can exec it cleanly
|
|
187
|
+
TMPSCRIPT=$(mktemp /tmp/task-opt-score.XXXXXX.py)
|
|
188
|
+
trap 'rm -f "$TMPSCRIPT"' EXIT
|
|
189
|
+
|
|
190
|
+
cat > "$TMPSCRIPT" << 'PYEOF'
|
|
191
|
+
import os
|
|
192
|
+
import sys
|
|
193
|
+
import json
|
|
194
|
+
import re
|
|
195
|
+
|
|
196
|
+
def main():
|
|
197
|
+
task = os.environ.get("_OPT_TASK", "")
|
|
198
|
+
registry_path = os.environ.get("_OPT_REGISTRY", "")
|
|
199
|
+
threshold = int(os.environ.get("_OPT_THRESHOLD", "25"))
|
|
200
|
+
|
|
201
|
+
# Default output
|
|
202
|
+
default_output = {"score": 0, "intent": "none", "skills": [], "agent": "", "matched_phrases": []}
|
|
203
|
+
|
|
204
|
+
# Load registry
|
|
205
|
+
if not registry_path or not os.path.exists(registry_path):
|
|
206
|
+
print(json.dumps(default_output))
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with open(registry_path, 'r') as f:
|
|
211
|
+
registry = json.load(f)
|
|
212
|
+
except (json.JSONDecodeError, IOError):
|
|
213
|
+
print(json.dumps(default_output))
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
task_lower = task.lower()
|
|
217
|
+
skills_list = registry.get("skills", [])
|
|
218
|
+
|
|
219
|
+
# Score each skill
|
|
220
|
+
skill_scores = []
|
|
221
|
+
for skill in skills_list:
|
|
222
|
+
triggers = skill.get("triggers", [])
|
|
223
|
+
weight = skill.get("weight", 1.0)
|
|
224
|
+
matched = []
|
|
225
|
+
|
|
226
|
+
for trigger in triggers:
|
|
227
|
+
trigger_lower = trigger.lower()
|
|
228
|
+
# Use word boundary matching for single words, substring for multi-word
|
|
229
|
+
if ' ' in trigger_lower:
|
|
230
|
+
# Multi-word: simple substring match
|
|
231
|
+
if trigger_lower in task_lower:
|
|
232
|
+
matched.append(trigger_lower)
|
|
233
|
+
else:
|
|
234
|
+
# Single word: word boundary match
|
|
235
|
+
pattern = r'\b' + re.escape(trigger_lower) + r'\b'
|
|
236
|
+
if re.search(pattern, task_lower):
|
|
237
|
+
matched.append(trigger_lower)
|
|
238
|
+
|
|
239
|
+
if matched:
|
|
240
|
+
# Score: 25 per unique match * weight, capped at 100
|
|
241
|
+
raw_score = len(matched) * 25 * weight
|
|
242
|
+
score = min(int(raw_score), 100)
|
|
243
|
+
skill_scores.append({
|
|
244
|
+
"skill": skill,
|
|
245
|
+
"score": score,
|
|
246
|
+
"matched": matched
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
if not skill_scores:
|
|
250
|
+
print(json.dumps(default_output))
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Sort by score descending
|
|
254
|
+
skill_scores.sort(key=lambda x: x["score"], reverse=True)
|
|
255
|
+
top = skill_scores[0]
|
|
256
|
+
|
|
257
|
+
if top["score"] < threshold:
|
|
258
|
+
print(json.dumps(default_output))
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
top_skill = top["skill"]
|
|
262
|
+
result_skills = [top_skill["id"]]
|
|
263
|
+
all_matched = list(top["matched"])
|
|
264
|
+
|
|
265
|
+
# Check bundles_with
|
|
266
|
+
bundles = top_skill.get("bundles_with", [])
|
|
267
|
+
for bundle_id in bundles:
|
|
268
|
+
# Find the bundle skill and check if it also matched
|
|
269
|
+
for ss in skill_scores[1:]:
|
|
270
|
+
if ss["skill"]["id"] == bundle_id and ss["matched"]:
|
|
271
|
+
if bundle_id not in result_skills:
|
|
272
|
+
result_skills.append(bundle_id)
|
|
273
|
+
all_matched.extend(ss["matched"])
|
|
274
|
+
break
|
|
275
|
+
|
|
276
|
+
# Check intent_overrides
|
|
277
|
+
overrides = registry.get("intent_overrides", {})
|
|
278
|
+
for override_key, override_val in overrides.items():
|
|
279
|
+
match_all = override_val.get("match_all", [])
|
|
280
|
+
if all(ma.lower() in task_lower for ma in match_all):
|
|
281
|
+
# Override applies
|
|
282
|
+
result_skills = override_val.get("skills", result_skills)
|
|
283
|
+
result_intent = override_key
|
|
284
|
+
result_agent = override_val.get("agent", top_skill.get("agent", ""))
|
|
285
|
+
output = {
|
|
286
|
+
"score": top["score"],
|
|
287
|
+
"intent": result_intent,
|
|
288
|
+
"skills": result_skills,
|
|
289
|
+
"agent": result_agent,
|
|
290
|
+
"matched_phrases": list(set(all_matched))
|
|
291
|
+
}
|
|
292
|
+
print(json.dumps(output))
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
# Build normal output
|
|
296
|
+
output = {
|
|
297
|
+
"score": top["score"],
|
|
298
|
+
"intent": top_skill.get("intent", "none"),
|
|
299
|
+
"skills": result_skills,
|
|
300
|
+
"agent": top_skill.get("agent", ""),
|
|
301
|
+
"matched_phrases": list(set(all_matched))
|
|
302
|
+
}
|
|
303
|
+
print(json.dumps(output))
|
|
304
|
+
|
|
305
|
+
if __name__ == '__main__':
|
|
306
|
+
main()
|
|
307
|
+
PYEOF
|
|
308
|
+
|
|
309
|
+
# Export inputs via env — never interpolated into code
|
|
310
|
+
export _OPT_TASK="$TASK"
|
|
311
|
+
export _OPT_REGISTRY="$REGISTRY"
|
|
312
|
+
export _OPT_THRESHOLD="$THRESHOLD"
|
|
313
|
+
|
|
314
|
+
# Run with 500ms timeout — try gtimeout (macOS Homebrew coreutils), then timeout, then perl alarm
|
|
315
|
+
_TIMEOUT_CMD=""
|
|
316
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
317
|
+
_TIMEOUT_CMD="gtimeout 0.5"
|
|
318
|
+
elif timeout --version >/dev/null 2>&1; then
|
|
319
|
+
_TIMEOUT_CMD="timeout 0.5"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
if [[ -n "$_TIMEOUT_CMD" ]]; then
|
|
323
|
+
OUT=$($_TIMEOUT_CMD python3 "$TMPSCRIPT" 2>/dev/null) || OUT=""
|
|
324
|
+
else
|
|
325
|
+
OUT=$(perl -e 'alarm 1; exec @ARGV' -- python3 "$TMPSCRIPT" 2>/dev/null) || OUT=""
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# Validate JSON; fall back to safe default
|
|
329
|
+
if python3 -c "import json,sys; json.loads(sys.stdin.read())" <<< "$OUT" 2>/dev/null; then
|
|
330
|
+
echo "$OUT"
|
|
331
|
+
else
|
|
332
|
+
echo '{"score":0,"intent":"none","skills":[],"agent":"","matched_phrases":[]}'
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-merlin-brain",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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",
|