#!/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 ruled_pendings(reviewed_text): """ The set of PENDING ids that rulings actually DISPOSE OF. Resolved by the PENDING each ruling NAMES in its header, never by the REVIEWED number. Those numbering sequences have drifted apart: REVIEWED-84 rules on PENDING-87. Matching on the number alone suppressed PENDING-84 from the wake digest on the very morning the steward's pulling thread pointed at it, and closing that item later produced no visible change because it had never been counted. Measured at the time: 9 suppressed, 8 correctly, 1 falsely. A ruling whose header names no PENDING (REVIEWED-78, -81, -82 …) suppresses nothing. The correction runs in BOTH directions, and an earlier draft of this docstring claimed otherwise — that it could only ever surface more, never fewer. That was an overclaim, caught by the change proof rather than by reading. Measured against the live files at the time of the fix: 3 items surfaced that had been falsely hidden (PENDING-78, -81, -82 — like-numbered rulings exist, concerning other matters), and 2 stopped being shown that were genuinely ruled (PENDING-87 by REVIEWED-84, PENDING-88 by REVIEWED-85 — no REVIEWED-87 or -88 exists, so the number-match had never suppressed them). Net 18 → 19 visible. Number-matching was wrong in both directions; only subject-matching is right in either. """ return set(re.findall(r"^## REVIEWED-\S+\s*—\s*PENDING-(\S+?)\s*—", reviewed_text, re.M)) def sec_pending(): t = read(PENDING) if t is None: warn.append("PENDING.md unreadable") return [] rt = read(REVIEWED) or "" ruled = ruled_pendings(rt) items = [] for h, ln in open_items(t): m = re.match(r"PENDING-(\S+?)\s*—", h) if m and m.group(1) in ruled: 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("ruled_pendings resolves by the NAMED pending, not the REVIEWED number", ruled_pendings("## REVIEWED-84 — PENDING-87 — Order attestation") == {"87"}) chk("ruled_pendings does NOT suppress the like-numbered item [the 2026-08-01 bug]", "84" not in ruled_pendings("## REVIEWED-84 — PENDING-87 — Order attestation")) chk("ruled_pendings ignores a ruling that names no PENDING", ruled_pendings("## REVIEWED-82 — Read-only MCP server: eyes on the substrate") == set()) chk("ruled_pendings handles non-numeric families", ruled_pendings("## REVIEWED-9 — PENDING-S2 — hook-aware deposit") == {"S2"}) chk("ruled_pendings returns empty on empty input [positive control]", ruled_pendings("") == set()) 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} ===")