[FIX] REVIEWED-131: build (e) and (c), and annotate the digest per (b)
(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
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d8d4bfe9c
commit
70440db147
+236
-12
@@ -16,7 +16,7 @@ if it cannot be computed it says so rather than emitting nothing.
|
|||||||
Provenance: 2026-07-28, steward-authorized alongside the PENDING split and the
|
Provenance: 2026-07-28, steward-authorized alongside the PENDING split and the
|
||||||
CLAUDE.md doctrine annotation. Sibling of governance-drift-check.py.
|
CLAUDE.md doctrine annotation. Sibling of governance-drift-check.py.
|
||||||
"""
|
"""
|
||||||
import calendar, os, re, subprocess, sys, time
|
import calendar, json, os, re, subprocess, sys, time
|
||||||
|
|
||||||
HOME = os.path.expanduser("~")
|
HOME = os.path.expanduser("~")
|
||||||
D = os.path.join(HOME, "dotfiles")
|
D = os.path.join(HOME, "dotfiles")
|
||||||
@@ -340,6 +340,26 @@ def wrap_verdict(span, events, lead=300, lag=10800):
|
|||||||
return "unwrapped", ""
|
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():
|
def sec_unwrapped():
|
||||||
"""
|
"""
|
||||||
PENDING-S2's obligation, rebuilt on our own substrate.
|
PENDING-S2's obligation, rebuilt on our own substrate.
|
||||||
@@ -361,16 +381,7 @@ def sec_unwrapped():
|
|||||||
4. a two-valued verdict over an assessment that can fail.
|
4. a two-valued verdict over an assessment that can fail.
|
||||||
Fixing only (4), as scoped, would have left the false alarms firing.
|
Fixing only (4), as scoped, would have left the false alarms firing.
|
||||||
"""
|
"""
|
||||||
proj = os.path.dirname(os.path.join(HOME, ".claude", "projects",
|
prev = previous_session()
|
||||||
"-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
|
|
||||||
prev = previous_transcript(time.time(), [(p,) + transcript_span(p) for p in tx])
|
|
||||||
if prev is None:
|
if prev is None:
|
||||||
return None
|
return None
|
||||||
_path, a, b = prev
|
_path, a, b = prev
|
||||||
@@ -862,6 +873,143 @@ def stray_maps():
|
|||||||
return []
|
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 ----------
|
# ---------- self-test ----------
|
||||||
|
|
||||||
def selftest():
|
def selftest():
|
||||||
@@ -979,6 +1127,57 @@ def selftest():
|
|||||||
chk("classify_pointers returns clean on empty input",
|
chk("classify_pointers returns clean on empty input",
|
||||||
classify_pointers("", MEMDIR) == (0, [], [], []))
|
classify_pointers("", MEMDIR) == (0, [], [], []))
|
||||||
NOWHERE = "no-such-file-anywhere-xyzzy-9931.md"
|
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]:")
|
print("\ncode spans [2026-08-09: the canary flagged its own specification]:")
|
||||||
chk("a pointer INSIDE a code span is not a pointer",
|
chk("a pointer INSIDE a code span is not a pointer",
|
||||||
classify_pointers(f"`[y]({NOWHERE})`", MEMDIR) == (0, [], [], []))
|
classify_pointers(f"`[y]({NOWHERE})`", MEMDIR) == (0, [], [], []))
|
||||||
@@ -1127,6 +1326,23 @@ def selftest():
|
|||||||
+ " — a spread is not a proof of correctness; the controls above are.")
|
+ " — a spread is not a proof of correctness; the controls above are.")
|
||||||
|
|
||||||
print("\nlive substrate:")
|
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("PENDING.md readable", read(PENDING) is not None)
|
||||||
chk("REVIEWED.md readable", read(REVIEWED) is not None)
|
chk("REVIEWED.md readable", read(REVIEWED) is not None)
|
||||||
chk("drift check runs", sec_drift() is not None)
|
chk("drift check runs", sec_drift() is not None)
|
||||||
@@ -1147,10 +1363,18 @@ def main():
|
|||||||
unwrapped = sec_unwrapped()
|
unwrapped = sec_unwrapped()
|
||||||
if unwrapped:
|
if unwrapped:
|
||||||
o.append(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:
|
if th:
|
||||||
o.append(f"\nPULLING THREAD — {th}")
|
o.append(f"\nPULLING THREAD — {th}")
|
||||||
if q:
|
if q:
|
||||||
o.append(f"\nOPEN QUESTION — {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;"
|
o.append(f"\nOPEN AUTHORIZATION ITEMS ({len(items)}) — bodies in ~/PENDING.md at the line given;"
|
||||||
" closed items are in ~/PENDING-archive.md")
|
" closed items are in ~/PENDING-archive.md")
|
||||||
|
|||||||
Reference in New Issue
Block a user