test: изоляция HOME и git-идентичности, мелкие правки ревью

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzkPQWjfA7RpqYWkjnqJwt
This commit is contained in:
Andrey Epifancev 2026-10-12 00:48:58 +04:00
parent d2889483bc
commit 874c6c4c57
6 changed files with 21 additions and 10 deletions

View File

@ -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`.

View File

@ -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), закрыты.

View File

@ -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)

View File

@ -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 ]

View File

@ -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"

View File

@ -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")