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.
Files changed (104) hide show
  1. package/README.md +38 -4
  2. package/bin/install.cjs +20 -0
  3. package/dist/server/server.d.ts.map +1 -1
  4. package/dist/server/server.js +75 -0
  5. package/dist/server/server.js.map +1 -1
  6. package/dist/server/tools/context.d.ts.map +1 -1
  7. package/dist/server/tools/context.js +60 -0
  8. package/dist/server/tools/context.js.map +1 -1
  9. package/dist/server/tools/project-picture.d.ts +17 -0
  10. package/dist/server/tools/project-picture.d.ts.map +1 -0
  11. package/dist/server/tools/project-picture.js +204 -0
  12. package/dist/server/tools/project-picture.js.map +1 -0
  13. package/dist/server/tools/types.d.ts +24 -0
  14. package/dist/server/tools/types.d.ts.map +1 -1
  15. package/files/CLAUDE.md +2 -0
  16. package/files/agents/challenger-academic.md +8 -0
  17. package/files/agents/challenger-arbiter.md +8 -0
  18. package/files/agents/challenger-insider.md +8 -0
  19. package/files/agents/code-organization-supervisor.md +8 -0
  20. package/files/agents/codex-planner.md +36 -9
  21. package/files/agents/context-guardian.md +8 -0
  22. package/files/agents/docs-keeper.md +8 -0
  23. package/files/agents/dry-refactor.md +8 -0
  24. package/files/agents/elite-code-refactorer.md +8 -0
  25. package/files/agents/hardening-guard.md +8 -0
  26. package/files/agents/implementation-dev.md +8 -0
  27. package/files/agents/merlin-access-control-reviewer.md +8 -0
  28. package/files/agents/merlin-api-designer.md +8 -0
  29. package/files/agents/merlin-codebase-mapper.md +8 -0
  30. package/files/agents/merlin-debugger.md +8 -0
  31. package/files/agents/merlin-dependency-auditor.md +8 -0
  32. package/files/agents/merlin-edge-case-hunter.md +8 -0
  33. package/files/agents/merlin-executor.md +8 -0
  34. package/files/agents/merlin-frontend.md +8 -0
  35. package/files/agents/merlin-input-validator.md +8 -0
  36. package/files/agents/merlin-integration-checker.md +8 -0
  37. package/files/agents/merlin-migrator.md +8 -0
  38. package/files/agents/merlin-milestone-auditor.md +8 -0
  39. package/files/agents/merlin-party-review.md +8 -0
  40. package/files/agents/merlin-performance.md +8 -0
  41. package/files/agents/merlin-planner.md +9 -1
  42. package/files/agents/merlin-researcher.md +8 -0
  43. package/files/agents/merlin-reviewer.md +8 -0
  44. package/files/agents/merlin-sast-reviewer.md +8 -0
  45. package/files/agents/merlin-secret-scanner.md +8 -0
  46. package/files/agents/merlin-security.md +8 -0
  47. package/files/agents/merlin-verifier.md +8 -0
  48. package/files/agents/merlin-work-verifier.md +8 -0
  49. package/files/agents/merlin.md +8 -0
  50. package/files/agents/ops-railway.md +8 -0
  51. package/files/agents/orchestrator-retrofit.md +8 -0
  52. package/files/agents/product-spec.md +8 -0
  53. package/files/agents/remotion.md +8 -0
  54. package/files/agents/system-architect.md +8 -0
  55. package/files/agents/tests-qa.md +8 -0
  56. package/files/commands/merlin/course-correct.md +8 -0
  57. package/files/commands/merlin/design-audit.md +92 -0
  58. package/files/commands/merlin/health.md +8 -0
  59. package/files/commands/merlin/next.md +8 -0
  60. package/files/commands/merlin/optimize.md +89 -0
  61. package/files/commands/merlin/polish.md +99 -0
  62. package/files/commands/merlin/quick.md +8 -0
  63. package/files/commands/merlin/readiness-gate.md +8 -0
  64. package/files/commands/merlin/redesign.md +108 -0
  65. package/files/hooks/session-start.sh +88 -2
  66. package/files/hooks/statusline.sh +9 -1
  67. package/files/loop/README.md +11 -0
  68. package/files/merlin/skills/SKILLS-INDEX.md +16 -1
  69. package/files/merlin/skills/TASK-OPTIMIZER.json +310 -0
  70. package/files/merlin/skills/coding/focus-mode.md +8 -0
  71. package/files/merlin/skills/design/emil-design-eng.md +31 -0
  72. package/files/merlin/skills/design/impeccable.md +36 -0
  73. package/files/merlin/skills/duo/offer.md +20 -7
  74. package/files/merlin/skills/duo/on.md +26 -17
  75. package/files/merlin/skills/duo/status.md +10 -3
  76. package/files/merlin/templates/DEBUG.md +11 -0
  77. package/files/merlin/templates/UAT.md +11 -0
  78. package/files/merlin/templates/phase-prompt.md +11 -0
  79. package/files/merlin/templates/project.md +11 -0
  80. package/files/merlin/templates/requirements.md +11 -0
  81. package/files/merlin/templates/roadmap.md +11 -0
  82. package/files/merlin/templates/state.md +11 -0
  83. package/files/merlin/templates/verification-report.md +11 -0
  84. package/files/merlin/workflows/execute-phase.md +11 -0
  85. package/files/merlin/workflows/plan-phase.md +11 -0
  86. package/files/merlin/workflows/progress.md +11 -0
  87. package/files/merlin/workflows/resume-project.md +11 -0
  88. package/files/merlin/workflows/verify-phase.md +11 -0
  89. package/files/merlin/workflows/verify-work.md +11 -0
  90. package/files/merlin-system-prompt.txt +35 -1
  91. package/files/rules/codex-routing.md +19 -0
  92. package/files/rules/duo-routing.md +109 -10
  93. package/files/rules/merlin-routing.md +40 -0
  94. package/files/scripts/codex-as.sh +5 -2
  95. package/files/scripts/design-intent-detect.sh +8 -0
  96. package/files/scripts/duo-badge.sh +3 -5
  97. package/files/scripts/duo-installed.sh +3 -3
  98. package/files/scripts/duo-mode-read.sh +30 -10
  99. package/files/scripts/duo-mode-write.sh +28 -3
  100. package/files/scripts/duo-pre-route.sh +2 -8
  101. package/files/scripts/install-design-skills.sh +86 -0
  102. package/files/scripts/merlin-codex.sh +84 -0
  103. package/files/scripts/task-optimize.sh +335 -0
  104. 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: Codex absent stay solo silently (never mention duo)
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 3a: suppression check — use a temp python script to avoid heredoc-in-subshell issues
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.0.1",
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",