claude-harness/install.sh
andrey.epifantsev 897fa0f3a7 feat: install.sh ставит allow-правила цикла фичи, --no-permissions для отказа (#24, 0.5.0)
Пересмотр решения владельца 2: allow-правила (git worktree, git branch
-m/-D, push фичевых и chore-веток, push --delete, tea issues/pulls
create/list/edit/comments) ставит install.sh в ~/.claude/settings.json,
а не владелец руками. Список — global/permissions.json; merge и push в
main в нём нет. Правила только дописываются, чужие не трогаются; копия
списка — ~/.claude/harness/permissions.json для doctor.sh.

- install.sh: шаг 5 — слияние permissions.allow; --no-permissions;
  неизвестный аргумент — ошибка; --check показывает permissions N/M.
- doctor.sh, «Машина»: ok / WARN со списком недостающих / MISS.
- adopt/new-project: строка push --delete — для --no-permissions.
- harness-doctor: агент чинит install.sh --no-permissions, права ставит
  владелец (иначе обход отказа классификатора auto mode).
- EnterWorktree не входит: вход в worktree вне .claude/worktrees/
  спрашивает подтверждение при любых правилах (#25).
- README, HARNESS.md, roles/dev.md, CLAUDE.md, ретро (пометка о
  пересмотре решения 2), CHANGELOG 0.5.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 00:01:57 +04:00

141 lines
6.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# claude-harness: установка глобальной части (CLAUDE.md, хуки, конфиг caveman, слияние settings.json).
# Идемпотентно. Запуск: bash install.sh [--no-permissions] | проверка окружения: bash install.sh --check
# --no-permissions — не дописывать allow-правила цикла фичи (global/permissions.json) в settings.json.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
CC="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
check() {
local ok=0
echo "== Проверка окружения"
for bin in claude git python3 node npm tea openspec codegraph; do
if command -v "$bin" >/dev/null 2>&1; then echo " ok $bin ($(command -v "$bin"))"; else echo " MISS $bin"; ok=1; fi
done
if [ -f "$CC/plugins/installed_plugins.json" ]; then
for p in superpowers caveman; do
if grep -q "\"$p@" "$CC/plugins/installed_plugins.json"; then echo " ok plugin $p"; else echo " MISS plugin $p (/plugin install ...)"; fi
done
for p in beads template-bridge; do
grep -q "\"$p@" "$CC/plugins/installed_plugins.json" && echo " WARN plugin $p установлен — конфликтует с обвязкой, удалить" || true
done
else
echo " WARN не найден $CC/plugins/installed_plugins.json"
fi
if [ -f "$CC/settings.json" ] && grep -q 'codegraph prompt-hook' "$CC/settings.json"; then
echo " WARN codegraph prompt-hook в settings.json — рекомендуется отключить на время пилота"
fi
local pr; pr=$(python3 - "$CC/settings.json" "$HERE/global/permissions.json" <<'PY' 2>/dev/null
import json, sys
try:
s = json.load(open(sys.argv[1], encoding="utf-8"))
except Exception:
s = {}
have = (s.get("permissions") or {}).get("allow") or []
need = json.load(open(sys.argv[2], encoding="utf-8"))["permissions"]["allow"]
miss = [r for r in need if r not in have]
print("%d/%d %s" % (len(need) - len(miss), len(need), " ".join(miss)))
PY
) || true
case "$pr" in
"") echo " WARN permissions обвязки: не прочитаны ($CC/settings.json)" ;;
*" ") echo " ok permissions обвязки: ${pr% }" ;;
*) echo " WARN permissions обвязки: ${pr%% *}, нет: ${pr#* } — bash install.sh (или осознанно --no-permissions)" ;;
esac
echo "== Установленная часть обвязки ($CC)"
for f in CLAUDE.md hooks/guard-main.sh hooks/require-plan.sh hooks/harness_target.py hooks/harness-check.sh hooks/role-hint.sh \
roles/dev.md roles/admin.md roles/analyst.md roles/qa.md skills/role/SKILL.md agents/briefing.md \
harness/adopt-project.sh harness/new-project.sh harness/link-roles.sh skills/harness-doctor/SKILL.md; do
[ -f "$CC/$f" ] && echo " ok $f" || { echo " MISS $f (bash install.sh)"; ok=1; }
done
return $ok
}
PERMS=1
for a in "$@"; do
case "$a" in
--check) check; exit $? ;;
--no-permissions) PERMS=0 ;;
*) echo "ERR: неизвестный аргумент $a (есть --check, --no-permissions)"; exit 1 ;;
esac
done
mkdir -p "$CC/hooks" "$HOME/.config/caveman"
stamp=$(date +%Y%m%d-%H%M%S)
# 1. Глобальный CLAUDE.md
if [ -f "$CC/CLAUDE.md" ] && ! cmp -s "$HERE/global/CLAUDE.md" "$CC/CLAUDE.md"; then
cp "$CC/CLAUDE.md" "$CC/CLAUDE.md.bak.$stamp"; echo "backup: CLAUDE.md.bak.$stamp"
fi
cp "$HERE/global/CLAUDE.md" "$CC/CLAUDE.md"
# 2. Хуки
cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harness-check.sh" "$HERE/hooks/role-hint.sh" \
"$HERE/hooks/harness_target.py" "$CC/hooks/"
# 2a. Скрипты бутстрапа проекта — агент запускает их из сессии
mkdir -p "$CC/harness"
rm -rf "$CC/harness/project-template"
cp -R "$HERE/adopt-project.sh" "$HERE/new-project.sh" "$HERE/link-roles.sh" "$HERE/project-template" "$HERE/global/permissions.json" "$CC/harness/"
chmod +x "$CC/harness/"*.sh
# 2b. Skill диагностики
mkdir -p "$CC/skills"
rm -rf "$CC/skills/harness-doctor" && cp -R "$HERE/skills/harness-doctor" "$CC/skills/harness-doctor"
chmod +x "$CC/skills/harness-doctor/scripts/"*.sh
# 2c. Роли, skill /role, агент briefing
mkdir -p "$CC/roles" "$CC/skills/role" "$CC/agents"
cp "$HERE/roles/"*.md "$CC/roles/"
cp "$HERE/skills/role/SKILL.md" "$CC/skills/role/SKILL.md"
cp "$HERE/agents/briefing.md" "$CC/agents/briefing.md"
chmod +x "$CC/hooks/"*.sh
# 3. caveman off by default
cp "$HERE/caveman/config.json" "$HOME/.config/caveman/config.json"
# 4. settings.json: слить блок hooks, вычистить хвосты beads/template-bridge
SETTINGS="$CC/settings.json"
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"
cp "$SETTINGS" "$SETTINGS.bak.$stamp"
python3 - "$SETTINGS" "$HERE/hooks/settings.hooks.json" <<'PY'
import json, sys
sp, hp = sys.argv[1], sys.argv[2]
s = json.load(open(sp)); h = json.load(open(hp))
hooks = s.setdefault("hooks", {})
pre = hooks.setdefault("PreToolUse", [])
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", [])
ours_ss = ("harness-check.sh", "role-hint.sh")
ss[:] = [e for e in ss if not any(any(n in x.get("command","") for n in ours_ss) 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]:
e["hooks"] = [x for x in e.get("hooks",[]) if "bd prime" not in x.get("command","") and "WORKFLOW REMINDER" not in x.get("command","")]
hooks[ev] = [e for e in hooks[ev] if e.get("hooks")]
if not hooks[ev]: del hooks[ev]
json.dump(s, open(sp,"w"), ensure_ascii=False, indent=2)
print("settings.json: hooks merged")
PY
# 5. settings.json: allow-правила цикла фичи — только дописываются, чужие не трогаются
if [ "$PERMS" = 1 ]; then
python3 - "$SETTINGS" "$HERE/global/permissions.json" <<'PY'
import json, sys
sp, pp = sys.argv[1], sys.argv[2]
s = json.load(open(sp)); need = json.load(open(pp))["permissions"]["allow"]
allow = s.setdefault("permissions", {}).setdefault("allow", [])
new = [r for r in need if r not in allow]
allow.extend(new)
json.dump(s, open(sp, "w"), ensure_ascii=False, indent=2)
print("settings.json: permissions +%d%s" % (len(new), (" (" + ", ".join(new) + ")") if new else " (все на месте)"))
PY
else
echo "settings.json: permissions пропущены (--no-permissions)"
fi
echo "done: $CC/CLAUDE.md, $CC/skills/harness-doctor, $CC/hooks/{guard-main,require-plan,harness-check,role-hint}.sh + harness_target.py, $CC/roles/, $CC/skills/role, $CC/agents/briefing.md, $CC/harness/, ~/.config/caveman/config.json"
echo "Плагины Claude Code ставятся вручную: /plugin install superpowers@claude-plugins-official"
check || true