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.
Files changed (151) hide show
  1. package/bin/install.cjs +146 -22
  2. package/bin/runtime-adapters.cjs +396 -0
  3. package/dist/server/cost/tracker.d.ts +38 -2
  4. package/dist/server/cost/tracker.d.ts.map +1 -1
  5. package/dist/server/cost/tracker.js +87 -15
  6. package/dist/server/cost/tracker.js.map +1 -1
  7. package/dist/server/server.d.ts.map +1 -1
  8. package/dist/server/server.js +74 -30
  9. package/dist/server/server.js.map +1 -1
  10. package/dist/server/tools/adaptive.js +1 -1
  11. package/dist/server/tools/adaptive.js.map +1 -1
  12. package/dist/server/tools/agents-index.js +3 -3
  13. package/dist/server/tools/agents-index.js.map +1 -1
  14. package/dist/server/tools/agents.js +5 -5
  15. package/dist/server/tools/agents.js.map +1 -1
  16. package/dist/server/tools/behaviors.js +4 -4
  17. package/dist/server/tools/behaviors.js.map +1 -1
  18. package/dist/server/tools/context.js +7 -7
  19. package/dist/server/tools/context.js.map +1 -1
  20. package/dist/server/tools/cost.d.ts +3 -1
  21. package/dist/server/tools/cost.d.ts.map +1 -1
  22. package/dist/server/tools/cost.js +66 -13
  23. package/dist/server/tools/cost.js.map +1 -1
  24. package/dist/server/tools/discoveries.js +6 -6
  25. package/dist/server/tools/discoveries.js.map +1 -1
  26. package/dist/server/tools/index.d.ts +4 -0
  27. package/dist/server/tools/index.d.ts.map +1 -1
  28. package/dist/server/tools/index.js +4 -0
  29. package/dist/server/tools/index.js.map +1 -1
  30. package/dist/server/tools/learning.d.ts +12 -0
  31. package/dist/server/tools/learning.d.ts.map +1 -0
  32. package/dist/server/tools/learning.js +269 -0
  33. package/dist/server/tools/learning.js.map +1 -0
  34. package/dist/server/tools/project.js +7 -7
  35. package/dist/server/tools/project.js.map +1 -1
  36. package/dist/server/tools/promote.d.ts +11 -0
  37. package/dist/server/tools/promote.d.ts.map +1 -0
  38. package/dist/server/tools/promote.js +315 -0
  39. package/dist/server/tools/promote.js.map +1 -0
  40. package/dist/server/tools/route.d.ts.map +1 -1
  41. package/dist/server/tools/route.js +65 -24
  42. package/dist/server/tools/route.js.map +1 -1
  43. package/dist/server/tools/session-restore.d.ts +18 -0
  44. package/dist/server/tools/session-restore.d.ts.map +1 -0
  45. package/dist/server/tools/session-restore.js +154 -0
  46. package/dist/server/tools/session-restore.js.map +1 -0
  47. package/dist/server/tools/session-search.d.ts +16 -0
  48. package/dist/server/tools/session-search.d.ts.map +1 -0
  49. package/dist/server/tools/session-search.js +240 -0
  50. package/dist/server/tools/session-search.js.map +1 -0
  51. package/dist/server/tools/sights-index.js +2 -2
  52. package/dist/server/tools/sights-index.js.map +1 -1
  53. package/dist/server/tools/smart-route.d.ts.map +1 -1
  54. package/dist/server/tools/smart-route.js +4 -5
  55. package/dist/server/tools/smart-route.js.map +1 -1
  56. package/dist/server/tools/verification.js +1 -1
  57. package/dist/server/tools/verification.js.map +1 -1
  58. package/files/agents/code-organization-supervisor.md +9 -0
  59. package/files/agents/context-guardian.md +9 -0
  60. package/files/agents/docs-keeper.md +11 -1
  61. package/files/agents/dry-refactor.md +12 -1
  62. package/files/agents/elite-code-refactorer.md +10 -0
  63. package/files/agents/hardening-guard.md +13 -1
  64. package/files/agents/implementation-dev.md +12 -1
  65. package/files/agents/merlin-access-control-reviewer.md +248 -0
  66. package/files/agents/merlin-api-designer.md +9 -0
  67. package/files/agents/merlin-codebase-mapper.md +9 -1
  68. package/files/agents/merlin-debugger.md +10 -0
  69. package/files/agents/merlin-dependency-auditor.md +216 -0
  70. package/files/agents/merlin-executor.md +12 -1
  71. package/files/agents/merlin-frontend.md +9 -0
  72. package/files/agents/merlin-input-validator.md +247 -0
  73. package/files/agents/merlin-integration-checker.md +9 -1
  74. package/files/agents/merlin-migrator.md +9 -0
  75. package/files/agents/merlin-milestone-auditor.md +8 -0
  76. package/files/agents/merlin-performance.md +8 -0
  77. package/files/agents/merlin-planner.md +10 -0
  78. package/files/agents/merlin-researcher.md +10 -0
  79. package/files/agents/merlin-reviewer.md +42 -7
  80. package/files/agents/merlin-sast-reviewer.md +182 -0
  81. package/files/agents/merlin-secret-scanner.md +203 -0
  82. package/files/agents/merlin-security.md +9 -0
  83. package/files/agents/merlin-verifier.md +9 -0
  84. package/files/agents/merlin-work-verifier.md +9 -0
  85. package/files/agents/merlin.md +10 -0
  86. package/files/agents/ops-railway.md +11 -1
  87. package/files/agents/orchestrator-retrofit.md +9 -1
  88. package/files/agents/product-spec.md +11 -1
  89. package/files/agents/remotion.md +8 -0
  90. package/files/agents/system-architect.md +11 -1
  91. package/files/agents/tests-qa.md +12 -1
  92. package/files/commands/merlin/course-correct.md +219 -0
  93. package/files/commands/merlin/debug.md +2 -2
  94. package/files/commands/merlin/execute-phase.md +96 -199
  95. package/files/commands/merlin/execute-plan.md +118 -182
  96. package/files/commands/merlin/health.md +385 -0
  97. package/files/commands/merlin/loop-recipes.md +93 -36
  98. package/files/commands/merlin/map-codebase.md +4 -4
  99. package/files/commands/merlin/next.md +240 -0
  100. package/files/commands/merlin/optimize-prompts.md +158 -0
  101. package/files/commands/merlin/plan-phase.md +1 -1
  102. package/files/commands/merlin/profiles.md +215 -0
  103. package/files/commands/merlin/promote.md +176 -0
  104. package/files/commands/merlin/quick.md +229 -0
  105. package/files/commands/merlin/readiness-gate.md +208 -0
  106. package/files/commands/merlin/research-phase.md +2 -2
  107. package/files/commands/merlin/research-project.md +4 -4
  108. package/files/commands/merlin/resume-work.md +27 -1
  109. package/files/commands/merlin/route.md +43 -1
  110. package/files/commands/merlin/sandbox.md +359 -0
  111. package/files/commands/merlin/usage.md +55 -0
  112. package/files/commands/merlin/verify-work.md +1 -1
  113. package/files/docker/Dockerfile.merlin +20 -0
  114. package/files/docker/docker-compose.merlin.yml +23 -0
  115. package/files/hook-templates/auto-commit.sh +64 -0
  116. package/files/hook-templates/auto-format.sh +95 -0
  117. package/files/hook-templates/auto-test.sh +117 -0
  118. package/files/hook-templates/branch-protection.sh +72 -0
  119. package/files/hook-templates/changelog-reminder.sh +76 -0
  120. package/files/hook-templates/complexity-check.sh +112 -0
  121. package/files/hook-templates/import-audit.sh +83 -0
  122. package/files/hook-templates/license-header.sh +84 -0
  123. package/files/hook-templates/pr-description.sh +100 -0
  124. package/files/hook-templates/todo-tracker.sh +80 -0
  125. package/files/hooks/check-file-size.sh +17 -4
  126. package/files/hooks/config-change.sh +44 -16
  127. package/files/hooks/instructions-loaded.sh +22 -5
  128. package/files/hooks/notify-desktop.sh +157 -0
  129. package/files/hooks/notify-webhook.sh +141 -0
  130. package/files/hooks/pre-edit-sights-check.sh +76 -9
  131. package/files/hooks/security-scanner.sh +153 -0
  132. package/files/hooks/session-end-memory-sync.sh +97 -0
  133. package/files/hooks/session-end.sh +274 -1
  134. package/files/hooks/session-start.sh +19 -6
  135. package/files/hooks/smart-approve.sh +270 -0
  136. package/files/hooks/teammate-idle-verify.sh +87 -12
  137. package/files/hooks/worktree-create.sh +20 -3
  138. package/files/hooks/worktree-remove.sh +21 -3
  139. package/files/merlin/references/plan-format.md +37 -9
  140. package/files/merlin/sandbox.json +9 -0
  141. package/files/merlin/security.json +11 -0
  142. package/files/merlin/templates/ci/docs-update.yml +81 -0
  143. package/files/merlin/templates/ci/pr-review.yml +50 -0
  144. package/files/merlin/templates/ci/security-audit.yml +74 -0
  145. package/files/merlin/templates/config.json +9 -1
  146. package/files/rules/api-rules.md +30 -0
  147. package/files/rules/frontend-rules.md +25 -0
  148. package/files/rules/hooks-rules.md +36 -0
  149. package/files/rules/mcp-rules.md +30 -0
  150. package/files/rules/worker-rules.md +29 -0
  151. 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: docs-keeper and merlin-verifier agents are exempt
8
- # from the 400-line limit (they work with doc/verification files).
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
- # Check agent typeskip enforcement for non-implementation agents
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
- docs-keeper|merlin-verifier)
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
- # Also logs config changes to Sights for enterprise audit trail.
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 '{"agent_id":"%s","agent_type":"%s","has_key":"%s"}' "$AGENT_ID" "$AGENT_TYPE" "$([ -n "$MERLIN_API_KEY" ] && echo true || echo false)")"
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 after config change" >&2
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"}' "$AGENT_ID" "$AGENT_TYPE" "${PWD:-}")"
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
- # Signal that Merlin instructions were loaded
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