[FIX] The wrap seam detected the wrong thing, and the correct fix is what blinded it

The literal question left at the wrap was 'did the wrap seam fire?'. It did not, and the
diagnosis is exact: 0 user-typed /wrap-up records, 29 assistant Skill invocations. The
steward wrote 'then wrap' in prose and the executor invoked the skill. The detector looked
for the steward TYPING the command.

⚠ The detector was not broken. It did exactly what it was built to do. What it was built
to detect is not how a wrap actually arrives — and it was built while the steward was
instructing in prose, which is the only way a wrap had ever arrived in that session.

⚠ AND THE EARLIER FIX IS WHAT CAUSED THIS. Restricting to type=user with string content
was the correct answer to the self-reference bug, where the executor's own tool_use inputs
matched the literal marker. That same restriction excludes the legitimate path. 'The
correct fix caused the next failure' is not a shape any control can see, and it is
PENDING-160's subject exactly.

Now accepts a tool_use whose NAME is Skill and whose input names wrap-up — structural, so
a Bash command echoing the string still does not match. Negative controls for both that
and for a different skill.

And a heartbeat: one timestamp, OVERWRITTEN never appended, so the hook can prove it runs
at all. That closes the silent-net objection this surface carried from the day it shipped
— an append-only log of every turn would be noise and would become the ledger §9 forbids.

25/25. wrap_invoked() now returns True on the live transcript. ⚠ Unproven until the next
Stop actually fires — which is the same claim that was wrong last time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6hZXNYSxEfZseBGTni4sf
This commit is contained in:
David F Glidden
2026-08-25 19:49:32 +02:00
co-authored by Claude Opus 5
parent 3079c0d94e
commit 8740c8aa12
2 changed files with 67 additions and 6 deletions
+55 -6
View File
@@ -35,6 +35,12 @@ from tarbuckle_mumble_shim import (acceptable, soul_register, session_material,
echoes_soul, log_rejection, REJECTS)
FIRED = os.path.expanduser("~/.claude/state/tarbuckle-wrap-fired")
# ⚠ ONE TIMESTAMP, OVERWRITTEN — not appended. The silent-net objection against this hook
# was that it writes nothing on an ordinary turn, so there is no evidence it is invoked at
# all. A heartbeat answers that; an append-only log of every turn would be noise and would
# become the ledger §9 forbids. Overwrite is the proportionate form: it proves liveness and
# remembers nothing.
HEARTBEAT = os.path.expanduser("~/.claude/state/tarbuckle-wrap-lastrun")
TIMEOUT_S = 15
TAIL_LINES = 400 # bounded: this runs on EVERY assistant turn
@@ -74,13 +80,36 @@ def wrap_invoked(transcript_path: str) -> bool:
rec = json.loads(ln)
except Exception:
continue
if rec.get("type") != "user":
continue # an executor writing about it is not the steward doing it
content = (rec.get("message") or {}).get("content")
if not isinstance(content, str):
continue # a real invocation is a plain string
if any(m in content for m in WRAP_MARKERS):
return True
# (a) the steward TYPES /wrap-up -> a user record whose content is a plain string
if rec.get("type") == "user" and isinstance(content, str):
if any(m in content for m in WRAP_MARKERS):
return True
# (b) the steward says "wrap" in prose and the EXECUTOR invokes the skill.
#
# ⚠ THIS BRANCH IS THE WHOLE BUG, FOUND BY THE WRAP IT WAS BUILT FOR. The first
# version had only (a), and on the day it shipped the steward wrote "then wrap"
# and the executor called the Skill tool: zero user-typed records, 29 assistant
# invocations, detector correctly returns False, fool silent. The detector was
# not broken — what it was built to detect is not how a wrap actually arrives.
#
# ⚠ AND THE FIX THAT MADE IT CORRECT IS WHAT BLINDED IT. Restricting to user
# records was the right answer to the self-reference bug (the executor's own
# tool_use inputs matched the literal). The same restriction excludes the real
# path. Recorded because "the correct fix caused the next failure" is not a
# shape the controls can see; it is PENDING-160's subject exactly.
#
# Structural, not textual: a `tool_use` block whose NAME is Skill and whose
# input names the wrap-up skill. A Bash command that merely echoes the string
# has name="Bash" and does not match.
if rec.get("type") == "assistant" and isinstance(content, list):
for blk in content:
if not isinstance(blk, dict) or blk.get("type") != "tool_use":
continue
if blk.get("name") != "Skill":
continue
if "wrap-up" in str(blk.get("input") or {}):
return True
return False
@@ -146,6 +175,12 @@ def main() -> int:
payload = json.loads(sys.stdin.read() or "{}") or {}
except Exception:
return 0
try:
os.makedirs(os.path.dirname(HEARTBEAT), exist_ok=True)
with open(HEARTBEAT, "w") as fh:
fh.write(time.strftime("%Y-%m-%dT%H:%M:%S%z"))
except OSError:
pass
session_id = str(payload.get("session_id") or "")
transcript = payload.get("transcript_path") or ""
if not transcript or not wrap_invoked(transcript):
@@ -229,6 +264,20 @@ def selftest() -> int:
ck("W3 the marker is assembled, never literal in source",
source_lacks(__file__, "<command-", "name>/wrap-up"))
ck("W3n missing transcript is not a wrap", wrap_invoked(os.path.join(td, "nope")) is False)
# W3b — THE REAL PATH: steward says "wrap" in prose, executor invokes the skill.
open(t, "w").write(json.dumps({"type": "assistant", "message": {"content": [
{"type": "tool_use", "name": "Skill", "input": {"skill": "wrap-up"}}]}}) + "\n")
ck("W3b executor Skill invocation IS a wrap", wrap_invoked(t) is True)
# and it must still exclude the executor merely writing about it
open(t, "w").write(json.dumps({"type": "assistant", "message": {"content": [
{"type": "tool_use", "name": "Bash",
"input": {"command": "echo wrap-up " + WRAP_MARKERS[0]}}]}}) + "\n")
ck("W3bn a Bash echo of the marker is NOT a wrap", wrap_invoked(t) is False)
open(t, "w").write(json.dumps({"type": "assistant", "message": {"content": [
{"type": "tool_use", "name": "Skill", "input": {"skill": "wake-up"}}]}}) + "\n")
ck("W3bn a different skill is NOT a wrap", wrap_invoked(t) is False)
ck("W4 heartbeat is overwritten, never appended",
'open(HEARTBEAT, "w")' in src)
# W4 — idempotence, and consumption BEFORE the attempt.
global FIRED