119 lines
11 KiB
Bash
Executable File
119 lines
11 KiB
Bash
Executable File
#!/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
|