2026-10-03 18:17:33 +04:00

78 lines
7.0 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; 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
[ -x "$CC/harness/adopt-project.sh" ] && ok "~/.claude/harness/ (adopt/new-project)" || miss "~/.claude/harness/adopt-project.sh"
[ -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"
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
rm -rf "$T"
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
else info "не git-репозиторий"; fi