141 lines
7.6 KiB
Python
141 lines
7.6 KiB
Python
#!/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}")
|