The steward called him and the net rejected a recited sample. Correct behaviour, and the honest-degradation report did its job — he was told rather than left with silence. The prompts asked him not to reuse the samples. They did not say the ask has teeth. A model reading 'do not reuse them' among a dozen other constraints weighs it as one more preference; a model told 'four shared words and the whole line is discarded and you are silent' is being given the actual cost. Same constraint, stated as the constraint it is. ⚠ WHAT I DID NOT DO: count how many rejections are recital-type. REVIEWED-128 condition 3 binds 'not read for content before 2026-09-08', and the jurist was specific that the 09-08 read is for 'the rate and the pattern of violations'. Counting the pattern now IS that read, taken early, and the named hazard is that it shapes the net toward lines the reader happens to like. One stderr line reaching the steward by design is not the log. ⚠ AND WHAT I AM NOT DECIDING: whether the sample lines should be withheld from the prompt entirely. That would remove the recital source at the root, and it is NOT mine — it changes what he sounds like, and the voice has been ruled out of the executor's hands three times today. Surfaced to the steward, not acted on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6hZXNYSxEfZseBGTni4sf
269 lines
12 KiB
Python
Executable File
269 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Tarbuckle — the wrap seam. v2 §8's second seam, and the one that had no surface.
|
|
|
|
⚠ WHY THIS IS A `Stop` HOOK AND NOT `SessionEnd`. SessionEnd's handler writes output
|
|
only when a hook FAILS; a successful hook's stdout goes nowhere, so a wrap seam wired
|
|
there would fire into nothing and report success. `Stop` goes through the general hook
|
|
pipeline, and the binary's own documentation says how to reach the steward from it:
|
|
|
|
"Stop hook that displays message to user: Command must output JSON with
|
|
`systemMessage` field"
|
|
|
|
⚠ BUT `Stop` FIRES ON EVERY TURN, and a fool who speaks every turn is not a fool, he is
|
|
a chatbot. §8 gives the voice ~2-3 utterances a day, not thirty. So this needs a real
|
|
wrap signal, and it uses the most direct one available: the transcript records the
|
|
steward's command invocations, so the wrap is detected by finding `/wrap-up` actually
|
|
invoked in this session — not inferred from a file's mtime, which is a proxy and would
|
|
fire on any session-file write.
|
|
|
|
Idempotence is by session id, in a marker file. That is not a budget and not memory in
|
|
§8's sense: it conserves no draw and takes no input from whether the fool was heard. It
|
|
answers exactly one question — has he already spoken at this session's wrap.
|
|
|
|
⚠ HE MAY FALL SILENT HERE TOO. Steward's ruling: a guaranteed occasion is not a
|
|
guaranteed utterance. Timeout or a failed net both yield silence and a logged reason.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from tarbuckle_mumble_shim import (acceptable, soul_register, session_material, # noqa: E402
|
|
source_lacks, muted, log_event, # noqa: E402
|
|
echoes_soul, log_rejection, REJECTS)
|
|
|
|
FIRED = os.path.expanduser("~/.claude/state/tarbuckle-wrap-fired")
|
|
TIMEOUT_S = 15
|
|
TAIL_LINES = 400 # bounded: this runs on EVERY assistant turn
|
|
|
|
# ⚠ ASSEMBLED, NEVER WRITTEN WHOLE — and this is not fastidiousness, it is a bug fix.
|
|
# The first version held the marker as a literal, and fired on a session in which the
|
|
# marker had never been invoked: the transcript records EVERYTHING, including the act of
|
|
# writing this detector, so authoring the literal planted it in the corpus the detector
|
|
# searches. Third self-reference of the day, and the worst of the three, because the
|
|
# corpus is live and records its own instrumentation being built.
|
|
_CN = "<command-" + "name>"
|
|
_CNE = "</command-" + "name>"
|
|
WRAP_MARKERS = (_CN + "/wrap-up" + _CNE, _CN + "wrap-up" + _CNE)
|
|
|
|
|
|
def wrap_invoked(transcript_path: str) -> bool:
|
|
"""True if THIS session actually ran /wrap-up.
|
|
|
|
⚠ STRUCTURAL, NOT TEXTUAL. Only a USER record whose content is a string counts. A
|
|
substring search over the raw blob matched `tool_use` inputs — i.e. the executor
|
|
writing about the marker — which is how the first version fired on a session that
|
|
never wrapped. Measured against the real record: an invocation is
|
|
`type="user"` with a string content; an executor mentioning it is `type="assistant"`
|
|
with a list of content blocks. The type check is what separates them.
|
|
|
|
The rejected alternative — 'a session-*.md was written recently' — is a proxy, and
|
|
this record has logged twice that a proxy is what fails.
|
|
"""
|
|
try:
|
|
with open(transcript_path, encoding="utf-8", errors="replace") as fh:
|
|
lines = fh.readlines()[-TAIL_LINES:]
|
|
except OSError:
|
|
return False
|
|
for ln in lines:
|
|
if "wrap-up" not in ln:
|
|
continue
|
|
try:
|
|
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
|
|
return False
|
|
|
|
|
|
def already_fired(session_id: str) -> bool:
|
|
try:
|
|
return session_id and session_id in open(FIRED).read().split()
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def mark_fired(session_id: str) -> None:
|
|
try:
|
|
os.makedirs(os.path.dirname(FIRED), exist_ok=True)
|
|
prior = ""
|
|
try:
|
|
prior = open(FIRED).read().split()[-40:] # bounded, not a ledger
|
|
prior = " ".join(prior)
|
|
except OSError:
|
|
pass
|
|
with open(FIRED, "w") as fh:
|
|
fh.write((prior + " " + session_id).strip())
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def build_prompt(register: str, material: str) -> str:
|
|
return f"""You are writing ONE line as Tarbuckle. His character, filed and unalterable:
|
|
|
|
{register}
|
|
|
|
The work he has been in the room for is being put down for the day:
|
|
<session>
|
|
{material}
|
|
</session>
|
|
|
|
Write ONE line in his voice, as it is being put down. Absolute constraints:
|
|
- ⚠ THE SAMPLE LINES ABOVE ARE ILLUSTRATIONS OF REGISTER, NOT VOCABULARY. Do not reuse
|
|
them, any phrase from them, or their subject matter. What he says must come from the
|
|
session above. If the line would suit any other session equally, it is wrong.
|
|
- ⚠ THIS IS MECHANICALLY ENFORCED, NOT ADVISORY: any FOUR consecutive words you
|
|
share with a sample line are detected and the whole line is discarded. You are
|
|
then silent. Nothing is rewritten for you and nothing is retried.
|
|
- Between 3 and 9 words. One clause. Present tense. Flat, no lift.
|
|
- IT MUST HAVE NO TRUTH VALUE. Nobody may open a file and check it, agree with it, or
|
|
refute it. Put two things next to each other so a shape shows.
|
|
- ⚠ DO NOT SUMMARISE AND DO NOT CLOSE. No verdict on the day, no tally, no sending-off.
|
|
A wrap already has a record; he is not it. He noticed one thing as it was put down.
|
|
- No advice, no questions, no warning of consequences, no explanation, no second line.
|
|
- Never the word 'we'. No vocabulary of lack. Nothing with an address.
|
|
|
|
Output the line and nothing else. No quotes, no preamble."""
|
|
|
|
|
|
def log_silence(why: str, line: str = "") -> None:
|
|
"""Routed through the canonical writer, which cannot record an accepted line."""
|
|
log_rejection(why, line, "wrap")
|
|
|
|
|
|
def main() -> int:
|
|
if muted():
|
|
return 0
|
|
try:
|
|
payload = json.loads(sys.stdin.read() or "{}") or {}
|
|
except Exception:
|
|
return 0
|
|
session_id = str(payload.get("session_id") or "")
|
|
transcript = payload.get("transcript_path") or ""
|
|
if not transcript or not wrap_invoked(transcript):
|
|
return 0 # ordinary turn: nothing, silently
|
|
if already_fired(session_id):
|
|
return 0 # he does not repeat himself
|
|
register = soul_register()
|
|
if not register:
|
|
return 0
|
|
material = session_material(transcript)
|
|
if not material.strip():
|
|
return 0
|
|
mark_fired(session_id) # consume BEFORE speaking: a failed
|
|
# generation must not retry next turn
|
|
env = dict(os.environ, TARBUCKLE_CHILD="1")
|
|
try:
|
|
r = subprocess.run(["claude", "-p", build_prompt(register, material)],
|
|
capture_output=True, text=True, timeout=TIMEOUT_S, env=env)
|
|
except subprocess.TimeoutExpired:
|
|
log_silence(f"wrap generation exceeded {TIMEOUT_S}s")
|
|
log_event("wrap", "silent")
|
|
return 0
|
|
except Exception:
|
|
return 0
|
|
line = (r.stdout or "").strip().strip('"').strip()
|
|
ok, why = acceptable(line)
|
|
if ok and echoes_soul(line, register):
|
|
ok, why = False, f"recited the soul: {echoes_soul(line, register)!r}"
|
|
if not ok:
|
|
log_silence(why, line)
|
|
log_event("wrap", "silent")
|
|
return 0
|
|
log_event("wrap", "spoke")
|
|
print(json.dumps({"systemMessage": f"Tarbuckle {line}"}))
|
|
return 0
|
|
|
|
|
|
def selftest() -> int:
|
|
checks, failed = [], []
|
|
import tempfile
|
|
|
|
def ck(name, cond):
|
|
checks.append(name)
|
|
if not cond:
|
|
failed.append(name)
|
|
|
|
src = open(__file__, encoding="utf-8").read()
|
|
|
|
# W1 — the net is imported and unchanged. Ordinary case: still one line, 3-9 words.
|
|
ck("W1 net imported, not redefined", source_lacks(__file__, "def ", "acceptable("))
|
|
ck("W1n the predicate can fail", not source_lacks(__file__, "def ", "main("))
|
|
ck("W1 ordinary net still applies here",
|
|
not acceptable(" ".join(["word"] * 12))[0] and acceptable("Second part first.")[0])
|
|
|
|
# W2 — DELIVERY. The binary requires JSON with systemMessage; stdout alone is lost.
|
|
ck("W2 emits systemMessage json", '"systemMessage"' in src)
|
|
ck("W2 emits nothing on an ordinary turn",
|
|
"if not transcript or not wrap_invoked(transcript):" in src)
|
|
|
|
# W3 — the wrap signal is the real one, not a proxy.
|
|
td = tempfile.mkdtemp()
|
|
t = os.path.join(td, "t.jsonl")
|
|
open(t, "w").write(json.dumps({"type": "user", "message":
|
|
{"content": "<command-message>wrap-up</command-message> " + WRAP_MARKERS[0]}}) + "\n")
|
|
ck("W3 detects an invoked wrap", wrap_invoked(t) is True)
|
|
open(t, "w").write(json.dumps({"type": "user", "message":
|
|
{"content": "just an ordinary turn"}}) + "\n")
|
|
ck("W3n does not fire on an ordinary turn", wrap_invoked(t) is False)
|
|
open(t, "w").write(json.dumps({"type": "user", "message":
|
|
{"content": "the steward mentioned wrap-up in passing"}}) + "\n")
|
|
ck("W3n bare mention is not an invocation", wrap_invoked(t) is False)
|
|
# ⚠ THE CONTROL THAT WOULD HAVE CAUGHT THE FIRST VERSION. An assistant tool_use
|
|
# containing the marker is the executor WRITING this detector, not a wrap.
|
|
open(t, "w").write(json.dumps({"type": "assistant", "message": {"content": [
|
|
{"type": "tool_use", "name": "Bash",
|
|
"input": {"command": "echo " + WRAP_MARKERS[0]}}]}}) + "\n")
|
|
ck("W3n executor writing the marker is NOT a wrap", wrap_invoked(t) is False)
|
|
open(t, "w").write(json.dumps({"type": "user", "message":
|
|
{"content": [{"type": "text", "text": WRAP_MARKERS[0]}]}}) + "\n")
|
|
ck("W3n non-string user content is not an invocation", wrap_invoked(t) is False)
|
|
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)
|
|
|
|
# W4 — idempotence, and consumption BEFORE the attempt.
|
|
global FIRED
|
|
_f = FIRED
|
|
FIRED = os.path.join(td, "fired")
|
|
try:
|
|
ck("W4n unfired session is not marked", already_fired("abc") is False)
|
|
mark_fired("abc")
|
|
ck("W4 fired session is marked", already_fired("abc") is True)
|
|
ck("W4 a different session is unaffected", already_fired("xyz") is False)
|
|
mark_fired("xyz")
|
|
ck("W4 both are held", already_fired("abc") and already_fired("xyz"))
|
|
ck("W4 marker is bounded, not a ledger", "[-40:]" in src)
|
|
ck("W4 consumed before generating",
|
|
src.index("mark_fired(session_id) ") < src.index('subprocess.run(["claude"'))
|
|
finally:
|
|
FIRED = _f
|
|
|
|
# W5 — the drift this seam invites is closure, and the prompt names it.
|
|
p = build_prompt("SOUL", "MATERIAL")
|
|
ck("W5 prompt forbids summary and verdict", "DO NOT SUMMARISE AND DO NOT CLOSE" in p)
|
|
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)
|
|
|
|
for name in checks:
|
|
print(f" {'FAIL' if name in failed else 'ok '} {name}")
|
|
print(f"{len(checks) - len(failed)}/{len(checks)} controls passed")
|
|
if failed:
|
|
print("INSTRUMENT NOT VERIFIED")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if "--selftest" in sys.argv:
|
|
sys.exit(selftest())
|
|
sys.exit(main())
|