From 874c6c4c57cdb632b4294a424b66b5e068a24679 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 12 Oct 2026 00:48:58 +0400 Subject: [PATCH] =?UTF-8?q?test:=20=D0=B8=D0=B7=D0=BE=D0=BB=D1=8F=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20HOME=20=D0=B8=20git-=D0=B8=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D1=87=D0=BD=D0=BE=D1=81=D1=82=D0=B8,=20=D0=BC?= =?UTF-8?q?=D0=B5=D0=BB=D0=BA=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run.sh задаёт HOME во временный каталог на каждый тест и фиксирует git-идентичность; test-install.sh передаёт HOME и CLAUDE_CONFIG_DIR inline (M-1, T9.2). doctor.sh распознаёт шапку таблицы с лишними пробелами и пропускает пустые и «-» каталоги, тест (M-5, T8.1). Исправлены ячейка таблицы в HARNESS.md (T10.1), «Что это» и «Тесты» в CLAUDE.md репозитория (T10.3, T10.4). Refs #2 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MzkPQWjfA7RpqYWkjnqJwt --- CLAUDE.md | 4 ++-- docs/HARNESS.md | 2 +- skills/harness-doctor/scripts/doctor.sh | 4 ++-- tests/run.sh | 6 +++++- tests/test-doctor.sh | 7 +++++++ tests/test-install.sh | 8 ++++---- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 90ebc26..b93eee1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # claude-harness ## Что это -Пакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся. +Пакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука и неблокирующий `role-hint`, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся. ## Стек и ограничения - Bash (`set -euo pipefail`) и markdown. JSON из stdin хуков разбирается встроенным `python3`; других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки. @@ -33,7 +33,7 @@ ## Тесты - `tests/run.sh` — автоматические тесты источников (хуки через фиктивный JSON, скрипты во временных каталогах, размеры). Самотест хуков в `doctor.sh` проверяет установленные копии; перед ним `bash install.sh`. - Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`. -- Регрессионные тесты хуков на реальных командах из транскриптов — бэклог ретро B2, пока не сделаны. +- `tests/` покрывает `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`; регрессионные тесты `guard-main` и `require-plan` на реальных командах из транскриптов (бэклог ретро B2) пока не сделаны. ## Текущая работа Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`. diff --git a/docs/HARNESS.md b/docs/HARNESS.md index f64718f..5ea7820 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -72,7 +72,7 @@ Fix loop: раунды 1–3 продолжают того же implementer, 4 | Никаких изменений на main / detached HEAD | `guard-main.sh`, PreToolUse на Edit/Write/MultiEdit/Bash | проверяет ветку; для Bash блокирует команды, похожие на запись (`>`, `tee`, `sed -i`, `mv`, `rm`, `git commit/push`…), пропускает `git worktree add`; коммит/push в main разрешены только если изменены исключительно файлы в `openspec/` (архивация) | | Правки в коде только при плане или `.bounded` | `require-plan.sh`, PreToolUse на Edit/Write/MultiEdit | защищает любой `src/` на любой глубине (`api/src/`, `packages/*/src/`) или каталоги из `.claude/code-paths`; ищет план в `docs/superpowers/plans/` для текущей ветки или файл `.bounded` | | Обвязка подключена к проекту | `harness-check.sh`, SessionStart | при старте сессии проверяет `openspec/`, `.codegraph/`, секцию в CLAUDE.md; если чего-то нет — пишет агенту инструкцию запустить `~/.claude/harness/adopt-project.sh` (спросив пользователя; `HARNESS_AUTO_ADOPT=1` — без вопроса) | -| Подсказка роли по заголовку сессии | `role-hint.sh`, SessionStart (все источники) | читает `session_title`; если начинается с `dev|admin|analyst|qa` — одна строка-подсказка вызвать `/role`; ничего не блокирует | +| Подсказка роли по заголовку сессии | `role-hint.sh`, SessionStart (все источники) | читает `session_title`; если начинается с «dev, admin, analyst или qa» — одна строка-подсказка вызвать `/role`; ничего не блокирует | | Тесты зелёные перед коммитом | git pre-commit в проекте (не входит в пакет) | обычный git-хук с командой тестов | Exit 2 блокирует вызов и показывает агенту stderr, поэтому агент сам делает то, что просит сообщение. Оба обхода, замеченные на пилоте (запись через `echo >` вместо Write; push через detached HEAD), закрыты. diff --git a/skills/harness-doctor/scripts/doctor.sh b/skills/harness-doctor/scripts/doctor.sh index 2c16183..587acc2 100755 --- a/skills/harness-doctor/scripts/doctor.sh +++ b/skills/harness-doctor/scripts/doctor.sh @@ -86,7 +86,7 @@ if root=$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null); then info "remote: $(git -C "$root" remote get-url origin)" (cd "$root" && tea issues ls --limit 5 2>/dev/null | sed 's/^/ /') || warn "tea не отвечает (логин/токен?)" fi -elif [ -f "$DIR/CLAUDE.md" ] && grep -q '^| Каталог | Репозиторий | Роль |' "$DIR/CLAUDE.md"; then +elif [ -f "$DIR/CLAUDE.md" ] && grep -Eq '^\|[[:space:]]*Каталог[[:space:]]*\|[[:space:]]*Репозиторий[[:space:]]*\|[[:space:]]*Роль[[:space:]]*\|' "$DIR/CLAUDE.md"; then ws=$(cd "$DIR" && pwd) info "workspace (не git): $ws" n=$(wc -c < "$ws/CLAUDE.md" | tr -d ' '); [ "$n" -le 5120 ] && ok "CLAUDE.md workspace $n Б (≤ 5 КБ)" || warn "CLAUDE.md workspace $n Б > 5 КБ" @@ -98,7 +98,7 @@ elif [ -f "$DIR/CLAUDE.md" ] && grep -q '^| Каталог | Репозитор else info "$name — обычный файл (не ссылка)"; fi done done - grep -E '^\|' "$ws/CLAUDE.md" | awk -F'|' '{gsub(/ /,"",$4); gsub(/^ +| +$/,"",$2); if ($4=="admin"||$4=="analyst"||$4=="qa") print $2, $4}' | while read -r d r; do + grep -E '^\|' "$ws/CLAUDE.md" | awk -F'|' '{gsub(/ /,"",$4); gsub(/^ +| +$/,"",$2); if (($4=="admin"||$4=="analyst"||$4=="qa") && $2!="" && $2 !~ /^-/) print $2, $4}' | while read -r d r; do [ -f "$ws/${d%/}/.claude/roles/$r.md" ] || warn "нет ${d%/}/.claude/roles/$r.md — запусти link-roles.sh после создания" done # slug каталога памяти Claude Code: все не-алфавитно-цифровые символы пути → «-» (например, /Users/a.b/x → -Users-a-b-x) diff --git a/tests/run.sh b/tests/run.sh index b1ff550..c91cafb 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -4,6 +4,9 @@ set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)"; ROOT="$(dirname "$HERE")" pass=0; fail=0 +REAL_HOME="$HOME" +# git-идентичность для коммитов в тестах не зависит от машины +export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.invalid GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.invalid 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'"; } @@ -15,9 +18,10 @@ for t in "$HERE"/test-*.sh; do [ -n "${1:-}" ] && [[ "$(basename "$t")" != *"$1"* ]] && continue echo "## $(basename "$t")" TMP=$(mktemp -d); export TMP ROOT + export HOME="$TMP/home"; mkdir -p "$HOME" # shellcheck disable=SC1090 . "$t" - rm -rf "$TMP" + export HOME="$REAL_HOME"; rm -rf "$TMP" done echo "passed $pass, failed $fail" [ "$fail" = 0 ] diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index f52b8b7..5c03860 100644 --- a/tests/test-doctor.sh +++ b/tests/test-doctor.sh @@ -26,3 +26,10 @@ assert_grep "проект: индекс памяти > 3 КБ" 'WARN +индек rm -f "$CC/roles/qa.md" out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1) assert_grep "машина: MISS roles/qa.md" 'MISS +roles/qa.md' <(printf '%s' "$out") +# workspace с «выровненной» шапкой таблицы (лишние пробелы) определяется так же +W6="$TMP/ws6"; mkdir -p "$W6/admin/.claude/roles" "$W6/.claude/roles" +printf '# p\n\n| Каталог | Репозиторий | Роль |\n|---|---|---|\n| admin/ | o/a | admin |\n| | x | qa |\n| - | y | qa |\n' > "$W6/CLAUDE.md" +out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W6" 2>&1) +assert_grep "workspace с выровненной шапкой" 'workspace \(не git\)' <(printf '%s' "$out") +assert_grep "выровненная шапка: нет файла роли admin" 'нет admin/\.claude/roles/admin\.md' <(printf '%s' "$out") +assert_eq "пустой и «-» каталоги пропущены" "$(printf '%s' "$out" | grep -c 'нет .*roles/qa\.md')" "0" diff --git a/tests/test-install.sh b/tests/test-install.sh index 993ca4b..aec06bb 100644 --- a/tests/test-install.sh +++ b/tests/test-install.sh @@ -1,7 +1,7 @@ # install.sh: новые файлы ставятся, повторный запуск не дублирует хуки, --check их видит. -export HOME="$TMP/home"; export CLAUDE_CONFIG_DIR="$TMP/home/.claude"; mkdir -p "$CLAUDE_CONFIG_DIR" +TH="$TMP/home"; CLAUDE_CONFIG_DIR="$TH/.claude"; mkdir -p "$CLAUDE_CONFIG_DIR" echo '{"hooks":{"SessionStart":[{"matcher":"startup","hooks":[{"type":"command","command":"\"$HOME/.claude/hooks/harness-check.sh\""}]}]}}' > "$CLAUDE_CONFIG_DIR/settings.json" -bash "$ROOT/install.sh" >/dev/null 2>&1; bash "$ROOT/install.sh" >/dev/null 2>&1 +HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" >/dev/null 2>&1; HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" >/dev/null 2>&1 CC="$CLAUDE_CONFIG_DIR" for r in dev admin analyst qa; do assert_file "roles/$r.md установлен" "$CC/roles/$r.md"; done assert_file "skills/role/SKILL.md установлен" "$CC/skills/role/SKILL.md" @@ -15,10 +15,10 @@ cnt=$(python3 -c "import json,sys; s=json.load(open(sys.argv[1])); print(sum(1 f assert_eq "role-hint.sh в settings.json один раз после двух установок" "$cnt" "1" cnt=$(python3 -c "import json,sys; s=json.load(open(sys.argv[1])); print(sum(1 for e in s['hooks']['SessionStart'] for h in e.get('hooks',[]) if 'harness-check.sh' in h['command']))" "$CC/settings.json") assert_eq "harness-check.sh в settings.json один раз" "$cnt" "1" -out=$(bash "$ROOT/install.sh" --check 2>&1) +out=$(HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" --check 2>&1) assert_grep "--check: roles" 'roles/' <(printf '%s' "$out") assert_grep "--check: skill role" 'skills/role' <(printf '%s' "$out") assert_grep "--check: agent briefing" 'agents/briefing' <(printf '%s' "$out") rm "$CC/agents/briefing.md" -out=$(bash "$ROOT/install.sh" --check 2>&1) +out=$(HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" --check 2>&1) assert_grep "--check: MISS briefing" 'MISS +.*briefing' <(printf '%s' "$out")