create-merlin-brain 3.10.0 → 3.12.0
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/bin/install.cjs +146 -22
- package/bin/runtime-adapters.cjs +396 -0
- package/dist/server/cost/tracker.d.ts +38 -2
- package/dist/server/cost/tracker.d.ts.map +1 -1
- package/dist/server/cost/tracker.js +87 -15
- package/dist/server/cost/tracker.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +74 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/tools/adaptive.js +1 -1
- package/dist/server/tools/adaptive.js.map +1 -1
- package/dist/server/tools/agents-index.js +3 -3
- package/dist/server/tools/agents-index.js.map +1 -1
- package/dist/server/tools/agents.js +5 -5
- package/dist/server/tools/agents.js.map +1 -1
- package/dist/server/tools/behaviors.js +4 -4
- package/dist/server/tools/behaviors.js.map +1 -1
- package/dist/server/tools/context.js +7 -7
- package/dist/server/tools/context.js.map +1 -1
- package/dist/server/tools/cost.d.ts +3 -1
- package/dist/server/tools/cost.d.ts.map +1 -1
- package/dist/server/tools/cost.js +66 -13
- package/dist/server/tools/cost.js.map +1 -1
- package/dist/server/tools/discoveries.js +6 -6
- package/dist/server/tools/discoveries.js.map +1 -1
- package/dist/server/tools/index.d.ts +4 -0
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +4 -0
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/learning.d.ts +12 -0
- package/dist/server/tools/learning.d.ts.map +1 -0
- package/dist/server/tools/learning.js +269 -0
- package/dist/server/tools/learning.js.map +1 -0
- package/dist/server/tools/project.js +7 -7
- package/dist/server/tools/project.js.map +1 -1
- package/dist/server/tools/promote.d.ts +11 -0
- package/dist/server/tools/promote.d.ts.map +1 -0
- package/dist/server/tools/promote.js +315 -0
- package/dist/server/tools/promote.js.map +1 -0
- package/dist/server/tools/route.d.ts.map +1 -1
- package/dist/server/tools/route.js +65 -24
- package/dist/server/tools/route.js.map +1 -1
- package/dist/server/tools/session-restore.d.ts +18 -0
- package/dist/server/tools/session-restore.d.ts.map +1 -0
- package/dist/server/tools/session-restore.js +154 -0
- package/dist/server/tools/session-restore.js.map +1 -0
- package/dist/server/tools/session-search.d.ts +16 -0
- package/dist/server/tools/session-search.d.ts.map +1 -0
- package/dist/server/tools/session-search.js +240 -0
- package/dist/server/tools/session-search.js.map +1 -0
- package/dist/server/tools/sights-index.js +2 -2
- package/dist/server/tools/sights-index.js.map +1 -1
- package/dist/server/tools/smart-route.d.ts.map +1 -1
- package/dist/server/tools/smart-route.js +4 -5
- package/dist/server/tools/smart-route.js.map +1 -1
- package/dist/server/tools/verification.js +1 -1
- package/dist/server/tools/verification.js.map +1 -1
- package/files/agents/code-organization-supervisor.md +9 -0
- package/files/agents/context-guardian.md +9 -0
- package/files/agents/docs-keeper.md +11 -1
- package/files/agents/dry-refactor.md +12 -1
- package/files/agents/elite-code-refactorer.md +10 -0
- package/files/agents/hardening-guard.md +13 -1
- package/files/agents/implementation-dev.md +12 -1
- package/files/agents/merlin-access-control-reviewer.md +248 -0
- package/files/agents/merlin-api-designer.md +9 -0
- package/files/agents/merlin-codebase-mapper.md +9 -1
- package/files/agents/merlin-debugger.md +10 -0
- package/files/agents/merlin-dependency-auditor.md +216 -0
- package/files/agents/merlin-executor.md +12 -1
- package/files/agents/merlin-frontend.md +9 -0
- package/files/agents/merlin-input-validator.md +247 -0
- package/files/agents/merlin-integration-checker.md +9 -1
- package/files/agents/merlin-migrator.md +9 -0
- package/files/agents/merlin-milestone-auditor.md +8 -0
- package/files/agents/merlin-performance.md +8 -0
- package/files/agents/merlin-planner.md +10 -0
- package/files/agents/merlin-researcher.md +10 -0
- package/files/agents/merlin-reviewer.md +42 -7
- package/files/agents/merlin-sast-reviewer.md +182 -0
- package/files/agents/merlin-secret-scanner.md +203 -0
- package/files/agents/merlin-security.md +9 -0
- package/files/agents/merlin-verifier.md +9 -0
- package/files/agents/merlin-work-verifier.md +9 -0
- package/files/agents/merlin.md +10 -0
- package/files/agents/ops-railway.md +11 -1
- package/files/agents/orchestrator-retrofit.md +9 -1
- package/files/agents/product-spec.md +11 -1
- package/files/agents/remotion.md +8 -0
- package/files/agents/system-architect.md +11 -1
- package/files/agents/tests-qa.md +12 -1
- package/files/commands/merlin/course-correct.md +219 -0
- package/files/commands/merlin/debug.md +2 -2
- package/files/commands/merlin/execute-phase.md +96 -199
- package/files/commands/merlin/execute-plan.md +118 -182
- package/files/commands/merlin/health.md +385 -0
- package/files/commands/merlin/loop-recipes.md +93 -36
- package/files/commands/merlin/map-codebase.md +4 -4
- package/files/commands/merlin/next.md +240 -0
- package/files/commands/merlin/optimize-prompts.md +158 -0
- package/files/commands/merlin/plan-phase.md +1 -1
- package/files/commands/merlin/profiles.md +215 -0
- package/files/commands/merlin/promote.md +176 -0
- package/files/commands/merlin/quick.md +229 -0
- package/files/commands/merlin/readiness-gate.md +208 -0
- package/files/commands/merlin/research-phase.md +2 -2
- package/files/commands/merlin/research-project.md +4 -4
- package/files/commands/merlin/resume-work.md +27 -1
- package/files/commands/merlin/route.md +43 -1
- package/files/commands/merlin/sandbox.md +359 -0
- package/files/commands/merlin/usage.md +55 -0
- package/files/commands/merlin/verify-work.md +1 -1
- package/files/docker/Dockerfile.merlin +20 -0
- package/files/docker/docker-compose.merlin.yml +23 -0
- package/files/hook-templates/auto-commit.sh +64 -0
- package/files/hook-templates/auto-format.sh +95 -0
- package/files/hook-templates/auto-test.sh +117 -0
- package/files/hook-templates/branch-protection.sh +72 -0
- package/files/hook-templates/changelog-reminder.sh +76 -0
- package/files/hook-templates/complexity-check.sh +112 -0
- package/files/hook-templates/import-audit.sh +83 -0
- package/files/hook-templates/license-header.sh +84 -0
- package/files/hook-templates/pr-description.sh +100 -0
- package/files/hook-templates/todo-tracker.sh +80 -0
- package/files/hooks/check-file-size.sh +17 -4
- package/files/hooks/config-change.sh +44 -16
- package/files/hooks/instructions-loaded.sh +22 -5
- package/files/hooks/notify-desktop.sh +157 -0
- package/files/hooks/notify-webhook.sh +141 -0
- package/files/hooks/pre-edit-sights-check.sh +76 -9
- package/files/hooks/security-scanner.sh +153 -0
- package/files/hooks/session-end-memory-sync.sh +97 -0
- package/files/hooks/session-end.sh +274 -1
- package/files/hooks/session-start.sh +19 -6
- package/files/hooks/smart-approve.sh +270 -0
- package/files/hooks/teammate-idle-verify.sh +87 -12
- package/files/hooks/worktree-create.sh +20 -3
- package/files/hooks/worktree-remove.sh +21 -3
- package/files/merlin/references/plan-format.md +37 -9
- package/files/merlin/sandbox.json +9 -0
- package/files/merlin/security.json +11 -0
- package/files/merlin/templates/ci/docs-update.yml +81 -0
- package/files/merlin/templates/ci/pr-review.yml +50 -0
- package/files/merlin/templates/ci/security-audit.yml +74 -0
- package/files/merlin/templates/config.json +9 -1
- package/files/rules/api-rules.md +30 -0
- package/files/rules/frontend-rules.md +25 -0
- package/files/rules/hooks-rules.md +36 -0
- package/files/rules/mcp-rules.md +30 -0
- package/files/rules/worker-rules.md +29 -0
- package/package.json +1 -1
|
@@ -176,16 +176,29 @@ _merlin_auto_update() {
|
|
|
176
176
|
}
|
|
177
177
|
_merlin_auto_update &
|
|
178
178
|
|
|
179
|
-
# ── 3.
|
|
179
|
+
# ── 3. Voice mode check (background) ──────────────────────────
|
|
180
|
+
# If voice_mode_concise is enabled in settings, export env var for the session.
|
|
181
|
+
_merlin_check_voice_mode() {
|
|
182
|
+
local settings_file="${HOME}/.claude/merlin/settings.local.json"
|
|
183
|
+
[ -f "${settings_file}" ] || return 0
|
|
184
|
+
command -v jq >/dev/null 2>&1 || return 0
|
|
185
|
+
local voice
|
|
186
|
+
voice=$(jq -r '.voice_mode_concise // false' "${settings_file}" 2>/dev/null) || return 0
|
|
187
|
+
[ "${voice}" = "true" ] && export MERLIN_VOICE_MODE=1
|
|
188
|
+
}
|
|
189
|
+
_merlin_check_voice_mode
|
|
190
|
+
|
|
191
|
+
# ── 4. Context injection (the only stdout output) ──────────────
|
|
180
192
|
# Output additionalContext JSON for Claude to see at session start.
|
|
181
193
|
# Full boot instructions are in CLAUDE.md — this is a lightweight nudge.
|
|
182
|
-
|
|
183
|
-
{
|
|
194
|
+
_voice_note=""
|
|
195
|
+
[ "${MERLIN_VOICE_MODE:-}" = "1" ] && _voice_note=" Voice mode active: keep all responses short and direct."
|
|
196
|
+
|
|
197
|
+
printf '{
|
|
184
198
|
"hookSpecificOutput": {
|
|
185
199
|
"hookEventName": "SessionStart",
|
|
186
|
-
"additionalContext": "STOP. Your FIRST action must be: call merlin_get_selected_repo, then call merlin_get_project_status, then show the user a status summary. Do not respond to the user until you complete these calls
|
|
200
|
+
"additionalContext": "STOP. Your FIRST action must be: call merlin_get_selected_repo, then call merlin_get_project_status, then show the user a status summary. Do not respond to the user until you complete these calls.%s"
|
|
187
201
|
}
|
|
188
|
-
}
|
|
189
|
-
CONTEXT_JSON
|
|
202
|
+
}\n' "${_voice_note}"
|
|
190
203
|
|
|
191
204
|
exit 0
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: smart-approve.sh
|
|
4
|
+
# Event: PreToolUse (Bash only)
|
|
5
|
+
#
|
|
6
|
+
# Auto-approves safe read-only commands and blocks known-dangerous ones.
|
|
7
|
+
# Unknown commands pass through (exit 0) to let Claude Code handle them.
|
|
8
|
+
#
|
|
9
|
+
# Exit codes:
|
|
10
|
+
# 0 = allow (safe command or unknown — pass through)
|
|
11
|
+
# 2 = block with JSON reason
|
|
12
|
+
#
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
trap 'echo "{}"; exit 0' ERR
|
|
15
|
+
|
|
16
|
+
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
[ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
|
|
18
|
+
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Read stdin
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
input=""
|
|
23
|
+
if [ ! -t 0 ]; then
|
|
24
|
+
input=$(cat 2>/dev/null || true)
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
[ -z "$input" ] && { echo '{}'; exit 0; }
|
|
28
|
+
|
|
29
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
# Extract the command string
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
command_str=""
|
|
33
|
+
if command -v jq >/dev/null 2>&1; then
|
|
34
|
+
command_str=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
|
|
35
|
+
else
|
|
36
|
+
command_str=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
[ -z "$command_str" ] && { echo '{}'; exit 0; }
|
|
40
|
+
|
|
41
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
# Helpers
|
|
43
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
_block_cmd() {
|
|
45
|
+
local reason="$1"
|
|
46
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
47
|
+
log_event "smart_approve_block" "$(printf '{"reason":"%s"}' "$reason")"
|
|
48
|
+
fi
|
|
49
|
+
printf '{"decision":"block","reason":"%s"}\n' "$reason"
|
|
50
|
+
exit 2
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Extract the base (first) word of a command, stripping env var prefixes like KEY=val cmd
|
|
54
|
+
_base_cmd() {
|
|
55
|
+
local segment="$1"
|
|
56
|
+
# Strip leading VAR=val pairs
|
|
57
|
+
local stripped
|
|
58
|
+
stripped=$(echo "$segment" | sed 's/^[A-Z_][A-Z0-9_]*=[^ ]* //g' | sed 's/^[A-Z_][A-Z0-9_]*=[^ ]* //g')
|
|
59
|
+
echo "$stripped" | awk '{print $1}'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
# DANGEROUS PATTERNS — check first, before safe list
|
|
64
|
+
# These are hard blocks regardless of context.
|
|
65
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
# Pipe-to-shell (curl/wget | bash/sh) — extremely dangerous, check raw command string
|
|
68
|
+
if echo "$command_str" | grep -qE '(curl|wget)\s[^|]*\|\s*(bash|sh|zsh|python|ruby|perl)\b' 2>/dev/null; then
|
|
69
|
+
_block_cmd "Dangerous: pipe-to-shell execution detected (curl/wget | bash/sh)"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# sudo / su privilege escalation
|
|
73
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)(sudo|su)\s' 2>/dev/null; then
|
|
74
|
+
_block_cmd "Dangerous: privilege escalation (sudo/su) not permitted"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# chmod 777 or chown root
|
|
78
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)(chmod\s+777|chown\s+root)' 2>/dev/null; then
|
|
79
|
+
_block_cmd "Dangerous: unsafe permission change (chmod 777 / chown root)"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# Global package installs
|
|
83
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)npm\s+(install|i)\s+-g\b' 2>/dev/null; then
|
|
84
|
+
_block_cmd "Dangerous: global npm install (-g) requires explicit approval"
|
|
85
|
+
fi
|
|
86
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)pip\s+install\b' 2>/dev/null; then
|
|
87
|
+
# Allow pip install inside venv (if VIRTUAL_ENV is set — can't check at hook time, so just warn)
|
|
88
|
+
if ! echo "$command_str" | grep -qE '\-\-user\b|\bvenv\b|virtualenv' 2>/dev/null; then
|
|
89
|
+
_block_cmd "Dangerous: pip install outside virtualenv requires explicit approval"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)gem\s+install\b' 2>/dev/null; then
|
|
93
|
+
_block_cmd "Dangerous: gem install requires explicit approval"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Network firewall mutation
|
|
97
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)(iptables|ufw|firewall-cmd)\b' 2>/dev/null; then
|
|
98
|
+
_block_cmd "Dangerous: firewall modification requires explicit approval"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
# GIT DESTRUCTIVE SUBCOMMANDS — check before generic git safe list
|
|
103
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)git\b' 2>/dev/null; then
|
|
105
|
+
# git push --force / --force-with-lease
|
|
106
|
+
if echo "$command_str" | grep -qE 'git\s+push\s+.*--force' 2>/dev/null; then
|
|
107
|
+
_block_cmd "Dangerous: git push --force can overwrite remote history"
|
|
108
|
+
fi
|
|
109
|
+
# git reset --hard
|
|
110
|
+
if echo "$command_str" | grep -qE 'git\s+reset\s+.*--hard' 2>/dev/null; then
|
|
111
|
+
_block_cmd "Dangerous: git reset --hard discards uncommitted changes"
|
|
112
|
+
fi
|
|
113
|
+
# git clean -f / -fd / -fx
|
|
114
|
+
if echo "$command_str" | grep -qE 'git\s+clean\s+.*-[a-z]*f' 2>/dev/null; then
|
|
115
|
+
_block_cmd "Dangerous: git clean -f deletes untracked files permanently"
|
|
116
|
+
fi
|
|
117
|
+
# git branch -D (force delete)
|
|
118
|
+
if echo "$command_str" | grep -qE 'git\s+branch\s+.*-D\b' 2>/dev/null; then
|
|
119
|
+
_block_cmd "Dangerous: git branch -D force-deletes without merge check"
|
|
120
|
+
fi
|
|
121
|
+
# git checkout . (discard all working dir changes)
|
|
122
|
+
if echo "$command_str" | grep -qE 'git\s+checkout\s+\.\s*$' 2>/dev/null; then
|
|
123
|
+
_block_cmd "Dangerous: git checkout . discards all working directory changes"
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
# rm DESTRUCTIVE — rm -rf or rm -r
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
if echo "$command_str" | grep -qE '(^|\s|;|&&|\|\|)rm\s' 2>/dev/null; then
|
|
131
|
+
if echo "$command_str" | grep -qE '\brm\s+(-[a-z]*r[a-z]*f|--recursive.*--force|-rf|-fr)\b' 2>/dev/null; then
|
|
132
|
+
_block_cmd "Dangerous: rm -rf detected — destructive recursive delete"
|
|
133
|
+
fi
|
|
134
|
+
if echo "$command_str" | grep -qE '\brm\s+(-[a-z]*r|-R|--recursive)\b' 2>/dev/null; then
|
|
135
|
+
_block_cmd "Dangerous: rm -r detected — recursive delete requires explicit approval"
|
|
136
|
+
fi
|
|
137
|
+
if echo "$command_str" | grep -qE '\bshred\b' 2>/dev/null; then
|
|
138
|
+
_block_cmd "Dangerous: shred detected — permanent file destruction"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
# SAFE COMMANDS — auto-approve (exit 0, output {})
|
|
144
|
+
# We split the command on pipe/semicolon/&& and check each segment.
|
|
145
|
+
# A command is safe only if ALL segments are safe.
|
|
146
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
# Safe base command list (exact match on first word)
|
|
149
|
+
SAFE_CMDS="cat|head|tail|less|more|wc|file|stat|du|df|which|type|whereis|man|echo|printf|ls|find|tree|fd|grep|rg|ag|ack|fzf|jest|vitest|mocha|pytest|cargo|go|npm|yarn|pnpm|make|tsc|prettier|eslint|rustfmt|black|flake8|mypy|rubocop|uname|hostname|whoami|id|env|printenv|date|uptime|ps|top|git|node|python|python3"
|
|
150
|
+
|
|
151
|
+
# Git read-only subcommands
|
|
152
|
+
GIT_SAFE_SUBS="status|log|diff|branch|show|stash|remote|tag|blame|describe|ls-files|ls-remote|rev-parse|symbolic-ref|config --get|config --list|shortlog|whatchanged"
|
|
153
|
+
|
|
154
|
+
_is_safe_segment() {
|
|
155
|
+
local seg
|
|
156
|
+
seg=$(echo "$1" | xargs 2>/dev/null || echo "$1") # trim whitespace
|
|
157
|
+
[ -z "$seg" ] && return 0
|
|
158
|
+
|
|
159
|
+
local base
|
|
160
|
+
base=$(_base_cmd "$seg")
|
|
161
|
+
[ -z "$base" ] && return 0
|
|
162
|
+
|
|
163
|
+
# Check if base is in safe list
|
|
164
|
+
if ! echo "$base" | grep -qE "^(${SAFE_CMDS})$" 2>/dev/null; then
|
|
165
|
+
return 1 # not in safe list — unknown
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Additional checks for commands that are only safe in certain forms
|
|
169
|
+
case "$base" in
|
|
170
|
+
git)
|
|
171
|
+
# Only allow read-only git subcommands
|
|
172
|
+
local sub
|
|
173
|
+
sub=$(echo "$seg" | awk '{print $2}')
|
|
174
|
+
if ! echo "$sub" | grep -qE "^(status|log|diff|branch|show|stash|remote|tag|blame|describe|ls-files|ls-remote|rev-parse|symbolic-ref|shortlog|whatchanged|config)$" 2>/dev/null; then
|
|
175
|
+
return 1 # git write op — not in our safe list
|
|
176
|
+
fi
|
|
177
|
+
;;
|
|
178
|
+
make)
|
|
179
|
+
# make with clean or install targets is risky — not safe
|
|
180
|
+
if echo "$seg" | grep -qE '\b(clean|install|uninstall|distclean|mrproper)\b' 2>/dev/null; then
|
|
181
|
+
return 1
|
|
182
|
+
fi
|
|
183
|
+
;;
|
|
184
|
+
npm)
|
|
185
|
+
# Only safe for read commands
|
|
186
|
+
local npm_sub
|
|
187
|
+
npm_sub=$(echo "$seg" | awk '{print $2}')
|
|
188
|
+
if ! echo "$npm_sub" | grep -qE "^(list|ls|outdated|info|search|audit|run|test|exec)$" 2>/dev/null; then
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
;;
|
|
192
|
+
yarn)
|
|
193
|
+
local yarn_sub
|
|
194
|
+
yarn_sub=$(echo "$seg" | awk '{print $2}')
|
|
195
|
+
if ! echo "$yarn_sub" | grep -qE "^(list|info|why|audit|run|test)$" 2>/dev/null; then
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
;;
|
|
199
|
+
pnpm)
|
|
200
|
+
local pnpm_sub
|
|
201
|
+
pnpm_sub=$(echo "$seg" | awk '{print $2}')
|
|
202
|
+
if ! echo "$pnpm_sub" | grep -qE "^(list|ls|info|why|audit|run|test)$" 2>/dev/null; then
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
;;
|
|
206
|
+
cargo)
|
|
207
|
+
local cargo_sub
|
|
208
|
+
cargo_sub=$(echo "$seg" | awk '{print $2}')
|
|
209
|
+
if ! echo "$cargo_sub" | grep -qE "^(test|check|clippy|build|run|bench|doc|fmt|tree|search)$" 2>/dev/null; then
|
|
210
|
+
return 1
|
|
211
|
+
fi
|
|
212
|
+
;;
|
|
213
|
+
go)
|
|
214
|
+
local go_sub
|
|
215
|
+
go_sub=$(echo "$seg" | awk '{print $2}')
|
|
216
|
+
if ! echo "$go_sub" | grep -qE "^(test|vet|build|run|fmt|doc|list|mod|env|version)$" 2>/dev/null; then
|
|
217
|
+
return 1
|
|
218
|
+
fi
|
|
219
|
+
;;
|
|
220
|
+
top)
|
|
221
|
+
# top -l 1 (one snapshot, macOS) or top -n 1 (Linux) is safe; interactive top is not
|
|
222
|
+
if ! echo "$seg" | grep -qE 'top\s+(-l\s+1|-n\s+1|--lines=1)\b' 2>/dev/null; then
|
|
223
|
+
return 1
|
|
224
|
+
fi
|
|
225
|
+
;;
|
|
226
|
+
tsc)
|
|
227
|
+
# Only safe in check mode
|
|
228
|
+
if ! echo "$seg" | grep -qE 'tsc\s+.*--noEmit\b' 2>/dev/null; then
|
|
229
|
+
return 1
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
232
|
+
rustfmt)
|
|
233
|
+
if ! echo "$seg" | grep -qE 'rustfmt\s+.*--check\b' 2>/dev/null; then
|
|
234
|
+
return 1
|
|
235
|
+
fi
|
|
236
|
+
;;
|
|
237
|
+
black)
|
|
238
|
+
if ! echo "$seg" | grep -qE 'black\s+.*--check\b' 2>/dev/null; then
|
|
239
|
+
return 1
|
|
240
|
+
fi
|
|
241
|
+
;;
|
|
242
|
+
esac
|
|
243
|
+
|
|
244
|
+
return 0
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Split on pipe, semicolon, &&, || and check each segment
|
|
248
|
+
# Replace separators with newlines, then iterate
|
|
249
|
+
all_safe=true
|
|
250
|
+
while IFS= read -r segment; do
|
|
251
|
+
[ -z "$segment" ] && continue
|
|
252
|
+
if ! _is_safe_segment "$segment"; then
|
|
253
|
+
all_safe=false
|
|
254
|
+
break
|
|
255
|
+
fi
|
|
256
|
+
done < <(echo "$command_str" | tr '|;&' '\n')
|
|
257
|
+
|
|
258
|
+
if $all_safe; then
|
|
259
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
260
|
+
log_event "smart_approve_safe" '{}'
|
|
261
|
+
fi
|
|
262
|
+
echo '{}'
|
|
263
|
+
exit 0
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
# Unknown command — pass through (let Claude Code decide)
|
|
268
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
echo '{}'
|
|
270
|
+
exit 0
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: TeammateIdle
|
|
4
|
-
#
|
|
4
|
+
# Fires when a teammate finishes or goes idle in Agent Teams mode.
|
|
5
|
+
#
|
|
6
|
+
# Responsibilities:
|
|
7
|
+
# 1. Verify quality of completed work against Sights patterns
|
|
8
|
+
# 2. Detect if a teammate has been idle for 2+ minutes and alert the Lead
|
|
9
|
+
# 3. Log the event for analytics
|
|
10
|
+
#
|
|
5
11
|
# Advisory only — always exits 0, never blocks teammates.
|
|
12
|
+
# Alert messages go to stderr (visible to Lead session).
|
|
6
13
|
#
|
|
7
14
|
set -euo pipefail
|
|
8
15
|
trap 'echo "{}"; exit 0' ERR
|
|
@@ -11,31 +18,99 @@ HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
11
18
|
|
|
12
19
|
# Source shared libraries
|
|
13
20
|
# shellcheck source=lib/analytics.sh
|
|
14
|
-
. "${HOOKS_DIR}/lib/analytics.sh"
|
|
21
|
+
[ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
|
|
15
22
|
|
|
16
|
-
#
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Parse hook input (JSON from Claude Code Agent Teams runtime)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
17
26
|
input=""
|
|
18
27
|
if [ ! -t 0 ]; then
|
|
19
28
|
input=$(cat 2>/dev/null || true)
|
|
20
29
|
fi
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
teammate_id=""
|
|
32
|
+
teammate_name=""
|
|
23
33
|
modified_files=""
|
|
34
|
+
idle_seconds=""
|
|
35
|
+
status=""
|
|
36
|
+
|
|
24
37
|
if [ -n "$input" ] && command -v jq >/dev/null 2>&1; then
|
|
25
|
-
|
|
38
|
+
teammate_id=$(echo "$input" | jq -r '.teammate_id // empty' 2>/dev/null || true)
|
|
39
|
+
teammate_name=$(echo "$input" | jq -r '.teammate_name // empty' 2>/dev/null || true)
|
|
40
|
+
modified_files=$(echo "$input"| jq -r '.files_modified // empty' 2>/dev/null || true)
|
|
41
|
+
idle_seconds=$(echo "$input" | jq -r '.idle_seconds // empty' 2>/dev/null || true)
|
|
42
|
+
status=$(echo "$input" | jq -r '.status // empty' 2>/dev/null || true)
|
|
26
43
|
fi
|
|
27
44
|
|
|
28
|
-
#
|
|
29
|
-
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Idle detection — alert Lead if teammate stuck for 2+ minutes (120 seconds)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
IDLE_THRESHOLD=120 # seconds
|
|
49
|
+
ALERT_ISSUED=false
|
|
50
|
+
|
|
51
|
+
if [ -n "$idle_seconds" ] && [ "$idle_seconds" -ge "$IDLE_THRESHOLD" ] 2>/dev/null; then
|
|
52
|
+
IDLE_MINUTES=$(( idle_seconds / 60 ))
|
|
53
|
+
DISPLAY_NAME="${teammate_name:-${teammate_id:-unknown}}"
|
|
54
|
+
|
|
55
|
+
# Alert goes to stderr — visible to the Lead session in Agent Teams
|
|
56
|
+
echo "[merlin] Alert: Teammate \"${DISPLAY_NAME}\" has been idle for ${IDLE_MINUTES}+ minutes" >&2
|
|
57
|
+
echo "[merlin] Recommend: Check teammate status or consider aborting stuck session" >&2
|
|
58
|
+
|
|
59
|
+
# Write alert to a shared state file the Lead can poll
|
|
60
|
+
MERLIN_STATE_DIR="${HOME}/.claude/merlin/teams-state"
|
|
61
|
+
if [ -d "$MERLIN_STATE_DIR" ] || mkdir -p "$MERLIN_STATE_DIR" 2>/dev/null; then
|
|
62
|
+
ALERT_FILE="${MERLIN_STATE_DIR}/idle-alert-${teammate_id:-unknown}.json"
|
|
63
|
+
printf '{"teammate_id":"%s","teammate_name":"%s","idle_seconds":%s,"alert_time":"%s"}\n' \
|
|
64
|
+
"${teammate_id:-unknown}" \
|
|
65
|
+
"${DISPLAY_NAME}" \
|
|
66
|
+
"${idle_seconds}" \
|
|
67
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
68
|
+
> "$ALERT_FILE" 2>/dev/null || true
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
ALERT_ISSUED=true
|
|
72
|
+
|
|
73
|
+
# Log idle alert
|
|
74
|
+
if command -v log_event >/dev/null 2>&1; then
|
|
75
|
+
log_event "teammate_idle_alert" \
|
|
76
|
+
"$(printf '{"teammate_id":"%s","idle_seconds":%s}' \
|
|
77
|
+
"${teammate_id:-unknown}" "${idle_seconds}")"
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Completion verification — run when teammate finishes (status = completed)
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
if [ "${status}" = "completed" ] || [ -z "$status" ]; then
|
|
85
|
+
# Log teammate idle/completion event
|
|
86
|
+
if command -v log_event >/dev/null 2>&1; then
|
|
87
|
+
log_event "teammate_idle_verify" \
|
|
88
|
+
"$(printf '{"teammate_id":"%s","modified_files":"%s","alert_issued":%s}' \
|
|
89
|
+
"${teammate_id:-none}" \
|
|
90
|
+
"${modified_files:-none}" \
|
|
91
|
+
"${ALERT_ISSUED}")"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# If Merlin CLI is available, run quick Sights check on modified files
|
|
95
|
+
if [ -n "$modified_files" ] && command -v merlin >/dev/null 2>&1; then
|
|
96
|
+
merlin context "verify changes in ${modified_files}" >/dev/null 2>&1 &
|
|
97
|
+
fi
|
|
30
98
|
|
|
31
|
-
#
|
|
32
|
-
if [ -n "$
|
|
33
|
-
|
|
99
|
+
# Clear any stale idle alert for this teammate (they finished)
|
|
100
|
+
if [ -n "$teammate_id" ]; then
|
|
101
|
+
MERLIN_STATE_DIR="${HOME}/.claude/merlin/teams-state"
|
|
102
|
+
rm -f "${MERLIN_STATE_DIR}/idle-alert-${teammate_id}.json" 2>/dev/null || true
|
|
103
|
+
fi
|
|
34
104
|
fi
|
|
35
105
|
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
36
107
|
# Quick build check (non-blocking, advisory only)
|
|
37
|
-
|
|
38
|
-
|
|
108
|
+
# Only run when not in Agent Teams context to avoid redundant checks
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
if [ -z "${CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS:-}" ]; then
|
|
111
|
+
if command -v npm >/dev/null 2>&1; then
|
|
112
|
+
npm run build --if-present >/dev/null 2>&1 || true
|
|
113
|
+
fi
|
|
39
114
|
fi
|
|
40
115
|
|
|
41
116
|
# Claude Code command hooks must output valid JSON to stdout
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: WorktreeCreate
|
|
4
4
|
# Purpose: Propagate .merlin/ config into git worktrees so sub-agents
|
|
5
|
-
# spawned in worktrees have Sights access.
|
|
5
|
+
# spawned in worktrees have Sights access and full analytics continuity.
|
|
6
6
|
#
|
|
7
7
|
# Always exits 0 — never blocks Claude Code.
|
|
8
8
|
#
|
|
@@ -23,7 +23,7 @@ if [ -z "$WORKTREE_PATH" ]; then
|
|
|
23
23
|
exit 0
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
# Copy Merlin config to worktree
|
|
26
|
+
# Copy project-level Merlin config to worktree
|
|
27
27
|
if [ -d ".merlin" ]; then
|
|
28
28
|
cp -r .merlin "$WORKTREE_PATH/.merlin" 2>/dev/null || true
|
|
29
29
|
fi
|
|
@@ -33,9 +33,26 @@ if [ -f "CLAUDE.md" ] && [ ! -f "$WORKTREE_PATH/CLAUDE.md" ]; then
|
|
|
33
33
|
cp CLAUDE.md "$WORKTREE_PATH/CLAUDE.md" 2>/dev/null || true
|
|
34
34
|
fi
|
|
35
35
|
|
|
36
|
+
# Propagate essential Merlin state files so the worktree agent has
|
|
37
|
+
# immediate access to Sights context without a cold start.
|
|
38
|
+
MERLIN_DIR="${HOME}/.claude/merlin"
|
|
39
|
+
if [ -d "$MERLIN_DIR" ]; then
|
|
40
|
+
WORKTREE_MERLIN_DIR="$WORKTREE_PATH/.merlin-session"
|
|
41
|
+
mkdir -p "$WORKTREE_MERLIN_DIR" 2>/dev/null || true
|
|
42
|
+
|
|
43
|
+
# Copy last-sights-check so pre-edit hook doesn't warn immediately
|
|
44
|
+
[ -f "$MERLIN_DIR/.last-sights-check" ] && \
|
|
45
|
+
cp "$MERLIN_DIR/.last-sights-check" "$WORKTREE_MERLIN_DIR/.last-sights-check" 2>/dev/null || true
|
|
46
|
+
|
|
47
|
+
# Copy selected repo state for Sights context
|
|
48
|
+
[ -f "$MERLIN_DIR/.selected-repo" ] && \
|
|
49
|
+
cp "$MERLIN_DIR/.selected-repo" "$WORKTREE_MERLIN_DIR/.selected-repo" 2>/dev/null || true
|
|
50
|
+
fi
|
|
51
|
+
|
|
36
52
|
# Log event if analytics available
|
|
37
53
|
if declare -f log_event >/dev/null 2>&1; then
|
|
38
|
-
log_event "worktree_create" "$(printf '{"path":"%s","agent_id":"%s","agent_type":"%s"}'
|
|
54
|
+
log_event "worktree_create" "$(printf '{"path":"%s","agent_id":"%s","agent_type":"%s"}' \
|
|
55
|
+
"$WORKTREE_PATH" "$AGENT_ID" "$AGENT_TYPE")"
|
|
39
56
|
fi
|
|
40
57
|
|
|
41
58
|
echo "Merlin: propagated config to worktree ${WORKTREE_PATH} (agent: ${AGENT_TYPE})" >&2
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: WorktreeRemove
|
|
4
4
|
# Purpose: Cleanup Merlin state from removed worktrees.
|
|
5
|
+
# Also logs worktree lifecycle duration to session analytics.
|
|
5
6
|
#
|
|
6
7
|
# Always exits 0 — never blocks Claude Code.
|
|
7
8
|
#
|
|
@@ -22,15 +23,32 @@ if [ -z "$WORKTREE_PATH" ]; then
|
|
|
22
23
|
exit 0
|
|
23
24
|
fi
|
|
24
25
|
|
|
26
|
+
# Calculate worktree lifetime if we can (from creation timestamp)
|
|
27
|
+
CREATED_TS=""
|
|
28
|
+
if [ -f "$WORKTREE_PATH/.merlin-session/.last-sights-check" ]; then
|
|
29
|
+
CREATED_TS=$(cat "$WORKTREE_PATH/.merlin-session/.last-sights-check" 2>/dev/null || echo "")
|
|
30
|
+
fi
|
|
31
|
+
REMOVED_TS=$(date +%s)
|
|
32
|
+
LIFETIME_S=""
|
|
33
|
+
if [ -n "$CREATED_TS" ] && [ "$CREATED_TS" -gt 0 ] 2>/dev/null; then
|
|
34
|
+
LIFETIME_S=$(( REMOVED_TS - CREATED_TS ))
|
|
35
|
+
fi
|
|
36
|
+
|
|
25
37
|
# Remove Merlin artifacts from worktree (if still present)
|
|
26
38
|
rm -rf "$WORKTREE_PATH/.merlin" 2>/dev/null || true
|
|
39
|
+
rm -rf "$WORKTREE_PATH/.merlin-session" 2>/dev/null || true
|
|
27
40
|
|
|
28
|
-
# Log event if analytics available
|
|
41
|
+
# Log worktree lifecycle event if analytics available
|
|
29
42
|
if declare -f log_event >/dev/null 2>&1; then
|
|
30
|
-
|
|
43
|
+
LIFETIME_VAL="${LIFETIME_S:-null}"
|
|
44
|
+
log_event "worktree_remove" "$(printf \
|
|
45
|
+
'{"path":"%s","agent_id":"%s","agent_type":"%s","lifetime_seconds":%s}' \
|
|
46
|
+
"$WORKTREE_PATH" "$AGENT_ID" "$AGENT_TYPE" "$LIFETIME_VAL")"
|
|
31
47
|
fi
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
LIFETIME_MSG=""
|
|
50
|
+
[ -n "$LIFETIME_S" ] && LIFETIME_MSG=" (lifetime: ${LIFETIME_S}s)"
|
|
51
|
+
echo "Merlin: cleaned up worktree ${WORKTREE_PATH}${LIFETIME_MSG} (agent: ${AGENT_TYPE})" >&2
|
|
34
52
|
|
|
35
53
|
echo '{}'
|
|
36
54
|
exit 0
|
|
@@ -345,11 +345,15 @@ Reference files that Claude needs to understand before implementing.
|
|
|
345
345
|
</context_references>
|
|
346
346
|
|
|
347
347
|
<verification_section>
|
|
348
|
-
Overall phase verification (beyond individual task verification)
|
|
348
|
+
Overall phase verification (beyond individual task verification).
|
|
349
|
+
|
|
350
|
+
**Always use checkboxes** — VS Code plan view renders `- [ ]` as interactive checkboxes.
|
|
349
351
|
|
|
350
352
|
```markdown
|
|
351
353
|
<verification>
|
|
352
|
-
|
|
354
|
+
|
|
355
|
+
## Verification Checklist {#verification}
|
|
356
|
+
|
|
353
357
|
- [ ] `npm run build` succeeds without errors
|
|
354
358
|
- [ ] `npm test` passes all tests
|
|
355
359
|
- [ ] No TypeScript errors
|
|
@@ -360,21 +364,45 @@ Before declaring phase complete:
|
|
|
360
364
|
</verification_section>
|
|
361
365
|
|
|
362
366
|
<success_criteria_section>
|
|
363
|
-
Measurable criteria for phase completion
|
|
367
|
+
Measurable criteria for phase completion.
|
|
368
|
+
|
|
369
|
+
**Always use checkboxes** — VS Code plan view renders these as trackable items.
|
|
370
|
+
**Always tag code blocks** with a language identifier for /copy compatibility (e.g., ` ```bash `, ` ```typescript `).
|
|
364
371
|
|
|
365
372
|
```markdown
|
|
366
373
|
<success_criteria>
|
|
367
374
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
-
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
|
|
375
|
+
## Success Criteria {#success-criteria}
|
|
376
|
+
|
|
377
|
+
- [ ] All tasks completed
|
|
378
|
+
- [ ] All verification checks pass
|
|
379
|
+
- [ ] No errors or warnings introduced
|
|
380
|
+
- [ ] JWT auth flow works end-to-end
|
|
381
|
+
- [ ] Protected routes redirect unauthenticated users
|
|
382
|
+
</success_criteria>
|
|
374
383
|
```
|
|
375
384
|
|
|
376
385
|
</success_criteria_section>
|
|
377
386
|
|
|
387
|
+
<vscode_plan_view>
|
|
388
|
+
PLAN.md files render in VS Code's plan view. To maximize compatibility:
|
|
389
|
+
|
|
390
|
+
1. **Checkboxes for every trackable item** — use `- [ ]` in `<verification>` and `<success_criteria>` sections. VS Code renders these as interactive checkboxes.
|
|
391
|
+
|
|
392
|
+
2. **Anchor headers for navigation** — use `## Section Name {#anchor}` for major sections so the plan view can jump to them.
|
|
393
|
+
|
|
394
|
+
3. **Language-tagged code blocks** — always specify a language so /copy works cleanly:
|
|
395
|
+
````markdown
|
|
396
|
+
```bash
|
|
397
|
+
npm run build
|
|
398
|
+
```
|
|
399
|
+
````
|
|
400
|
+
|
|
401
|
+
4. **Concise task names** — the plan view truncates long headings; keep `<name>` under 60 chars.
|
|
402
|
+
|
|
403
|
+
These rules apply to all generated PLAN.md files.
|
|
404
|
+
</vscode_plan_view>
|
|
405
|
+
|
|
378
406
|
<output_section>
|
|
379
407
|
Specify the SUMMARY.md structure:
|
|
380
408
|
|