Files
dotfiles/scripts/wake-digest.py
T
David F GliddenandClaude Opus 5 19bddd5ecb [FIX] A home for steward-facing maps; memory pointers made portable and self-diagnosing
Two loose ends closed before the Harrison re-gate, both surfaced by the steward.

Memory pointers. Six links in MEMORY.md / MEMORY-reference.md pointed at files
that all existed, via hand-counted relative depths that resolved from neither of
the memory dir's two addresses (it is ~/dotfiles/claude/memory, symlinked from
~/.claude/projects/…). The wake canary detected this correctly FOUR times over
two days and the banked remedy was to change the canary's path resolution — i.e.
to silence a true positive. The defect was never the pointers: the alarm emitted
one undifferentiated word, MISSING, so every firing had to be re-diagnosed by
hand and the cheapest re-diagnosis is always "known bug". wake-digest.py now
reports four outcomes (ok / mis-authored / dead / non-portable), hands back the
exact replacement, and carries a regression control replaying this bug's shape.
Pointers are home-anchored (~/…), not absolute — steward's correction; absolute
hardcodes this machine into the repo whose purpose is surviving a machine change.

Maps. With the noise gone, one genuine dead pointer surfaced:
arc-current-state-2026-05-07.md, a live ARC dashboard the steward read to orient.
It lived only on the Desktop and went with a tidy-up. A census found four more in
the same condition, zero copies anywhere — including the Making-Sequence
architecture and reading list, load-bearing for current corpus work. The cause is
structural: code, session records and memories are durable; the one artifact
class addressed to the steward had no home. All five now live in maps/ and are
symlinked back to their exact Desktop paths (Desktop view unchanged), moved under
a checksum gate with a positive control. wake-digest.py reports stray Desktop
maps; it never moves them — the Desktop is the steward's.

How to verify:
  python3 scripts/wake-digest.py --selftest     # 28 controls, PASS
  python3 scripts/wake-digest.py | grep -A3 'MEMORY POINTERS'
  cd ~/Desktop && shasum -a 256 *.md            # reads through the symlinks

What was not changed: ~/CLAUDE.md and REVIEWED.md untouched (Constraint #1). No
Desktop file was deleted or renamed. MEMORY-reference.md's May entry is marked
superseded, not rewritten.

Known limitation: maps/ has no successor for the ARC map's FUNCTION — the
open-work register carries the content, but nothing exists that the steward can
open and orient by. Named in the entry rather than quietly closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuMjg3ipEVa3n8CoSzoyvc
2026-07-28 14:10:03 +02:00

600 lines
25 KiB
Python
Executable File

#!/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-<digits>`)
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 <date>' 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} ===")