119 lines
11 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 doctor: снимок состояния обвязки на машине и в проекте + самотест хуков.
# Запуск: bash doctor.sh [папка проекта] — ничего не меняет, только читает.
set -uo pipefail
CC="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
DIR="${1:-.}"
ok(){ printf ' ok %s\n' "$*"; }; miss(){ printf ' MISS %s\n' "$*"; }; warn(){ printf ' WARN %s\n' "$*"; }; info(){ printf ' - %s\n' "$*"; }
echo "## harness-doctor $(date +%F' '%T)"
echo "## Машина"
for b in claude git python3 node npm tea openspec codegraph; do
command -v "$b" >/dev/null 2>&1 && ok "$b $( { $b --version 2>/dev/null || true; } | head -1 | cut -c1-40)" || miss "$b"
done
if [ -f "$CC/plugins/installed_plugins.json" ]; then
for p in superpowers caveman; do grep -q "\"$p@" "$CC/plugins/installed_plugins.json" && ok "plugin $p" || miss "plugin $p"; done
for p in beads template-bridge; do grep -q "\"$p@" "$CC/plugins/installed_plugins.json" && warn "plugin $p установлен (конфликт)"; done
else warn "нет $CC/plugins/installed_plugins.json"; fi
[ -f "$CC/CLAUDE.md" ] && ok "глобальный CLAUDE.md ($(grep -c . "$CC/CLAUDE.md") строк; маркировка: $(grep -q 'R-n' "$CC/CLAUDE.md" && echo да || echo нет); codegraph projectPath: $(grep -q projectPath "$CC/CLAUDE.md" && echo да || echo нет))" || miss "глобальный CLAUDE.md"
for h in guard-main require-plan harness-check role-hint; do
f="$CC/hooks/$h.sh"
if [ -x "$f" ]; then
grep -q "$h.sh" "$CC/settings.json" 2>/dev/null && ok "hook $h.sh установлен и зарегистрирован" || warn "hook $h.sh есть, но не зарегистрирован в settings.json"
else miss "hook $h.sh"; fi
done
[ -f "$CC/hooks/harness_target.py" ] && ok "hooks/harness_target.py (ядро guard-main и require-plan)" || miss "hooks/harness_target.py (bash install.sh)"
[ -x "$CC/harness/adopt-project.sh" ] && ok "~/.claude/harness/ (adopt/new-project)" || miss "~/.claude/harness/adopt-project.sh"
[ -x "$CC/harness/link-roles.sh" ] && ok "~/.claude/harness/link-roles.sh" || miss "~/.claude/harness/link-roles.sh"
rmiss=(); for r in dev admin analyst qa; do [ -f "$CC/roles/$r.md" ] || rmiss+=("$r"); done
if [ ${#rmiss[@]} -eq 0 ]; then ok "roles/ (dev admin analyst qa)"; else for r in "${rmiss[@]}"; do miss "roles/$r.md"; done; fi
[ -f "$CC/skills/role/SKILL.md" ] && ok "skill role" || miss "skill role (~/.claude/skills/role/SKILL.md)"
[ -f "$CC/agents/briefing.md" ] && ok "agent briefing" || miss "agent briefing (~/.claude/agents/briefing.md)"
if [ -f "$CC/CLAUDE.md" ]; then n=$(wc -c < "$CC/CLAUDE.md" | tr -d ' '); [ "$n" -le 3072 ] && ok "глобальный CLAUDE.md $n Б (≤ 3 КБ)" || warn "глобальный CLAUDE.md $n Б > 3 КБ"; fi
[ -f "$HOME/.config/caveman/config.json" ] && ok "caveman config: $(tr -d ' \n' < "$HOME/.config/caveman/config.json" | cut -c1-40)" || warn "нет ~/.config/caveman/config.json"
grep -q 'codegraph prompt-hook' "$CC/settings.json" 2>/dev/null && warn "codegraph prompt-hook в settings.json"
env | grep -E '^(AITUNNEL|OPENAI|GROQ|ANTHROPIC)_API_KEY=' | cut -d= -f1 | while read -r k; do warn "в окружении есть $k — виден всем агентам, тесты с ключом платные"; done
echo "## Самотест хуков (фиктивные вызовы, ничего не пишется)"
T=$(mktemp -d); git -C "$T" init -q -b main 2>/dev/null; mkdir -p "$T/src"
hk(){ # name json expected
local r; printf '%s' "$2" | bash "$CC/hooks/$1.sh" >/dev/null 2>&1; r=$?
[ "$r" = "$3" ] && ok "$1: $4 → exit $r" || warn "$1: $4 → exit $r (ожидалось $3)"
}
if [ -x "$CC/hooks/guard-main.sh" ]; then
hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$T/x\"}}" 2 "Write на main"
hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo hi > x\"}}" 2 "Bash echo > на main"
hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git worktree add ../w -b f\"}}" 0 "git worktree на main"
mkdir -p "$T/openspec"; echo a > "$T/openspec/x.md"; git -C "$T" add openspec 2>/dev/null
hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m archive\"}}" 2 "git commit openspec-only на main"
git -C "$T" reset -q 2>/dev/null
git -C "$T" switch -q -c feat/1-x 2>/dev/null
hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$T/x\"}}" 0 "Write на ветке"
fi
if [ -x "$CC/hooks/require-plan.sh" ]; then
hk require-plan "{\"cwd\":\"$T\",\"tool_input\":{\"file_path\":\"$T/src/a.ts\"}}" 2 "src/ без плана"
hk require-plan "{\"cwd\":\"$T\",\"tool_input\":{\"file_path\":\"$T/api/src/a.py\"}}" 2 "api/src/ без плана"
touch "$T/.bounded"; hk require-plan "{\"cwd\":\"$T\",\"tool_input\":{\"file_path\":\"$T/src/a.ts\"}}" 0 "src/ с .bounded"
fi
if [ -x "$CC/hooks/role-hint.sh" ]; then
o=$(printf '{"session_title":"admin: спринт"}' | bash "$CC/hooks/role-hint.sh" 2>/dev/null)
case "$o" in *"Роль сессии: admin"*) ok "role-hint: заголовок admin → подсказка" ;; *) warn "role-hint: заголовок admin → '$o' (ожидалась подсказка)" ;; esac
o=$(printf '{"cwd":"/tmp","source":"resume"}' | bash "$CC/hooks/role-hint.sh" 2>/dev/null)
[ -z "$o" ] && ok "role-hint: без заголовка → тишина" || warn "role-hint: без заголовка → '$o' (ожидалась тишина)"
fi
rm -rf "$T"
HC="$CC/skills/harness-doctor/scripts/hook_corpus.py"
if [ -f "$HC" ] && [ -f "$CC/hooks/harness_target.py" ]; then
out=$(python3 "$HC" "$CC/hooks" --summary 2>&1); rc=$?
printf '%s\n' "$out" | grep '^MISMATCH' | sed 's/^MISMATCH / /'
[ "$rc" = 0 ] && ok "$(printf '%s\n' "$out" | tail -1)" || warn "$(printf '%s\n' "$out" | tail -1) — расхождения выше"
else miss "корпус хуков (skills/harness-doctor/scripts/hook_corpus.py, hooks/harness_target.py)"; fi
echo "## Проект:$(cd "$DIR" 2>/dev/null && pwd)"
if root=$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null); then
main=$(git -C "$root" rev-parse --git-common-dir 2>/dev/null); main=$(cd "$root" && cd "$(dirname "$main")" 2>/dev/null && pwd)
info "git root: $root$( [ "$main" != "$root" ] && echo " (worktree; основной: $main)")"
info "ветка: $(git -C "$root" branch --show-current 2>/dev/null || echo 'detached') чистое дерево: $([ -z "$(git -C "$root" status --porcelain)" ] && echo да || echo НЕТ)"
git -C "$root" worktree list 2>/dev/null | sed 's/^/ /'
[ -f "$root/CLAUDE.md" ] && { grep -q '^## OpenSpec и superpowers' "$root/CLAUDE.md" && ok "CLAUDE.md с секцией харнеса" || warn "CLAUDE.md без секции «OpenSpec и superpowers»"; } || miss "CLAUDE.md"
[ -f "$root/ARCHITECTURE.md" ] && ok "ARCHITECTURE.md" || info "ARCHITECTURE.md нет"
if [ -d "$root/openspec" ]; then
ok "openspec: specs $(ls "$root/openspec/specs" 2>/dev/null | wc -l | tr -d ' '), changes $(ls -d "$root"/openspec/changes/*/ 2>/dev/null | grep -v archive | wc -l | tr -d ' '), archive $(ls "$root/openspec/changes/archive" 2>/dev/null | wc -l | tr -d ' ')"
for c in "$root"/openspec/changes/*/; do [ "$(basename "$c")" = archive ] && continue; [ -d "$c" ] || continue
info "change $(basename "$c"): decisions $(grep -c '^[0-9]\+\. ' "$c/design.md" 2>/dev/null || echo 0)"; done
else miss "openspec/"; fi
if [ -d "$root/.codegraph" ]; then
[ -f "$root/.codegraph/codegraph.db" ] && ok "codegraph: индекс есть$( [ -f "$root/.codegraph/daemon.pid" ] && echo ', демон pid '$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["pid"])' "$root/.codegraph/daemon.pid" 2>/dev/null))" || warn "codegraph: папка есть, индекса нет — codegraph init"
[ "$main" != "$root" ] && [ -f "$main/.codegraph/codegraph.db" ] && info "MCP, скорее всего, смотрит на индекс основного checkout: в вызовах нужен projectPath=$root"
else miss ".codegraph/ (codegraph init)"; fi
info "планы: $(ls "$root"/docs/superpowers/plans/*.md 2>/dev/null | wc -l | tr -d ' ')$( [ -f "$root/.bounded" ] && echo ' .bounded: есть')"
[ -d "$root/.superpowers/sdd" ] && info "SDD workspace: $(ls "$root/.superpowers/sdd" | tr '\n' ' ')"
[ -f "$root/.claude/code-paths" ] && info "code-paths: $(grep -v '^#' "$root/.claude/code-paths" | tr '\n' ' ')" || info "code-paths: умолчание (любой src/)"
[ -f "$root/.claude/readonly-paths" ] && info "readonly-paths: $(grep -v '^#' "$root/.claude/readonly-paths" | tr '\n' ' ')"
git -C "$root" check-ignore -q .bounded 2>/dev/null || warn ".bounded не в .gitignore"
[ -d "$root/.git/hooks" ] && [ -x "$root/.git/hooks/pre-commit" ] && ok "git pre-commit есть" || info "git pre-commit нет"
if command -v tea >/dev/null 2>&1 && git -C "$root" remote get-url origin >/dev/null 2>&1; 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 -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 КБ"
for kind in roles agents; do
for l in "$ws/.claude/$kind"/*; do
[ -e "$l" ] || [ -L "$l" ] || continue
name="$kind/$(basename "$l")"
if [ -L "$l" ]; then [ -e "$l" ] && ok "$name → $(readlink "$l")" || warn "$name — битая ссылка на $(readlink "$l")"
else info "$name — обычный файл (не ссылка)"; fi
done
done
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)
slug=$(printf '%s' "$ws" | sed 's#[^A-Za-z0-9]#-#g'); mem="$CC/projects/$slug/memory/MEMORY.md"
if [ -f "$mem" ]; then n=$(wc -c < "$mem" | tr -d ' '); [ "$n" -le 3072 ] && ok "индекс auto-memory $n Б (≤ 3 КБ)" || warn "индекс auto-memory $n Б > 3 КБ ($mem)"; else info "индекс auto-memory: нет ($mem)"; fi
[ -x "$CC/harness/link-roles.sh" ] && info "обновить ссылки: bash $CC/harness/link-roles.sh $ws"
else info "не git-репозиторий и не workspace (нет таблицы репозиториев в CLAUDE.md)"; fi