#!/usr/bin/env python3 """Wake digest — cheap, computed session-start context. Emits ~600 tokens of CURRENT state at SessionStart, replacing the ~1,000-token session-handoff.md snapshot that had gone five months stale. The governing property: this is DERIVED, never CACHED. A stored digest drifts silently from the substrate — that is precisely how MemPalace's boot context failed. A computation over the substrate cannot drift; it can only have bugs, and bugs are found by positive controls. Every section here degrades honestly: if it cannot be computed it says so rather than emitting nothing. wake-digest.py emit the digest wake-digest.py --selftest prove each extractor detects presence AND absence Provenance: 2026-07-28, steward-authorized alongside the PENDING split and the CLAUDE.md doctrine annotation. Sibling of governance-drift-check.py. """ import os, re, subprocess, sys, time HOME = os.path.expanduser("~") D = os.path.join(HOME, "dotfiles") MEM = os.path.join(HOME, ".claude", "projects", "-Users-davidglidden", "memory") PENDING = os.path.join(D, "PENDING.md") REVIEWED = os.path.join(D, "REVIEWED.md") DRIFT = os.path.join(D, "scripts", "governance-drift-check.py") REPOS = ["CapableMind-AI", "BetterMemories.io", "chamber-library", "animal-davidglidden-eu", "studium-engine"] warn = [] def sh(args, cwd=None, timeout=8): try: r = subprocess.run(args, cwd=cwd, capture_output=True, text=True, timeout=timeout) return r.stdout.strip() if r.returncode == 0 else None except Exception: return None def read(p): try: return open(p, encoding="utf-8").read() except Exception: return None # ---------- extractors (pure functions of text, so --selftest can exercise them) ---------- def item_spans(text): """-> [(header, start, end)] for every item; 1-based start, end exclusive. THE single definition of "an item" for every consumer. Two lessons are welded in: - **Any `## ` header is an item.** A family-specific regex (`^## PENDING-`) hid twenty items on 2026-07-28, ten of them open. Never match on family. - **Fenced blocks are not the document's structure.** Governance drafts are written as plain fenced markdown carrying their own `## REVIEWED-N` headers (the steward's copy-paste-clean practice), so a fenced header would otherwise register as a phantom item AND truncate the real item containing it. Zero such headers exist in the substrate today; the trigger is a drafting habit already in use, so this is a defect waiting on the next draft rather than a hypothetical. """ lines = text.split("\n") heads, fence = [], False for i, l in enumerate(lines): s = l.lstrip() if s.startswith("```") or s.startswith("~~~"): fence = not fence continue if not fence and l.startswith("## "): heads.append((l[3:].strip(), i + 1)) return [(h, s, heads[k + 1][1] if k + 1 < len(heads) else len(lines) + 1) for k, (h, s) in enumerate(heads)] def open_items(text): """-> [(header, line_no)] for every item lacking closure evidence. Closure evidence = a REVIEWED-N (resolved by the caller) or CLOSED/COMPLETED in the header.""" return [(h, s) for h, s, _e in item_spans(text) if "CLOSED" not in h and not h.startswith("COMPLETED")] def tag_of(text, header): m = re.search(re.escape("## " + header) + r".*?\n\*\*Tag:\*\*\s*(\[[A-Z]+\])", text, re.S) return m.group(1) if m else "" def strip_frontmatter(text): """YAML frontmatter restates the thread inside `description:`, and matching there runs the extraction on into `metadata:`. The body is the record.""" m = re.match(r"^---\n.*?\n---\n", text, re.S) return text[m.end():] if m else text def extract_anchor(text, anchor, limit=900): """Verbatim text following an anchor. Never summarised — a digest that paraphrases the steward's own words is the lossy extraction this whole architecture is designed to avoid. Handles both wrap shapes: `**ANCHOR: content**` (label and content share the bold run) and `**ANCHOR for next-Claude:** content` (label closes first).""" text = strip_frontmatter(text) i = text.find(anchor) if i < 0: return None s = text[i + len(anchor): i + len(anchor) + limit + 120] c = s.find(":") if 0 <= c <= 40: # remainder of the label, not content s = s[c + 1:] cut = re.search(r"\n\s*\n", s) if cut: s = s[: cut.start()] s = re.sub(r"\s+", " ", s.replace("**", "")).strip(" *—-") return (s[:limit].rstrip() + " …[truncated]") if len(s) > limit else s # ---------- sections ---------- def sec_pause(): files = [f for f in os.listdir(MEM) if f.startswith("session-") and not f.startswith("session-ledger-")] if not files: warn.append("no session memory files found") return None, None newest = max(files, key=lambda f: os.path.getmtime(os.path.join(MEM, f))) age = time.time() - os.path.getmtime(os.path.join(MEM, newest)) h = age / 3600 span = f"{age/60:.0f} min" if h < 1 else (f"{h:.1f} h" if h < 48 else f"{h/24:.1f} days") return newest, span def sec_pending(): t = read(PENDING) if t is None: warn.append("PENDING.md unreadable") return [] rt = read(REVIEWED) or "" rev = {m for m in re.findall(r"^## REVIEWED-(\S+)", rt, re.M)} items = [] for h, ln in open_items(t): m = re.match(r"PENDING-(\S+?)\s*—", h) if m and m.group(1) in rev: continue items.append((h, ln, tag_of(t, h))) if not items: warn.append("PENDING.md parsed but yielded no open items — check the parser") return items def sec_reviewed(n=3): t = read(REVIEWED) if t is None: warn.append("REVIEWED.md unreadable") return [] hs = re.findall(r"^## (REVIEWED-\S+ —.*)$", t, re.M) out = [] for h in hs[-n:]: m = re.search(re.escape("## " + h) + r".*?\n\*\*Decision:\*\*\s*(\w+)", t, re.S) out.append((h[:88], m.group(1) if m else "?")) return out def sec_drift(): o = sh([sys.executable, DRIFT]) if o is None: warn.append("drift check did not run") return None if "INSTRUMENT NOT VERIFIED" in o: return "INSTRUMENT NOT VERIFIED — treat as unestablished" m = re.search(r"(\d+) claim", o) return m.group(1) if m else "0" def sec_repos(): rows = [] for r in REPOS: p = os.path.join(HOME, "_Dev", r) if not os.path.isdir(os.path.join(p, ".git")): continue sb = sh(["git", "-C", p, "status", "-sb", "--porcelain"]) or "" lines = sb.split("\n") head = lines[0][3:] if lines and lines[0].startswith("##") else "?" dirty = sum(1 for l in lines[1:] if l.strip()) subj = sh(["git", "-C", p, "log", "-1", "--format=%s"]) or "?" rows.append((r, head, dirty, subj[:60])) if not rows: warn.append("no repos resolved") return rows def sec_thread(newest): t = read(os.path.join(MEM, newest)) if newest else None if t is None: warn.append("session file unreadable — no thread or question") return None, None th = extract_anchor(t, "PULLING THREAD", 400) q = extract_anchor(t, "LITERAL QUESTION", 900) if th is None: warn.append("no PULLING THREAD anchor in the last wrap") if q is None: warn.append("no LITERAL QUESTION anchor in the last wrap") return th, q # ---------- the .app brief ---------- # The jurist reads Claude.app's personal preferences and has NO filesystem access, so # unlike CLAUDE.md that document cannot compute its state — it can only cache it. A cache # drifts; the mitigations are to keep it small, generate rather than hand-write it, and # date it so staleness is visible rather than misleading (Constitutional Constraint #4 # across a boundary we cannot check). This emits a paste-ready block and records when it # was generated, so the wake can report the age. BRIEF_PATH = os.path.join(D, "claude", "app-brief.md") def trackers(): t = read(os.path.join(MEM, "MEMORY.md")) if t is None: warn.append("MEMORY.md unreadable — no workstreams for the brief") return [] m = re.search(r"^## Canonical Workstream Trackers\s*$(.*?)^## ", t, re.S | re.M) if not m: warn.append("MEMORY.md has no 'Canonical Workstream Trackers' heading") return [] def clip(s, n): s = re.sub(r"\s+", " ", s).strip() if len(s) <= n: return s return s[:n].rsplit(" ", 1)[0] + "…" out = [] for line in m.group(1).split("\n"): if not line.startswith("- "): continue body = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", line[2:]) # unlink body = re.sub(r"[*`]", "", body) name, _, gist = body.partition(" — ") out.append((clip(name, 38), clip(gist, 72))) return out def commits_30d(path): o = sh(["git", "-C", path, "log", "--since=30.days", "--oneline"]) return len(o.split("\n")) if o else 0 def brief(stamp): """Replaces ONLY the projects half of the .app preferences' §Standing Context. The personal entries there (the orchestra conflict, the divorce, fraternal practice) are steward-held, unverifiable from any substrate, and must never be generated.""" items = sec_pending() o = [f"### Standing Context — Projects *(generated {stamp}; do not hand-edit)*", "", "Regenerate: `python3 ~/dotfiles/scripts/wake-digest.py --brief`. This is a snapshot,", "not a live view — the reader of this document has no filesystem access, so it cannot", "be computed here. If today is more than ~30 days after the date above, treat every", "line as unverified rather than current. Personal standing context is kept separately", "and by hand.", "", "TRACKER INDEX (from MEMORY.md — what exists, not what is hot; see repo activity" " below for that)"] for n, tail in trackers(): o.append(f" {n:<38} {tail}") o.append(f"\nOPEN AUTHORIZATION ITEMS ({len(items)}) — full text in ~/PENDING.md; " "closed items in ~/PENDING-archive.md") for h, _ln, tag in items: o.append(f" {tag:<12} {h[:92]}") o.append("\nLAST RULINGS") for h, d in sec_reviewed(4): o.append(f" {d:<10} {h}") o.append("\nREPO ACTIVITY (commits, last 30 days)") for r in REPOS: p = os.path.join(HOME, "_Dev", r) if os.path.isdir(os.path.join(p, ".git")): o.append(f" {r:<24} {commits_30d(p):>4}") o.append(f"\nGOVERNANCE DRIFT — ~/CLAUDE.md: {sec_drift()} substrate-contradicted claim(s)") o.append("\n*(end generated block)*") return "\n".join(o) + "\n" def brief_age_days(): """Age from the date written INSIDE the brief, never from mtime: git does not preserve mtime, so a fresh clone would make a stale brief read as newly generated — false freshness inside the freshness instrument.""" t = read(BRIEF_PATH) if t is None: return None m = re.search(r"generated (\d{4})-(\d{2})-(\d{2})", t) if not m: warn.append("app-brief.md carries no 'generated ' stamp — age unknown") return None y, mo, d = (int(x) for x in m.groups()) return (time.time() - time.mktime((y, mo, d, 12, 0, 0, 0, 0, -1))) / 86400 # ---------- memory pointers ---------- # # Provenance: 2026-07-28. Earned expensively. Six pointers in MEMORY.md / # MEMORY-reference.md pointed at files that all EXIST via paths that never # resolved (two at depth 4, four at depth 5 — hand-counted, never checked). # The wake canary detected this and FIRED FOUR TIMES over two days. Each firing # was re-diagnosed by hand and dismissed as "known canary bug", and the banked # remedy was to change the canary's path resolution — i.e. to make the # instrument agree with the broken data and stop reporting a TRUE POSITIVE. # # The defect was never the pointers; it was that the alarm said only MISSING. # An undifferentiated alarm has to be re-diagnosed on every firing, and the # cheapest re-diagnosis is always "known bug". So this reports three outcomes, # never one, and hands back the exact replacement string: # # OK resolves from the file's real location # MIS-AUTHORED target exists, path as written does not reach it -> fix given # DEAD no file of that name anywhere -> a decision # # Relative pointers are resolved against the file's REAL directory, because the # memory dir has two live addresses (~/.claude/projects/…/memory is a symlink to # ~/dotfiles/claude/memory) and a relative escape is only ever valid from one of # them. That ambiguity is structural: any pointer LEAVING the directory must be # home-anchored (`~/…`) to be correct via both routes. # # Home-anchored, NOT absolute — steward's correction, same session. `/Users/ # davidglidden/…` resolves today and hardcodes this machine into a dotfiles repo # whose whole purpose is surviving a machine change. `expanduser` is already the # idiom three lines into this file, and `~/_Dev/…` is already the idiom in the # memory files' own prose (17 uses). Absolute still VALIDATES here — it is not # wrong, only unportable — but it is counted and reported so it cannot re-enter # silently. NONPORTABLE_RE = re.compile(r"^/(Users|home)/") MEM_INDEXES = ["MEMORY.md", "MEMORY-reference.md"] POINTER_RE = re.compile(r"\]\(([^)\s]+\.md)\)") def find_basename(name): """Bounded search for a file of this basename. None => genuinely dead.""" local = os.path.join(os.path.realpath(MEM), name) if os.path.exists(local): return local hit = sh(["find", os.path.join(HOME, "_Dev"), "-maxdepth", "6", "-name", name, "-not", "-path", "*/node_modules/*", "-not", "-path", "*/.git/*"], timeout=8) return hit.splitlines()[0] if hit else None def resolve(raw, base): """Home-anchored, absolute, or relative-to-the-file's-real-directory.""" if raw.startswith("~"): return os.path.expanduser(raw) if os.path.isabs(raw): return raw return os.path.join(base, raw) def classify_pointers(text, base): """(n_checked, mis_authored, dead, nonportable) — outcomes, never one word.""" n, mis, dead, nonport = 0, [], [], [] for m in POINTER_RE.finditer(text): raw = m.group(1) if raw.startswith(("http://", "https://", "mailto:")): continue n += 1 line = text.count("\n", 0, m.start()) + 1 if NONPORTABLE_RE.match(raw): nonport.append((line, raw)) if os.path.exists(resolve(raw, base)): continue found = find_basename(os.path.basename(raw)) if found: home = os.path.realpath(HOME) real = os.path.realpath(found) fix = "~" + real[len(home):] if real.startswith(home + os.sep) else real mis.append((line, raw, fix)) else: dead.append((line, raw, None)) return n, mis, dead, nonport def sec_pointers(): total, mis, dead, nonport = 0, [], [], [] for name in MEM_INDEXES: path = os.path.realpath(os.path.join(MEM, name)) text = read(path) if text is None: warn.append(f"memory pointers: {name} unreadable") continue n, m_, d_, np_ = classify_pointers(text, os.path.dirname(path)) total += n mis += [(name,) + t for t in m_] dead += [(name,) + t for t in d_] nonport += [(name,) + t for t in np_] return total, mis, dead, nonport # ---------- stray maps ---------- # # A "map" is an artifact the steward reads directly to orient (dashboard, corpus # index, reading list). Provenance 2026-07-28: arc-current-state-2026-05-07.md # was exactly that, lived only on the Desktop, and vanished with a tidy-up; a # census then found four more in the same condition, zero copies anywhere. Maps # now live in ~/dotfiles/maps and are symlinked back to the Desktop. This reports # any Desktop .md that is NOT such a symlink — it never moves anything, because # the Desktop is the steward's. See maps/README.md. DESKTOP = os.path.join(HOME, "Desktop") MAPS_DIR = os.path.join(D, "maps") def is_homed(path, maps_root): """True iff path is a symlink resolving inside maps_root.""" if not os.path.islink(path): return False return os.path.realpath(path).startswith(os.path.realpath(maps_root) + os.sep) def stray_maps(): try: return [n for n in sorted(os.listdir(DESKTOP)) if n.endswith(".md") and not is_homed(os.path.join(DESKTOP, n), MAPS_DIR)] except Exception: warn.append("stray-map check: Desktop unreadable") return [] # ---------- self-test ---------- def selftest(): ok = True def chk(name, cond): nonlocal ok ok = ok and cond print(f" [{'ok ' if cond else 'FAIL'}] {name}") print("extractor controls (presence AND absence):") chk("open_items finds a plain item", open_items("## PENDING-1 — x") == [("PENDING-1 — x", 1)]) chk("open_items finds a non-numeric family [v1 splitter's blind spot]", open_items("## PENDING-S2 — x\n## PENDING — ICP-19") and len(open_items("## PENDING-S2 — x\n## PENDING — ICP-19")) == 2) chk("open_items excludes CLOSED / COMPLETED", open_items("## PENDING-1 — x (CLOSED)\n## COMPLETED — y") == []) chk("open_items returns empty on empty input", open_items("") == []) FENCED = "## PENDING-9 — real\nbody\n```markdown\n## REVIEWED-9 — a draft\n```\ntail" chk("open_items ignores a header inside a fenced block [drafting-habit trigger]", [h for h, _ in open_items(FENCED)] == ["PENDING-9 — real"]) chk("open_items DOES find that same header unfenced [positive control for the line above]", [h for h, _ in open_items(FENCED.replace("```markdown\n", "").replace("```\n", ""))] == ["PENDING-9 — real", "REVIEWED-9 — a draft"]) chk("item_spans runs a fenced item to the tail, not to the fenced header", item_spans(FENCED)[0][2] == 7) chk("tag_of reads a tag", tag_of("## PENDING-9 — t\n**Date:** d\n**Tag:** [ESCALATE]\n", "PENDING-9 — t") == "[ESCALATE]") chk("tag_of returns '' when absent", tag_of("## PENDING-9 — t\n", "PENDING-9 — t") == "") chk("extract_anchor pulls verbatim", extract_anchor("noise\n**PULLING THREAD:** do the thing.\n\nmore", "PULLING THREAD") == "do the thing.") chk("extract_anchor handles label-inside-bold (**A: content**)", extract_anchor("**PULLING THREAD: do the thing.**\n\nmore", "PULLING THREAD") == "do the thing.") chk("extract_anchor drops a label remnant (**A for next-Claude:** content)", extract_anchor("**LITERAL QUESTION for next-Claude:** Is it?\n\nx", "LITERAL QUESTION") == "Is it?") chk("extract_anchor skips YAML frontmatter", extract_anchor('---\ndescription: "PULLING THREAD: the wrong one."\ntype: x\n---\n' "**PULLING THREAD: the right one.**\n\nz", "PULLING THREAD") == "the right one.") chk("extract_anchor marks truncation", extract_anchor("**A:** " + "w " * 200, "A", limit=50).endswith("…[truncated]")) chk("extract_anchor returns None when the anchor is absent", extract_anchor("no anchor here", "PULLING THREAD") is None) MEMDIR = os.path.realpath(MEM) TOUCH = "~/_Dev/studium-engine/docs/the-chamber-touchstone.md" chk("classify_pointers accepts an in-directory relative pointer", classify_pointers("see [x](MEMORY.md)", MEMDIR) == (1, [], [], [])) chk("classify_pointers accepts a HOME-ANCHORED pointer [the chosen form]", classify_pointers(f"[t]({TOUCH})", MEMDIR) == (1, [], [], [])) chk("classify_pointers calls a real-target/bad-path pointer MIS-AUTHORED" " [the 4-firing bug, as a regression control]", (lambda r: r[0] == 1 and len(r[1]) == 1 and not r[2])( classify_pointers( "[t](../../../../_Dev/studium-engine/docs/the-chamber-touchstone.md)", MEMDIR))) chk("...and the fix it hands back is home-anchored, not machine-absolute", classify_pointers("[t](../../../../_Dev/studium-engine/docs/" "the-chamber-touchstone.md)", MEMDIR)[1][0][2] == TOUCH) chk("classify_pointers calls a nowhere-target pointer DEAD [must NOT collapse" " into mis-authored]", (lambda r: r[0] == 1 and not r[1] and len(r[2]) == 1)( classify_pointers("[x](no-such-file-anywhere-xyzzy-9931.md)", MEMDIR))) chk("classify_pointers flags a machine-absolute pointer NON-PORTABLE even though" " it resolves [prevents silent re-entry]", (lambda r: len(r[3]) == 1 and not r[1] and not r[2])( classify_pointers(f"[t]({os.path.expanduser(TOUCH)})", MEMDIR))) chk("classify_pointers ignores http links", classify_pointers("[x](https://a.md)", MEMDIR) == (0, [], [], [])) chk("classify_pointers returns clean on empty input", classify_pointers("", MEMDIR) == (0, [], [], [])) chk("is_homed TRUE for a Desktop map symlinked into maps/ [live substrate]", is_homed(os.path.join(DESKTOP, "corpus-index-2026-07-26.md"), MAPS_DIR)) chk("is_homed FALSE for a regular file [negative control — must not pass everything]", not is_homed(os.path.join(MAPS_DIR, "README.md"), MAPS_DIR)) chk("is_homed FALSE for a symlink pointing OUTSIDE maps/", not is_homed(os.path.join(HOME, ".claude"), MAPS_DIR)) print("\nlive substrate:") chk("PENDING.md readable", read(PENDING) is not None) chk("REVIEWED.md readable", read(REVIEWED) is not None) chk("drift check runs", sec_drift() is not None) chk("memory dir resolves", os.path.isdir(MEM)) print("\nSELFTEST", "PASS" if ok else "FAIL") return 0 if ok else 2 # ---------- render ---------- def main(): newest, span = sec_pause() th, q = sec_thread(newest) items, revs, drift, repos = sec_pending(), sec_reviewed(), sec_drift(), sec_repos() o = ["=== WAKE DIGEST (computed now — not a stored snapshot) ==="] o.append(f"Last wrap: {span} ago ({newest})" if span else "Last wrap: UNKNOWN") if th: o.append(f"\nPULLING THREAD — {th}") if q: o.append(f"\nOPEN QUESTION — {q}") o.append(f"\nOPEN AUTHORIZATION ITEMS ({len(items)}) — bodies in ~/PENDING.md at the line given;" " closed items are in ~/PENDING-archive.md") for h, ln, tag in items: o.append(f" L{ln:<5} {tag:<12} {h[:78]}") if revs: o.append("\nLAST RULINGS") for h, d in revs: o.append(f" {d:<10} {h}") o.append(f"\nGOVERNANCE DRIFT — ~/CLAUDE.md: {drift} substrate-contradicted claim(s)" " (detection only; correction needs [ESCALATE])") ptot, pmis, pdead, pnp = sec_pointers() o.append(f"\nMEMORY POINTERS — {ptot} checked · {len(pmis)} mis-authored · {len(pdead)} dead" + (f" · {len(pnp)} non-portable" if pnp else "")) for f, ln, raw, fix in pmis: o.append(f" MIS-AUTHORED {f}:{ln} {raw}") o.append(f" → target exists; replace with: {fix}") for f, ln, raw, _ in pdead: o.append(f" DEAD {f}:{ln} {raw} (no file of that name in ~/_Dev or the memory dir)") for f, ln, raw in pnp: o.append(f" NON-PORTABLE {f}:{ln} {raw} (resolves, but hardcodes this machine)") strays = stray_maps() if strays: o.append(f"\nSTRAY MAPS — {len(strays)} Desktop .md with no tracked copy in" " ~/dotfiles/maps/ (one tidy-up from gone; see maps/README.md)") for s in strays: o.append(f" {s}") age = brief_age_days() if age is None: o.append("\n.APP BRIEF — never generated. The jurist's §Standing Context cannot be" " checked from here; run `wake-digest.py --brief` and paste it.") elif age > 30: o.append(f"\n.APP BRIEF — last GENERATED {age:.0f} days ago. This tracks generation," " not pasting: it is a lower bound on the jurist's staleness, never a" " guarantee of freshness.") if repos: o.append("\nREPOS") for r, head, dirty, subj in repos: flag = f" ~{dirty} dirty" if dirty else "" o.append(f" {r:<24} {head:<34}{flag} {subj}") if warn: o.append("\n⚠ DIGEST DEGRADED — these sections could not be computed:") for w in warn: o.append(f" - {w}") o.append("=== END WAKE DIGEST ===") print("\n".join(o)) def emit_brief(): stamp = sh(["date", "+%Y-%m-%d"]) or "date-unavailable" text = brief(stamp) os.makedirs(os.path.dirname(BRIEF_PATH), exist_ok=True) open(BRIEF_PATH, "w", encoding="utf-8").write(text) print(text) print(f"[written to {BRIEF_PATH} — the wake reports this file's age]", file=sys.stderr) if warn: print("⚠ brief degraded: " + "; ".join(warn), file=sys.stderr) if __name__ == "__main__": try: if "--brief" in sys.argv: sys.exit(emit_brief()) sys.exit(selftest() if "--selftest" in sys.argv else main()) except Exception as e: # never break session start print(f"=== WAKE DIGEST UNAVAILABLE — {type(e).__name__}: {e} ===")