- C-1: транскрипты сабагентов (<сессия>/subagents/*.jsonl) вливаются в сессию-родителя — ходы, токены, инструменты, codegraph, блокировки, отказы; промпты, роль, стартовый контекст — только из основного. hook_blocks_export.py выгружает их блокировки с сессией-родителем. - I-7: период режется по меткам времени записей; сессия на стыке делится между ретро, стартовый контекст — если первый ход в периоде. - I-1: простые команды Bash — shlex с punctuation_chars, «|» в кавычках команду не делит. I-2: правило src/ ловит и сам каталог src. - I-3: clip() — redact до обрезки (кандидаты, выгрузка), роль — redact. - I-4: без --since период берётся из существующего --out; --from рисует «Цифры» из сохранённого среза. SKILL.md передаёт период явно. - I-5: выгрузка ретро без разметки — сборка корпуса падает. - I-6: сводка компакции — не промпт; «перечитай» — побуждение перечитать. - 2026-10-11.metrics.json: несравнимое помечено (без сабагентов, широкий «перечитай»). Спека и design.md (Decisions) обновлены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
7.8 KiB
Python
131 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Сборка регрессионного корпуса хуков из сырой выгрузки блокировок (B2, #6).
|
||
|
||
Запуск из корня репозитория:
|
||
python3 docs/retro/scripts/hook_cases_build.py [--fixtures-dir <каталог>] > skills/harness-doctor/fixtures/hook-cases.jsonl
|
||
Вход: <fixtures>/hook-blocks.jsonl (hook_blocks_export.py, 12.10.2026, 126 записей) и пары ретро
|
||
hook-blocks-<дата>.jsonl + hook-labels-<дата>.json (B12, #29).
|
||
Выход — сегменты: строка {"home": …, "layout": […]} (раскладка песочницы), дальше по записи на случай:
|
||
поля выгрузки + id, expected (allow|block), why (класс)[, known]. Первый сегмент — пилот 12.10 и живые случаи,
|
||
дальше по сегменту на пару ретро; записи со skip не входят. Вердикт размечен вручную по спеке
|
||
openspec/specs/hook-guards/spec.md, а не прогоном хука.
|
||
"""
|
||
import glob
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
FIX = os.path.join(HERE, "..", "fixtures")
|
||
HOME = "/Users/andrey.epifancev"
|
||
AW, CH = "projects/awp-ca", "projects/cc-skills/claude-harness"
|
||
|
||
LAYOUT = [{"path": AW}, {"path": AW + "/reference-old-arm"}]
|
||
LAYOUT += [{"path": "%s/%s" % (AW, r), "branch": "main"}
|
||
for r in ("arm-core", "docs", "tabular-parser", "scan-gateway", "arm-web", "fixtures")]
|
||
LAYOUT += [{"path": "%s/.wt/%s/%s" % (AW, n, r), "worktree_of": "%s/%s" % (AW, r), "branch": "feat/" + n}
|
||
for n, r in (("web-proto", "arm-core"), ("web-proto", "arm-web"), ("ocr-q1", "fixtures"),
|
||
("ocr-q1", "scan-gateway"), ("stmt-template", "arm-web"), ("qa-guide-web", "docs"),
|
||
("slice-0", "scan-gateway"), ("slice-3b", "arm-core"))]
|
||
LAYOUT += [{"path": CH, "branch": "main"}]
|
||
LAYOUT += [{"path": "%s/.wt/%s" % (CH, n), "worktree_of": CH, "branch": b}
|
||
for n, b in (("retro", "retro/2026-10-11"), ("roles", "feat/2-roles-and-memory"),
|
||
("guard", "feat/6-guard-main-target"))]
|
||
LAYOUT += [{"path": ".claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory"},
|
||
{"path": ".claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory"},
|
||
{"path": ".claude/hooks"}, {"path": ".claude/jobs"}]
|
||
|
||
# Блок по спеке: архивный коммит openspec прямо в main (решение владельца 1 — архив отдельным PR).
|
||
BLOCK = {"c031", "c069", "c070", "c103"}
|
||
# Класс для записей, где он не выводится общим правилом ниже.
|
||
WHY = {
|
||
"c002": "unparseable", "c008": "var-target", "c023": "outside-repo", "c026": "outside-repo",
|
||
"c029": "worktree", "c030": "outside-repo", "c040": "worktree-setup", "c044": "worktree", "c045": "worktree",
|
||
"c055": "outside-repo", "c064": "worktree-setup", "c072": "switch-then-commit", "c073": "worktree-setup",
|
||
"c078": "outside-repo", "c080": "heredoc", "c081": "heredoc", "c083": "outside-repo", "c091": "heredoc",
|
||
"c099": "worktree-setup", "c100": "worktree-setup", "c101": "worktree", "c102": "worktree",
|
||
"c104": "worktree-setup", "c106": "worktree-setup", "c119": "heredoc",
|
||
}
|
||
# Ложные блоки этой сессии (15.10.2026), до перехода в worktree: cwd — основной checkout на main.
|
||
LIVE_RE = ('git show origin/feat/6-guard-main-target:docs/retro/fixtures/hook-blocks.jsonl | python3 -c \'\n'
|
||
'import sys,json,re,collections\n'
|
||
'rs=[json.loads(l) for l in sys.stdin if l.strip()]\n'
|
||
'c=collections.Counter()\n'
|
||
'for r in rs:\n'
|
||
' cmd=r.get("command") or ""\n'
|
||
' if re.search(r"\\d>{1,2}\\s*/dev/null|\\d>&\\d",cmd): c["bash: N>/dev/null или N>&M"]+=1\n'
|
||
' if re.search(r"mkdir\\s+-p\\s+\\S*\\.wt/",cmd): c["bash: mkdir -p .wt/"]+=1\n'
|
||
' if re.search(r"\\bsed\\s+-i",cmd): c["bash: sed -i"]+=1\n'
|
||
'for k,v in c.most_common(): print(f"{v:4} {k}")\'')
|
||
LIVE = [
|
||
("l001", "grep-pattern", 'grep -rn "npm i\\|npm install\\|brew install\\|plugin install\\|go install" '
|
||
'--include="*.md" --include="*.sh" . | grep -v "^./docs/retro/2026" | head -20'),
|
||
("l002", "devnull", 'grep -rn "plugin add\\|marketplace\\|caveman@\\|superpowers@" --include="*.md" '
|
||
'--include="*.sh" README.md docs global install.sh 2>/dev/null | head -20'),
|
||
("l003", "regex-text", LIVE_RE),
|
||
]
|
||
DEVNULL = re.compile(r"\d>{1,2}\s*/dev/null|\d>&\d")
|
||
|
||
|
||
def why_of(cid, r):
|
||
if cid in BLOCK:
|
||
return "main-git"
|
||
if cid in WHY:
|
||
return WHY[cid]
|
||
if "hook error" not in (r.get("message") or ""):
|
||
return "not-a-block" # вывод успешной команды, где встретилось слово BLOCKED
|
||
if r["tool"] != "Bash":
|
||
return "outside-repo"
|
||
return "devnull" if DEVNULL.search(r.get("command") or "") else "read-only"
|
||
|
||
|
||
def label_segments(fix):
|
||
"""Сегменты ретро: пары hook-blocks-<дата>.jsonl + hook-labels-<дата>.json. Разметка должна совпадать с выгрузкой."""
|
||
for bp in sorted(glob.glob(os.path.join(fix, "hook-blocks-*.jsonl"))):
|
||
date = os.path.basename(bp)[len("hook-blocks-"):-len(".jsonl")]
|
||
if not os.path.isfile(os.path.join(fix, "hook-labels-%s.json" % date)):
|
||
raise SystemExit("%s: нет разметки hook-labels-%s.json — ретро не уходит с неразмеченными блокировками"
|
||
% (os.path.basename(bp), date))
|
||
for lp in sorted(glob.glob(os.path.join(fix, "hook-labels-*.json"))):
|
||
date = os.path.basename(lp)[len("hook-labels-"):-len(".json")]
|
||
lab = json.load(open(lp, encoding="utf-8"))
|
||
cases = lab.get("cases", {})
|
||
recs = [json.loads(l) for l in open(os.path.join(fix, "hook-blocks-%s.jsonl" % date), encoding="utf-8")
|
||
if l.strip()]
|
||
ids = [r.get("id") for r in recs]
|
||
missing, extra = [i for i in ids if i not in cases], sorted(set(cases) - set(ids))
|
||
if missing or extra:
|
||
raise SystemExit("hook-labels-%s.json: нет разметки для %s; лишняя разметка %s"
|
||
% (date, ", ".join(missing) or "—", ", ".join(extra) or "—"))
|
||
yield {"home": lab["home"], "layout": lab["layout"]}
|
||
for r in recs:
|
||
c = cases[r["id"]]
|
||
if "skip" in c:
|
||
continue
|
||
r.update({"expected": c["expected"], "why": c["why"]})
|
||
if c.get("known"):
|
||
r["known"] = c["known"]
|
||
yield r
|
||
|
||
|
||
def main(argv):
|
||
fix = argv[argv.index("--fixtures-dir") + 1] if "--fixtures-dir" in argv else FIX
|
||
print(json.dumps({"home": HOME, "layout": LAYOUT}, ensure_ascii=False))
|
||
raw = os.path.join(fix, "hook-blocks.jsonl")
|
||
for n, line in enumerate(l for l in open(raw, encoding="utf-8") if l.strip()):
|
||
r = json.loads(line)
|
||
cid = "c%03d" % (n + 1)
|
||
r.update({"id": cid, "expected": "block" if cid in BLOCK else "allow", "why": why_of(cid, r)})
|
||
print(json.dumps(r, ensure_ascii=False))
|
||
for cid, why, cmd in LIVE:
|
||
print(json.dumps({"id": cid, "session": "live-1015", "cwd": HOME + "/" + CH, "branch": "main",
|
||
"tool": "Bash", "command": cmd, "file_path": None, "hook": "guard-main(Bash)",
|
||
"message": "", "expected": "allow", "why": why}, ensure_ascii=False))
|
||
for obj in label_segments(fix):
|
||
print(json.dumps(obj, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main(sys.argv)
|