Пересмотр решения владельца 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>
141 lines
6.9 KiB
Bash
Executable File
141 lines
6.9 KiB
Bash
Executable File
#!/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
|