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