scripts/claude_session_end_v5.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Claude Code Stop hook — v5 state.md soft reminder (default) or legacy strict shift gate.
Usage (see .claude/settings.json Stop hook):
BLACKBOARD_ROOT=/path/to/.blackboard python3 scripts/claude_session_end_v5.py
Default: validate state.md (_last_synced + four sections), WARN on stderr, exit 0.
Legacy: BLACKBOARD_STRICT=1 delegates to butler.blackboard.integrations.claude_session_end.
SSOT: docs/plans/decisions/v5-engineering-handoff-2026-08.md
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from pathlib import Path
_STATE_SECTIONS = ("## 主线", "## 下一步", "## 不要做", "## 上一班")
_LAST_SYNCED_RE = re.compile(r"^_last_synced:\s*(.+?)_\s*$", re.MULTILINE)
def _blackboard_root() -> Path:
env = os.environ.get("BLACKBOARD_ROOT")
return Path(env) if env else Path.cwd() / ".blackboard"
def check_state_snapshot(*, root: Path | None = None, max_lines: int = 45) -> str | None:
"""Return warning text if state.md incomplete; None if OK."""
state_path = (root or _blackboard_root()) / "state.md"
if not state_path.is_file():
return (
"[blackboard] ⚠ 缺 .blackboard/state.md。"
"会话结束前请写短快照(见 docs/plans/decisions/v5-engineering-handoff-2026-08.md)。"
)
text = state_path.read_text(encoding="utf-8")
line_count = len(text.splitlines())
if line_count > max_lines:
return (
f"[blackboard] ⚠ state.md 超过 {max_lines} 行(当前 {line_count});"
"建议压到约 40 行内。"
)
synced = _LAST_SYNCED_RE.search(text)
synced_val = synced.group(1).strip() if synced else ""
if not synced_val or synced_val in {"(none)", "(pending)", "test"}:
return "[blackboard] ⚠ state.md 缺有效 _last_synced;请更新后再结束会话。"
for heading in _STATE_SECTIONS:
if heading not in text:
return (
f"[blackboard] ⚠ state.md 缺 {heading} 段;"
"见 docs/plans/decisions/v5-engineering-handoff-2026-08.md §2。"
)
return None
def main() -> int:
if os.environ.get("BLACKBOARD_STRICT", "0") == "1":
env = os.environ.copy()
env["BLACKBOARD_STRICT"] = "1"
proc = subprocess.run(
[sys.executable, "-m", "butler.blackboard.integrations.claude_session_end"],
env=env,
)
return proc.returncode
max_lines_raw = os.environ.get("BLACKBOARD_STATE_MAX_LINES", "45")
try:
max_lines = max(20, int(max_lines_raw))
except ValueError:
max_lines = 45
msg = check_state_snapshot(max_lines=max_lines)
if msg:
print(msg, file=sys.stderr)
else:
print("[blackboard] ✓ state.md 快照就绪", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())