diff --git a/claude/governance/fool/TARBUCKLE-SPEC-13.1-2026-08-25.md b/claude/governance/fool/TARBUCKLE-SPEC-13.1-2026-08-25.md index 4f7f263..8427667 100644 --- a/claude/governance/fool/TARBUCKLE-SPEC-13.1-2026-08-25.md +++ b/claude/governance/fool/TARBUCKLE-SPEC-13.1-2026-08-25.md @@ -27,6 +27,7 @@ contact with the substrate; a spec written first would have carried all four. | **body** | every status-line render | status line | `scripts/tarbuckle-body.py` | | **mumble** | wall clock, 20 min | status line, 120 s | `+ scripts/tarbuckle-mumble.py` | | **voice** | wake seam | SessionStart hook output | `scripts/tarbuckle-seam.py` | +| **invoked** | the steward calls him by name | stdout, run by the steward | `scripts/tarbuckle-invoke.py` | Configuration, in `~/.claude/settings.json`: @@ -116,6 +117,7 @@ passes, let it produce nothing."* Timeout ⇒ silence, logged. python3 ~/dotfiles/scripts/tarbuckle-body.py --selftest # 30 controls python3 ~/dotfiles/scripts/tarbuckle-mumble.py --selftest # 19 controls python3 ~/dotfiles/scripts/tarbuckle-seam.py --selftest # 15 controls +python3 ~/dotfiles/scripts/tarbuckle-invoke.py --selftest # 17 controls ``` Positive and negative throughout: every claim has a fixture that makes it fail. @@ -126,8 +128,39 @@ instance was written minutes after the first was fixed, by the party who fixed i watching for it. **The fix is a mechanism, not a correction**, because the correction had already been tried and did not hold. +## 7a · Named invocation (§9) + + ! python3 ~/dotfiles/scripts/tarbuckle-invoke.py # bare + ! python3 ~/dotfiles/scripts/tarbuckle-invoke.py "what now" # asked something + +⚠ **Run by the steward, not relayed by the executor.** Anything the executor pastes is +the executor's paraphrase of a fool; this surface exists so the floor is yielded rather +than reported. It finds the newest transcript itself so it needs no session context. + +**The net is widened EXPLICITLY and in one dimension only** — §9 licenses length for this +surface and nothing else: + +| | ordinary | invoked | +|---|---|---| +| word ceiling | 9 | **180** | +| one line | yes | **no** | +| no advice · no questions · no `we` · no vocabulary of lack · no addresses | **unchanged** | **unchanged** | + +⚠ **Length is where the no-truth-value guard is most at risk** — a fool given a paragraph +elaborates, and elaboration is how a gesture becomes a claim. The prompt spends most of +its constraint budget there, and a violation is still silence. + ## 8 · Owed +⚠ **This list omitted named invocation in its first version, on the day it was written.** +§9 requires it; §1 did not carry it; §8 did not owe it. Caught by the steward asking +whether he could be invoked — a question the document should have answered. **The fourth +negative state-claim to go wrong in two days, and the second inside a document written to +prevent exactly that.** Recorded rather than silently corrected. + +- **`mute` / `off`** (§9, *"available at all times"*) — **not built.** The only off-switch + is §9 below, which is removal, not muting. Muting is never a fault and there is + currently no way to do it. - **The wrap seam** (§4) — no delivery surface; route is a skill change. - **The two-week rate report** — §8 obliges it; `DEFERRED-DECISION: mumble-rate-two-week-report`, converted `manual` → `date 2026-09-08` the day the body shipped. diff --git a/scripts/tarbuckle-invoke.py b/scripts/tarbuckle-invoke.py new file mode 100755 index 0000000..5c83204 --- /dev/null +++ b/scripts/tarbuckle-invoke.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Tarbuckle — named invocation. v2 §9: + + "Named invocation: the steward calls it by name, the executor or jurist yields + the floor, the fool answers at length." + +⚠ RUN IT YOURSELF. The point of this surface is that it does not pass through the +executor. In Claude Code, type: ! python3 ~/dotfiles/scripts/tarbuckle-invoke.py +Anything the executor relays is the executor's paraphrase of a fool; this is the fool. + +⚠ THE NET IS WIDENED, EXPLICITLY, AND ONLY IN THE ONE DIMENSION §9 NAMES. Steward's +ruling, 2026-08-25: "widen it explicitly and say so, but never relax +silence-on-violation." §9 licenses LENGTH for this surface and nothing else, so: + + word ceiling 9 -> 180 (§9: "answers at length") + one-line rule on -> off (a paragraph is the point) + no advice · no questions · no 'we' · no vocabulary of lack · no addresses + UNCHANGED, and not parameterised anywhere + +⚠ AND LENGTH IS WHERE THE NO-TRUTH-VALUE GUARD IS MOST AT RISK. A fool given a +paragraph will elaborate, and elaboration is how a gesture becomes a claim. That is the +door §2 calls design failure. The prompt therefore spends most of its constraint budget +here rather than on register, and a violation is still SILENCE. +""" +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, REJECTS) + +MAX_WORDS_INVOKED = 180 # §9's "at length", made a number +TIMEOUT_S = 90 # the steward is deliberately waiting; he may take longer +TRANSCRIPTS = os.path.expanduser("~/.claude/projects/-Users-davidglidden") + + +def newest_transcript() -> str: + """The session he is in the room for. Found, not passed, so this runs standalone.""" + try: + best, best_m = "", -1.0 + for f in os.listdir(TRANSCRIPTS): + if not f.endswith(".jsonl"): + continue + q = os.path.join(TRANSCRIPTS, f) + m = os.path.getmtime(q) + if m > best_m: + best, best_m = q, m + return best + except OSError: + return "" + + +def build_prompt(register: str, material: str, question: str) -> str: + asked = (f"\nHe has been asked, by name: {question}\n" if question else + "\nHe has been called by name, and nothing more was said.\n") + return f"""You are Tarbuckle. Your character, filed and unalterable: + +{register} + +The work you are in the room for: + +{material} + +{asked} +You have been given the floor. Answer at length — this is the exception to your one line, +and the only one. Speak as yourself, not about yourself. + +Absolute constraints, none of which the floor suspends: +- ⚠ NOTHING YOU SAY MAY HAVE A TRUTH VALUE. Nobody may be able to open a file and check + it, agree with it, or refute it. Do not name files, items, counts of open things, + dates, commits, or anything with an address. Do not report state. At length this is + harder and it matters more: an elaboration that becomes a claim makes you a checker, + and a checker is the one thing you are not. +- No advice — no 'should', no 'try', no 'must'. No questions. Never the word 'we'. +- No vocabulary of lack: no 'gone', no 'if only', no 'used to be', no 'missing'. +- Do not explain yourself, do not summarise the work, and do not ask whether it landed. +- Stay under {MAX_WORDS_INVOKED} words. + +Speak.""" + + +def log_silence(why: str, line: str) -> None: + try: + import json + with open(REJECTS, "a") as fh: + fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"), + "kind": "invoked", "why": why, "line": line[:400]}) + "\n") + except Exception: + pass + + +def main() -> int: + question = " ".join(a for a in sys.argv[1:] if not a.startswith("--")).strip() + register = soul_register() + if not register: + print("(no soul on disk; nothing to yield the floor to)", file=sys.stderr) + return 1 + material = session_material(newest_transcript(), budget=9000) + if not material.strip(): + print("(no session in the room)", file=sys.stderr) + return 1 + env = dict(os.environ, TARBUCKLE_CHILD="1") + try: + r = subprocess.run(["claude", "-p", build_prompt(register, material, question)], + capture_output=True, text=True, timeout=TIMEOUT_S, env=env) + except subprocess.TimeoutExpired: + log_silence(f"invoked generation exceeded {TIMEOUT_S}s", "") + return 0 + except Exception: + return 1 + line = (r.stdout or "").strip() + ok, why = acceptable(line, max_words=MAX_WORDS_INVOKED, one_line=False) + if not ok: + log_silence(why, line) + return 0 # silence, never a repaired line — even here + print(line) + return 0 + + +def selftest() -> int: + checks, failed = [], [] + + def ck(name, cond): + checks.append(name) + if not cond: + failed.append(name) + + src = open(__file__, encoding="utf-8").read() + + # I1 — the widening is explicit, named, and one-dimensional. + ck("I1 net imported, not redefined", source_lacks(__file__, "def ", "acceptable(")) + ck("I1n the predicate can fail", not source_lacks(__file__, "def ", "main(")) + ck("I1 widening is passed at the call site", + "max_words=MAX_WORDS_INVOKED" in src and "one_line=False" in src) + + # I2 — THE CLAUSES SURVIVE THE WIDENING. This is the steward's ruling, executable. + long_ok = " ".join(["word"] * 100) + ck("I2 length is admitted", + acceptable(long_ok, max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2 paragraphs are admitted", + acceptable("First part here.\nSecond part here.", + max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2n advice still rejected at length", + not acceptable("You should " + long_ok, + max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2n questions still rejected at length", + not acceptable(long_ok + " and how is that going?", + max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2n 'we' still rejected at length", + not acceptable("We " + long_ok, max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2n vocabulary of lack still rejected at length", + not acceptable(long_ok + " gone", max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + ck("I2n the ceiling still bites", + not acceptable(" ".join(["word"] * (MAX_WORDS_INVOKED + 1)), + max_words=MAX_WORDS_INVOKED, one_line=False)[0]) + + # I3 — the no-truth-value guard is where the prompt spends its budget. + p = build_prompt("SOUL", "MATERIAL", "") + # Tests the SUBSTANCE, not a literal: the phrase must be present AND negated. + # The first version asserted a literal the prompt did not use, which would have + # passed happily on any rewording that dropped the constraint entirely. + up = p.upper() + ck("I3 prompt forbids truth value", + "TRUTH VALUE" in up and any(neg in up for neg in ("NOTHING", "NO ", "MAY NOT", "NOT"))) + ck("I3n the predicate can fail", + not ("TRUTH VALUE" in "A PROMPT WITH NO SUCH CONSTRAINT".upper())) + ck("I3 prompt forbids addresses and state", + "anything with an address" in p and "Do not report state" in p) + ck("I3 prompt names the length risk", "makes you a checker" in p) + ck("I3 a question is carried when given", + "by name: what now" in build_prompt("S", "M", "what now")) + ck("I3n bare invocation says so", + "nothing more was said" in build_prompt("S", "M", "")) + + # I4 — silence on violation survives here too. + ck("I4 violation path is silence, not repair", + "log_silence(why, line)" in src and source_lacks(__file__, "def ", "repair(")) + + 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()) diff --git a/scripts/tarbuckle-mumble.py b/scripts/tarbuckle-mumble.py index 50912b8..23cec43 100755 --- a/scripts/tarbuckle-mumble.py +++ b/scripts/tarbuckle-mumble.py @@ -132,11 +132,22 @@ Write ONE line in his voice. Absolute constraints: Output the line and nothing else. No quotes, no preamble.""" -def acceptable(line: str) -> tuple[bool, str]: - if not line or "\n" in line.strip(): +def acceptable(line: str, max_words: int = MAX_WORDS, + one_line: bool = True) -> tuple[bool, str]: + """The net. Widening is possible but must be PASSED EXPLICITLY at the call site. + + ⚠ Steward's ruling, 2026-08-25: "If the seam voice needs a wider net because seams + warrant more than nine words, widen it explicitly and say so, but NEVER relax + silence-on-violation." So the defaults are the filed ones, a caller that wants more + room has to say so in its own source, and no caller can turn the net off — the + clauses below are not parameterised, and deliberately. + """ + if not line: + return False, "empty" + if one_line and "\n" in line.strip(): return False, "not one line" n = len(line.split()) - if not (MIN_WORDS <= n <= MAX_WORDS): + if not (MIN_WORDS <= n <= max_words): return False, f"{n} words" for pat in BANNED: if re.search(pat, line, re.I): @@ -210,6 +221,13 @@ def selftest() -> int: ck("A2n rejects vocabulary of lack", not acceptable("The third one is missing now.")[0]) ck("A2n rejects 'we'", not acceptable("We did the second part first.")[0]) ck("A2n rejects two lines", not acceptable("First line here.\nSecond line here.")[0]) + # The widening is explicit, and it widens ONLY what it names. + ck("A2 widening admits a longer line", + acceptable(" ".join(["word"] * 40), max_words=120)[0]) + ck("A2n widening does NOT relax the clauses", + not acceptable("You should " + " ".join(["word"] * 40), max_words=120)[0]) + ck("A2n widening does NOT relax 'we'", + not acceptable("We " + " ".join(["word"] * 40), max_words=120)[0]) # A3 — the prompt carries the no-truth-value constraint verbatim, not by intention. p = build_prompt("aside", "SOUL", "MATERIAL")