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:
parent
d2889483bc
commit
874c6c4c57
@ -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`.
|
||||
|
||||
@ -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), закрыты.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user