diff --git a/scripts/tarbuckle-wrap.py b/scripts/tarbuckle-wrap.py index c722919..4ab6db4 100755 --- a/scripts/tarbuckle-wrap.py +++ b/scripts/tarbuckle-wrap.py @@ -181,6 +181,23 @@ def main() -> int: fh.write(time.strftime("%Y-%m-%dT%H:%M:%S%z")) except OSError: pass + + # ⚠ `Stop` FIRES FOR `claude -p` TOO, so this hook re-enters inside the fool's own + # generator child. Measured over the fortnight to 2026-09-09: 3 of 10 wrap runs landed + # in a generation subprocess, and 18 s of the 80 s of blocking `Stop` went on occasions + # that were the end of a generation nobody was waiting on. The child already carries the + # variable — it is set on the spawn below, and by the three sibling seams. + # ⚠ THE HEARTBEAT STAYS ABOVE THIS GATE, deliberately: it answers "did the hook fire", + # and gating it would quietly narrow the one artifact that can tell a hook that never + # fires from a hook that fires and does nothing. + # ⚠ THE VARIABLE NOW MEANS TWO THINGS AND NEITHER IMPLIES THE OTHER. In + # `tarbuckle-body.py` it means DO NOT TICK — a fork-bomb guard on the status line. + # Here it means DO NOT SPEAK OR WORK — a generation's `Stop` is not a turn, and he has + # no business wrapping a session that is one of his own sentences. Whoever deletes one + # meaning must check the other: they are separate guards that share a name. + if os.environ.get("TARBUCKLE_CHILD"): + return 0 + session_id = str(payload.get("session_id") or "") transcript = payload.get("transcript_path") or "" if not transcript or not wrap_invoked(transcript): @@ -219,6 +236,9 @@ def main() -> int: def selftest() -> int: + # W6 rebinds these three for the duration of one control and restores them; Python + # requires the declaration ahead of first use, which W3's calls would otherwise be. + global wrap_invoked, muted, HEARTBEAT checks, failed = [], [] import tempfile @@ -302,6 +322,44 @@ def selftest() -> int: ck("W5 prompt keeps no truth value", "NO TRUTH VALUE" in p) ck("W5n wrap prompt differs from the wake prompt", "put down" in p) + # W6 — `Stop` fires for `claude -p`, so this hook re-enters inside the fool's own + # generator. ⚠ BEHAVIOURAL, NOT A SOURCE STRING. The source-string form of exactly + # this kind of check is what passed vacuously for its whole life two files away + # (PENDING-180), and no string in this file can see whether the gate is reached + # BEFORE the work — which is the entire claim. + import io as _io + _wi, _mu, _hb = wrap_invoked, muted, HEARTBEAT + _env, _stdin, seen = os.environ.get("TARBUCKLE_CHILD"), sys.stdin, [] + _hbt = os.path.join(td, "hb") + + def _spy(path): + seen.append(path) + return False + + _payload = json.dumps({"session_id": "w6", "transcript_path": t}) + try: + # muted() is neutralised for the duration: a control whose outcome depends on a + # switch it does not name is not a control, and mute is a different question. + wrap_invoked, muted, HEARTBEAT = _spy, (lambda: False), _hbt + os.environ["TARBUCKLE_CHILD"] = "1" + sys.stdin = _io.StringIO(_payload) + ck("W6 a generation's Stop returns without reading the transcript", + main() == 0 and not seen) + ck("W6 the heartbeat is written above the gate, so liveness still reports", + os.path.exists(_hbt)) + os.environ.pop("TARBUCKLE_CHILD", None) + sys.stdin = _io.StringIO(_payload) + main() + ck("W6n the predicate can fail: an ordinary Stop does read the transcript", + len(seen) == 1) + finally: + wrap_invoked, muted, HEARTBEAT = _wi, _mu, _hb + sys.stdin = _stdin + if _env is None: + os.environ.pop("TARBUCKLE_CHILD", None) + else: + os.environ["TARBUCKLE_CHILD"] = _env + for name in checks: print(f" {'FAIL' if name in failed else 'ok '} {name}") print(f"{len(checks) - len(failed)}/{len(checks)} controls passed")