docs: старт #6 — экспорт блокировок хуков из транскриптов, текущая работа в CLAUDE.md
hook_blocks_export.py выгружает блокировки хуков в JSONL (cwd, ветка, команда, ответ хука) — сырьё для регрессионных тестов B2. Похожее на секреты скрывается. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2c599a1324
commit
5fd8346ef9
@ -28,7 +28,7 @@
|
||||
- `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение.
|
||||
- `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code.
|
||||
- `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта.
|
||||
- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро.
|
||||
- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро; `hook_blocks_export.py <каталог> [...]` — выгрузка блокировок в JSONL (сырьё для тестов хуков).
|
||||
|
||||
## Тесты
|
||||
- `tests/run.sh` — автоматические тесты источников (хуки через фиктивный JSON, скрипты во временных каталогах, размеры). Самотест хуков в `doctor.sh` проверяет установленные копии; перед ним `bash install.sh`.
|
||||
@ -36,7 +36,10 @@
|
||||
- `tests/` покрывает `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`; регрессионные тесты `guard-main` и `require-plan` на реальных командах из транскриптов (бэклог ретро B2) пока не сделаны.
|
||||
|
||||
## Текущая работа
|
||||
Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`.
|
||||
Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Находки ретро маркируются `RT-n`.
|
||||
- Сделано: B5+B6 часть 1 — слой ролей (PR #4, 0.3.0). Часть 2 (awp-ca) — issue #2, отложенные находки — issue #5.
|
||||
- В работе: B1+B2 и отмена исключения openspec-only для guard-main — issue #6, ветка `feat/6-guard-main-target`.
|
||||
- Дальше: B3/B4/B10, B12.
|
||||
|
||||
## OpenSpec и superpowers
|
||||
- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.
|
||||
|
||||
71
docs/retro/scripts/hook_blocks_export.py
Normal file
71
docs/retro/scripts/hook_blocks_export.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Выгрузка блокировок хуков из транскриптов Claude Code в JSONL — сырьё для регрессионных тестов хуков (B2).
|
||||
|
||||
Запуск: python3 hook_blocks_export.py <каталог-транскриптов> [<каталог> ...] > blocks.jsonl
|
||||
Одна строка — одна блокировка: сессия, время, cwd и ветка на момент вызова, инструмент, команда или путь файла,
|
||||
первая строка ответа хука. Поле expected не заполняется: вердикт (block/allow) проставляется при разметке.
|
||||
Похожие на секреты подстроки заменяются на <redacted>.
|
||||
"""
|
||||
import json, sys, glob, os, re
|
||||
|
||||
RE_SECRET = re.compile(
|
||||
r'(?i)gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|\b[0-9a-f]{40}\b'
|
||||
r'|(?P<kv>(?:token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)\S+'
|
||||
r'|(?P<flag>--(?:token|password)\s+)\S+|(?P<auth>Authorization:\s*\S+\s+)\S+'
|
||||
)
|
||||
|
||||
|
||||
def redact(s):
|
||||
def sub(m):
|
||||
keep = m.group("kv") or m.group("flag") or m.group("auth") or ""
|
||||
return keep + "<redacted>"
|
||||
return RE_SECRET.sub(sub, s)
|
||||
|
||||
|
||||
def hook_of(txt):
|
||||
if "shell write" in txt:
|
||||
return "guard-main(Bash)"
|
||||
if "you are on" in txt:
|
||||
return "guard-main(Edit/Write)"
|
||||
if "without a plan" in txt:
|
||||
return "require-plan"
|
||||
if "commit/push в main" in txt or "ветке main" in txt or "reference-old-arm" in txt or "docs/source" in txt:
|
||||
return "workspace-guard"
|
||||
return "other"
|
||||
|
||||
|
||||
for d in sys.argv[1:]:
|
||||
for f in sorted(glob.glob(os.path.join(os.path.expanduser(d), "*.jsonl"))):
|
||||
sid = os.path.basename(f)[:8]
|
||||
pending = {} # tool_use_id -> (name, input, cwd, branch, ts)
|
||||
for line in open(f, encoding="utf-8", errors="replace"):
|
||||
try:
|
||||
j = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
m = j.get("message") or {}
|
||||
cont = m.get("content")
|
||||
if j.get("type") == "assistant":
|
||||
for b in cont or []:
|
||||
if isinstance(b, dict) and b.get("type") == "tool_use":
|
||||
pending[b.get("id")] = (b.get("name"), b.get("input") or {}, j.get("cwd", ""),
|
||||
j.get("gitBranch", ""), j.get("timestamp", ""))
|
||||
elif j.get("type") == "user" and 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))
|
||||
if not ("BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400])):
|
||||
continue
|
||||
name, inp, cwd, branch, ts = pending.get(b.get("tool_use_id"), ("?", {}, "", "", ""))
|
||||
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
|
||||
continue
|
||||
msg = next((l for l in txt.splitlines() if "BLOCKED" in l), "")
|
||||
print(json.dumps({
|
||||
"session": sid, "ts": ts, "cwd": cwd, "branch": branch, "tool": name,
|
||||
"command": redact(inp.get("command", "")) if name == "Bash" else None,
|
||||
"file_path": inp.get("file_path") if name != "Bash" else None,
|
||||
"hook": hook_of(txt), "message": redact(msg[:300]), "expected": None,
|
||||
}, ensure_ascii=False))
|
||||
Loading…
x
Reference in New Issue
Block a user