feat(hooks): role-hint.sh — подсказка роли по session_title; tests/run.sh

Refs #2

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzkPQWjfA7RpqYWkjnqJwt
This commit is contained in:
Andrey Epifancev 2026-10-12 00:26:22 +04:00
parent 9dc8d9f70e
commit b6c71b0d7a
4 changed files with 70 additions and 0 deletions

17
hooks/role-hint.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# SessionStart hook (все источники): если заголовок сессии начинается с имени роли, напомнить вызвать /role.
# Ничего не блокирует: любой сбой → тихий exit 0. Заголовок приходит в поле session_title (claude -n "<роль> …", /rename).
set -uo pipefail
input=$(cat 2>/dev/null || true)
[ -n "$input" ] || exit 0
title=$(printf '%s' "$input" | python3 -c 'import sys,json
try: print((json.load(sys.stdin) or {}).get("session_title") or "")
except Exception: print("")' 2>/dev/null) || exit 0
[ -n "$title" ] || exit 0
# первое слово: нижний регистр, без ведущих пробелов и префикса алиаса cc-; только латинские буквы.
# LC_ALL=C — чтобы BSD tr/sed работали побайтово и не спотыкались о кириллицу в заголовке.
first=$(printf '%s' "$title" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed -E 's/^[[:space:]]*(cc-)?([a-z]*).*$/\2/')
case "$first" in
dev|admin|analyst|qa) echo "[claude-harness] Роль сессии: $first. Вызови /role $first до первого ответа." ;;
esac
exit 0

View File

@ -29,6 +29,14 @@
"command": "\"$HOME/.claude/hooks/harness-check.sh\""
}
]
},
{
"hooks": [
{
"type": "command",
"command": "\"$HOME/.claude/hooks/role-hint.sh\""
}
]
}
]
}

23
tests/run.sh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Тесты обвязки: bash, без зависимостей, работают с источниками в репозитории.
# Запуск: bash tests/run.sh [подстрока имени файла теста]
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"; ROOT="$(dirname "$HERE")"
pass=0; fail=0
ok(){ pass=$((pass+1)); printf ' ok %s\n' "$1"; }
fail(){ fail=$((fail+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
assert_eq(){ [ "$2" = "$3" ] && ok "$1" || fail "$1" "получено: '$2', ожидалось: '$3'"; }
assert_file(){ [ -e "$2" ] && ok "$1" || fail "$1" "нет файла $2"; }
assert_grep(){ grep -qE -- "$2" "$3" 2>/dev/null && ok "$1" || fail "$1" "в $3 нет /$2/"; }
assert_max_bytes(){ local n; n=$(wc -c < "$2" | tr -d ' '); [ "$n" -le "$3" ] && ok "$1 ($n Б)" || fail "$1" "$n Б > $3"; }
assert_symlink(){ [ -L "$2" ] && [ -e "$2" ] && ok "$1" || fail "$1" "$2 не симлинк или битый"; }
for t in "$HERE"/test-*.sh; do
[ -n "${1:-}" ] && [[ "$(basename "$t")" != *"$1"* ]] && continue
echo "## $(basename "$t")"
TMP=$(mktemp -d); export TMP ROOT
# shellcheck disable=SC1090
. "$t"
rm -rf "$TMP"
done
echo "passed $pass, failed $fail"
[ "$fail" = 0 ]

22
tests/test-role-hint.sh Normal file
View File

@ -0,0 +1,22 @@
# role-hint.sh: подсказка роли по session_title, никогда не блокирует.
H="$ROOT/hooks/role-hint.sh"
assert_file "role-hint.sh существует" "$H"
run(){ printf '%s' "$1" | bash "$H" 2>/dev/null; echo "rc=$?"; }
assert_eq "admin: … → подсказка" "$(run '{"session_title":"admin: спринт 3","source":"startup"}')" \
"[claude-harness] Роль сессии: admin. Вызови /role admin до первого ответа.
rc=0"
assert_eq "qa без двоеточия → подсказка" "$(run '{"session_title":"qa прогон Q1"}')" \
"[claude-harness] Роль сессии: qa. Вызови /role qa до первого ответа.
rc=0"
assert_eq "регистр и пробелы в начале" "$(run '{"session_title":" Admin — Jira"}')" \
"[claude-harness] Роль сессии: admin. Вызови /role admin до первого ответа.
rc=0"
assert_eq "префикс алиаса cc-" "$(run '{"session_title":"cc-analyst ТЗ v4"}')" \
"[claude-harness] Роль сессии: analyst. Вызови /role analyst до первого ответа.
rc=0"
assert_eq "чужое слово → тишина" "$(run '{"session_title":"harness: роли"}')" "rc=0"
assert_eq "слово с продолжением (devops) → тишина" "$(run '{"session_title":"devops"}')" "rc=0"
assert_eq "без заголовка → тишина" "$(run '{"cwd":"/tmp","source":"resume"}')" "rc=0"
assert_eq "пустой stdin → тишина, exit 0" "$(run '')" "rc=0"
assert_eq "не JSON → тишина, exit 0" "$(run 'not json at all')" "rc=0"
assert_grep "settings.hooks.json регистрирует role-hint" 'role-hint\.sh' "$ROOT/hooks/settings.hooks.json"