#!/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)") 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 "BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400]): if name not in ("Bash", "Edit", "Write", "MultiEdit"): continue if "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}")