session 2026-07-27 evening: PENDING-76/77/78 placed; governance drift-check built + wired into /wake-up
PENDING-76 remanded by jurist — required count returned 0 of 11 (the package's own IV.2 refinement proved its target category empty); executor recommends withdrawal. PENDING-77 (5 structural defects) and PENDING-78 (.app preferences) released by the ruling from needing it. Drift check reports contradicted state claims at every wake and corrects nothing — detection needs no authorization, correction does. MEMORY.md compacted 20.5KB -> 17.1KB (budget hook); prior Active Session demoted to MEMORY-reference.md. CLAUDE.md and REVIEWED.md untouched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xefg5EXwcpd9RMAr63dWrD
This commit is contained in:
co-authored by
Claude
parent
84a23d8550
commit
e8b6ce06a3
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
governance-drift-check — report state claims in ~/CLAUDE.md that the substrate contradicts.
|
||||
|
||||
Reports. Does not correct. Detection needs no authorization; correction does
|
||||
(Constitutional Constraint #1). This exists so that a stale governance document
|
||||
is *visible* rather than *misleading* — Constitutional Constraint #4, honest
|
||||
degradation, applied to the governance document itself.
|
||||
|
||||
Every check carries a POSITIVE CONTROL in the same run: an absence is not
|
||||
evidence until the instrument is shown capable of detecting presence.
|
||||
(Ratified 2026-07-27 as an epistemic standard, jurist Q2.)
|
||||
|
||||
Built 2026-07-27 on steward authorization. Exit code is always 0 — this is a
|
||||
report, not a gate.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
HOME = Path.home()
|
||||
CLAUDE_MD = HOME / "dotfiles" / "CLAUDE.md"
|
||||
MONTHS = ("january february march april may june july august september "
|
||||
"october november december").split()
|
||||
|
||||
findings: list[str] = []
|
||||
controls: list[tuple[str, bool]] = [] # (label, passed)
|
||||
|
||||
|
||||
def control(label: str, passed: bool) -> bool:
|
||||
controls.append((label, passed))
|
||||
return passed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- load
|
||||
if not CLAUDE_MD.exists():
|
||||
print(f"drift-check: CANNOT RUN — {CLAUDE_MD} not found")
|
||||
sys.exit(0)
|
||||
|
||||
text = CLAUDE_MD.read_text()
|
||||
lines = text.split("\n")
|
||||
|
||||
|
||||
# ------------------------------------------------- 1. referenced paths
|
||||
# Backticked paths that look like filesystem locations.
|
||||
raw = set(re.findall(r"`(~[^`\s]+|/Users/[^`]+?)`", text))
|
||||
paths = {p for p in raw if ("/" in p) and not p.endswith(("`",))}
|
||||
|
||||
control("path-check instrument reaches the filesystem", CLAUDE_MD.exists())
|
||||
for p in sorted(paths):
|
||||
expanded = Path(os.path.expanduser(p.strip().rstrip(".,;")))
|
||||
if not expanded.exists():
|
||||
ln = next((i + 1 for i, l in enumerate(lines) if p in l), None)
|
||||
findings.append(f"L{ln}: path does not resolve — {p}")
|
||||
|
||||
|
||||
# ------------------------------------- 2. named MCP tools / servers
|
||||
# Tool names the document instructs the executor to call.
|
||||
tool_names = set(re.findall(r"`(kg_\w+|diary_\w+|find_tunnels|traverse)`", text))
|
||||
configured: set[str] = set()
|
||||
for cfg in (HOME / ".claude/settings.json", HOME / ".claude/settings.local.json",
|
||||
HOME / ".claude.json", HOME / ".mcp.json"):
|
||||
if cfg.exists():
|
||||
try:
|
||||
configured |= set((json.loads(cfg.read_text()).get("mcpServers") or {}).keys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
control("MCP config readable (>=1 server found somewhere)", bool(configured))
|
||||
if tool_names and not any("mempal" in s.lower() for s in configured):
|
||||
ln = next((i + 1 for i, l in enumerate(lines) if "kg_query" in l), None)
|
||||
findings.append(
|
||||
f"L{ln}: {len(tool_names)} named tools do not resolve — "
|
||||
f"{', '.join(sorted(tool_names))} (no mempalace MCP server configured; "
|
||||
f"servers present: {sorted(configured) or 'none'})"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------- 3. hooks claimed to fire
|
||||
hook_claims = re.findall(r"(Stop and PreCompact hooks fire|PreCompact hook)", text)
|
||||
if hook_claims:
|
||||
live: set[str] = set()
|
||||
for cfg in (HOME / ".claude/settings.json", HOME / ".claude/settings.local.json"):
|
||||
if cfg.exists():
|
||||
try:
|
||||
h = json.loads(cfg.read_text()).get("hooks") or {}
|
||||
live |= {k for k, v in h.items() if v}
|
||||
except Exception:
|
||||
pass
|
||||
control("hooks config readable (>=1 hook event configured)", bool(live))
|
||||
missing = [e for e in ("Stop", "PreCompact") if e not in live]
|
||||
if missing:
|
||||
ln = next((i + 1 for i, l in enumerate(lines) if "PreCompact" in l), None)
|
||||
findings.append(
|
||||
f"L{ln}: document claims these hooks fire, but they are unconfigured — "
|
||||
f"{', '.join(missing)} (configured: {sorted(live) or 'none'})"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------- 4. expired horizons
|
||||
today = date.today()
|
||||
horizon_re = re.compile(r"through\s+(?:the\s+)?end\s+of\s+(\w+)\s+(\d{4})", re.I)
|
||||
control("horizon regex matches a known-present phrase",
|
||||
bool(horizon_re.search(text)) or "through end of" not in text.lower())
|
||||
for i, line in enumerate(lines, 1):
|
||||
for m in horizon_re.finditer(line):
|
||||
month, year = m.group(1).lower(), int(m.group(2))
|
||||
if month in MONTHS:
|
||||
mi = MONTHS.index(month) + 1
|
||||
if (year, mi) < (today.year, today.month):
|
||||
findings.append(
|
||||
f"L{i}: horizon expired — \"{m.group(0)}\" "
|
||||
f"(elapsed {(today.year - year) * 12 + today.month - mi} months)"
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------ 5. structural integrity
|
||||
if not text.endswith("\n"):
|
||||
findings.append(f"L{len(lines)}: no terminal newline "
|
||||
f"(wc -l undercounts by 1 — breaks line-referenced patches)")
|
||||
for i, line in enumerate(lines, 1):
|
||||
# `||` inside a line carrying table pipes = two rows fused onto one line.
|
||||
# Do NOT require the line to start with `|`: the real instance (L243) began
|
||||
# mid-sentence, which is exactly how the fusion hides.
|
||||
if "||" in line and line.count("|") >= 3:
|
||||
findings.append(f"L{i}: table rows fused on one line (`||`) — row will not render")
|
||||
if re.match(r"^\s+\|", line):
|
||||
findings.append(f"L{i}: table row has stray leading whitespace — breaks the table")
|
||||
if re.match(r"^#{2,4} .*\s+$", line) and i < len(lines) and lines[i].startswith("#"):
|
||||
findings.append(f"L{i}: empty heading stub immediately followed by a heading "
|
||||
f"— orphans the section beneath it")
|
||||
|
||||
|
||||
# ------------------------------------------------------------- report
|
||||
failed_controls = [lbl for lbl, ok in controls if not ok]
|
||||
if failed_controls:
|
||||
print("⚠ drift-check: INSTRUMENT NOT VERIFIED — treat results as unestablished")
|
||||
for lbl in failed_controls:
|
||||
print(f" failed control: {lbl}")
|
||||
print()
|
||||
|
||||
if not findings:
|
||||
print(f"✓ governance drift-check: CLAUDE.md clean "
|
||||
f"({len(controls)}/{len(controls)} controls passed, {len(paths)} paths verified)")
|
||||
else:
|
||||
print(f"⚑ governance drift-check: {len(findings)} claim(s) in ~/CLAUDE.md "
|
||||
f"contradicted by substrate")
|
||||
for f in findings:
|
||||
print(f" {f}")
|
||||
print("\n Correction requires [ESCALATE] (Constitutional Constraint #1). "
|
||||
"This report is detection only.")
|
||||
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user