claude-harness/docs/retro/scripts/retro_blocks.py

141 lines
7.6 KiB
Python
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 python3
"""Классификация блокировок хуков, отказов auto mode, стартового контекста и ручных напоминаний."""
import json, sys, glob, os, re, collections
d = sys.argv[1]
files = sorted(glob.glob(os.path.join(d, "*.jsonl")))
RE_REAL = re.compile(r'(^|[^<\d])>{1,2}[^&]|\btee\b|\bsed\s+-i|\bmv\b|\bcp\b|\brm\b|\btouch\b|\bmkdir\b|\bgit\s+(add|commit|push|apply|stash\s+pop)\b|\bnpm\s+(i|install|init)\b|\bpip\s+install\b')
RE_STDERR = re.compile(r'\d>{1,2}\s*/dev/null|\d>&\d')
RE_HEREDOC_PY = re.compile(r"<<\s*'?(PY|EOF)")
RE_HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") # только ошибка хука, не вывод команды со словом BLOCKED
blocks = collections.Counter()
by_hook = collections.Counter()
by_session = collections.Counter()
examples = collections.defaultdict(list)
denials = collections.Counter()
denial_ex = []
start_ctx = []
recall_prompts = []
codegraph_init = 0
cd_main = 0
n_prompts_total = 0
for f in files:
sid = os.path.basename(f)[:8]
pending = {} # tool_use_id -> (name, input)
first_usage = None
title = ""
for line in open(f, encoding="utf-8", errors="replace"):
try:
j = json.loads(line)
except Exception:
continue
t = j.get("type")
if t == "ai-title" and not title:
title = j.get("aiTitle", "")
if t == "assistant":
m = j.get("message", {})
u = m.get("usage") or {}
if first_usage is None and u:
first_usage = (u.get("input_tokens", 0), u.get("cache_creation_input_tokens", 0), u.get("cache_read_input_tokens", 0))
for b in m.get("content", []) or []:
if isinstance(b, dict) and b.get("type") == "tool_use":
pending[b.get("id")] = (b.get("name"), b.get("input") or {})
if b.get("name") == "Bash":
cmd = (b.get("input") or {}).get("command", "")
if "codegraph init" in cmd:
codegraph_init += 1
if re.search(r"\bcd\s+\S*(arm-core|docs|tabular-parser|scan-gateway|arm-web)/?\s*($|;|&&)", cmd) and "/.wt/" not in cmd:
cd_main += 1
if t == "user":
m = j.get("message", {})
cont = m.get("content", "")
if isinstance(cont, str) and not cont.startswith("<") and not j.get("isMeta"):
n_prompts_total += 1
if re.search(r"памят|перечитай|сверься|контекст|вспомни|прочитай .*md", cont, re.I):
recall_prompts.append((sid, cont[:140].replace("\n", " ")))
if isinstance(cont, list):
for b in cont:
if not (isinstance(b, dict) and b.get("type") == "tool_result"):
continue
txt = b.get("content", "")
if isinstance(txt, list):
txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict))
name, inp = pending.get(b.get("tool_use_id"), ("?", {}))
if RE_HOOK_ERROR.match(txt) and "BLOCKED" in txt:
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
continue
if "[guard-main]" in txt:
hook = "guard-main(Bash)" if name == "Bash" else "guard-main(Edit/Write)"
elif "[require-plan]" in txt:
hook = "require-plan"
elif "shell write" in txt:
hook = "guard-main(Bash)"
elif "you are on" in txt:
hook = "guard-main(Edit/Write)"
elif "without a plan" in txt:
hook = "require-plan"
elif "commit/push в main" in txt or "ветке main" in txt or "reference-old-arm" in txt or "docs/source" in txt:
hook = "workspace-guard"
else:
hook = "other"
by_hook[hook] += 1
by_session[sid] += 1
cmd = inp.get("command", "") if name == "Bash" else inp.get("file_path", "")
if name == "Bash":
if RE_STDERR.search(cmd) and not RE_REAL.sub("", cmd) != cmd:
cls = "stderr-redirect-only"
else:
stripped = RE_STDERR.sub("", cmd)
if RE_REAL.search(stripped):
mm = RE_REAL.search(stripped).group(0)
if re.match(r"^\s*(cat|python3|grep|ls|find|sed -n|head|git (log|status|diff|show|branch|worktree list))", cmd) and ("mkdir" not in cmd and "git commit" not in cmd and "git push" not in cmd and "git add" not in cmd and "rm " not in cmd):
cls = "read-only-but-matched:" + mm.strip()[:12]
else:
cls = "real-write:" + mm.strip()[:12]
else:
cls = "no-pattern(workspace or cwd)"
else:
cls = name
blocks[(hook, cls)] += 1
if len(examples[(hook, cls)]) < 3:
examples[(hook, cls)].append(f"[{sid}] {cmd[:150]!r}")
if "denied by the Claude Code auto mode classifier" in txt or "denied by a built-in Claude Code safety check" in txt or "The user doesn't want to proceed" in txt:
r = re.search(r"Reason: \[([^\]]+)\]", txt)
key = r.group(1) if r else ("safety-check" if "safety check" in txt else "user-rejected")
denials[key] += 1
cmd = inp.get("command", "") if name == "Bash" else name
denial_ex.append(f"[{sid}] {key}: {cmd[:140]!r}")
if first_usage:
start_ctx.append((sid, title[:30], sum(first_usage), first_usage))
print("== Блокировки по хукам")
for k, v in by_hook.most_common():
print(f"{k}: {v}")
print("\n== Блокировки по классу")
for k, v in sorted(blocks.items(), key=lambda x: -x[1]):
print(f"{v:3d} {k[0]} / {k[1]}")
for e in examples[k]:
print(" ", e)
print("\n== Сессии с наибольшим числом блокировок")
for k, v in by_session.most_common(8):
print(f"{k}: {v}")
print("\n== Отказы auto mode / safety / пользователь")
for k, v in denials.most_common():
print(f"{k}: {v}")
for e in denial_ex:
print(" ", e)
print("\n== Стартовый контекст (первый ответ модели): input+cache_create+cache_read")
start_ctx.sort(key=lambda x: -x[2])
for s, t, tot, fu in start_ctx:
print(f"{s} {tot:7d} (in={fu[0]} create={fu[1]} read={fu[2]}) {t}")
vals = [x[2] for x in start_ctx if x[2] > 0]
vals.sort()
print(f"median={vals[len(vals)//2]} min={vals[0]} max={vals[-1]} n={len(vals)}")
print(f"\n== codegraph init в командах: {codegraph_init}; cd в основной checkout (не .wt): {cd_main}")
print(f"\n== Промпты владельца с просьбой перечитать память/контекст: {len(recall_prompts)} из {n_prompts_total}")
for s, p in recall_prompts:
print(f" [{s}] {p}")