#!/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 calendar, json, 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. Matching is CASE-INSENSITIVE. /wrap-up names these fields in prose ("The pulling thread", "**Pulling thread:**") and has never mandated a case, so a case-sensitive reader demands of the wrap a shape its own producer was never told to emit. Censused 2026-08-23 by running this function over all 195 wrap files: 86 threads and 84 questions sat in the body, correctly written, and were unreadable for that reason alone — including the last three wraps. Shapes handled (the four the wraps actually emit): **ANCHOR:** content label closes first **ANCHOR: content** label and content share the bold run **ANCHOR for next-Claude:** content label runs long **ANCHOR** *(aside)*:\n\ncontent content in the NEXT paragraph The label ends at the first ':' ON THE ANCHOR'S OWN LINE. The old 40-char window was a proxy for "same line"; when an aside pushed the colon past it the function returned THE LABEL and the wake inherited a heading in place of the question (2026-08-22). Where the anchor appears more than once, a match in LABEL POSITION wins over an earlier narrative mention — case-insensitivity would otherwise let "…as the pulling thread showed…" outrank the field it is describing. """ text = strip_frontmatter(text) cands = list(re.finditer(re.escape(anchor), text, re.I)) if not cands: return None def in_label_position(m): bol = text.rfind("\n", 0, m.start()) + 1 pre = re.sub(r"(?i)\b(the|a|our)\b", "", text[bol:m.start()]) return pre.strip(" *#->\t") == "" m = next((c for c in cands if in_label_position(c)), cands[0]) s = text[m.end():] line, nl, rest = s.partition("\n") c = line.find(":") if c >= 0: # remainder of the label, not content line = line[c + 1:] if not line.replace("*", "").strip(): # label ended the line — content is below s = rest.lstrip("\n") else: s = line + nl + rest 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 # By git add-time, NOT mtime. Editing an old wrap record — a CODA, a # correction, a mechanical repair — moves its mtime to today and makes it # look like the newest wrap. That is not hypothetical: on 2026-08-17 a # frontmatter repair touched 20 April–May records at once and this line # promoted a session from April to "last wrap", losing the thread and the # question in the same stroke. Add-time cannot be moved by an edit. # Residual, stated: add-time is when the wrap was COMMITTED, which lags when # it was written (08-14's record was committed 08-15 09:16, ~14 h later). So # `Last wrap` is an upper bound on elapsed time, never exact — but it is wrong # by hours, where mtime was wrong by months. when = {os.path.basename(p): e for p, e, _s in wrap_events()} stamp = lambda f: when.get(f, os.path.getmtime(os.path.join(MEM, f))) newest = max(files, key=stamp) age = time.time() - stamp(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 wrap_records(): """Wrap records only — session-ledger-*.md is a Symmetria artifact, not a wrap.""" return [os.path.join(MEM, f) for f in os.listdir(MEM) if f.startswith("session-") and not f.startswith("session-ledger-")] def parse_iso(s): """ISO-8601 -> true epoch, honouring the zone suffix. THE 2026-08-17 BUG, and it is the whole reason the alarm below misfired. Transcript timestamps are UTC (`2026-08-14T07:29:36.852Z`). The previous implementation dropped the `Z` and called `time.mktime`, which reads a struct_time as LOCAL — putting the span in a different time base from `os.path.getmtime()`, which returns a true epoch. Measured that day: a +2 h (CEST) skew against a 15 min tolerance. Since the skew exceeds the tolerance, a wrap written at the end of a session could NEVER land inside the window — so `PREVIOUS SESSION DID NOT WRAP` was systematic, not occasional. It had been overridden by hand at two consecutive wakes. Naive strings (no zone) are still read as local: that is what they mean. """ m = re.match(r"(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})", s or "") if not m: return None try: st = time.strptime(f"{m.group(1)}T{m.group(2)}", "%Y-%m-%dT%H:%M:%S") except (ValueError, OverflowError): return None z = re.match(r"(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})", s[m.end():]) if not z: try: return time.mktime(st) except (ValueError, OverflowError): return None tag = z.group(1) if tag == "Z": return calendar.timegm(st) sign = 1 if tag[0] == "+" else -1 return calendar.timegm(st) - sign * (int(tag[1:3]) * 3600 + int(tag[-2:]) * 60) def transcript_span(path): """ A session's true span, from the timestamps INSIDE the transcript — never mtime. mtime says when the file was last touched; it cannot say when the session ran. Returns (first_epoch, last_epoch), or (None, None) if unreadable. """ first = last = None try: with open(path, errors="ignore") as f: for line in f: m = re.search(r'"timestamp"\s*:\s*"([^"]{19,40})"', line) if m: e = parse_iso(m.group(1)) if e is None: continue if first is None: first = e last = e except OSError: return None, None return first, last def wrap_events(): """-> [(path, epoch, source)] — when each wrap record CAME INTO EXISTENCE. Not mtime. A wrap record edited later — a CODA appended, a correction made — carries the edit's mtime, so an ordinary and correct act destroys the evidence that the wrap happened inside its own session. Measured 2026-08-17: the 08-14 record's mtime read 08-17 for exactly that reason. Git's add-time cannot move once committed; mtime is the fallback for a not-yet-committed record and is labelled so the caller can tell the two apart. """ added, at = {}, None out = sh(["git", "-C", D, "log", "--diff-filter=A", "--format=%at", "--name-only", "--", os.path.relpath(os.path.realpath(MEM), D)]) for line in (out or "").split("\n"): line = line.strip() if re.fullmatch(r"\d{9,11}", line): at = int(line) elif line and at is not None: base = line.rsplit("/", 1)[-1] if base not in added or at < added[base]: added[base] = at # earliest add wins ev = [] for p in wrap_records(): base = os.path.basename(p) if base in added: ev.append((p, added[base], "git")) else: try: ev.append((p, os.path.getmtime(p), "mtime")) except OSError: pass return ev def previous_transcript(now, spans): """The session before this one, selected by SPAN — never by 'quiet for >60 s'. The old rule excluded any transcript touched within 60 s as 'the current session'. That is wrong at precisely the moment it matters: on 2026-08-17 the previous session had ended 12 s before the wake, was excluded as too recent, and the digest reported the session from four days earlier instead — 'ended ~Aug 13 21:36'. The defect is time-dependent and disappears ~60 s later, which is why re-running the digest afterwards showed nothing wrong. THIS session's transcript is the one most recently STARTED that is still being appended. Keying on start alone breaks once the current session has run a while; keying on end alone cannot tell a live session from one that ended seconds ago. Both together identify it, and at most one is ever excluded — so a just-ended prior session can no longer be skipped. Residual, stated rather than hidden: if the current session's transcript holds no parseable timestamp yet, the newest START is the previous session, and if it also ended moments ago it is excluded and the one before it is reported. That is the old failure in a much smaller window, and it is why the caller treats a 'no wrap' result as advisory rather than as proof. """ dated = [r for r in spans if r[1] is not None and r[2] is not None] if not dated: return None live = max(dated, key=lambda r: r[1]) # newest start … rest = [r for r in dated if r is not live] if live[2] >= now - 120 else dated return max(rest, key=lambda r: r[2]) if rest else None # … and still growing def wrap_verdict(span, events, lead=300, lag=10800): """-> (state, detail). FOUR outcomes, because 'I could not tell' is two things. REVIEWED-104: a check whose subject can be absent may not be two-valued, and the third outcome is itself two kinds — subject absent (environment, must not block) versus check broken (defect). The previous version folded both into silence via `verdict is None`, so 'I could not look' read as 'it wrapped'. """ a, b = span if a is None or b is None: return "unassessable-subject", ("the previous session's transcript carries " "no readable timestamps") if not events: return "unassessable-check", "no wrap records could be enumerated" if any(a - lead <= e <= b + lag for _p, e, _s in events): return "wrapped", "" # No event in the window. Before alarming, ask whether some wrap record NAMES a # day inside this span: commit lag can push real evidence past the window, and # an alarm that cannot be substantiated is worse than an honest non-answer. days, t, guard = set(), int(a), 0 while t <= int(b) + 86400 and guard < 60: days.add(time.strftime("%Y-%m-%d", time.localtime(t))) t += 86400 guard += 1 named = [os.path.basename(p) for p, _e, _s in events if any(d in os.path.basename(p) for d in days)] if named: return ("unassessable-check", f"no wrap event inside the span, but {named[0]} names a day within " f"it — a real wrap with lagged evidence cannot be told apart from " f"another session's record") return "unwrapped", "" def previous_session(): """THE single selection of 'the session before this one', for every consumer. Lifted out of sec_unwrapped 2026-08-31 when the loop check (REVIEWED-131) needed the same transcript. This file already refuses a second definition of 'an item' (item_spans); a second definition of 'the previous session' would be the same bug at a different site. -> (path, start_epoch, end_epoch) or None. """ proj = os.path.dirname(os.path.join(HOME, ".claude", "projects", "-Users-davidglidden", "memory")) try: tx = [os.path.join(proj, f) for f in os.listdir(proj) if f.endswith(".jsonl")] except OSError: warn.append("session transcripts unreadable — wrap check did not run") return None if not tx: return None return previous_transcript(time.time(), [(p,) + transcript_span(p) for p in tx]) def sec_unwrapped(): """ PENDING-S2's obligation, rebuilt on our own substrate. A session that ends without /wrap-up writes no memory file, so `Last wrap` — computed from mtime — silently reports the session BEFORE it, and the thread below is inherited from the wrong session. The original proposal detected this via a MemPalace Stop hook; that hook is retired, but the failure it named is live. The transcripts are the only witness that a session ran at all. Rebuilt 2026-08-17, after the alarm fired falsely at three consecutive wakes and was overridden by hand at two of them. FOUR defects were found, and only one was the one the work had been scoped to: 1. a UTC/local time-base mismatch (see `parse_iso`) — THE CAUSE, and large enough that an end-of-session wrap could never be seen; 2. a selection rule that skipped a session which had just ended; 3. wrap evidence read from mutable mtimes; 4. a two-valued verdict over an assessment that can fail. Fixing only (4), as scoped, would have left the false alarms firing. """ prev = previous_session() if prev is None: return None _path, a, b = prev state, detail = wrap_verdict((a, b), wrap_events()) if state == "wrapped": return None ended = time.strftime("%b %d %H:%M", time.localtime(b)) if state == "unwrapped": return (f"⚠ PREVIOUS SESSION DID NOT WRAP (ended ~{ended}). The thread and " f"question below are inherited from an OLDER session — treat them as " f"possibly stale, and expect no record of what that session did.\n" f" Not established: whether that session did work worth keeping. The " f"transcript is on disk and can be read if the gap matters.") return (f"◌ WRAP STATUS UNDETERMINED for the previous session (ended ~{ended}) — " f"{detail}.\n" f" This is neither an alarm nor a clean pass: the check could establish " f"neither. Open the newest session record to see what it actually holds.") 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 suppresses nothing — which is the rule's correct behaviour in general and its FALSE-OPEN class in particular. See below. 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 (PENDING-78, -81, -82), 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. ⚠ CORRECTED 2026-08-17 (PENDING-142; jurist-ruled to correct independently of the mechanism). This docstring previously described those 3 as *"falsely hidden"* by "like-numbered rulings … concerning other matters." **Both halves are false, and the substrate says so in its own words.** REVIEWED-78/-81/-82 are the AUTHORIZED rulings ON PENDING-78/-81/-82 — same date (2026-07-28), same titles verbatim, and REVIEWED-81 names "PENDING-81" twice in its body. They are like-numbered ON PURPOSE: REVIEWED-78's own Notes state it was filed as a separate entry *"for a mechanical reason: the closure rule in `wake-digest.py` matches a PENDING item to `REVIEWED-`, so a cross-numbered closure stated only in prose would leave PENDING-78 listed as open at every wake."* So the number→subject fix broke the three entries that had been deliberately authored to satisfy the rule it replaced, and then recorded their compliance as coincidence. Surfacing them was a REGRESSION, not a repair; they have read open since 2026-07-28. The change proof could not see it because it measured a COUNT (18 → 19) while the claim was about each item's disposition — and the counts stay equal: removing 3 false-opens and restoring 3 false-closeds both leave 29. Number-matching was wrong in both directions. Subject-matching is right in neither until it matches on the SUBJECT — the title or the decision — rather than on the presence of an id token in a header. Remedy is PENDING-142, unbuilt; this note exists so the code does not keep asserting the opposite while it waits. """ 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. # # 2026-08-10 — three completions, all earned before this code existed. # # (1) CODE SPANS. The matcher read `](file.md)` inside backticks as a pointer, # so on 2026-08-09 it reported exactly one DEAD pointer: the link pattern # written inside MEMORY.md's own SPECIFICATION OF THIS CANARY. An # instrument that flags its own documentation flags it every wake, forever, # and the real signal drowns in a permanent known-false line — the same # "known canary bug" dismissal the block above was written to end, arriving # by a second route. Code spans and fenced blocks are blanked before # matching, offsets preserved so every reported line number stays true. # Inline spans may not cross a newline: a stray unmatched backtick must # bound its damage to one line rather than blanking the rest of the file # and HIDING dead pointers. An unterminated fence simply does not match, # so the failure direction is over-reporting, never silence. # # (2) WIKILINKS. `reference-verification-ladder.md` has specified the canary as # covering "every `](file.md)` and `[[wikilink]]`" since 2026-07-06; only # the first half was ever built. Reported as UNWRITTEN, not DEAD, because # ~/CLAUDE.md rules a wikilink with no file yet to be legitimate — it marks # something worth writing. A permitted forward-reference and a broken index # pointer are different findings and must not share a word. # # (3) PRE-EXISTING vs NEWLY BROKEN. The 2026-08-09 spec asked for it. Derived # from git, NOT from a stored prior run: a state file would make this the # one cached section in a digest whose governing property is that it is # computed. Where git cannot answer — a target outside this repo — it says # so rather than guessing. NONPORTABLE_RE = re.compile(r"^/(Users|home)/") MEM_INDEXES = ["MEMORY.md", "MEMORY-reference.md"] POINTER_RE = re.compile(r"\]\(([^)\s]+\.md)\)") WIKILINK_RE = re.compile(r"\[\[([^\[\]|#\n]+?)(?:#[^\[\]|\n]*)?(?:\|[^\[\]\n]*)?\]\]") FENCE_RE = re.compile(r"^(?P`{3,}|~{3,})[^\n]*\n.*?^(?P=f)[^\n]*$", re.M | re.S) INLINE_CODE_RE = re.compile(r"`+[^`\n]*`+") def blank_code(text): """Blank code-span and fenced-block CONTENT, preserving offsets and newlines. Preserving offsets is the point: every line number this module reports is computed from the blanked text, so it must still address the real line. """ def blank(m): return "".join("\n" if c == "\n" else " " for c in m.group(0)) return INLINE_CODE_RE.sub(blank, FENCE_RE.sub(blank, text)) 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, [], [], [] text = blank_code(text) 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 near_slugs(slug, memdir): """Memory files this wikilink was plainly REACHING FOR. Prefix-family only. `[[trust-prior-pass-frame]]` wants `feedback-trust-prior-pass-frame.md`; the slug is the tail of the real stem after a hyphen. Matching on that boundary (never on a bare substring) keeps `[[arc]]` from claiming every file with "arc" inside a word. """ out = [] try: stems = [f[:-3] for f in os.listdir(memdir) if f.endswith(".md")] except OSError: return out for stem in stems: if stem.endswith("-" + slug) or slug.endswith("-" + stem): out.append(stem) return sorted(out) def classify_wikilinks(text, memdir): """(n_checked, mis_authored, unwritten) — three outcomes, never one word. The same discipline as the path pointers above, and for the same reason: the first build of this function reported only UNWRITTEN, and its two live hits were both `[[trust-prior-pass-frame]]`, whose file EXISTS as `feedback-trust-prior-pass-frame.md`. One word would have sent a fixable typo to the wake every morning wearing the label "nothing to do here". UNWRITTEN, never DEAD, for the genuine misses: ~/CLAUDE.md rules that a `[[name]]` matching no file yet "is fine — it marks something worth writing later, not an error." A permitted forward-reference is not a defect. """ n, mis, unwritten = 0, [], [] text = blank_code(text) for m in WIKILINK_RE.finditer(text): slug = m.group(1).strip() if not slug: continue n += 1 stem = slug[:-3] if slug.endswith(".md") else slug if os.path.exists(os.path.join(memdir, stem + ".md")): continue line = text.count("\n", 0, m.start()) + 1 near = near_slugs(stem, memdir) if near: mis.append((line, slug, " | ".join(near))) else: unwritten.append((line, slug)) return n, mis, unwritten # Breakage age is DERIVED from git, never from a stored prior run — a cache is # the one thing this digest's governing property forbids. Two questions, both # answerable at HEAD: was the pointer already written, and did its target # already exist? Their four combinations are the four honest verdicts. def breakage(raw, head_text, target_at_head): """PRE-EXISTING vs NEWLY BROKEN. `None` inputs mean 'git cannot say'.""" if head_text is None: return "age unknown — no committed version of this index to compare" if raw not in head_text: return "NEW — this pointer was added since HEAD" if target_at_head is True: return "NEWLY BROKEN — target existed at HEAD and is gone now" if target_at_head is False: return "pre-existing — already broken at HEAD" return "pre-existing at HEAD — target lives outside this repo, break date unknown" def head_text_of(relpath): """The index as committed. None => never committed, or git unavailable.""" return sh(["git", "-C", D, "show", f"HEAD:{relpath}"]) def target_at_head(raw, memrel): """Did the pointer's target exist at HEAD? None where git cannot know.""" base = os.path.basename(raw) if os.path.normpath(resolve(raw, os.path.realpath(MEM))) != os.path.normpath( os.path.join(os.path.realpath(MEM), base)): return None # leaves the memory dir — outside git's reach here return sh(["git", "-C", D, "cat-file", "-e", f"HEAD:{memrel}/{base}"]) is not None def sec_pointers(): total, mis, dead, nonport = 0, [], [], [] wtotal, wmis, unwritten = 0, [], [] memdir = os.path.realpath(MEM) memrel = os.path.relpath(memdir, D) 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 head = head_text_of(f"{memrel}/{name}") mis += [(name,) + t for t in m_] dead += [(name, ln, raw, breakage(raw, head, target_at_head(raw, memrel))) for ln, raw, _ in d_] nonport += [(name,) + t for t in np_] wn, wm, wu = classify_wikilinks(text, memdir) wtotal += wn wmis += [(name,) + t for t in wm] unwritten += [(name,) + t for t in wu] return total, mis, dead, nonport, wtotal, wmis, unwritten # ---------- 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 [] # ---------- (e) + (c): is the loop currently removable? REVIEWED-131 (2026-08-31) ---------- # EARNED 2026-08-31. A binary upgrade restarted the daemon, which respawned a parked # worker with --reply-on-resume. THIS digest was injected into that session as its only # instruction, and an executor worked and committed with no human present (PENDING-172). # (e) reads harness state on disk; (c) reads the session transcript. Two substrates, # deliberately — REVIEWED-131 cond. 2 requires the output to name which one fired. JOBS_DIR = os.path.expanduser("~/.claude/jobs") ARM_FLAG = "--reply-on-resume" def parked_workers(jobs_dir=JOBS_DIR): """(e) — REVIEWED-131 cond. 1. Returns (rows, unreadable, established). Whether a parked worker will take a turn with no human present is recorded per session in //state.json's `respawnFlags`. This reads the HARNESS, not the session: the only check here whose enforcement does not depend on the party being checked. `established` is False whenever the directory is missing or unreadable. A control over parked workers whose negative result is an opaque zero is the failure PENDING-172 is about, so this NEVER reports absence of evidence as evidence of absence. Every dir read and every dir refused is enumerated. """ rows, unreadable = [], [] if not os.path.isdir(jobs_dir): return rows, unreadable, False try: entries = sorted(os.listdir(jobs_dir)) except OSError as e: return rows, [("", type(e).__name__)], False for name in entries: d = os.path.join(jobs_dir, name) if not os.path.isdir(d): continue f = os.path.join(d, "state.json") try: with open(f, encoding="utf-8") as fh: st = json.load(fh) except Exception as e: unreadable.append((name, type(e).__name__)) continue flags = st.get("respawnFlags") # An absent or non-list respawnFlags is NOT "unarmed" — it is unreadable state. if not isinstance(flags, list): unreadable.append((name, "respawnFlags missing or not a list")) continue rows.append((name, str(st.get("state") or "?"), ARM_FLAG in flags, str(st.get("name") or "")[:52])) return rows, unreadable, True NONHUMAN = ("", "", "", "") def human_turns(path): """(c) — REVIEWED-131 cond. 2. Count of genuine human turns in a transcript. A hook-injected digest arrives as a `user` record, and so do /clear and /exit; none of them is a person. Returns None when the transcript cannot be read — unreadable is not zero. ⚠ RETROSPECTIVE BY CONSTRUCTION. At SessionStart the current session has no turns yet, so this is applied to the most recent COMPLETED session. It answers 'did an executor just run unattended?', which is what nothing detected on 2026-08-31 — not 'am I about to'. (e) is the forward-looking half. """ try: with open(path, encoding="utf-8", errors="replace") as fh: lines = fh.readlines() except Exception: return None n = 0 for line in lines: try: o = json.loads(line) except Exception: continue if o.get("type") != "user" or o.get("isMeta"): continue c = o.get("message", {}).get("content") if isinstance(c, list): c = " ".join(x.get("text", "") for x in c if isinstance(x, dict)) if not isinstance(c, str) or not c.strip(): continue if any(m in c for m in NONHUMAN): continue n += 1 return n def sec_loop(prev_transcript=None): """Assemble (e) and (c), naming which control produced each line.""" o = [] rows, unreadable, established = parked_workers() if not established: o.append("PARKED WORKERS [control e — harness state] — NOT ESTABLISHED:" f" {JOBS_DIR} is missing or unreadable." " This is 'could not assess', not 'none parked'.") else: armed = [r for r in rows if r[2]] head = (f"PARKED WORKERS [control e — harness state] — {len(rows)} job dir(s) read," f" {len(unreadable)} unreadable, {len(armed)} ARMED") o.append("⚠ " + head if armed else head) for jid, state, arm, name in rows: o.append(f" {'⚠ ARMED' if arm else ' ok '} {jid} state={state} {name}") for jid, why in unreadable: o.append(f" NOT ESTABLISHED {jid} ({why}) — counted, not classified") # ⚠ A stopped job still carries its flags. Whether the daemon respawns a # STOPPED job is NOT ESTABLISHED — the 2026-08-31 event respawned one that was # parked (state=blocked). Reporting both as equally live would assert what was # never measured; reporting the stopped one as safe would assume it. live = [r for r in armed if r[1] not in ("stopped",)] stopped = [r for r in armed if r[1] in ("stopped",)] if live: o.append(f" ⇒ {len(live)} PARKED worker(s) carry {ARM_FLAG}: a daemon restart" " (an auto-update will do) makes them take a turn with no human" " present. PENDING-172 / REVIEWED-131.") if stopped: o.append(f" ⇒ {len(stopped)} STOPPED job(s) still carry {ARM_FLAG}." " Whether the daemon respawns a stopped job is NOT ESTABLISHED —" " the 2026-08-31 respawn took a job that was parked, not stopped." " Neither live nor proven safe.") if prev_transcript: n = human_turns(prev_transcript) label = "LAST SESSION [control c — transcript]" if n is None: o.append(f"{label} — NOT ESTABLISHED: transcript unreadable.") elif n == 0: o.append(f"⚠ {label} — NO HUMAN TURN in {os.path.basename(prev_transcript)}." " That session ran unattended. PENDING-172.") else: o.append(f"{label} — {n} human turn(s); a person was present.") return o # ---------- 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")) # ⚠ RELIANCE ON THE DISCARDED READING — reported under REVIEWED-122 cond. 6, # NOT repaired here (the repair is inside the gated mechanism; see PENDING-142 # ADDENDUM 1). The assertion is mechanically TRUE and will stay true. What is # wrong is the fixture and what the name implies: `REVIEWED-82` is not a # ruling-about-something-else, it is the AUTHORIZED ruling on PENDING-82, and # this line therefore presents the false-open class as intended behaviour. # Whoever builds PENDING-142 will see this check fail, or be tempted to keep it # passing. Failing is the fix working. Re-derive the fixture from the property # ("does this ruling dispose of that item?") using two genuinely unrelated # documents, per REVIEWED-122 cond. 2. 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) # --- 2026-08-23 [FIX]: the three shapes the last three wraps emitted, none of # which the case-sensitive/40-char version could read. Each earned from a real # miss, not invented: 86 threads + 84 questions across 195 wraps. chk("extract_anchor matches case-insensitively (**The pulling thread: …**)", extract_anchor("**The pulling thread: the beacon, 12:00Z. Run ONCE.**\n\nnext", "PULLING THREAD") == "the beacon, 12:00Z. Run ONCE.") chk("extract_anchor takes the NEXT paragraph when the label ends the line", extract_anchor("**Literal question for next-Claude** *(checkable, and the " "REVIEWED half is not my corpus)*:\n\nHas the rate risen?\n\nz", "LITERAL QUESTION") == "Has the rate risen?") chk("extract_anchor prefers a LABEL over an earlier narrative mention" " [negative control — case-insensitivity must not let prose outrank the field]", extract_anchor("Yesterday the pulling thread went slack.\n\n" "**Pulling thread:** the beacon.\n\nz", "PULLING THREAD") == "the beacon.") chk("extract_anchor still finds a narrative-only mention when no label exists" " [positive control — proves the check above is not passing vacuously]", extract_anchor("Yesterday the pulling thread: went slack.\n\nz", "PULLING THREAD") == "went slack.") 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, [], [], [])) NOWHERE = "no-such-file-anywhere-xyzzy-9931.md" print("\nloop checks (e)+(c) [REVIEWED-131, earned 2026-08-31]:") import tempfile, shutil _tmp = tempfile.mkdtemp(prefix="wd-loop-") try: def _job(name, payload): d = os.path.join(_tmp, name) os.makedirs(d, exist_ok=True) open(os.path.join(d, "state.json"), "w").write(payload) _job("armed", json.dumps({"state": "blocked", "name": "x", "respawnFlags": ["--reply-on-resume", "--allowed-tools"]})) _job("quiet", json.dumps({"state": "blocked", "name": "y", "respawnFlags": []})) _job("broken", "{not json") _job("noflags", json.dumps({"state": "blocked", "name": "z"})) rows, unread, est = parked_workers(_tmp) chk("parked_workers ESTABLISHES on a readable dir", est is True) chk("parked_workers DETECTS an armed worker [must-detect]", [r for r in rows if r[0] == "armed" and r[2] is True] != []) chk("parked_workers does NOT flag respawnFlags=[] [must-not-flag]", [r for r in rows if r[0] == "quiet" and r[2] is False] != []) chk("parked_workers ENUMERATES unparseable state.json, never drops it", "broken" in [j for j, _ in unread]) chk("absent respawnFlags is NOT ESTABLISHED, not 'unarmed' [permissive direction]", "noflags" in [j for j, _ in unread] and "noflags" not in [r[0] for r in rows]) _job("armedstopped", json.dumps({"state": "stopped", "name": "s", "respawnFlags": ["--reply-on-resume"]})) _rows3, _u3, _e3 = parked_workers(_tmp) chk("a STOPPED job still reports its flag [must-detect: flags outlive the process]", [r for r in _rows3 if r[0] == "armedstopped" and r[2] is True and r[1] == "stopped"] != []) chk("...and its state is carried, so the report can separate it [must-not-conflate]", {r[1] for r in _rows3 if r[2]} == {"blocked", "stopped"}) _rows2, _u2, est2 = parked_workers(os.path.join(_tmp, "does-not-exist")) chk("missing jobs dir -> NOT ESTABLISHED, never 'none parked'", est2 is False) _t = os.path.join(_tmp, "t.jsonl") _hook = json.dumps({"type": "user", "message": {"content": "=== WAKE DIGEST ===\nOPEN QUESTION — do the thing"}}) _cmd = json.dumps({"type": "user", "message": {"content": "x"}}) _human = json.dumps({"type": "user", "message": {"content": "please file it"}}) open(_t, "w").write(_hook + "\n" + _cmd + "\n") chk("human_turns returns 0 on a hook-only transcript [must-detect]", human_turns(_t) == 0) open(_t, "w").write(_hook + "\n" + _human + "\n") chk("human_turns does NOT flag a real human turn [must-not-flag]", human_turns(_t) == 1) chk("human_turns on an unreadable path -> None, not 0", human_turns(os.path.join(_tmp, "nope.jsonl")) is None) finally: shutil.rmtree(_tmp, ignore_errors=True) print("\ncode spans [2026-08-09: the canary flagged its own specification]:") chk("a pointer INSIDE a code span is not a pointer", classify_pointers(f"`[y]({NOWHERE})`", MEMDIR) == (0, [], [], [])) chk("...and the SAME pointer outside one still fires [direction control —" " blanking must not swallow everything]", classify_pointers(f"[y]({NOWHERE})", MEMDIR)[0] == 1) chk("MEMORY.md's own canary spec is silent [the exact 2026-08-09 false positive]", (lambda t: classify_pointers(t, MEMDIR) == (0, [], [], []) and classify_wikilinks(t, MEMDIR) == (0, [], []))( "→ `~/dotfiles/scripts/`, invoked not described; `](file.md)` +" " `[[wikilink]]` over both memory indexes")) chk("blanking preserves LINE NUMBERS [offsets kept, not deleted]", classify_pointers(f"`](x.md)`\n[y]({NOWHERE})\n", MEMDIR)[2][0][0] == 2) chk("a fenced block is blanked", classify_pointers(f"```\n[y]({NOWHERE})\n```\n", MEMDIR) == (0, [], [], [])) chk("a STRAY unmatched backtick blanks nothing [damage bounded to one line;" " a greedy matcher would HIDE dead pointers]", classify_pointers(f"a ` b\n[y]({NOWHERE})\n", MEMDIR)[0] == 1) chk("an UNTERMINATED fence blanks nothing [fails toward reporting, not silence]", classify_pointers(f"```\n[y]({NOWHERE})\n", MEMDIR)[0] == 1) print("\nwikilinks [the ladder specified them 2026-07-06; never built until now]:") chk("a wikilink whose file exists resolves", classify_wikilinks("see [[MEMORY]]", MEMDIR) == (1, [], [])) chk("a wikilink with no file is UNWRITTEN, not dead [~/CLAUDE.md rules it legitimate]", (lambda r: r[0] == 1 and not r[1] and len(r[2]) == 1)( classify_wikilinks("see [[no-such-memory-xyzzy-9931]]", MEMDIR))) chk("a NEAR-MISS slug is MIS-AUTHORED with the real file handed back" " [live: the two hits the one-word version mislabelled]", (lambda r: len(r[1]) == 1 and not r[2] and r[1][0][2] == "feedback-trust-prior-pass-frame")( classify_wikilinks("[[trust-prior-pass-frame]]", MEMDIR))) chk("...and a genuine miss does NOT collapse into mis-authored [direction control]", not classify_wikilinks("[[no-such-memory-xyzzy-9931]]", MEMDIR)[1]) chk("near_slugs matches on the HYPHEN boundary, not bare substring" " [else [[arc]] would claim every file with 'arc' in a word]", all(s.endswith("-arc") or "arc".endswith("-" + s) for s in near_slugs("arc", MEMDIR))) chk("an alias and a heading are stripped down to the slug", classify_wikilinks("[[MEMORY#Index|the index]]", MEMDIR) == (1, [], [])) chk("a wikilink inside a code span is not a wikilink", classify_wikilinks("`[[no-such-memory-xyzzy-9931]]`", MEMDIR) == (0, [], [])) chk("classify_wikilinks returns clean on empty input [positive control]", classify_wikilinks("", MEMDIR) == (0, [], [])) print("\nbreakage age [derived from git — this digest may not cache]:") chk("a pointer absent at HEAD is NEW", breakage("x.md", "nothing here", False).startswith("NEW")) chk("a pointer present at HEAD whose target was there too is NEWLY BROKEN", breakage("x.md", "[a](x.md)", True).startswith("NEWLY BROKEN")) chk("a pointer present at HEAD whose target was already gone is pre-existing", breakage("x.md", "[a](x.md)", False).startswith("pre-existing")) chk("an out-of-repo target says the break date is unknown [honest degradation]", "unknown" in breakage("x.md", "[a](x.md)", None)) chk("no committed index reads as UNKNOWN, never as pre-existing" " [negative control — absence of evidence]", breakage("x.md", None, None).startswith("age unknown")) 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("\nunwrapped-session detector [PENDING-S2's obligation, our substrate]:") # --- time base. The 2026-08-17 cause, and the control that was missing. ---- _utc = calendar.timegm(time.strptime("2026-08-14T07:29:36", "%Y-%m-%dT%H:%M:%S")) chk("parse_iso reads a 'Z' timestamp as UTC [the 2026-08-17 cause]", parse_iso("2026-08-14T07:29:36.852Z") == _utc) _agrees = (parse_iso("2026-08-14T07:29:36.852Z") == time.mktime(time.strptime("2026-08-14T07:29:36", "%Y-%m-%dT%H:%M:%S"))) _utc_box = (time.altzone if time.daylight else time.timezone) == 0 chk("parse_iso differs from the old mktime reading unless the box is on UTC" " [negative control — proves the line above can fail]", _agrees == _utc_box) chk("parse_iso honours an explicit offset", parse_iso("2026-08-14T09:29:36+02:00") == parse_iso("2026-08-14T07:29:36Z")) chk("parse_iso returns None on junk [must not invent a span]", parse_iso("not a timestamp") is None and parse_iso("") is None) # THE END-TO-END FORM, derived from the property rather than from the check: # a transcript's last inner timestamp and its file mtime are two readings of # one moment. If they disagree by more than minutes, the detector is comparing # two different clocks — which is exactly what it was doing (~7200 s skew). _proj = os.path.dirname(MEM) _tx = sorted((os.path.join(_proj, f) for f in os.listdir(_proj) if f.endswith(".jsonl")), key=os.path.getmtime) # MEDIAN, not max, and the reason is itself a finding: a clock mismatch is # SYSTEMATIC — it shifts every transcript by the same amount, so it moves the # median. A lone large skew is a different phenomenon: 2026-08-10's transcript # was touched 31 h after its last line. That is the very mutable-mtime problem # `wrap_events` routes around, measured here on the transcript side. Max is # printed so the outlier stays visible instead of being averaged away. _skew = sorted(abs(os.path.getmtime(p) - transcript_span(p)[1]) for p in _tx[-8:] if transcript_span(p)[1] is not None) _med = _skew[len(_skew) // 2] if _skew else -1 chk(f"transcript span END agrees with file mtime" f" [median {_med:.0f}s, max {max(_skew) if _skew else -1:.0f}s;" f" median was ~7200s before the fix]", bool(_skew) and _med < 900) # --- the four outcomes ----------------------------------------------------- _E = [("/w/session-2026-01-02-x.md", 1500.0, "git")] chk("wrap_verdict WRAPPED when an event lands inside the span", wrap_verdict((1000.0, 2000.0), _E)[0] == "wrapped") chk("wrap_verdict WRAPPED for an event just after the last write [wraps land near the end]", wrap_verdict((1000.0, 2000.0), [("/w/session-2026-01-02-x.md", 2400.0, "git")])[0] == "wrapped") chk("wrap_verdict UNWRAPPED when every event is outside [negative control]", wrap_verdict((1000.0, 2000.0), [("/w/session-1999-01-01-a.md", 500.0, "git"), ("/w/session-1999-01-01-b.md", 5.0e8, "git")])[0] == "unwrapped") chk("wrap_verdict UNASSESSABLE-SUBJECT on an unreadable span [must NOT read as 'no wrap']", wrap_verdict((None, None), _E)[0] == "unassessable-subject") chk("wrap_verdict UNASSESSABLE-CHECK when no wrap record can be enumerated", wrap_verdict((1000.0, 2000.0), [])[0] == "unassessable-check") _t0 = time.time() chk("wrap_verdict declines to alarm when a record NAMES a day inside the span" " [commit lag must not become a false alarm]", wrap_verdict((_t0, _t0 + 3600), [(f"/w/session-{time.strftime('%Y-%m-%d', time.localtime(_t0))}-x.md", _t0 + 10 * 86400, "git")])[0] == "unassessable-check") # --- selection ------------------------------------------------------------- _now = 1_000_000.0 _spans = [("cur.jsonl", _now - 5, _now), ("prev.jsonl", _now - 86400, _now - 12), ("old.jsonl", _now - 400000, _now - 350000)] chk("previous_transcript picks the session that ended 12 s ago [the 2026-08-17 miss]", previous_transcript(_now, _spans)[0] == "prev.jsonl") _mid = [("cur.jsonl", _now - 1380, _now), # running 23 min — a manual run ("prev.jsonl", _now - 86400, _now - 1400), ("old.jsonl", _now - 400000, _now - 350000)] chk("previous_transcript excludes a LONG-running current session [manual mid-session run;" " keying on start alone regressed here]", previous_transcript(_now, _mid)[0] == "prev.jsonl") chk("previous_transcript keeps a quiet newest-start transcript [negative control —" " exclusion needs BOTH signals, not just newest start]", previous_transcript(_now, _mid[1:])[0] == "prev.jsonl") # --- real substrate -------------------------------------------------------- _ev = wrap_events() _git = sum(1 for _p, _e, _s in _ev if _s == "git") chk(f"wrap evidence is taken from git add-time, not mutable mtimes" f" [{_git} of {len(_ev)} from git]", len(_ev) > 0 and _git > len(_ev) // 2) _v = [wrap_verdict(transcript_span(p), _ev)[0] for p in _tx[-14:-1]] chk(f"a real session reads as WRAPPED end-to-end [{_v.count('wrapped')} of {len(_v)}]", "wrapped" in _v) # ⚠ The old gate here demanded a real UNWRAPPED instance too, and PASSED on # 2026-08-17 while the detector was systematically broken — it established that # both verdicts OCCUR, never that either was CORRECT. Its subject was the # spread of outcomes; the claim was their truth. Reported, not asserted. print(f" [note] real-session outcome spread: " + ", ".join(f"{s}={_v.count(s)}" for s in sorted(set(_v))) + " — a spread is not a proof of correctness; the controls above are.") print("\nlive substrate:") _PROJ = os.path.join(HOME, ".claude", "projects", "-Users-davidglidden") # Controls drawn from the 2026-08-31 event itself. They are marked NOT ESTABLISHED # rather than FAIL when the transcript ages out of the 30-day window — a control # that silently degrades into a pass is the failure this whole item is about. for _sid, _want, _why in ( ("b7e7eb39-1e81-4af3-9be7-c323c794c1d9", 0, "the unattended session of 2026-08-31 (PENDING-172)"), ("62083f11-b56f-4e67-bbbb-135efeba0ed2", "gt0", "the attended session that found it")): _p = os.path.join(_PROJ, _sid + ".jsonl") if not os.path.exists(_p): print(f" [ .. ] NOT ESTABLISHED — {_sid[:8]} aged out; control not run ({_why})") continue _n = human_turns(_p) chk(f"human_turns on {_sid[:8]} — {_why}", (_n == 0) if _want == 0 else (isinstance(_n, int) and _n > 0)) 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") unwrapped = sec_unwrapped() if unwrapped: o.append(unwrapped) _prev = previous_session() for line in sec_loop(_prev[0] if _prev else None): o.append("\n" + line if line.startswith(("PARKED", "⚠ PARKED")) else line) if th: o.append(f"\nPULLING THREAD — {th}") if q: # (b) — REVIEWED-131 cond. 3: ANNOTATION, explicitly NOT a control. It cannot # enforce anything and must never be reported as a mitigation. It exists because # on 2026-08-31 this field was read by an unattended executor as an instruction. o.append(f"\nOPEN QUESTION [orientation, not an instruction — inherited from a" f" human and answerable to one] — {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, wtot, wmis, wun = sec_pointers() o.append(f"\nMEMORY POINTERS — {ptot} checked · {len(pmis)} mis-authored · {len(pdead)} dead" + (f" · {len(pnp)} non-portable" if pnp else "") + f" | {wtot} wikilinks · {len(wmis)} mis-authored · {len(wun)} unwritten") 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, age in pdead: o.append(f" DEAD {f}:{ln} {raw} (no file of that name in ~/_Dev or the memory dir)") o.append(f" → {age}") for f, ln, raw in pnp: o.append(f" NON-PORTABLE {f}:{ln} {raw} (resolves, but hardcodes this machine)") for f, ln, slug, near in wmis: o.append(f" MIS-AUTHORED {f}:{ln} [[{slug}]]") o.append(f" → the file exists; replace with: [[{near}]]") for f, ln, slug in wun: o.append(f" UNWRITTEN {f}:{ln} [[{slug}]] (permitted forward-reference, not an error)") 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} ===")