From 9f06efcc9ad28dc1b2c0e8475ca3e9a0695ed6bb Mon Sep 17 00:00:00 2001 From: David F Glidden Date: Tue, 25 Aug 2026 17:11:22 +0200 Subject: [PATCH] [FIX] The rate the steward asked for was uncomputable: only failures were recorded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Two weeks of true-versus-drawn rate" needs a denominator, and there was none. Rejections were logged; draws were not, successes were not. The 73% that comes up silent — the whole denominator — left no trace at all. The instrument named as the diagnostic could report only its own failures, which is the shape of a log that always looks alarming and can never be checked. ⚠ §8 AND §9 LOOK LIKE THEY COLLIDE HERE AND DO NOT, on the reading taken: §9's "filed nowhere — no PENDING entry, no log, no item" governs the fool's OUTPUT entering the record; §8 orders "report the observed mumble rate after two weeks". So this records THAT something happened and never WHAT was said. log_event() takes a surface and an outcome and structurally cannot be handed a line — asserted on co_varnames, because a comment promising it would be a comment promising behaviour. ⚠ THE REJECTIONS LOG IS A DIFFERENT CASE AND IS NOT SETTLED. It holds up to 200 characters of his words, added at the steward's instruction so the log could answer whether register and net are mismatched. That is nearer to filing than counting, and §9 says no log. Flagged in the code and put to the steward rather than resolved by the party that wrote it. Nothing reads any of it back. Measurement, not memory. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01J6hZXNYSxEfZseBGTni4sf --- scripts/tarbuckle-body.py | 13 +++++++++++++ scripts/tarbuckle-invoke.py | 5 ++++- scripts/tarbuckle-mumble.py | 33 +++++++++++++++++++++++++++++++++ scripts/tarbuckle-seam.py | 5 ++++- scripts/tarbuckle-wrap.py | 5 ++++- 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/scripts/tarbuckle-body.py b/scripts/tarbuckle-body.py index fa64526..86510a6 100755 --- a/scripts/tarbuckle-body.py +++ b/scripts/tarbuckle-body.py @@ -92,6 +92,18 @@ def log_invocation(payload: dict) -> None: pass +def _log_draw(kind: str) -> None: + """Occurrence only. Local, so the body needs nothing importable to tick.""" + try: + d = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl") + os.makedirs(os.path.dirname(d), exist_ok=True) + with open(d, "a") as fh: + fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "surface": "tick", "outcome": kind}) + "\n") + except Exception: + pass + + def _muted() -> str: """Local, not imported: the body must not depend on the generator being importable.""" try: @@ -148,6 +160,7 @@ def fire_tick(now: float, transcript: str) -> str: """ _write_tick(now) kind = draw() + _log_draw(kind) # the 73% is the denominator; unrecorded, no rate exists if kind == "silent" or not transcript: return kind try: diff --git a/scripts/tarbuckle-invoke.py b/scripts/tarbuckle-invoke.py index 46cb02f..012a29d 100755 --- a/scripts/tarbuckle-invoke.py +++ b/scripts/tarbuckle-invoke.py @@ -29,7 +29,7 @@ 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, REJECTS) + source_lacks, muted, log_event, REJECTS) MAX_WORDS_INVOKED = 180 # §9's "at length", made a number TIMEOUT_S = 90 # the steward is deliberately waiting; he may take longer @@ -109,6 +109,7 @@ def main() -> int: capture_output=True, text=True, timeout=TIMEOUT_S, env=env) except subprocess.TimeoutExpired: log_silence(f"invoked generation exceeded {TIMEOUT_S}s", "") + log_event("invoke", "silent") return 0 except Exception: return 1 @@ -116,6 +117,7 @@ def main() -> int: ok, why = acceptable(line, max_words=MAX_WORDS_INVOKED, one_line=False) if not ok: log_silence(why, line) + log_event("invoke", "silent") # ⚠ THE LINE IS STILL WITHHELD — silence-on-violation is not relaxed, and the # rejected text is never printed. But the INSTRUMENT reports its own state. # @@ -130,6 +132,7 @@ def main() -> int: print(f"(called; nothing passed the net — {why}. logged, not shown.)", file=sys.stderr) return 0 + log_event("invoke", "spoke") print(line) return 0 diff --git a/scripts/tarbuckle-mumble.py b/scripts/tarbuckle-mumble.py index 53f08ff..a36d08f 100755 --- a/scripts/tarbuckle-mumble.py +++ b/scripts/tarbuckle-mumble.py @@ -48,6 +48,31 @@ BANNED = ( MUTE = os.path.expanduser("~/.claude/state/tarbuckle-mute") +DRAWS = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl") + + +def log_event(surface: str, outcome: str) -> None: + """Count occurrences. NEVER content. §8 obliges a rate; §9 forbids a log. + + ⚠ The two clauses look like they collide and do not, on this reading: §9's "filed + nowhere — no PENDING entry, no log, no item" is about the fool's OUTPUT entering the + record, and §8 explicitly orders "report the observed mumble rate after two weeks." + A rate needs a denominator. So this records THAT something happened and never WHAT + was said — occurrence, not utterance. + + ⚠ The rejections log is a different case and is NOT settled: it holds up to 200 + characters of his words, at the steward's instruction, and that is nearer to filing. + Flagged rather than resolved; see the note put to the steward 2026-08-25. + + Nothing reads this back. It is measurement, not memory. + """ + try: + os.makedirs(os.path.dirname(DRAWS), exist_ok=True) + with open(DRAWS, "a") as fh: + fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "surface": surface, "outcome": outcome}) + "\n") + except Exception: + pass def muted() -> str: @@ -199,6 +224,7 @@ def main() -> int: "kind": kind, "why": why, "line": line[:200]}) + "\n") except Exception: pass + log_event(kind, "rejected") return 1 # silence. The draw was already consumed. try: os.makedirs(os.path.dirname(SLOT), exist_ok=True) @@ -206,6 +232,7 @@ def main() -> int: json.dump({"utterance": line, "kind": kind, "written": int(time.time())}, fh) except Exception: return 1 + log_event(kind, "spoke") return 0 @@ -278,6 +305,12 @@ def selftest() -> int: ck("A5n a junk value is not a mute", muted() == "") finally: MUTE = _m + # A6 — the occurrence log is content-free, and that is the whole point. + ck("A6 log_event takes no content", + log_event.__code__.co_argcount == 2 + and set(log_event.__code__.co_varnames[:2]) == {"surface", "outcome"}) + ck("A6n it cannot be handed a line", + "line" not in log_event.__code__.co_varnames) ck("A4n the predicate can fail", any(("PENDING" in q) for q in paths + ("/x/PENDING.md",))) diff --git a/scripts/tarbuckle-seam.py b/scripts/tarbuckle-seam.py index c7538a2..920bdcb 100755 --- a/scripts/tarbuckle-seam.py +++ b/scripts/tarbuckle-seam.py @@ -41,7 +41,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # reimplementing is the "one canonical source" rule applied to a constraint: a second # copy of `acceptable()` is a second, quietly divergent standard. from tarbuckle_mumble_shim import (acceptable, soul_register, session_material, # noqa: E402 - source_lacks, muted, REJECTS) + source_lacks, muted, log_event, REJECTS) LAST_TICK = os.path.expanduser("~/.claude/state/tarbuckle-last-tick") SEAM_TIMEOUT_S = 15 @@ -123,6 +123,7 @@ def main() -> int: timeout=SEAM_TIMEOUT_S, env=env) except subprocess.TimeoutExpired: log_silence(f"seam generation exceeded {SEAM_TIMEOUT_S}s") + log_event("seam", "silent") return 0 # the occasion was guaranteed; the utterance is not except Exception: return 0 @@ -130,7 +131,9 @@ def main() -> int: ok, why = acceptable(line) if not ok: log_silence(why, line) + log_event("seam", "silent") return 0 + log_event("seam", "spoke") print(f"Tarbuckle {line}") return 0 diff --git a/scripts/tarbuckle-wrap.py b/scripts/tarbuckle-wrap.py index 8a6cb48..bafdb56 100755 --- a/scripts/tarbuckle-wrap.py +++ b/scripts/tarbuckle-wrap.py @@ -31,7 +31,7 @@ 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, REJECTS) + source_lacks, muted, log_event, REJECTS) FIRED = os.path.expanduser("~/.claude/state/tarbuckle-wrap-fired") TIMEOUT_S = 15 @@ -163,6 +163,7 @@ def main() -> int: 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 @@ -170,7 +171,9 @@ def main() -> int: ok, why = acceptable(line) 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