From b6c71b0d7ae058dc340bc1821917df3557aae19a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 12 Oct 2026 00:26:22 +0400 Subject: [PATCH] =?UTF-8?q?feat(hooks):=20role-hint.sh=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0=D0=B7=D0=BA=D0=B0=20=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=B8=20=D0=BF=D0=BE=20session=5Ftitle;=20tests/ru?= =?UTF-8?q?n.sh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #2 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MzkPQWjfA7RpqYWkjnqJwt --- hooks/role-hint.sh | 17 +++++++++++++++++ hooks/settings.hooks.json | 8 ++++++++ tests/run.sh | 23 +++++++++++++++++++++++ tests/test-role-hint.sh | 22 ++++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100755 hooks/role-hint.sh create mode 100755 tests/run.sh create mode 100644 tests/test-role-hint.sh diff --git a/hooks/role-hint.sh b/hooks/role-hint.sh new file mode 100755 index 0000000..9df89e9 --- /dev/null +++ b/hooks/role-hint.sh @@ -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 diff --git a/hooks/settings.hooks.json b/hooks/settings.hooks.json index f45b3d4..acd64c5 100644 --- a/hooks/settings.hooks.json +++ b/hooks/settings.hooks.json @@ -29,6 +29,14 @@ "command": "\"$HOME/.claude/hooks/harness-check.sh\"" } ] + }, + { + "hooks": [ + { + "type": "command", + "command": "\"$HOME/.claude/hooks/role-hint.sh\"" + } + ] } ] } diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..b1ff550 --- /dev/null +++ b/tests/run.sh @@ -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 ] diff --git a/tests/test-role-hint.sh b/tests/test-role-hint.sh new file mode 100644 index 0000000..0ddef7b --- /dev/null +++ b/tests/test-role-hint.sh @@ -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"