[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:
co-authored by
Claude Opus 5
parent
6f0ccde4c6
commit
3df5e4f4bb
+148
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user