From 37a2c86bdba124dfe6eaf3392a26557d787a6f4e Mon Sep 17 00:00:00 2001 From: David F Glidden Date: Fri, 11 Sep 2026 18:43:41 +0200 Subject: [PATCH] [FIX] tarbuckle-body: the selftest wrote the live occurrence log its comment said it never touched (PENDING-184) The D block opened "Run against a temp dir, never live state", but _log_draw hardcoded ~/.claude/state/tarbuckle-draws.jsonl and the selftest rebound only LAST_TICK and SLOT, so D5's fire_tick appended a real-time tick record to the live log on every run, and every mutant copy did the same. Two strays are confirmed (2026-09-09T21:48:39, 2026-09-10T18:52:40); nothing in a tick record separates live from test, so that is a floor. The path is now a module global DRAWS, rebound and restored with the others. Two new controls, both behavioural rather than source-string: D9 the live log is untouched (existence, size, SHA-256, before vs after) D10 the D block's tick landed in the redirected log. An absence-only check passes when the write silently vanishes; this is the arm that fails then. Verified under a throwaway HOME: fixed 34/34 with the fake live log unchanged; the pre-fix file 32/32 green while writing it; M1 (rebinding deleted) fails exactly D9+D10; M2 (write vanishes) fails exactly D10 with D9 passing; main() with a due tick still records its tick. Real HOME: 34/34, live log SHA-256 unchanged, measured outside the selftest. Five-selftest census: none writes. Tag claim: this addresses the instance. The class census covered the five Tarbuckle selftests only; fleet-wide the class is unassessed, and pursuing it would be [HARDENING]. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PJM5fwqp456LDGzqiZXsgu --- scripts/tarbuckle-body.py | 46 +++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/scripts/tarbuckle-body.py b/scripts/tarbuckle-body.py index 86510a6..aeda97d 100755 --- a/scripts/tarbuckle-body.py +++ b/scripts/tarbuckle-body.py @@ -55,6 +55,7 @@ MUMBLE_INTERVAL_MIN = 20 # §8, determined 2026-08-25 LOG = os.path.expanduser("~/.claude/state/tarbuckle-invocations.jsonl") LAST_TICK = os.path.expanduser("~/.claude/state/tarbuckle-last-tick") SLOT = os.path.expanduser("~/.claude/state/tarbuckle-slot.json") +DRAWS = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl") MUMBLE = os.path.expanduser("~/dotfiles/scripts/tarbuckle-mumble.py") # 73% silent · 20% brief aside · 7% notable — v1 §8's table, hardcoded, unchanged. @@ -93,9 +94,13 @@ def log_invocation(payload: dict) -> None: def _log_draw(kind: str) -> None: - """Occurrence only. Local, so the body needs nothing importable to tick.""" + """Occurrence only. Local, so the body needs nothing importable to tick. + + Writes DRAWS, a module global, so the selftest can redirect it. Hardcoded, it + could not — and every selftest run wrote a real tick into the live log (PENDING-184). + """ try: - d = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl") + d = DRAWS os.makedirs(os.path.dirname(d), exist_ok=True) with open(d, "a") as fh: fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"), @@ -277,11 +282,23 @@ def selftest() -> int: ck("C6 marks are distinct", len(set(MARKS)) == len(MARKS)) # --- D: the tick, the draw, the slot. Run against a temp dir, never live state. - import tempfile, collections - global LAST_TICK, SLOT - _lt, _sl = LAST_TICK, SLOT + # ⚠ That sentence was false until 2026-09-11: DRAWS was hardcoded, so D5's tick + # landed in the live occurrence log on every run (PENDING-184). D9/D10 now check + # it, in both directions. + import tempfile, collections, hashlib + global LAST_TICK, SLOT, DRAWS + _lt, _sl, _dr = LAST_TICK, SLOT, DRAWS + + def _fingerprint(p): + try: + b = open(p, "rb").read() + return (True, len(b), hashlib.sha256(b).hexdigest()) + except OSError: + return (False, 0, "") + live_before = _fingerprint(_dr) td = tempfile.mkdtemp() - LAST_TICK, SLOT = os.path.join(td, "tick"), os.path.join(td, "slot") + _redir = os.path.join(td, "draws.jsonl") + LAST_TICK, SLOT, DRAWS = os.path.join(td, "tick"), os.path.join(td, "slot"), _redir try: # D1 — the draw is keyed to nothing. Structural, like C3. ck("D1 draw takes no arguments", draw.__code__.co_argcount == 0) @@ -321,8 +338,23 @@ def selftest() -> int: fresh_utterance(now + DISPLAY_WINDOW_S + 1) is None) ck("D6n absent slot is silence", (os.remove(SLOT), fresh_utterance(now))[1] is None) + + # D9/D10 — the D block touches no live state, AND its writes land where they + # were sent. Both arms: an absence-only check passes when the write silently + # vanishes, and D10 is the arm that fails then (PENDING-184, mutation M2). + # ⚠ D9 can fail falsely if a real tick fires from another pane inside this + # sub-second block. That is the loud direction; it is not engineered away. + ck("D9 selftest leaves the live occurrence log untouched", + _fingerprint(_dr) == live_before) + try: + recs = [json.loads(l) for l in open(_redir) if l.strip()] + except OSError: + recs = [] + ck("D10 the D block's tick landed in the redirected log", + len(recs) == 1 and recs[0].get("surface") == "tick" + and recs[0].get("outcome") in {k for k, _ in DRAW}) finally: - LAST_TICK, SLOT = _lt, _sl + LAST_TICK, SLOT, DRAWS = _lt, _sl, _dr # D8 — §9's switches. mute keeps the body; off removes it. import tempfile