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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Hook Template: pr-description.sh
|
|
4
|
+
# Event: Stop
|
|
5
|
+
#
|
|
6
|
+
# At the end of a session, generates a draft PR description from the git diff
|
|
7
|
+
# and outputs it to a file. Surfaces a reminder with the file path.
|
|
8
|
+
#
|
|
9
|
+
# HOW TO INSTALL:
|
|
10
|
+
# Copy this file to ~/.claude/merlin/hooks/pr-description.sh
|
|
11
|
+
# Then add to your .claude/settings.local.json:
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "Stop": [
|
|
16
|
+
# {
|
|
17
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/merlin/hooks/pr-description.sh" }]
|
|
18
|
+
# }
|
|
19
|
+
# ]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
#
|
|
23
|
+
# BEHAVIOR: Advisory only. Writes a draft PR description to .claude/pr-draft.md
|
|
24
|
+
# and injects a reminder. Does not open PRs — that stays in your control.
|
|
25
|
+
# REQUIRES: git
|
|
26
|
+
#
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
trap 'echo "{}"; exit 0' ERR
|
|
29
|
+
|
|
30
|
+
# Only run if we're in a git repo with changes
|
|
31
|
+
git rev-parse --git-dir >/dev/null 2>&1 || { echo '{}'; exit 0; }
|
|
32
|
+
|
|
33
|
+
# Check if there are commits ahead of the merge base (comparing to main/master)
|
|
34
|
+
BASE_BRANCH=""
|
|
35
|
+
for candidate in main master develop; do
|
|
36
|
+
if git show-ref --verify --quiet "refs/heads/${candidate}" 2>/dev/null || \
|
|
37
|
+
git show-ref --verify --quiet "refs/remotes/origin/${candidate}" 2>/dev/null; then
|
|
38
|
+
BASE_BRANCH="$candidate"
|
|
39
|
+
break
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
[ -z "$BASE_BRANCH" ] && { echo '{}'; exit 0; }
|
|
44
|
+
|
|
45
|
+
# Current branch
|
|
46
|
+
CURRENT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
|
|
47
|
+
[ -z "$CURRENT_BRANCH" ] && { echo '{}'; exit 0; }
|
|
48
|
+
|
|
49
|
+
# Skip if already on base branch
|
|
50
|
+
[ "$CURRENT_BRANCH" = "$BASE_BRANCH" ] && { echo '{}'; exit 0; }
|
|
51
|
+
|
|
52
|
+
# Get commits since diverging from base
|
|
53
|
+
COMMITS=$(git log --oneline "${BASE_BRANCH}..HEAD" 2>/dev/null | head -20)
|
|
54
|
+
[ -z "$COMMITS" ] && { echo '{}'; exit 0; }
|
|
55
|
+
|
|
56
|
+
# Get changed files
|
|
57
|
+
CHANGED_FILES=$(git diff --name-only "${BASE_BRANCH}...HEAD" 2>/dev/null | head -30)
|
|
58
|
+
|
|
59
|
+
# Get summary stats
|
|
60
|
+
FILES_COUNT=$(echo "$CHANGED_FILES" | grep -c . 2>/dev/null || echo 0)
|
|
61
|
+
COMMIT_COUNT=$(echo "$COMMITS" | grep -c . 2>/dev/null || echo 0)
|
|
62
|
+
INSERTIONS=$(git diff --stat "${BASE_BRANCH}...HEAD" 2>/dev/null | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
|
|
63
|
+
DELETIONS=$(git diff --stat "${BASE_BRANCH}...HEAD" 2>/dev/null | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
|
|
64
|
+
|
|
65
|
+
# Write draft PR description
|
|
66
|
+
DRAFT_FILE=".claude/pr-draft.md"
|
|
67
|
+
mkdir -p ".claude"
|
|
68
|
+
|
|
69
|
+
cat > "$DRAFT_FILE" << PREOF
|
|
70
|
+
## Summary
|
|
71
|
+
|
|
72
|
+
<!-- Fill in: what does this PR do and why? -->
|
|
73
|
+
|
|
74
|
+
### Changes
|
|
75
|
+
$(echo "$COMMITS" | sed 's/^/- /')
|
|
76
|
+
|
|
77
|
+
### Files Changed (${FILES_COUNT} files, +${INSERTIONS}/-${DELETIONS} lines)
|
|
78
|
+
\`\`\`
|
|
79
|
+
$(echo "$CHANGED_FILES")
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
## Test Plan
|
|
83
|
+
|
|
84
|
+
- [ ] Unit tests pass
|
|
85
|
+
- [ ] Manual test: <!-- describe -->
|
|
86
|
+
- [ ] No new linting errors
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
<!-- Anything reviewers should know? Breaking changes? Dependencies updated? -->
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
*Draft generated by Merlin pr-description hook — branch: \`${CURRENT_BRANCH}\`, ${COMMIT_COUNT} commit(s)*
|
|
94
|
+
PREOF
|
|
95
|
+
|
|
96
|
+
echo "PR DESCRIPTION: Draft written to ${DRAFT_FILE}. Review and edit before opening your PR." >&2
|
|
97
|
+
echo " Run: gh pr create --body \"\$(cat ${DRAFT_FILE})\"" >&2
|
|
98
|
+
|
|
99
|
+
echo '{}'
|
|
100
|
+
exit 0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Hook Template: todo-tracker.sh
|
|
4
|
+
# Event: PostToolUse (Write, Edit)
|
|
5
|
+
#
|
|
6
|
+
# Scans modified files for new TODO, FIXME, HACK, XXX, and TEMP comments.
|
|
7
|
+
# Reports them so they don't silently accumulate without notice.
|
|
8
|
+
#
|
|
9
|
+
# HOW TO INSTALL:
|
|
10
|
+
# Copy this file to ~/.claude/merlin/hooks/todo-tracker.sh
|
|
11
|
+
# Then add to your .claude/settings.local.json:
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "PostToolUse": [
|
|
16
|
+
# {
|
|
17
|
+
# "matcher": "Write|Edit",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/merlin/hooks/todo-tracker.sh" }]
|
|
19
|
+
# }
|
|
20
|
+
# ]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
#
|
|
24
|
+
# BEHAVIOR: Advisory only. Lists new TODO-style comments found in the changed file.
|
|
25
|
+
# Exits 0 (no blocking) — just surfaces the information.
|
|
26
|
+
#
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
trap 'echo "{}"; exit 0' ERR
|
|
29
|
+
|
|
30
|
+
TODO_PATTERN='TODO|FIXME|HACK|XXX|TEMP|NOCOMMIT|REMOVEME'
|
|
31
|
+
|
|
32
|
+
# Read tool input from stdin
|
|
33
|
+
input=""
|
|
34
|
+
if [ ! -t 0 ]; then
|
|
35
|
+
input=$(cat 2>/dev/null || true)
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
[ -z "$input" ] && { echo '{}'; exit 0; }
|
|
39
|
+
|
|
40
|
+
# Extract file path
|
|
41
|
+
file_path=""
|
|
42
|
+
if command -v jq >/dev/null 2>&1; then
|
|
43
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
[ -z "$file_path" ] || [ ! -f "$file_path" ] && { echo '{}'; exit 0; }
|
|
47
|
+
|
|
48
|
+
# Only scan source code files
|
|
49
|
+
case "$file_path" in
|
|
50
|
+
*.js|*.jsx|*.ts|*.tsx|*.mjs|*.py|*.go|*.java|*.rb|*.rs|*.swift|*.c|*.cpp|*.h|*.sh|*.md)
|
|
51
|
+
;;
|
|
52
|
+
*)
|
|
53
|
+
echo '{}'
|
|
54
|
+
exit 0
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# Skip node_modules and generated files
|
|
59
|
+
case "$file_path" in
|
|
60
|
+
*node_modules*|*dist/*|*build/*|*vendor/*)
|
|
61
|
+
echo '{}'
|
|
62
|
+
exit 0
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
|
|
66
|
+
# Find TODO-style comments with line numbers
|
|
67
|
+
todos=$(grep -nE "$TODO_PATTERN" "$file_path" 2>/dev/null | head -20 || true)
|
|
68
|
+
|
|
69
|
+
if [ -n "$todos" ]; then
|
|
70
|
+
count=$(echo "$todos" | wc -l | tr -d ' ')
|
|
71
|
+
echo "TODO TRACKER: ${count} annotation(s) found in ${file_path}:" >&2
|
|
72
|
+
echo "$todos" | while IFS= read -r line; do
|
|
73
|
+
echo " $line" >&2
|
|
74
|
+
done
|
|
75
|
+
echo " Track these in your issue tracker to avoid tech debt accumulation." >&2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Always exit 0 — informational only, never blocking
|
|
79
|
+
echo '{}'
|
|
80
|
+
exit 0
|
|
@@ -4,16 +4,29 @@
|
|
|
4
4
|
# Checks if the modified file exceeds the 400-line convention.
|
|
5
5
|
# Exits with code 2 to inject feedback when file is too large.
|
|
6
6
|
#
|
|
7
|
-
# Agent-type awareness:
|
|
8
|
-
#
|
|
7
|
+
# Agent-type awareness: only enforce for implementation agents.
|
|
8
|
+
# Non-code agents (docs, review, verify, etc.) are fully exempt.
|
|
9
9
|
#
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
trap 'echo "{}"; exit 0' ERR
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Read CLAUDE_AGENT_TYPE from env — support both var names
|
|
14
14
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-${CLAUDE_CODE_AGENT_TYPE:-main}}"
|
|
15
|
+
|
|
16
|
+
# Only enforce for implementation agents
|
|
17
|
+
# Skip for all doc/review/verification/analysis agents
|
|
15
18
|
case "$AGENT_TYPE" in
|
|
16
|
-
|
|
19
|
+
implementation-dev|merlin-executor)
|
|
20
|
+
# Enforcement is active for these agents — continue
|
|
21
|
+
;;
|
|
22
|
+
docs-keeper|merlin-reviewer|merlin-verifier|merlin-milestone-auditor|\
|
|
23
|
+
merlin-integration-checker|merlin-work-verifier|code-organization-supervisor|\
|
|
24
|
+
context-guardian|dry-refactor|tests-qa|merlin-codebase-mapper)
|
|
25
|
+
echo '{}'
|
|
26
|
+
exit 0
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
# For all other agents (including 'main'), skip enforcement
|
|
17
30
|
echo '{}'
|
|
18
31
|
exit 0
|
|
19
32
|
;;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: ConfigChange
|
|
4
4
|
# Purpose: Validate Merlin MCP key still works after config change.
|
|
5
|
-
#
|
|
5
|
+
# Logs config change events to Sights for audit trail.
|
|
6
6
|
#
|
|
7
7
|
# Always exits 0 — never blocks Claude Code.
|
|
8
8
|
#
|
|
@@ -13,31 +13,59 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
13
13
|
|
|
14
14
|
# Source shared libs if available
|
|
15
15
|
[ -f "${HOOK_DIR}/lib/analytics.sh" ] && . "${HOOK_DIR}/lib/analytics.sh"
|
|
16
|
+
[ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
|
|
16
17
|
|
|
17
|
-
MERLIN_API_KEY="${MERLIN_API_KEY:-}"
|
|
18
18
|
AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
|
|
19
19
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
20
|
+
CLAUDE_DIR="${HOME}/.claude"
|
|
21
|
+
|
|
22
|
+
# Resolve API key: env var takes priority, fall back to config.json
|
|
23
|
+
MERLIN_API_KEY="${MERLIN_API_KEY:-}"
|
|
24
|
+
if [ -z "$MERLIN_API_KEY" ] && declare -f get_merlin_api_key >/dev/null 2>&1; then
|
|
25
|
+
MERLIN_API_KEY="$(get_merlin_api_key)"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Detect which MCP servers are configured for audit trail
|
|
29
|
+
MCP_SERVERS=""
|
|
30
|
+
if [ -f "${CLAUDE_DIR}/config.json" ] && command -v jq >/dev/null 2>&1; then
|
|
31
|
+
MCP_SERVERS=$(jq -r '(.mcpServers // {}) | keys | join(",")' \
|
|
32
|
+
"${CLAUDE_DIR}/config.json" 2>/dev/null || echo "")
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
HAS_KEY="$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)"
|
|
36
|
+
KEY_VALID="unknown"
|
|
37
|
+
|
|
38
|
+
# Validate key format if present (valid prefixes: mrln_ or ccw_)
|
|
39
|
+
if [ -n "$MERLIN_API_KEY" ]; then
|
|
40
|
+
case "$MERLIN_API_KEY" in
|
|
41
|
+
mrln_*|ccw_*)
|
|
42
|
+
KEY_VALID="true"
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
KEY_VALID="false"
|
|
46
|
+
echo "Merlin: API key has unexpected format after config change" >&2
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Write audit trail entry to local log
|
|
52
|
+
AUDIT_DIR="${HOME}/.claude/merlin/audit"
|
|
53
|
+
mkdir -p "$AUDIT_DIR" 2>/dev/null || true
|
|
54
|
+
printf '{"ts":"%s","agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}\n' \
|
|
55
|
+
"$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
56
|
+
"$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS" \
|
|
57
|
+
>> "${AUDIT_DIR}/config-changes.log" 2>/dev/null || true
|
|
20
58
|
|
|
21
59
|
# Log config change event if analytics available
|
|
22
60
|
if declare -f log_event >/dev/null 2>&1; then
|
|
23
|
-
log_event "config_change" "$(printf
|
|
61
|
+
log_event "config_change" "$(printf \
|
|
62
|
+
'{"agent_id":"%s","agent_type":"%s","has_key":%s,"key_valid":"%s","mcp_servers":"%s"}' \
|
|
63
|
+
"$AGENT_ID" "$AGENT_TYPE" "$HAS_KEY" "$KEY_VALID" "$MCP_SERVERS")"
|
|
24
64
|
fi
|
|
25
65
|
|
|
26
66
|
if [ -z "$MERLIN_API_KEY" ]; then
|
|
27
|
-
echo "Merlin: No API key configured
|
|
28
|
-
echo '{}'
|
|
29
|
-
exit 0
|
|
67
|
+
echo "Merlin: No API key configured — Sights features disabled" >&2
|
|
30
68
|
fi
|
|
31
69
|
|
|
32
|
-
# Quick validation — check key format (valid prefixes: mrln_ or ccw_)
|
|
33
|
-
case "$MERLIN_API_KEY" in
|
|
34
|
-
mrln_*|ccw_*)
|
|
35
|
-
# Valid prefix — no-op
|
|
36
|
-
;;
|
|
37
|
-
*)
|
|
38
|
-
echo "Merlin: API key has unexpected format after config change" >&2
|
|
39
|
-
;;
|
|
40
|
-
esac
|
|
41
|
-
|
|
42
70
|
echo '{}'
|
|
43
71
|
exit 0
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
# Purpose: Pre-warm Merlin Sights connection when CLAUDE.md loads.
|
|
5
5
|
# Fires BEFORE session-start, giving us a head start on context loading.
|
|
6
6
|
#
|
|
7
|
+
# Cold-start optimization: touching the sights-check timestamp file signals
|
|
8
|
+
# that Merlin is active so the first pre-edit check doesn't fire a warning.
|
|
9
|
+
#
|
|
7
10
|
# Always exits 0 — never blocks Claude Code startup.
|
|
8
11
|
#
|
|
9
12
|
set -euo pipefail
|
|
@@ -17,18 +20,32 @@ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
17
20
|
# shellcheck source=lib/sights-check.sh
|
|
18
21
|
[ -f "${HOOK_DIR}/lib/sights-check.sh" ] && . "${HOOK_DIR}/lib/sights-check.sh"
|
|
19
22
|
|
|
20
|
-
# Log agent context if available
|
|
21
23
|
AGENT_ID="${CLAUDE_AGENT_ID:-unknown}"
|
|
22
24
|
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
25
|
+
MERLIN_DIR="${HOME}/.claude/merlin"
|
|
26
|
+
|
|
27
|
+
# Cold-start pre-warm: record that Sights is active so the pre-edit hook
|
|
28
|
+
# doesn't fire a "Sights not consulted" warning immediately after boot.
|
|
29
|
+
# Only touch if the file is stale (>300s old) or missing — avoids
|
|
30
|
+
# re-stamping on every CLAUDE.md reload within the same session.
|
|
31
|
+
if declare -f record_sights_call >/dev/null 2>&1; then
|
|
32
|
+
if ! sights_was_checked_recently 300 2>/dev/null; then
|
|
33
|
+
record_sights_call 2>/dev/null || true
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Write a lightweight cold-start marker so session-start can detect
|
|
38
|
+
# that instructions were already loaded (avoids double context fetch).
|
|
39
|
+
mkdir -p "${MERLIN_DIR}" 2>/dev/null || true
|
|
40
|
+
printf '%s' "$(date +%s)" > "${MERLIN_DIR}/.instructions-loaded-ts" 2>/dev/null || true
|
|
23
41
|
|
|
24
42
|
# Log the instructions-loaded event if analytics is available
|
|
25
43
|
if declare -f log_event >/dev/null 2>&1; then
|
|
26
|
-
log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}'
|
|
44
|
+
log_event "instructions_loaded" "$(printf '{"agent_id":"%s","agent_type":"%s","cwd":"%s"}' \
|
|
45
|
+
"$AGENT_ID" "$AGENT_TYPE" "${PWD:-}")"
|
|
27
46
|
fi
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
# The MCP server will handle actual Sights connection
|
|
31
|
-
echo "Merlin instructions loaded — Sights pre-warming (agent: ${AGENT_TYPE})" >&2
|
|
48
|
+
echo "Merlin instructions loaded — Sights pre-warmed (agent: ${AGENT_TYPE})" >&2
|
|
32
49
|
|
|
33
50
|
echo '{}'
|
|
34
51
|
exit 0
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: notify-desktop.sh
|
|
4
|
+
# Events: Notification, Stop
|
|
5
|
+
#
|
|
6
|
+
# Sends a desktop notification when Claude finishes work or needs input.
|
|
7
|
+
# Reads notification config from .merlin/config.json in the project root.
|
|
8
|
+
# Always exits 0 — notifications must never block the main session.
|
|
9
|
+
#
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
trap 'echo "{}"; exit 0' ERR
|
|
12
|
+
|
|
13
|
+
# ── Config resolution ─────────────────────────────────────────────
|
|
14
|
+
# Resolve project root: prefer CLAUDE_WORKTREE_PATH, fall back to PWD
|
|
15
|
+
PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
|
|
16
|
+
CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
|
|
17
|
+
|
|
18
|
+
# Read a value from .merlin/config.json using jq or python3 as fallback
|
|
19
|
+
_read_config() {
|
|
20
|
+
local key="${1}" default="${2:-}"
|
|
21
|
+
if [ ! -f "${CONFIG_FILE}" ]; then
|
|
22
|
+
echo "${default}"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
local val
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
|
|
28
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
29
|
+
val=$(python3 -c "
|
|
30
|
+
import json, sys
|
|
31
|
+
try:
|
|
32
|
+
d = json.load(open('${CONFIG_FILE}'))
|
|
33
|
+
keys = '${key}'.lstrip('.').split('.')
|
|
34
|
+
v = d
|
|
35
|
+
for k in keys:
|
|
36
|
+
v = v.get(k, None)
|
|
37
|
+
if v is None:
|
|
38
|
+
break
|
|
39
|
+
print(v if v is not None else '${default}')
|
|
40
|
+
except:
|
|
41
|
+
print('${default}')
|
|
42
|
+
" 2>/dev/null) || val="${default}"
|
|
43
|
+
else
|
|
44
|
+
val="${default}"
|
|
45
|
+
fi
|
|
46
|
+
echo "${val}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Check if desktop notifications are enabled (default: true)
|
|
50
|
+
DESKTOP_ENABLED=$(_read_config '.notifications.desktop' 'true')
|
|
51
|
+
if [ "${DESKTOP_ENABLED}" = "false" ]; then
|
|
52
|
+
echo '{}'
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Check notify_on list to see if this event type is included
|
|
57
|
+
HOOK_EVENT="${CLAUDE_HOOK_EVENT:-Stop}"
|
|
58
|
+
NOTIFY_ON=$(_read_config '.notifications.notify_on' '["stop","needs_input","error"]')
|
|
59
|
+
|
|
60
|
+
# Map hook event names to notify_on values
|
|
61
|
+
case "${HOOK_EVENT}" in
|
|
62
|
+
Stop) EVENT_KEY="stop" ;;
|
|
63
|
+
Notification) EVENT_KEY="needs_input" ;;
|
|
64
|
+
*) EVENT_KEY="stop" ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
# Check if this event is in the notify_on list
|
|
68
|
+
EVENT_ENABLED=false
|
|
69
|
+
if echo "${NOTIFY_ON}" | grep -q "\"${EVENT_KEY}\"" 2>/dev/null; then
|
|
70
|
+
EVENT_ENABLED=true
|
|
71
|
+
fi
|
|
72
|
+
# Default to true if we couldn't parse (degraded gracefully)
|
|
73
|
+
if [ "${NOTIFY_ON}" = '["stop","needs_input","error"]' ]; then
|
|
74
|
+
EVENT_ENABLED=true
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if [ "${EVENT_ENABLED}" = "false" ]; then
|
|
78
|
+
echo '{}'
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ── Build notification message ─────────────────────────────────────
|
|
83
|
+
# Read stdin for hook context (non-blocking, best-effort)
|
|
84
|
+
HOOK_INPUT=""
|
|
85
|
+
if [ -t 0 ]; then
|
|
86
|
+
# stdin is a terminal, no piped input
|
|
87
|
+
HOOK_INPUT=""
|
|
88
|
+
else
|
|
89
|
+
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Determine message based on event type
|
|
93
|
+
case "${HOOK_EVENT}" in
|
|
94
|
+
Stop)
|
|
95
|
+
# Extract agent type for context if available
|
|
96
|
+
AGENT_TYPE="${CLAUDE_AGENT_TYPE:-main}"
|
|
97
|
+
if [ "${AGENT_TYPE}" != "main" ] && [ "${AGENT_TYPE}" != "unknown" ]; then
|
|
98
|
+
MESSAGE="Task complete (${AGENT_TYPE} agent)"
|
|
99
|
+
else
|
|
100
|
+
MESSAGE="Task complete — Claude has finished working"
|
|
101
|
+
fi
|
|
102
|
+
;;
|
|
103
|
+
Notification)
|
|
104
|
+
MESSAGE="Claude needs your input"
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
MESSAGE="Merlin: Task complete"
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
|
|
111
|
+
TITLE="Merlin"
|
|
112
|
+
SOUND_ENABLED=$(_read_config '.notifications.sound' 'false')
|
|
113
|
+
|
|
114
|
+
# ── Detect OS and send notification ───────────────────────────────
|
|
115
|
+
OS_TYPE="$(uname -s 2>/dev/null || echo 'unknown')"
|
|
116
|
+
|
|
117
|
+
case "${OS_TYPE}" in
|
|
118
|
+
Darwin)
|
|
119
|
+
# macOS — use osascript
|
|
120
|
+
osascript -e "display notification \"${MESSAGE}\" with title \"${TITLE}\" sound name \"Glass\"" \
|
|
121
|
+
>/dev/null 2>&1 &
|
|
122
|
+
|
|
123
|
+
# Optional extra sound (beyond what osascript plays)
|
|
124
|
+
if [ "${SOUND_ENABLED}" = "true" ]; then
|
|
125
|
+
SOUND_FILE="/System/Library/Sounds/Glass.aiff"
|
|
126
|
+
if [ -f "${SOUND_FILE}" ]; then
|
|
127
|
+
afplay "${SOUND_FILE}" >/dev/null 2>&1 &
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
Linux)
|
|
133
|
+
# Linux — use notify-send if available
|
|
134
|
+
if command -v notify-send >/dev/null 2>&1; then
|
|
135
|
+
notify-send "${TITLE}" "${MESSAGE}" --icon=dialog-information \
|
|
136
|
+
>/dev/null 2>&1 &
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Optional sound on Linux
|
|
140
|
+
if [ "${SOUND_ENABLED}" = "true" ]; then
|
|
141
|
+
SOUND_FILE="/usr/share/sounds/freedesktop/stereo/complete.oga"
|
|
142
|
+
if [ -f "${SOUND_FILE}" ] && command -v paplay >/dev/null 2>&1; then
|
|
143
|
+
paplay "${SOUND_FILE}" >/dev/null 2>&1 &
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
;;
|
|
147
|
+
|
|
148
|
+
*)
|
|
149
|
+
# Unknown OS — exit silently
|
|
150
|
+
echo '{}'
|
|
151
|
+
exit 0
|
|
152
|
+
;;
|
|
153
|
+
esac
|
|
154
|
+
|
|
155
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
156
|
+
echo '{}'
|
|
157
|
+
exit 0
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: notify-webhook.sh
|
|
4
|
+
# Event: Stop
|
|
5
|
+
#
|
|
6
|
+
# Fires Slack and/or Discord webhooks when Claude finishes a task.
|
|
7
|
+
# Reads webhook URLs from .merlin/config.json in the project root.
|
|
8
|
+
# All curl calls are fire-and-forget (background, 5s timeout).
|
|
9
|
+
# Always exits 0 — webhooks must never block the main session.
|
|
10
|
+
#
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
trap 'echo "{}"; exit 0' ERR
|
|
13
|
+
|
|
14
|
+
# ── Require curl ──────────────────────────────────────────────────
|
|
15
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
16
|
+
echo '{}'
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# ── Config resolution ─────────────────────────────────────────────
|
|
21
|
+
PROJECT_ROOT="${CLAUDE_WORKTREE_PATH:-${PWD:-$(pwd)}}"
|
|
22
|
+
CONFIG_FILE="${PROJECT_ROOT}/.merlin/config.json"
|
|
23
|
+
|
|
24
|
+
_read_config() {
|
|
25
|
+
local key="${1}" default="${2:-}"
|
|
26
|
+
if [ ! -f "${CONFIG_FILE}" ]; then
|
|
27
|
+
echo "${default}"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
local val
|
|
31
|
+
if command -v jq >/dev/null 2>&1; then
|
|
32
|
+
val=$(jq -r "${key} // \"${default}\"" "${CONFIG_FILE}" 2>/dev/null) || val="${default}"
|
|
33
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
34
|
+
val=$(python3 -c "
|
|
35
|
+
import json, sys
|
|
36
|
+
try:
|
|
37
|
+
d = json.load(open('${CONFIG_FILE}'))
|
|
38
|
+
keys = '${key}'.lstrip('.').split('.')
|
|
39
|
+
v = d
|
|
40
|
+
for k in keys:
|
|
41
|
+
v = v.get(k, None)
|
|
42
|
+
if v is None:
|
|
43
|
+
break
|
|
44
|
+
print(v if v is not None else '${default}')
|
|
45
|
+
except:
|
|
46
|
+
print('${default}')
|
|
47
|
+
" 2>/dev/null) || val="${default}"
|
|
48
|
+
else
|
|
49
|
+
val="${default}"
|
|
50
|
+
fi
|
|
51
|
+
echo "${val}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
SLACK_WEBHOOK=$(_read_config '.notifications.slack_webhook' '')
|
|
55
|
+
DISCORD_WEBHOOK=$(_read_config '.notifications.discord_webhook' '')
|
|
56
|
+
|
|
57
|
+
# Exit early if no webhooks are configured
|
|
58
|
+
if [ -z "${SLACK_WEBHOOK}" ] && [ -z "${DISCORD_WEBHOOK}" ]; then
|
|
59
|
+
echo '{}'
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# ── Build context ─────────────────────────────────────────────────
|
|
64
|
+
AGENT_NAME="${CLAUDE_AGENT_TYPE:-main}"
|
|
65
|
+
if [ "${AGENT_NAME}" = "unknown" ] || [ -z "${AGENT_NAME}" ]; then
|
|
66
|
+
AGENT_NAME="main"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Calculate session duration from session analytics file if available
|
|
70
|
+
SESSION_ID="${MERLIN_SESSION_ID:-}"
|
|
71
|
+
DURATION_STR="unknown"
|
|
72
|
+
if [ -n "${SESSION_ID}" ]; then
|
|
73
|
+
SESSION_FILE="${HOME}/.claude/merlin/analytics/session-${SESSION_ID}.json"
|
|
74
|
+
if [ -f "${SESSION_FILE}" ] && command -v jq >/dev/null 2>&1; then
|
|
75
|
+
START_TS=$(jq -r '.startTime // ""' "${SESSION_FILE}" 2>/dev/null || echo "")
|
|
76
|
+
if [ -n "${START_TS}" ]; then
|
|
77
|
+
START_EPOCH=$(date -d "${START_TS}" +%s 2>/dev/null \
|
|
78
|
+
|| python3 -c "import datetime; print(int(datetime.datetime.fromisoformat('${START_TS}'.replace('Z','+00:00')).timestamp()))" 2>/dev/null \
|
|
79
|
+
|| echo "")
|
|
80
|
+
if [ -n "${START_EPOCH}" ]; then
|
|
81
|
+
NOW_EPOCH=$(date +%s)
|
|
82
|
+
ELAPSED=$(( NOW_EPOCH - START_EPOCH ))
|
|
83
|
+
MINUTES=$(( ELAPSED / 60 ))
|
|
84
|
+
SECONDS=$(( ELAPSED % 60 ))
|
|
85
|
+
DURATION_STR="${MINUTES}m ${SECONDS}s"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
STATUS_TEXT="Success"
|
|
92
|
+
STATUS_ICON="OK"
|
|
93
|
+
|
|
94
|
+
# ── Slack webhook ─────────────────────────────────────────────────
|
|
95
|
+
if [ -n "${SLACK_WEBHOOK}" ]; then
|
|
96
|
+
SLACK_BODY=$(cat <<SLACK_JSON
|
|
97
|
+
{
|
|
98
|
+
"text": "Merlin: Task completed",
|
|
99
|
+
"blocks": [
|
|
100
|
+
{
|
|
101
|
+
"type": "section",
|
|
102
|
+
"text": {
|
|
103
|
+
"type": "mrkdwn",
|
|
104
|
+
"text": "*Merlin Task Complete*\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
SLACK_JSON
|
|
110
|
+
)
|
|
111
|
+
curl \
|
|
112
|
+
--max-time 5 \
|
|
113
|
+
--silent \
|
|
114
|
+
--output /dev/null \
|
|
115
|
+
--request POST \
|
|
116
|
+
--header "Content-Type: application/json" \
|
|
117
|
+
--data "${SLACK_BODY}" \
|
|
118
|
+
"${SLACK_WEBHOOK}" &
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# ── Discord webhook ───────────────────────────────────────────────
|
|
122
|
+
if [ -n "${DISCORD_WEBHOOK}" ]; then
|
|
123
|
+
DISCORD_BODY=$(cat <<DISCORD_JSON
|
|
124
|
+
{
|
|
125
|
+
"content": "**Merlin Task Complete**\n\nAgent: ${AGENT_NAME}\nDuration: ${DURATION_STR}\nStatus: ${STATUS_ICON} ${STATUS_TEXT}"
|
|
126
|
+
}
|
|
127
|
+
DISCORD_JSON
|
|
128
|
+
)
|
|
129
|
+
curl \
|
|
130
|
+
--max-time 5 \
|
|
131
|
+
--silent \
|
|
132
|
+
--output /dev/null \
|
|
133
|
+
--request POST \
|
|
134
|
+
--header "Content-Type: application/json" \
|
|
135
|
+
--data "${DISCORD_BODY}" \
|
|
136
|
+
"${DISCORD_WEBHOOK}" &
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
140
|
+
echo '{}'
|
|
141
|
+
exit 0
|