0.2.0: harness-check SessionStart hook, adopt-project.sh

This commit is contained in:
Andrey Epifancev 2026-09-30 10:41:50 +04:00
parent d604ae3588
commit 096f5586fb
10 changed files with 134 additions and 5 deletions

View File

@ -1,6 +1,8 @@
# Changelog
## 0.2.0 — 2026-09-27
- SessionStart-хук `harness-check.sh`: при старте сессии в неподключённом проекте агент предлагает (или с `HARNESS_AUTO_ADOPT=1` — сразу выполняет) `adopt-project.sh`; скрипты бутстрапа ставятся в `~/.claude/harness/`.
- `adopt-project.sh` — подключение обвязки к существующему репозиторию (ветка → бутстрап → ff-merge); `project-template/CLAUDE.harness-section.md`.
- codegraph: `codegraph init` в worktree и `projectPath` в вызовах `codegraph_explore` — MCP-сервер видит только индекс папки запуска.
- Порядок старта фичи: worktree на временной ветке → issue → переименование ветки (хук guard-main блокирует запись на main).
- Маркировка находок и решений: `T<n>.<k>`, `C/I/M-n`, `R-n`, `O-n`, `H-n`; обязательный статус fixed / deferred / rejected.

View File

@ -31,6 +31,14 @@ docs/HARNESS.md как это работает: слои, цикл ф
docs/PILOT-2026-09-27.md результаты пилота и находки
```
## Существующий проект
```
bash ~/projects/cc-skills/claude-harness/adopt-project.sh ~/projects/foo # [--no-merge] [--push]
```
Запускать из терминала, не из Claude Code: хук guard-main не даёт агенту писать на main, а бутстрап — это запись на main. Скрипт делает ветку `chore/harness-init`, дописывает секцию харнеса в существующий `CLAUDE.md` (или создаёт его из шаблона), добавляет `ARCHITECTURE.md`, строки в `.gitignore`, `openspec init`, `codegraph init`, коммитит и вливает в main fast-forward. Заполнить описание проекта в `CLAUDE.md` потом можно агентом bounded-задачей на ветке.
## Установка
```bash

59
adopt-project.sh Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# claude-harness: подключение обвязки к СУЩЕСТВУЮЩЕМУ репозиторию.
# Запуск из терминала (не из Claude Code): bash adopt-project.sh [папка] [--no-merge] [--push]
# Делает всё на ветке chore/harness-init и по умолчанию вливает её в main fast-forward.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
DIR="."; MERGE=1; PUSH=0
for a in "$@"; do case "$a" in --no-merge) MERGE=0;; --push) PUSH=1;; *) DIR="$a";; esac; done
cd "$DIR"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "ERR: $DIR — не git-репозиторий"; exit 1; }
[ -z "$(git status --porcelain)" ] || { echo "ERR: рабочее дерево не чистое — закоммить или спрячь изменения"; exit 1; }
NAME="$(basename "$(git rev-parse --show-toplevel)")"
BASE="$(git branch --show-current)"
[ -n "$BASE" ] || { echo "ERR: detached HEAD"; exit 1; }
BR=chore/harness-init
git switch -c "$BR" >/dev/null
# CLAUDE.md: свой — дописать секцию харнеса; нет — из шаблона
if [ -f CLAUDE.md ]; then
grep -q '^## OpenSpec и superpowers' CLAUDE.md || { printf '\n' >> CLAUDE.md; cat "$HERE/project-template/CLAUDE.harness-section.md" >> CLAUDE.md; echo "CLAUDE.md: добавлена секция «OpenSpec и superpowers»"; }
else
sed "s/{{PROJECT_NAME}}/$NAME/" "$HERE/project-template/CLAUDE.md" > CLAUDE.md
echo "CLAUDE.md: создан из шаблона — заполни {{...}} (или попроси агента описать проект по коду на отдельной ветке)"
fi
[ -f ARCHITECTURE.md ] || { cp "$HERE/project-template/ARCHITECTURE.md" ARCHITECTURE.md; echo "ARCHITECTURE.md: заготовка"; }
# .gitignore: только строки обвязки
touch .gitignore
for line in '.superpowers/' '.bounded' '.DS_Store'; do
grep -qxF "$line" .gitignore || echo "$line" >> .gitignore
done
mkdir -p docs/superpowers/plans && touch docs/superpowers/plans/.gitkeep
if command -v openspec >/dev/null 2>&1; then
[ -d openspec ] || openspec init --tools claude 2>/dev/null || openspec init
else echo "WARN: openspec не установлен (npm i -g @fission-ai/openspec)"; fi
if command -v codegraph >/dev/null 2>&1; then
[ -d .codegraph ] || codegraph init >/dev/null 2>&1 || echo "WARN: codegraph init не удался"
else echo "WARN: codegraph не установлен"; fi
git add -A
if git diff --cached --quiet; then
echo "Нечего менять: обвязка уже подключена"; git switch "$BASE" >/dev/null; git branch -d "$BR" >/dev/null; exit 0
fi
git commit -qm "chore: claude harness bootstrap"
if [ "$MERGE" = 1 ]; then
git switch "$BASE" >/dev/null && git merge --ff-only "$BR" >/dev/null && git branch -d "$BR" >/dev/null
echo "Влито в $BASE fast-forward."
[ "$PUSH" = 1 ] && git push origin "$BASE"
else
echo "Ветка $BR оставлена, в $BASE не влита."
[ "$PUSH" = 1 ] && git push -u origin "$BR"
fi
cat <<MSG
Проект $NAME подключён к обвязке. Дальше:
cd $(pwd) && claude
«Опиши проект в CLAUDE.md по коду: разделы «Что это», «Стек», «Архитектура», «Команды». Bounded-задача.»
MSG

View File

@ -71,6 +71,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/` (архивация) |
| Правки в `src/` только при плане или `.bounded` | `require-plan.sh`, PreToolUse на Edit/Write/MultiEdit | ищет план в `docs/superpowers/plans/` для текущей ветки или файл `.bounded` |
| Обвязка подключена к проекту | `harness-check.sh`, SessionStart | при старте сессии проверяет `openspec/`, `.codegraph/`, секцию в CLAUDE.md; если чего-то нет — пишет агенту инструкцию запустить `~/.claude/harness/adopt-project.sh` (спросив пользователя; `HARNESS_AUTO_ADOPT=1` — без вопроса) |
| Тесты зелёные перед коммитом | git pre-commit в проекте (не входит в пакет) | обычный git-хук с командой тестов |
Exit 2 блокирует вызов и показывает агенту stderr, поэтому агент сам делает то, что просит сообщение. Оба обхода, замеченные на пилоте (запись через `echo >` вместо Write; push через detached HEAD), закрыты.

View File

@ -7,6 +7,7 @@
- Ledger superpowers (`.superpowers/sdd/`) — прогресс только внутри текущего плана. Задачи плана в Gitea не дублировать.
## Маршрутизация
- Сообщение `[claude-harness]` в начале сессии означает, что обвязка к проекту не подключена: действовать по его инструкции до всего остального. Скрипт `~/.claude/harness/adopt-project.sh` — единственная разрешённая запись на main помимо архивации OpenSpec.
- Bounded-задача (флаг, фикс, мелкая правка): без OpenSpec change и без плана. Дизайн в чате, затем создать файл `.bounded` в корне worktree (разрешает правки в src/ без плана; файл в `.gitignore`, в коммит не попадает), TDD, коммит, PR.
- Architectural-задача: brainstorming пишет спеку не в `docs/superpowers/specs/`, а в `openspec/changes/<id>/`. В поле **Spec** плана указывается этот путь.
- Перед architectural brainstorming прочитать релевантные файлы в `openspec/specs/` и ARCHITECTURE.md, если есть.

View File

@ -24,7 +24,7 @@ only_openspec() { # $1 = git diff range/args; true if every changed path is und
if [ "$tool" = "Bash" ]; then
case "$cmd" in
*"git worktree"*|*"git checkout -b"*|*"git switch -c"*|*"openspec archive"*) exit 0 ;;
*"git worktree"*|*"git checkout -b"*|*"git switch -c"*|*"openspec archive"*|*"/.claude/harness/adopt-project.sh"*) exit 0 ;;
esac
# archive exception: commit of staged openspec-only changes, push of openspec-only commits
if printf '%s' "$cmd" | grep -Eq '\bgit\s+commit\b'; then

29
hooks/harness-check.sh Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# SessionStart hook: проверяет, подключена ли обвязка к проекту в cwd.
# Всё на месте — молчит. Чего-то нет — пишет в контекст сессии инструкцию агенту.
# HARNESS_AUTO_ADOPT=1 в окружении → агенту велено подключать без вопроса.
set -uo pipefail
input=$(cat)
cwd=$(printf '%s' "$input" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("cwd",""))' 2>/dev/null)
[ -n "$cwd" ] || exit 0
git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
root=$(git -C "$cwd" rev-parse --show-toplevel)
missing=()
[ -d "$root/openspec" ] || missing+=("openspec/")
[ -d "$root/.codegraph" ] || missing+=(".codegraph/")
grep -qs '^## OpenSpec и superpowers' "$root/CLAUDE.md" || missing+=("секция «OpenSpec и superpowers» в CLAUDE.md")
[ ${#missing[@]} -eq 0 ] && exit 0
if [ -n "$(git -C "$root" status --porcelain)" ]; then
dirty=" Рабочее дерево не чистое — скрипт откажется; сначала попроси пользователя закоммитить или спрятать изменения."
else dirty=""; fi
if [ "${HARNESS_AUTO_ADOPT:-0}" = "1" ]; then
mode="Выполни это сейчас, до любых других действий, без вопроса, и одной строкой сообщи пользователю, что сделано."
else
mode="Прежде чем что-либо делать, спроси пользователя одним вопросом, подключить ли обвязку, и по «да» выполни."
fi
cat <<MSG
[claude-harness] В проекте $root не подключена обвязка: нет: $(printf '%s; ' "${missing[@]}" | sed 's/; $//').
Подключение: \`bash "$HOME/.claude/harness/adopt-project.sh" "$root"\` — скрипт сам делает ветку chore/harness-init, бутстрап, коммит и ff-merge; хук guard-main его пропускает.$dirty
$mode После этого продолжай с запросом пользователя.
MSG
exit 0

View File

@ -4,13 +4,30 @@
{
"matcher": "Edit|Write|MultiEdit|Bash",
"hooks": [
{ "type": "command", "command": "\"$HOME/.claude/hooks/guard-main.sh\"" }
{
"type": "command",
"command": "\"$HOME/.claude/hooks/guard-main.sh\""
}
]
},
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "\"$HOME/.claude/hooks/require-plan.sh\"" }
{
"type": "command",
"command": "\"$HOME/.claude/hooks/require-plan.sh\""
}
]
}
],
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$HOME/.claude/hooks/harness-check.sh\""
}
]
}
]

View File

@ -39,7 +39,11 @@ fi
cp "$HERE/global/CLAUDE.md" "$CC/CLAUDE.md"
# 2. Хуки
cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$CC/hooks/"
cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harness-check.sh" "$CC/hooks/"
# 2a. Скрипты бутстрапа проекта — агент запускает их из сессии
mkdir -p "$CC/harness"
cp -R "$HERE/adopt-project.sh" "$HERE/new-project.sh" "$HERE/project-template" "$CC/harness/"
chmod +x "$CC/harness/"*.sh
chmod +x "$CC/hooks/"*.sh
# 3. caveman off by default
@ -59,6 +63,9 @@ def ours(entry):
return any(("guard-main.sh" in x.get("command","") or "require-plan.sh" in x.get("command","")) for x in entry.get("hooks",[]))
pre[:] = [e for e in pre if not ours(e)]
pre.extend(h["hooks"]["PreToolUse"])
ss = hooks.setdefault("SessionStart", [])
ss[:] = [e for e in ss if not any("harness-check.sh" in x.get("command","") for x in e.get("hooks",[]))]
ss.extend(h["hooks"]["SessionStart"])
for ev in ("SessionStart","PreCompact"):
if ev in hooks:
for e in hooks[ev]:
@ -69,6 +76,6 @@ json.dump(s, open(sp,"w"), ensure_ascii=False, indent=2)
print("settings.json: hooks merged")
PY
echo "done: $CC/CLAUDE.md, $CC/hooks/{guard-main,require-plan}.sh, ~/.config/caveman/config.json"
echo "done: $CC/CLAUDE.md, $CC/hooks/{guard-main,require-plan,harness-check}.sh, $CC/harness/, ~/.config/caveman/config.json"
echo "Плагины Claude Code ставятся вручную: /plugin install superpowers@claude-plugins-official"
check || true

View File

@ -0,0 +1,5 @@
## OpenSpec и superpowers
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change <id>` (или `/opsx:propose <id>`), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.
- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.
- `openspec archive <id>` — после merge PR, как часть чеклиста закрытия.