create-merlin-brain 3.11.0 → 3.13.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 +156 -32
- package/bin/runtime-adapters.cjs +396 -0
- package/dist/server/api/types.d.ts +7 -0
- package/dist/server/api/types.d.ts.map +1 -1
- 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/__tests__/augmentation.test.d.ts +8 -0
- package/dist/server/tools/__tests__/augmentation.test.d.ts.map +1 -0
- package/dist/server/tools/__tests__/augmentation.test.js +76 -0
- package/dist/server/tools/__tests__/augmentation.test.js.map +1 -0
- package/dist/server/tools/__tests__/route-helpers.test.d.ts +5 -0
- package/dist/server/tools/__tests__/route-helpers.test.d.ts.map +1 -0
- package/dist/server/tools/__tests__/route-helpers.test.js +49 -0
- package/dist/server/tools/__tests__/route-helpers.test.js.map +1 -0
- package/dist/server/tools/adaptive.js +1 -1
- package/dist/server/tools/adaptive.js.map +1 -1
- package/dist/server/tools/agent-spawn.d.ts +25 -0
- package/dist/server/tools/agent-spawn.d.ts.map +1 -0
- package/dist/server/tools/agent-spawn.js +95 -0
- package/dist/server/tools/agent-spawn.js.map +1 -0
- 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/augmentation.d.ts +45 -0
- package/dist/server/tools/augmentation.d.ts.map +1 -0
- package/dist/server/tools/augmentation.js +167 -0
- package/dist/server/tools/augmentation.js.map +1 -0
- 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-helpers.d.ts +45 -0
- package/dist/server/tools/route-helpers.d.ts.map +1 -0
- package/dist/server/tools/route-helpers.js +93 -0
- package/dist/server/tools/route-helpers.js.map +1 -0
- package/dist/server/tools/route.d.ts +4 -3
- package/dist/server/tools/route.d.ts.map +1 -1
- package/dist/server/tools/route.js +80 -284
- 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 +1 -0
- package/files/agents/context-guardian.md +1 -0
- package/files/agents/docs-keeper.md +1 -0
- package/files/agents/dry-refactor.md +1 -0
- package/files/agents/elite-code-refactorer.md +1 -0
- package/files/agents/hardening-guard.md +1 -0
- package/files/agents/implementation-dev.md +1 -0
- package/files/agents/merlin-access-control-reviewer.md +248 -0
- package/files/agents/merlin-codebase-mapper.md +1 -1
- package/files/agents/merlin-dependency-auditor.md +216 -0
- package/files/agents/merlin-executor.md +1 -0
- package/files/agents/merlin-input-validator.md +247 -0
- package/files/agents/merlin-reviewer.md +1 -0
- package/files/agents/merlin-sast-reviewer.md +182 -0
- package/files/agents/merlin-secret-scanner.md +203 -0
- package/files/agents/tests-qa.md +1 -0
- package/files/commands/merlin/execute-phase.md +94 -197
- package/files/commands/merlin/execute-plan.md +116 -180
- package/files/commands/merlin/health.md +385 -0
- package/files/commands/merlin/loop-recipes.md +93 -36
- package/files/commands/merlin/optimize-prompts.md +158 -0
- 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/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/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 +5 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: security-scanner.sh
|
|
4
|
+
# Event: PreToolUse (Write, Edit, Bash, NotebookEdit)
|
|
5
|
+
#
|
|
6
|
+
# Scans content being written/executed for:
|
|
7
|
+
# A) Prompt injection patterns
|
|
8
|
+
# B) Secret/credential leaks
|
|
9
|
+
# C) Data exfiltration patterns
|
|
10
|
+
#
|
|
11
|
+
# Exit 2 + JSON {"decision":"block","reason":"..."} to block.
|
|
12
|
+
# Exit 0 to allow.
|
|
13
|
+
#
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
trap 'echo "{}"; exit 0' ERR
|
|
16
|
+
|
|
17
|
+
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
LOG_DIR="${HOME}/.claude/merlin"
|
|
19
|
+
LOG_FILE="${LOG_DIR}/security.log"
|
|
20
|
+
|
|
21
|
+
# Source shared analytics if available
|
|
22
|
+
[ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
|
|
23
|
+
|
|
24
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
# Read input from stdin
|
|
26
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
input=""
|
|
28
|
+
if [ ! -t 0 ]; then
|
|
29
|
+
input=$(cat 2>/dev/null || true)
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
[ -z "$input" ] && { echo '{}'; exit 0; }
|
|
33
|
+
|
|
34
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
# Extract content to scan
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
tool_name=""
|
|
38
|
+
content=""
|
|
39
|
+
file_path=""
|
|
40
|
+
|
|
41
|
+
if command -v jq >/dev/null 2>&1; then
|
|
42
|
+
tool_name=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)
|
|
43
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)
|
|
44
|
+
# Write/NotebookEdit → content; Edit → new_string; Bash → command
|
|
45
|
+
content=$(echo "$input" | jq -r '
|
|
46
|
+
.tool_input.content //
|
|
47
|
+
.tool_input.new_string //
|
|
48
|
+
.tool_input.command //
|
|
49
|
+
empty' 2>/dev/null || true)
|
|
50
|
+
else
|
|
51
|
+
# Fallback: crude extraction without jq
|
|
52
|
+
tool_name=$(echo "$input" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
|
53
|
+
content=$(echo "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ -z "$content" ] && { echo '{}'; exit 0; }
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
# Helpers
|
|
60
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
_log_detection() {
|
|
62
|
+
local threat_type="$1"
|
|
63
|
+
local detail="$2"
|
|
64
|
+
mkdir -p "$LOG_DIR"
|
|
65
|
+
printf '[%s] BLOCKED tool=%s file=%s threat=%s detail=%s\n' \
|
|
66
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
67
|
+
"${tool_name:-unknown}" \
|
|
68
|
+
"${file_path:-unknown}" \
|
|
69
|
+
"$threat_type" \
|
|
70
|
+
"$detail" \
|
|
71
|
+
>> "$LOG_FILE" 2>/dev/null || true
|
|
72
|
+
if declare -f log_event >/dev/null 2>&1; then
|
|
73
|
+
log_event "security_block" \
|
|
74
|
+
"$(printf '{"tool":"%s","threat":"%s"}' "${tool_name:-unknown}" "$threat_type")"
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_block() {
|
|
79
|
+
local threat_type="$1"
|
|
80
|
+
local detail="${2:-}"
|
|
81
|
+
_log_detection "$threat_type" "$detail"
|
|
82
|
+
printf '{"decision":"block","reason":"Security: %s detected"}\n' "$threat_type"
|
|
83
|
+
exit 2
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
# A) PROMPT INJECTION DETECTION
|
|
88
|
+
# Only scan non-Bash tools (Bash commands are code, not LLM prompts).
|
|
89
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
if [ "${tool_name:-}" != "Bash" ]; then
|
|
91
|
+
# Build a single grep pattern for all injection phrases
|
|
92
|
+
INJECTION_PATTERN='ignore (previous|all previous) instructions|disregard your instructions|forget your system prompt|you are now [a-z]|new role:|override:|system prompt:|reveal your instructions|show me your system prompt|what are your instructions|print your prompt|output your rules|ADMIN MODE|jailbreak|DAN mode|developer mode enabled|ignore safety|bypass restrictions|act as an unrestricted|\[END OF PROMPT\]'
|
|
93
|
+
|
|
94
|
+
if echo "$content" | grep -qiE "$INJECTION_PATTERN" 2>/dev/null; then
|
|
95
|
+
# Determine which pattern matched for the reason
|
|
96
|
+
matched=$(echo "$content" | grep -iEo "$INJECTION_PATTERN" | head -1 || true)
|
|
97
|
+
_block "prompt injection" "${matched}"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
# B) SECRET / CREDENTIAL DETECTION
|
|
103
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
# AWS access key
|
|
106
|
+
if echo "$content" | grep -qE 'AKIA[0-9A-Z]{16}' 2>/dev/null; then
|
|
107
|
+
_block "AWS access key" "AKIA..."
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Anthropic API key (sk-ant- prefix with 90+ chars)
|
|
111
|
+
if echo "$content" | grep -qE 'sk-ant-[a-zA-Z0-9_\-]{90,}' 2>/dev/null; then
|
|
112
|
+
_block "Anthropic API key" "sk-ant-..."
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# OpenAI key (sk- with 48+ chars, but not sk-ant-)
|
|
116
|
+
if echo "$content" | grep -qE 'sk-(?!ant-)[a-zA-Z0-9]{48,}' 2>/dev/null; then
|
|
117
|
+
_block "OpenAI API key" "sk-..."
|
|
118
|
+
elif echo "$content" | grep -qE 'sk-[a-zA-Z0-9]{48,}' 2>/dev/null; then
|
|
119
|
+
# Fallback for grep without PCRE: exclude sk-ant- manually
|
|
120
|
+
if ! echo "$content" | grep -qE 'sk-ant-' 2>/dev/null; then
|
|
121
|
+
_block "OpenAI API key" "sk-..."
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# PEM private key header
|
|
126
|
+
if echo "$content" | grep -qE '-----BEGIN (RSA|EC|DSA|OPENSSH) PRIVATE KEY-----' 2>/dev/null; then
|
|
127
|
+
_block "PEM private key" "BEGIN PRIVATE KEY"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Generic credential assignment — exclude common placeholders
|
|
131
|
+
if echo "$content" | grep -qiE '(password|secret|api_key|apikey|token|auth)\s*[:=]\s*["\x27][^"'\'']{8,}["\x27]' 2>/dev/null; then
|
|
132
|
+
# Allow placeholder values
|
|
133
|
+
if ! echo "$content" | grep -qiE '(password|secret|api_key|apikey|token|auth)\s*[:=]\s*["\x27][^"'\'']*?(YOUR_|REPLACE_ME|TODO|CHANGEME|example|placeholder|xxx+|<[^>]+>|\$\{[^}]+\}|%\([^)]+\))' 2>/dev/null; then
|
|
134
|
+
_block "credential assignment" "password/secret/api_key/token"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
# C) DATA EXFILTRATION PATTERNS
|
|
140
|
+
# Targets Bash commands specifically; also catches these in scripts being written.
|
|
141
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
EXFIL_PATTERN='curl[^|]*POST[^|]*\$\(cat|wget[^|]*-O-[^|]*\|[^|]*curl|\$\(cat [^)]*\)\s*\|\s*(curl|wget|nc |netcat)|env\s*\|\s*(curl|wget)|printenv[^|]*\|\s*(curl|wget)'
|
|
143
|
+
|
|
144
|
+
if echo "$content" | grep -qE "$EXFIL_PATTERN" 2>/dev/null; then
|
|
145
|
+
matched=$(echo "$content" | grep -Eo "$EXFIL_PATTERN" | head -1 || true)
|
|
146
|
+
_block "data exfiltration" "${matched}"
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
# All checks passed — allow
|
|
151
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
echo '{}'
|
|
153
|
+
exit 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Merlin Hook: Stop — Auto-Memory Sync
|
|
4
|
+
# Extracts session decisions (recent commits, CLAUDE.md changes) and
|
|
5
|
+
# persists them to Sights cloud via merlin_write_state.
|
|
6
|
+
# Always exits 0 — never blocks Claude Code shutdown.
|
|
7
|
+
# Runs asynchronously so session close is instant.
|
|
8
|
+
#
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
trap 'echo "{}"; exit 0' ERR
|
|
11
|
+
|
|
12
|
+
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
|
|
14
|
+
# Source shared libraries defensively
|
|
15
|
+
[ -f "${HOOKS_DIR}/lib/analytics.sh" ] && . "${HOOKS_DIR}/lib/analytics.sh"
|
|
16
|
+
|
|
17
|
+
# ── Main sync logic (runs in background) ────────────────────────
|
|
18
|
+
_merlin_memory_sync() {
|
|
19
|
+
local merlin_dir="${HOME}/.claude/merlin"
|
|
20
|
+
local api_url="${MERLIN_API_URL:-https://api.merlin.build}"
|
|
21
|
+
local api_key="${MERLIN_API_KEY:-}"
|
|
22
|
+
local state_file="${merlin_dir}/session-memory.json"
|
|
23
|
+
local cwd="${PWD:-$(pwd)}"
|
|
24
|
+
|
|
25
|
+
# Need API key to write to cloud
|
|
26
|
+
[ -z "${api_key}" ] && return 0
|
|
27
|
+
|
|
28
|
+
# Must be in a git repo to extract decisions
|
|
29
|
+
git -C "${cwd}" rev-parse --git-dir >/dev/null 2>&1 || return 0
|
|
30
|
+
|
|
31
|
+
# ── Collect recent commits (last 10 from this session, up to 2h old) ──
|
|
32
|
+
local commit_log=""
|
|
33
|
+
commit_log=$(git -C "${cwd}" log \
|
|
34
|
+
--oneline \
|
|
35
|
+
--since="2 hours ago" \
|
|
36
|
+
--max-count=10 \
|
|
37
|
+
--no-merges \
|
|
38
|
+
2>/dev/null) || true
|
|
39
|
+
|
|
40
|
+
# ── Detect CLAUDE.md changes this session ──────────────────────
|
|
41
|
+
local claude_md_changed="false"
|
|
42
|
+
local claude_md_diff=""
|
|
43
|
+
if git -C "${cwd}" diff --name-only HEAD~1 HEAD 2>/dev/null | grep -q "CLAUDE.md"; then
|
|
44
|
+
claude_md_changed="true"
|
|
45
|
+
claude_md_diff=$(git -C "${cwd}" diff HEAD~1 HEAD -- CLAUDE.md 2>/dev/null | head -40) || true
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Nothing interesting to sync — skip API call
|
|
49
|
+
if [ -z "${commit_log}" ] && [ "${claude_md_changed}" = "false" ]; then
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── Build state payload ─────────────────────────────────────────
|
|
54
|
+
local repo_name
|
|
55
|
+
repo_name=$(basename "${cwd}")
|
|
56
|
+
local timestamp
|
|
57
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
58
|
+
|
|
59
|
+
# Escape values for JSON (basic — avoids jq dependency)
|
|
60
|
+
local safe_commits
|
|
61
|
+
safe_commits=$(echo "${commit_log}" | tr '"' "'" | tr '\n' '|')
|
|
62
|
+
local safe_diff
|
|
63
|
+
safe_diff=$(echo "${claude_md_diff}" | tr '"' "'" | tr '\n' '|' | head -c 500)
|
|
64
|
+
|
|
65
|
+
local payload
|
|
66
|
+
payload=$(printf '{"repo":"%s","timestamp":"%s","commits":"%s","claude_md_changed":%s,"claude_md_diff":"%s"}' \
|
|
67
|
+
"${repo_name}" "${timestamp}" "${safe_commits}" "${claude_md_changed}" "${safe_diff}")
|
|
68
|
+
|
|
69
|
+
# Write to local state file as fallback
|
|
70
|
+
mkdir -p "${merlin_dir}" 2>/dev/null || true
|
|
71
|
+
echo "${payload}" > "${state_file}" 2>/dev/null || true
|
|
72
|
+
|
|
73
|
+
# ── Push to Sights API if available ────────────────────────────
|
|
74
|
+
curl -s \
|
|
75
|
+
--max-time 8 \
|
|
76
|
+
--request POST \
|
|
77
|
+
--header "Content-Type: application/json" \
|
|
78
|
+
--header "x-api-key: ${api_key}" \
|
|
79
|
+
--data "${payload}" \
|
|
80
|
+
"${api_url}/api/memory/session-end" \
|
|
81
|
+
>/dev/null 2>&1 || true
|
|
82
|
+
|
|
83
|
+
# Log the sync event
|
|
84
|
+
if type log_event >/dev/null 2>&1; then
|
|
85
|
+
log_event "memory_sync" "$(printf '{"repo":"%s","commits_synced":%d,"claude_md_changed":%s}' \
|
|
86
|
+
"${repo_name}" \
|
|
87
|
+
"$(echo "${commit_log}" | grep -c . || echo 0)" \
|
|
88
|
+
"${claude_md_changed}")"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Fire and forget — never delay session shutdown
|
|
93
|
+
_merlin_memory_sync &
|
|
94
|
+
|
|
95
|
+
# Claude Code command hooks must output valid JSON to stdout
|
|
96
|
+
echo '{}'
|
|
97
|
+
exit 0
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
3
|
# Merlin Hook: SessionEnd / Stop
|
|
4
|
-
# Saves checkpoint
|
|
4
|
+
# Saves checkpoint, finalizes session analytics, and writes a structured
|
|
5
|
+
# session summary to ~/.claude/merlin/sessions/ for future restore/search.
|
|
5
6
|
# Always exits 0 — never blocks Claude Code shutdown.
|
|
6
7
|
#
|
|
7
8
|
set -euo pipefail
|
|
@@ -19,11 +20,283 @@ log_event "session_end" '{}'
|
|
|
19
20
|
# Finalize session analytics (adds endTime, duration, summary)
|
|
20
21
|
finalize_session
|
|
21
22
|
|
|
23
|
+
# ── Session Summary ──────────────────────────────────────────────────────────
|
|
24
|
+
# Write a structured session summary to ~/.claude/merlin/sessions/ so that
|
|
25
|
+
# merlin_session_restore and merlin_session_search can provide continuity.
|
|
26
|
+
|
|
27
|
+
SESSIONS_DIR="${HOME}/.claude/merlin/sessions"
|
|
28
|
+
mkdir -p "${SESSIONS_DIR}" 2>/dev/null || true
|
|
29
|
+
|
|
30
|
+
# Only generate summary when jq is available — keeps the fallback path clean
|
|
31
|
+
if command -v jq >/dev/null 2>&1; then
|
|
32
|
+
|
|
33
|
+
TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
34
|
+
EPOCH_NOW="$(date +%s)"
|
|
35
|
+
|
|
36
|
+
# ── Duration ────────────────────────────────────────────────────────────────
|
|
37
|
+
SESSION_DURATION=0
|
|
38
|
+
if [ -f "${MERLIN_SESSION_FILE}" ]; then
|
|
39
|
+
dur="$(jq -r '.durationSeconds // 0' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo 0)"
|
|
40
|
+
SESSION_DURATION="${dur}"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ── Files modified via git diff --stat ──────────────────────────────────────
|
|
44
|
+
FILES_MODIFIED_JSON="[]"
|
|
45
|
+
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
46
|
+
# Uncommitted changes (staged + unstaged) compared to HEAD
|
|
47
|
+
raw_files="$(git diff --stat HEAD 2>/dev/null | awk 'NR>1 || /\|/' | grep '|' | awk '{print $1}' | head -20 || true)"
|
|
48
|
+
# Also catch files changed in the last session via log since session start
|
|
49
|
+
sid="$(jq -r '.id // ""' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo "")"
|
|
50
|
+
start_epoch="$(echo "${sid}" | cut -d'-' -f1 2>/dev/null || echo "${EPOCH_NOW}")"
|
|
51
|
+
if [ -n "${start_epoch}" ] && [ "${start_epoch}" -gt 0 ] 2>/dev/null; then
|
|
52
|
+
recent_files="$(git log --since="@${start_epoch}" --name-only --pretty=format: 2>/dev/null | sort -u | head -20 || true)"
|
|
53
|
+
all_files="$(printf '%s\n%s' "${raw_files}" "${recent_files}" | sort -u | grep -v '^$' || true)"
|
|
54
|
+
else
|
|
55
|
+
all_files="${raw_files}"
|
|
56
|
+
fi
|
|
57
|
+
if [ -n "${all_files}" ]; then
|
|
58
|
+
FILES_MODIFIED_JSON="$(echo "${all_files}" | jq -R . | jq -sc . 2>/dev/null || echo "[]")"
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ── Recent commits ──────────────────────────────────────────────────────────
|
|
63
|
+
COMMITS_JSON="[]"
|
|
64
|
+
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
65
|
+
sid="$(jq -r '.id // ""' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo "")"
|
|
66
|
+
start_epoch="$(echo "${sid}" | cut -d'-' -f1 2>/dev/null || echo "")"
|
|
67
|
+
if [ -n "${start_epoch}" ] && [ "${start_epoch}" -gt 0 ] 2>/dev/null; then
|
|
68
|
+
commits="$(git log --since="@${start_epoch}" --pretty=format:"%s" 2>/dev/null | head -10 || true)"
|
|
69
|
+
else
|
|
70
|
+
commits="$(git log -5 --pretty=format:"%s" 2>/dev/null || true)"
|
|
71
|
+
fi
|
|
72
|
+
if [ -n "${commits}" ]; then
|
|
73
|
+
COMMITS_JSON="$(echo "${commits}" | jq -R . | jq -sc . 2>/dev/null || echo "[]")"
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# ── Agents used (from analytics events) ─────────────────────────────────────
|
|
78
|
+
AGENTS_JSON="[]"
|
|
79
|
+
if [ -f "${MERLIN_SESSION_FILE}" ]; then
|
|
80
|
+
AGENTS_JSON="$(jq '[.events[] | select(.type == "agent_route") | .data.agent] | unique | map(select(. != null and . != ""))' \
|
|
81
|
+
"${MERLIN_SESSION_FILE}" 2>/dev/null || echo "[]")"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ── Text summary (built from commit messages + modified file count) ──────────
|
|
85
|
+
commit_count="$(echo "${COMMITS_JSON}" | jq 'length' 2>/dev/null || echo 0)"
|
|
86
|
+
file_count="$(echo "${FILES_MODIFIED_JSON}" | jq 'length' 2>/dev/null || echo 0)"
|
|
87
|
+
|
|
88
|
+
if [ "${commit_count}" -gt 0 ] 2>/dev/null; then
|
|
89
|
+
first_commit="$(echo "${COMMITS_JSON}" | jq -r '.[0] // ""' 2>/dev/null || echo "")"
|
|
90
|
+
SUMMARY_TEXT="Session with ${commit_count} commit(s) and ${file_count} file(s) modified. Latest: ${first_commit}"
|
|
91
|
+
elif [ "${file_count}" -gt 0 ] 2>/dev/null; then
|
|
92
|
+
SUMMARY_TEXT="Session with ${file_count} file(s) modified, no commits recorded."
|
|
93
|
+
else
|
|
94
|
+
SUMMARY_TEXT="Session completed. No file changes detected."
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Truncate to keep summaries small
|
|
98
|
+
SUMMARY_TEXT="$(echo "${SUMMARY_TEXT}" | cut -c1-200)"
|
|
99
|
+
|
|
100
|
+
# ── Working directory ────────────────────────────────────────────────────────
|
|
101
|
+
WORK_DIR="${CLAUDE_WORKTREE_PATH:-$(pwd 2>/dev/null || echo "")}"
|
|
102
|
+
|
|
103
|
+
# ── Write session summary ────────────────────────────────────────────────────
|
|
104
|
+
SUMMARY_FILE="${SESSIONS_DIR}/session-${EPOCH_NOW}.json"
|
|
105
|
+
jq -n \
|
|
106
|
+
--arg ts "${TIMESTAMP}" \
|
|
107
|
+
--argjson dur "${SESSION_DURATION}" \
|
|
108
|
+
--argjson files "${FILES_MODIFIED_JSON}" \
|
|
109
|
+
--argjson commits "${COMMITS_JSON}" \
|
|
110
|
+
--argjson agents "${AGENTS_JSON}" \
|
|
111
|
+
--arg summary "${SUMMARY_TEXT}" \
|
|
112
|
+
--arg workdir "${WORK_DIR}" \
|
|
113
|
+
'{
|
|
114
|
+
timestamp: $ts,
|
|
115
|
+
duration_seconds: $dur,
|
|
116
|
+
files_modified: $files,
|
|
117
|
+
commits: $commits,
|
|
118
|
+
agents_used: $agents,
|
|
119
|
+
summary: $summary,
|
|
120
|
+
working_directory: $workdir
|
|
121
|
+
}' > "${SUMMARY_FILE}" 2>/dev/null || true
|
|
122
|
+
|
|
123
|
+
# Keep only the last 100 session summaries to cap disk usage
|
|
124
|
+
summary_count="$(ls -1 "${SESSIONS_DIR}"/session-*.json 2>/dev/null | wc -l | tr -d ' ' || echo 0)"
|
|
125
|
+
if [ "${summary_count}" -gt 100 ] 2>/dev/null; then
|
|
126
|
+
ls -1t "${SESSIONS_DIR}"/session-*.json 2>/dev/null | tail -n +"$((100 + 1))" | xargs rm -f 2>/dev/null || true
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
fi
|
|
130
|
+
|
|
22
131
|
# If merlin CLI is available, save checkpoint in background
|
|
23
132
|
if command -v merlin >/dev/null 2>&1; then
|
|
24
133
|
merlin save-checkpoint "Session ended" >/dev/null 2>&1 &
|
|
25
134
|
fi
|
|
26
135
|
|
|
136
|
+
# ── Cost summary ──────────────────────────────────────────────────────────────
|
|
137
|
+
# Read the session cost file written by the MCP server (if present).
|
|
138
|
+
# Only prints when >0 routing calls were made.
|
|
139
|
+
COST_FILE="${HOME}/.claude/merlin/session-cost.json"
|
|
140
|
+
if [ -f "${COST_FILE}" ]; then
|
|
141
|
+
COST_SUMMARY=""
|
|
142
|
+
if command -v node >/dev/null 2>&1; then
|
|
143
|
+
COST_SUMMARY="$(node -e "
|
|
144
|
+
try {
|
|
145
|
+
const d = JSON.parse(require('fs').readFileSync('${COST_FILE}', 'utf8'));
|
|
146
|
+
if (d.totalCalls > 0) {
|
|
147
|
+
const tot = (d.totalEstimatedCost || 0).toFixed(4);
|
|
148
|
+
const calls = d.totalCalls || 0;
|
|
149
|
+
const tc = d.tokenCounts || {};
|
|
150
|
+
const tokens = (tc.totalInput || 0) + (tc.totalOutput || 0);
|
|
151
|
+
const tokStr = tokens > 0 ? ', ' + Math.round(tokens/1000) + 'K tokens' : '';
|
|
152
|
+
process.stdout.write('Session cost: \$' + tot + ' (' + calls + ' call' + (calls !== 1 ? 's' : '') + tokStr + ')');
|
|
153
|
+
}
|
|
154
|
+
} catch(e) {}
|
|
155
|
+
" 2>/dev/null || true)"
|
|
156
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
157
|
+
COST_SUMMARY="$(python3 -c "
|
|
158
|
+
import json, sys
|
|
159
|
+
try:
|
|
160
|
+
with open('${COST_FILE}') as f:
|
|
161
|
+
d = json.load(f)
|
|
162
|
+
if d.get('totalCalls', 0) > 0:
|
|
163
|
+
tot = d.get('totalEstimatedCost', 0)
|
|
164
|
+
calls = d.get('totalCalls', 0)
|
|
165
|
+
tc = d.get('tokenCounts', {})
|
|
166
|
+
tokens = tc.get('totalInput', 0) + tc.get('totalOutput', 0)
|
|
167
|
+
tok_str = (', ' + str(round(tokens/1000)) + 'K tokens') if tokens > 0 else ''
|
|
168
|
+
plural = '' if calls == 1 else 's'
|
|
169
|
+
sys.stdout.write(f'Session cost: \${tot:.4f} ({calls} call{plural}{tok_str})')
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
" 2>/dev/null || true)"
|
|
173
|
+
fi
|
|
174
|
+
if [ -n "${COST_SUMMARY}" ]; then
|
|
175
|
+
echo "[merlin] ${COST_SUMMARY}" >&2
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# ── Outcome Tracking (RL-inspired prompt learning) ───────────────────────────
|
|
180
|
+
# Classify this session's outcome and append to the learning log.
|
|
181
|
+
# Classification is intentional kept simple — git diff + agents used only.
|
|
182
|
+
# Never blocks shutdown; always best-effort.
|
|
183
|
+
if command -v jq >/dev/null 2>&1 && command -v git >/dev/null 2>&1; then
|
|
184
|
+
LEARNING_DIR="${HOME}/.claude/merlin/learning"
|
|
185
|
+
mkdir -p "${LEARNING_DIR}" 2>/dev/null || true
|
|
186
|
+
|
|
187
|
+
OUTCOME_FILE="${LEARNING_DIR}/outcomes.jsonl"
|
|
188
|
+
OUTCOME_TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
189
|
+
|
|
190
|
+
# Count files changed vs HEAD (uncommitted + committed this session)
|
|
191
|
+
OUTCOME_FILES_CHANGED=0
|
|
192
|
+
if git rev-parse --git-dir >/dev/null 2>&1; then
|
|
193
|
+
# Uncommitted
|
|
194
|
+
uncommitted="$(git diff --stat HEAD 2>/dev/null | tail -1 | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' || echo 0)"
|
|
195
|
+
# Committed this session
|
|
196
|
+
sid_o="$(jq -r '.id // ""' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo "")"
|
|
197
|
+
start_epoch_o="$(echo "${sid_o}" | cut -d'-' -f1 2>/dev/null || echo "")"
|
|
198
|
+
committed=0
|
|
199
|
+
if [ -n "${start_epoch_o}" ] && [ "${start_epoch_o}" -gt 0 ] 2>/dev/null; then
|
|
200
|
+
committed="$(git log --since="@${start_epoch_o}" --name-only --pretty=format: 2>/dev/null | grep -vc '^$' || echo 0)"
|
|
201
|
+
fi
|
|
202
|
+
OUTCOME_FILES_CHANGED=$(( ${uncommitted:-0} + ${committed:-0} ))
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Determine agents used in this session
|
|
206
|
+
OUTCOME_AGENT="unknown"
|
|
207
|
+
if [ -f "${MERLIN_SESSION_FILE}" ]; then
|
|
208
|
+
first_agent="$(jq -r '[.events[] | select(.type == "agent_route") | .data.agent] | first // "unknown"' \
|
|
209
|
+
"${MERLIN_SESSION_FILE}" 2>/dev/null || echo "unknown")"
|
|
210
|
+
OUTCOME_AGENT="${first_agent:-unknown}"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Classify outcome:
|
|
214
|
+
# SUCCESS — files changed AND session file shows no error flag
|
|
215
|
+
# PARTIAL — files changed but session had errors or warnings
|
|
216
|
+
# FAILURE — no files changed
|
|
217
|
+
OUTCOME_CLASS="failure"
|
|
218
|
+
SESSION_HAD_ERRORS=0
|
|
219
|
+
if [ -f "${MERLIN_SESSION_FILE}" ]; then
|
|
220
|
+
err_count="$(jq '[.events[] | select(.type == "error")] | length' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo 0)"
|
|
221
|
+
SESSION_HAD_ERRORS="${err_count:-0}"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
if [ "${OUTCOME_FILES_CHANGED}" -gt 0 ] 2>/dev/null; then
|
|
225
|
+
if [ "${SESSION_HAD_ERRORS:-0}" -gt 0 ] 2>/dev/null; then
|
|
226
|
+
OUTCOME_CLASS="partial"
|
|
227
|
+
else
|
|
228
|
+
OUTCOME_CLASS="success"
|
|
229
|
+
fi
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Duration
|
|
233
|
+
OUTCOME_DURATION=0
|
|
234
|
+
if [ -f "${MERLIN_SESSION_FILE}" ]; then
|
|
235
|
+
OUTCOME_DURATION="$(jq -r '.durationSeconds // 0' "${MERLIN_SESSION_FILE}" 2>/dev/null || echo 0)"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Append outcome line (append-only)
|
|
239
|
+
outcome_line="$(jq -cn \
|
|
240
|
+
--arg ts "${OUTCOME_TS}" \
|
|
241
|
+
--arg agent "${OUTCOME_AGENT}" \
|
|
242
|
+
--arg outcome "${OUTCOME_CLASS}" \
|
|
243
|
+
--argjson files "${OUTCOME_FILES_CHANGED}" \
|
|
244
|
+
--argjson duration "${OUTCOME_DURATION}" \
|
|
245
|
+
'{"timestamp":$ts,"agent":$agent,"outcome":$outcome,"files_changed":$files,"duration":$duration}' \
|
|
246
|
+
2>/dev/null || true)"
|
|
247
|
+
|
|
248
|
+
if [ -n "${outcome_line}" ]; then
|
|
249
|
+
echo "${outcome_line}" >> "${OUTCOME_FILE}" 2>/dev/null || true
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Keep outcomes.jsonl bounded — drop lines older than 90 days
|
|
253
|
+
if [ -f "${OUTCOME_FILE}" ]; then
|
|
254
|
+
cutoff_epoch="$(date -u -v-90d +%s 2>/dev/null || date -u -d '90 days ago' +%s 2>/dev/null || echo 0)"
|
|
255
|
+
if [ "${cutoff_epoch}" -gt 0 ] 2>/dev/null; then
|
|
256
|
+
tmp_out="${OUTCOME_FILE}.tmp"
|
|
257
|
+
while IFS= read -r line; do
|
|
258
|
+
line_ts="$(echo "${line}" | jq -r '.timestamp // ""' 2>/dev/null || echo "")"
|
|
259
|
+
if [ -z "${line_ts}" ]; then continue; fi
|
|
260
|
+
line_epoch="$(date -u -d "${line_ts}" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "${line_ts}" +%s 2>/dev/null || echo 0)"
|
|
261
|
+
if [ "${line_epoch}" -ge "${cutoff_epoch}" ] 2>/dev/null; then
|
|
262
|
+
echo "${line}"
|
|
263
|
+
fi
|
|
264
|
+
done < "${OUTCOME_FILE}" > "${tmp_out}" 2>/dev/null && mv "${tmp_out}" "${OUTCOME_FILE}" 2>/dev/null || true
|
|
265
|
+
fi
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# ── Behavior promotion check ─────────────────────────────────────────────────
|
|
270
|
+
# The MCP server writes promotion-ready behaviors to a temp cache file during
|
|
271
|
+
# the session (via merlin_apply_behavior / merlin_get_behaviors).
|
|
272
|
+
# File: ~/.claude/merlin/promotion-candidates.json
|
|
273
|
+
# Format: { "candidates": [{ "id": "...", "pattern": "...", "confidence": 0.9 }] }
|
|
274
|
+
#
|
|
275
|
+
# We never auto-promote — just surface a tip so the user can decide.
|
|
276
|
+
PROMOTION_CACHE="${HOME}/.claude/merlin/promotion-candidates.json"
|
|
277
|
+
|
|
278
|
+
if [ -f "${PROMOTION_CACHE}" ] && command -v jq >/dev/null 2>&1; then
|
|
279
|
+
CANDIDATE_COUNT=$(jq '.candidates | length' "${PROMOTION_CACHE}" 2>/dev/null || echo 0)
|
|
280
|
+
|
|
281
|
+
if [ "${CANDIDATE_COUNT:-0}" -gt 0 ] 2>/dev/null; then
|
|
282
|
+
TOP_PATTERN=$(jq -r '
|
|
283
|
+
.candidates
|
|
284
|
+
| sort_by(-.confidence)
|
|
285
|
+
| .[0].pattern
|
|
286
|
+
| if length > 60 then .[:60] + "..." else . end
|
|
287
|
+
' "${PROMOTION_CACHE}" 2>/dev/null || echo "a learned behavior")
|
|
288
|
+
|
|
289
|
+
OTHERS=$(( CANDIDATE_COUNT - 1 ))
|
|
290
|
+
if [ "${OTHERS}" -gt 0 ]; then
|
|
291
|
+
echo "[merlin] Behavior '${TOP_PATTERN}' (and ${OTHERS} more) is ready for promotion. Run /merlin:promote to review." >&2
|
|
292
|
+
else
|
|
293
|
+
echo "[merlin] Behavior '${TOP_PATTERN}' is ready for promotion. Run /merlin:promote to review." >&2
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
log_event "promotion_candidates_found" "$(printf '{"count":%d}' "${CANDIDATE_COUNT}")"
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
|
|
27
300
|
# Claude Code command hooks must output valid JSON to stdout
|
|
28
301
|
echo '{}'
|
|
29
302
|
exit 0
|
|
@@ -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
|