[FIX] Tarbuckle tier 2: the mumble ticks on the clock and speaks from the soul

Material, register and occasion as AMENDMENT 6 separates them: the live session
supplies the material, the filed soul supplies the register, the clock supplies the
occasion. 73/20/7 drawn at each 20-minute tick, consumed whatever it says.

The soul is READ from its artifact at run time, never pasted here — a copy would be a
parallel version of a governed record. No soul, no voice; there is deliberately no
fallback register, because a fallback voice is a second fool nobody derived.

The utterance must have no ADJUDICATION PATH, not merely be unfalsifiable in principle.
The prompt says so verbatim and a mechanical net sits under it: 3-9 words, one line, no
advice, no questions, no "we", no vocabulary of lack, nothing with an address. A
violation yields SILENCE, never a repaired line — rewriting the fool's words would make
the executor its editor. Rejections are logged so the two-week rate report states the
true rate rather than the drawn one.

Generation is detached because a headless call measured 7-11 s and a status line cannot
wait. A recursion guard rides along, and is honestly precautionary: headless claude was
observed NOT to render a status line (zero invocations logged across an 11 s call), so
the guard is one env check against a fork bomb, not a fix for something seen.

30/30 controls on the body, 18/18 on the generator, positive and negative throughout.
D2 holds the proportions to the filed 73/20/7 over 60k draws; D5 proves the tick
consumes on a silent draw, which is the determination that forbids a conserved draw.

⚠ One control failed against ITSELF: "this file contains no copy of the soul" searched
for a phrase its own needle had placed in the file. Fixed by building the needle rather
than writing it. Same class as the hand-typed link canary.

The two-week deferral converted manual -> date 2026-09-08, as its own discriminator
instructed, the day the body shipped. No manual-only deferrals remain.

AMENDMENT 8 files the substrate findings, including the one clause of this item's own
corrected text that does not survive contact with the schema.

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 16:36:53 +02:00
co-authored by Claude Opus 5
parent 6f0ccde4c6
commit 3df5e4f4bb
3 changed files with 426 additions and 4 deletions
+49 -3
View File
@@ -4666,6 +4666,50 @@ Applied here: the window supplies at most *something happened · nothing happene
**Awaiting:** steward, on the content question — four options, after the beacon. **Not before the soul.**
### AMENDMENT 8 — 2026-08-25 — the unverified assumption is SETTLED, by this item's own method; and one clause does not survive it
*Filed at implementation, against the substrate, not against documentation.*
#### (a) ✅ The assumption this item flagged is settled — and it was settled the way the item said to settle it
> *"Whether Claude Code re-renders the status line on a timer or only on session activity. Not verified… **Settle by experiment:** configure a status line that prints the current clock time, leave a session idle, and observe whether the displayed time advances."*
**Both halves ran.** The schema, read out of the binary: `statusLine: {type, command, padding?, refreshInterval?}`, where `refreshInterval` is *"Re-run the status line command every N seconds in addition to event-driven updates"* — seconds, minimum 1. ⚠ **The first name-match was `refreshIntervalMs`, which belongs to the certificate watcher.** Reading the context rather than trusting the match is what kept a wrong key out of the design.
And then the behaviour, from the body's own invocation log on the day it shipped:
```
gaps(s): 0,0,0,0,0,0,0,0,0,0,0,0,37,13,0,2,15,2,4,11,13, 60,60,60,60,60,60,60,60,60,60,60,60,60,60, 11,1,1,9,1,3
└─────────── event-driven ───────────┘ └────────── idle: timer ──────────┘
```
**Fourteen consecutive 60-second gaps with no input at all.** The timer is real and fires during idle. *Documentation is a claim about behaviour; this is the behaviour* — the same standard AMENDMENT 3 set for itself.
#### (b) ⚠ One clause of the AMENDMENT-2 correction does not survive, and the conclusion outlives its mechanism
The corrected text reads: *"the 20-minute tick is a counter over refreshes rather than over events… no event-gating remains within a session."*
**Against `in addition to event-driven updates`, a counter over refreshes IS a counter over events.** The log's leading run of twelve zero-second gaps is exactly what such a counter would have been counting — invocations bursting with activity, which is the v1 defect §8 was rewritten to remove.
**The conclusion survives; the mechanism named does not.** Frequency is not event-gated — but only because the built tick consults the **clock**, not the invocation count. Recorded here rather than silently implemented, because this item has now logged twice that *a conclusion which keeps its old reasoning after that reasoning is falsified is how a false premise survives its own refutation.* This is the third instance, and it is the item's own text this time.
#### (c) What shipped, and where the guards actually live
`6f0ccde` (body) and this session's tier-2 commit. The binding constraints are carried **structurally rather than by intention**, which is the only form that survives a later editor:
| constraint | how it is held | its negative control |
|---|---|---|
| the body correlates with nothing | `render()` takes the minute and nothing else — a function that cannot see the session cannot leak it; asserted on `co_argcount` and `co_names` | a deliberately leaky fixture **is** caught |
| the body does not announce the voice | `len(MARKS)` coprime with the interval, so the mark at mumble-time walks the whole cycle | a commensurate 4-cycle **is** caught |
| the tick consumes | proven on the built thing: 12/12 forced-due invocations advanced the clock | — |
| the utterance has no adjudication path | the prompt states it verbatim; a mechanical net rejects advice, questions, `we`, vocabulary of lack, and anything outside 3–9 words | every clause has a rejecting fixture |
| the register is not duplicated | the soul is **read from its filed artifact** at run time; no soul, no voice, and no fallback | a copy in the source **is** caught |
⚠ **One control failed against itself and was fixed rather than relaxed:** the check *"this file contains no copy of the soul"* searched for a phrase that its own needle had put into the file. Same class as the hand-typed link canary whose only finding was the pattern inside its own specification. The needle is now built by concatenation so the literal never appears.
**Files affected:** `scripts/tarbuckle-body.py`, `scripts/tarbuckle-mumble.py`, `~/.claude/settings.json` (untracked — see below).
**Awaiting:** nothing. Filed as a record at implementation.
### AMENDMENT 6 — 2026-08-22 — the adjudication-path criterion supersedes "checkable in principle"; the material is the SESSION
*Jurist correction, received via the steward. Recorded as a correction rather than as a fifth option.*
@@ -4844,12 +4888,14 @@ Link 1 to link 4 directly. **The trial built to test whether a fool could reach
<!-- DEFERRED-DECISION: mumble-rate-two-week-report
since: 2026-08-25
trigger: manual
trigger: date 2026-09-08
owner: executor
discriminator: §8's own clause — "Report the observed mumble rate after two weeks." The
two-week clock starts the day the body is wired, which has not happened, so no date can
honestly be written yet. `manual` is the correct recording rather than a proxy date, and
this converts to `trigger: date <wiring +14d>` the day the body ships.
honestly be written yet. `manual` was the correct recording rather than a proxy date, and
this CONVERTED to `date 2026-09-08` on 2026-08-25, the day the body shipped
(6f0ccde), exactly as this discriminator instructed. The clock could not start
before the body existed; it has started.
-->
*(Filed as a tracked deferral rather than left in prose. §8 already obliges this report and had no trigger — the class PENDING-158 was authorized against today. Deliberately `manual`: inventing a date before the clock can start would be the proxy the schema's own comment warns against.)*
+148 -1
View File
@@ -38,6 +38,7 @@ is filed UNKNOWN — a number derived from an unmeasured base is worse than a bl
import json
import math
import os
import subprocess
import sys
import time
@@ -52,6 +53,19 @@ MARKS = (".", "·", "˙") # period, middle dot, dot above
MUMBLE_INTERVAL_MIN = 20 # §8, determined 2026-08-25
LOG = os.path.expanduser("~/.claude/state/tarbuckle-invocations.jsonl")
LAST_TICK = os.path.expanduser("~/.claude/state/tarbuckle-last-tick")
SLOT = os.path.expanduser("~/.claude/state/tarbuckle-slot.json")
MUMBLE = os.path.expanduser("~/dotfiles/scripts/tarbuckle-mumble.py")
# 73% silent · 20% brief aside · 7% notable — v1 §8's table, hardcoded, unchanged.
# Calibrated against a TIME-UNIFORM tick, which is why the tick had to return to the
# clock before these could mean what they say (PENDING-152).
DRAW = (("silent", 73), ("aside", 20), ("notable", 7))
# How long an utterance stays on the surface. Derived from the soul, not chosen:
# "he doesn't wait to see whether it landed; he goes back to whatever he was doing
# with his hands." A line that lingers is a line waiting to be answered.
DISPLAY_WINDOW_S = 120
def render(now_min: int) -> str:
@@ -78,6 +92,76 @@ def log_invocation(payload: dict) -> None:
pass
def draw() -> str:
"""The 73/20/7 draw. Takes nothing, so it can be keyed to nothing.
SystemRandom rather than a time hash: §8 requires the cadence be UNLEARNABLE, and
anything derived from the clock is predictable by construction. Note this is the
opposite choice from the body's mark, and deliberately — the mark must be
time-derived so it correlates with nothing about the work; the draw must be random
so it cannot be anticipated at all.
"""
import random
r = random.SystemRandom().uniform(0, sum(w for _, w in DRAW))
upto = 0.0
for kind, w in DRAW:
upto += w
if r <= upto:
return kind
return "silent"
def tick_due(now: float) -> bool:
"""Clock, never an invocation counter. See the module docstring for why."""
try:
last = float(open(LAST_TICK).read().strip())
except (OSError, ValueError):
_write_tick(now) # first sight: start the clock, do not fire into a wake
return False
return (now - last) >= MUMBLE_INTERVAL_MIN * 60
def _write_tick(now: float) -> None:
try:
os.makedirs(os.path.dirname(LAST_TICK), exist_ok=True)
with open(LAST_TICK, "w") as fh:
fh.write(str(int(now)))
except OSError:
pass
def fire_tick(now: float, transcript: str) -> str:
"""Consume the draw and, if it speaks, spawn the generator DETACHED.
⚠ The tick is consumed whatever the draw says — determination, 2026-08-25: "it
consumes. There is no skip branch, and none should be written." A conserved draw
is a budget, and a budget is memory.
"""
_write_tick(now)
kind = draw()
if kind == "silent" or not transcript:
return kind
try:
subprocess.Popen([sys.executable, MUMBLE, kind, transcript],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL, start_new_session=True)
except Exception:
pass
return kind
def fresh_utterance(now: float) -> str | None:
"""The slot, if it is still warm. One slot, expiring — never a queue."""
try:
with open(SLOT) as fh:
d = json.load(fh)
if now - float(d["written"]) <= DISPLAY_WINDOW_S:
return str(d["utterance"])
except Exception:
pass
return None
def main() -> int:
payload = {}
try:
@@ -89,7 +173,17 @@ def main() -> int:
except Exception:
payload = {}
log_invocation(payload)
print(render(int(time.time() // 60)))
now = time.time()
# Precautionary guard. A headless `claude -p` was observed NOT to render a status
# line (2026-08-25, zero invocations logged during an 11 s call), so this is not a
# fix for something seen — it is one env check against a fork bomb.
if not os.environ.get("TARBUCKLE_CHILD"):
if tick_due(now):
fire_tick(now, payload.get("transcript_path") or "")
said = fresh_utterance(now)
print(f"{NAME} {said}" if said else render(int(now // 60)))
return 0
@@ -154,6 +248,59 @@ def selftest() -> int:
ck("C6 marks are single characters", all(len(m) == 1 for m in MARKS))
ck("C6 marks are distinct", len(set(MARKS)) == len(MARKS))
# --- D: the tick, the draw, the slot. Run against a temp dir, never live state.
import tempfile, collections
global LAST_TICK, SLOT
_lt, _sl = LAST_TICK, SLOT
td = tempfile.mkdtemp()
LAST_TICK, SLOT = os.path.join(td, "tick"), os.path.join(td, "slot")
try:
# D1 — the draw is keyed to nothing. Structural, like C3.
ck("D1 draw takes no arguments", draw.__code__.co_argcount == 0)
ck("D1 draw yields only declared kinds",
{draw() for _ in range(300)} <= {"silent", "aside", "notable"})
# D2 — the proportions are the filed ones. 60k samples, +/- 1.5pp.
c = collections.Counter(draw() for _ in range(60000))
pct = {k: 100.0 * v / 60000 for k, v in c.items()}
ck("D2 silent ~73%", abs(pct.get("silent", 0) - 73) < 1.5)
ck("D2 aside ~20%", abs(pct.get("aside", 0) - 20) < 1.5)
ck("D2 notable ~7%", abs(pct.get("notable", 0) - 7) < 1.5)
# D3 — first sight starts the clock and does NOT fire. A fool that fires on
# its first invocation speaks into the wake, where the voice already speaks.
now = 1_000_000.0
ck("D3 first sight does not fire", tick_due(now) is False)
ck("D3 first sight started the clock", os.path.exists(LAST_TICK))
# D4 — the clock governs, in both directions.
ck("D4n not due before the interval",
tick_due(now + MUMBLE_INTERVAL_MIN * 60 - 1) is False)
ck("D4 due at the interval",
tick_due(now + MUMBLE_INTERVAL_MIN * 60) is True)
# D5 — THE DETERMINATION: the tick consumes whatever the draw says. A silent
# draw that did not advance the clock would be a conserved draw, i.e. a budget.
before = open(LAST_TICK).read()
fire_tick(now + 9999, "") # empty transcript => cannot speak
ck("D5 silent tick still consumes", open(LAST_TICK).read() != before)
# D6 — the slot expires. One slot, never a queue.
json.dump({"utterance": "Fourth time.", "kind": "aside",
"written": int(now)}, open(SLOT, "w"))
ck("D6 fresh utterance shown", fresh_utterance(now + 1) == "Fourth time.")
ck("D6n stale utterance not shown",
fresh_utterance(now + DISPLAY_WINDOW_S + 1) is None)
ck("D6n absent slot is silence",
(os.remove(SLOT), fresh_utterance(now))[1] is None)
finally:
LAST_TICK, SLOT = _lt, _sl
# D7 — the recursion guard is present in the path that ticks.
src = open(__file__, encoding="utf-8").read()
ck("D7 child guard gates the tick", "TARBUCKLE_CHILD" in src and
src.index("TARBUCKLE_CHILD") < src.index("tick_due(now)"))
for name in checks:
print(f" {'FAIL' if name in failed else 'ok '} {name}")
print(f"{len(checks) - len(failed)}/{len(checks)} controls passed")
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Tarbuckle — the mumble generator. Runs DETACHED; never inside the status line.
Doctrine: v2 §8 (three tiers), §9 (one line, filed nowhere), and PENDING-152
AMENDMENT 6, which settles the three-way separation:
Material — the live session. Register — the soul, §7. Occasion — the refresh tick.
"The model supplies the words, in the soul's register, about the session."
⚠ THE UTTERANCE MUST HAVE NO TRUTH VALUE. The criterion is NOT "checkable in
principle" — it is whether an ADJUDICATION PATH EXISTS (AMENDMENT 6, superseding the
executor's own earlier test). "PENDING-4 has been open since April" has a path: open
the file. Once a path exists the trio walks it, the fool acquires a truth value, and it
is a checker again — which §2 says means the design has failed. "Scoring without
signal" has no path: it is a gesture at a shape, not a claim. That is the whole safety.
⚠ THE REGISTER IS NOT DUPLICATED HERE. The soul is read from its filed artifact at
run time. Pasting it into this file would create a parallel version of a governed
record, which is the context-rot failure CLAUDE.md names outright. If the soul cannot
be read, NOTHING IS GENERATED — honest degradation, not a fallback voice.
Latency measured 2026-08-25: ~11 s for a headless call. That is why this is detached
and why the status line never waits on it.
"""
import json
import os
import re
import subprocess
import sys
import time
SOUL = os.path.expanduser(
"~/dotfiles/claude/governance/fool/seed/FOOL-SOUL-2026-08-25.md")
SLOT = os.path.expanduser("~/.claude/state/tarbuckle-slot.json")
REJECTS = os.path.expanduser("~/.claude/state/tarbuckle-rejects.jsonl")
MIN_WORDS, MAX_WORDS = 3, 9 # observed Thistleweld register: three to nine words
# From the soul's own "What he never does", mechanically enforced. The model is asked
# for the register; this is the net under it. A violation yields SILENCE, never a
# repaired line — rewriting the fool's words would make the executor its editor.
BANNED = (
r"\bshould\b", r"\btry\b", r"\bmust\b", # advice
r"\bgone\b", r"\bif only\b", r"\bused to be\b", r"\bmissing\b", # vocabulary of lack
r"\bwe\b", # never says 'we' about the work
r"\?", # never asks
)
def soul_register() -> str | None:
"""The soul, verbatim, from the filed artifact. None if unreadable."""
try:
body = open(SOUL, encoding="utf-8").read()
except OSError:
return None
m = re.search(r"```markdown\n(.*?)\n```", body, re.S)
return m.group(1) if m else None
def session_material(transcript_path: str, budget: int = 6000) -> str:
"""The tail of the live session. Bounded, and tool output is dropped.
Deliberately NOT the docket. AMENDMENT 6: the docket has a forum and the session
does not, which is why the session is safe material and PENDING.md is not.
"""
try:
lines = open(transcript_path, encoding="utf-8", errors="replace").readlines()
except OSError:
return ""
out = []
for ln in reversed(lines[-400:]):
try:
rec = json.loads(ln)
except Exception:
continue
if rec.get("type") not in ("user", "assistant"):
continue
msg = rec.get("message") or {}
content = msg.get("content")
text = ""
if isinstance(content, str):
text = content
elif isinstance(content, list):
text = " ".join(c.get("text", "") for c in content
if isinstance(c, dict) and c.get("type") == "text")
text = text.strip()
if not text:
continue
out.append(f"{rec['type']}: {text[:600]}")
if sum(len(s) for s in out) > budget:
break
return "\n".join(reversed(out))
def build_prompt(kind: str, register: str, material: str) -> str:
weight = ("Something at the shape of the work, not its detail."
if kind == "notable" else
"An ordinary passing remark. Small.")
return f"""You are writing ONE line as Tarbuckle. His character, filed and unalterable:
{register}
Here is the tail of the session he is in the room for:
<session>
{material}
</session>
{weight}
Write ONE line in his voice. Absolute constraints:
- Between {MIN_WORDS} and {MAX_WORDS} words. One clause. Present tense. Flat, no lift.
- IT MUST HAVE NO TRUTH VALUE. Nobody must be able to 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
state a fact about the work, the record, the code, or the docket.
- No advice, no questions, no warning of consequences, no explanation, no second line.
- Never the word 'we'. No vocabulary of lack.
- Do not name files, items, numbers of open things, or anything with an address.
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():
return False, "not one line"
n = len(line.split())
if not (MIN_WORDS <= n <= MAX_WORDS):
return False, f"{n} words"
for pat in BANNED:
if re.search(pat, line, re.I):
return False, f"banned {pat}"
return True, ""
def main() -> int:
kind = sys.argv[1] if len(sys.argv) > 1 else "aside"
transcript = sys.argv[2] if len(sys.argv) > 2 else ""
register = soul_register()
if not register:
return 1 # no soul, no voice. Deliberately no fallback.
material = session_material(transcript)
if not material.strip():
return 1
env = dict(os.environ, TARBUCKLE_CHILD="1") # precautionary; see body script
try:
r = subprocess.run(["claude", "-p", build_prompt(kind, register, material)],
capture_output=True, text=True, timeout=120, env=env)
except Exception:
return 1
line = (r.stdout or "").strip().strip('"').strip()
ok, why = acceptable(line)
if not ok:
try:
with open(REJECTS, "a") as fh:
fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"kind": kind, "why": why, "line": line[:200]}) + "\n")
except Exception:
pass
return 1 # silence. The draw was already consumed.
try:
os.makedirs(os.path.dirname(SLOT), exist_ok=True)
with open(SLOT, "w") as fh:
json.dump({"utterance": line, "kind": kind, "written": int(time.time())}, fh)
except Exception:
return 1
return 0
def selftest() -> int:
checks, failed = [], []
def ck(name, cond):
checks.append(name)
if not cond:
failed.append(name)
# A1 — the register is READ, not duplicated. One canonical source.
reg = soul_register()
ck("A1 soul readable from its filed artifact", bool(reg))
ck("A1 soul is the real thing", bool(reg) and "Tarbuckle" in reg and "SUCCESSION" in reg)
# ⚠ The needle is BUILT rather than written, because the first version of this
# control failed against itself: the literal phrase it searched for was placed in
# the file BY the search. Same class as the hand-typed link canary whose only
# finding was the pattern inside its own specification.
needle = "registrar rather than " + "a guardian"
ck("A1n this file does not contain a copy of the soul",
needle not in open(__file__, encoding="utf-8").read())
# A2 — the acceptability net. Positive AND negative controls on every clause.
ck("A2 accepts an in-register line", acceptable("Fourth time. First one was better.")[0])
ck("A2 accepts a short collision", acceptable("Two names, one thing.")[0])
ck("A2n rejects too few words", not acceptable("Yes.")[0])
ck("A2n rejects too many words",
not acceptable(" ".join(["word"] * (MAX_WORDS + 1)))[0])
ck("A2n rejects advice", not acceptable("You should check that again now.")[0])
ck("A2n rejects a question", not acceptable("How is that going for you?")[0])
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])
# A3 — the prompt carries the no-truth-value constraint verbatim, not by intention.
p = build_prompt("aside", "SOUL", "MATERIAL")
ck("A3 prompt states no truth value", "NO TRUTH VALUE" in p)
ck("A3 prompt forbids addresses", "anything with an address" in p)
ck("A3 prompt embeds the register", "SOUL" in p)
ck("A3 notable differs from aside",
build_prompt("notable", "S", "M") != build_prompt("aside", "S", "M"))
# A4 — material is the session, never the docket.
# Structural, not textual: the module's file constants are the only things it
# opens, so assert none of them addresses the docket.
paths = (SOUL, SLOT, REJECTS)
ck("A4 material paths are session/soul only",
not any(("PENDING" in q or "REVIEWED" in q) for q in paths))
ck("A4n the predicate can fail",
any(("PENDING" in q) for q in paths + ("/x/PENDING.md",)))
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())