[FIX] wrap: a generation's Stop is not a turn (PENDING-169 §5, judgment 4)

`Stop` fires for `claude -p`, so the wrap hook re-entered 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` —
30% of occasions, 23% of the time — went on occasions that were the end of a
generation nobody was waiting on.

The gate is one predicate against TARBUCKLE_CHILD, which the four seams already
set on the spawn and which `tarbuckle-body.py:203` already reads as a fork-bomb
guard. ⚠ The variable now carries two meanings that do not imply each other —
DO NOT TICK in the body, DO NOT SPEAK OR WORK here — and both sites now say so,
because the next reader would otherwise remove the coupling as arbitrary.

The heartbeat write stays ABOVE the gate, deliberately: it answers "did the hook
fire", and gating it would narrow the one artifact that separates a hook that
never fires from a hook that fires and does nothing.

W6/W6n are BEHAVIOURAL, not source strings — the source-string form of this kind
of check is what passed vacuously for its whole life in the seam (PENDING-180).
Verified by mutation, not by the green run: deleting the gate fails W6 and W6n;
hoisting it above the heartbeat fails the heartbeat arm; nothing else moves.
28/28 controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1i5VRfHjD79hfaXjWsBXA
This commit is contained in:
David F Glidden
2026-09-10 18:53:06 +02:00
co-authored by Claude Opus 5
parent 1e019b407d
commit 7948c0929b
+58
View File
@@ -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")