[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:
co-authored by
Claude Opus 5
parent
1e019b407d
commit
7948c0929b
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user