(e) parked_workers reads ~/.claude/jobs/<id>/state.json's respawnFlags — harness state, not the session, so its enforcement does not depend on the party checked. Fails to NOT ESTABLISHED, never to safety: a missing dir, an unparseable state.json, and an absent respawnFlags are each enumerated rather than counted as "no parked workers" (cond. 1). (c) human_turns counts genuine human turns in a transcript, discounting hook-injected and /clear-/exit records. Retrospective by construction and says so. Output names which control produced each line (cond. 2). (b) the OPEN QUESTION field is marked orientation-not-instruction, in the code and in the output, explicitly not a control (cond. 3). Two corrections found while building. json was never imported, which the top-level guard would have turned into WAKE DIGEST UNAVAILABLE at every session start — the selftest caught it on first run. And the first draft asserted that a STOPPED job carrying the flag would take a turn on restart; whether the daemon respawns a stopped job is NOT ESTABLISHED, and it now says so rather than claiming either way. previous_session() lifted out of sec_unwrapped so both consumers share one definition of "the session before this one" — this file already refuses a second definition of "an item". Controls run in both directions, two of them against the real transcripts: b7e7eb39 (the unattended session) returns 0 human turns, this session returns 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wn9mxsFjHJkzYjuDdrtxwp
1458 lines
71 KiB
Python
Executable File
1458 lines
71 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 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-<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.
|
||
|
||
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-<same number>`, 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 <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.
|
||
#
|
||
# 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<f>`{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 <jobs>/<id>/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, [("<listing>", 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 = ("<local-command-", "<command-name>", "<command-message>", "<system-reminder",
|
||
"=== WAKE DIGEST", "<local-command-stdout>", "<local-command-caveat>")
|
||
|
||
|
||
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":
|
||
"<local-command-caveat>x</local-command-caveat>"}})
|
||
_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} ===")
|