Files
dotfiles/claude/governance/fool/TARBUCKLE-SPEC-13.1-2026-08-25.md
T
David F GliddenandClaude Opus 5 8740c8aa12 [FIX] The wrap seam detected the wrong thing, and the correct fix is what blinded it
The literal question left at the wrap was 'did the wrap seam fire?'. It did not, and the
diagnosis is exact: 0 user-typed /wrap-up records, 29 assistant Skill invocations. The
steward wrote 'then wrap' in prose and the executor invoked the skill. The detector looked
for the steward TYPING the command.

⚠ The detector was not broken. It did exactly what it was built to do. What it was built
to detect is not how a wrap actually arrives — and it was built while the steward was
instructing in prose, which is the only way a wrap had ever arrived in that session.

⚠ AND THE EARLIER FIX IS WHAT CAUSED THIS. Restricting to type=user with string content
was the correct answer to the self-reference bug, where the executor's own tool_use inputs
matched the literal marker. That same restriction excludes the legitimate path. 'The
correct fix caused the next failure' is not a shape any control can see, and it is
PENDING-160's subject exactly.

Now accepts a tool_use whose NAME is Skill and whose input names wrap-up — structural, so
a Bash command echoing the string still does not match. Negative controls for both that
and for a different skill.

And a heartbeat: one timestamp, OVERWRITTEN never appended, so the hook can prove it runs
at all. That closes the silent-net objection this surface carried from the day it shipped
— an append-only log of every turn would be noise and would become the ledger §9 forbids.

25/25. wrap_invoked() now returns True on the live transcript. ⚠ Unproven until the next
Stop actually fires — which is the same claim that was wrong last time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J6hZXNYSxEfZseBGTni4sf
2026-08-25 19:49:32 +02:00

13 KiB
Raw Blame History

name, description, metadata
name description metadata
TARBUCKLE-SPEC-13.1-2026-08-25 v2 §13 deliverable 1 — the spec for what was actually built on 2026-08-25: three tiers wired, the wrap seam not built and why, every deviation from the v2 draft named with its reason. Written AFTER the build, per §12's build order, so it describes what exists rather than what was intended.
node_type type
governance-artifact reference

Tarbuckle — implementation spec (§13.1)

Provenance. Implements BUDDY-PATTERN-jurist-draft-v2-2026-08-22.md, against the bones in seed/FOOL-BONES-2026-08-25.md and the register in seed/FOOL-SOUL-2026-08-25.md. Cadence determinations: PENDING.md, PENDING-152 and the three ⚖ blocks of 2026-08-25. Steward's four build rulings: this session, recorded in §6 below. Commits 6f0ccde · 3df5e4f · 7a9dbf2.

⚠ Written last on purpose. §12's order puts the spec after the wiring so that it records the built thing. Four claims in the v2 draft and in PENDING-152 did not survive contact with the substrate; a spec written first would have carried all four.


1 · What exists

tier trigger surface file
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
voice wrap seam Stop hook systemMessage scripts/tarbuckle-wrap.py

Called by ! tarbuckle (bin/tarbuckle, on PATH): bare or with a question; mute, off, on, status.

Configuration, in ~/.claude/settings.json:

"statusLine": { "type": "command",
                "command": "~/dotfiles/scripts/tarbuckle-body.py",
                "refreshInterval": 60 }

plus tarbuckle-seam.py appended to the SessionStart hook chain.

State — three files under ~/.claude/state/, none of which is a budget: tarbuckle-last-tick (one epoch), tarbuckle-slot.json (one utterance, expiring), tarbuckle-invocations.jsonl and tarbuckle-rejects.jsonl (instrumentation, never read back into behaviour).

2 · The body

Tarbuckle plus one mark, where the mark is one dot at three heights — . · ˙ — advancing once per wall-clock minute.

The binding constraint is carried structurally, not by intention. render() takes the minute and nothing else; a function that cannot see the session cannot leak it. Asserted on co_argcount and co_names, with a deliberately leaky fixture proving the assertion can fail.

⚠ len(MARKS) is coprime with the mumble interval. Had it been four marks rather than three, the mark visible whenever a mumble landed would have been fixed, and the body would have silently announced the voice. Asserted, with a commensurate 4-cycle as the negative control. This is the first of two collisions that were invisible until checked for.

3 · The mumble

Occasion: the clock, 20 minutes. Draw: 73 / 20 / 7, consumed whatever it says — a conserved draw is a budget and a budget is memory. Material: the live session, tool output stripped. Register: the soul, read from its filed artifact at run time.

⚠ The register is never duplicated in code. No soul, no voice — there is deliberately no fallback register, because a fallback voice is a second fool nobody derived.

The net — 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 his words would make the executor his editor.

Generation is detached: a headless call measures 7–12 s and a status line cannot wait.

4 · The voice, and the half that is not built

The wake seam speaks through the SessionStart hook, bounded at 15 s.

⚠ THE WRAP SEAM IS NOT BUILT. SessionEnd exists as a hook event, but its handler writes to stderr only when a hook fails; a successful hook's stdout goes nowhere. §9 requires output to reach the steward, so wiring the wrap seam there would be a mechanism that fires into nothing and reports success. Owed, not dropped. The remaining route is the /wrap-up skill itself, which is a skill change and therefore goes through the skill-harvest register rather than being taken unilaterally.

⚠ THE WRAP SEAM DID NOT FIRE ON ITS FIRST REAL WRAP, and the reason is the sharpest finding in PENDING-160. It detected the steward typing /wrap-up. On the day it shipped the steward wrote "then wrap" in prose and the executor invoked the skill: 0 user-typed records, 29 assistant invocations, detector correctly returns False, fool silent. The detector was not broken — what it was built to detect is not how a wrap arrives. ⚠ And the fix that made it correct is what blinded it: restricting to user records was the right answer to the self-reference bug, and that same restriction excludes the real path. Fixed to accept a Skill tool_use naming wrap-up, with negative controls for a Bash echo and for a different skill. A heartbeat file (tarbuckle-wrap-lastrun, overwritten, never appended) now proves the hook runs at all — the silent-net objection this surface carried from the start.

⚠ Bounded generation is legitimate because of the steward's ruling, not despite it: "A guaranteed occasion is not a guaranteed utterance. If a seam produces nothing that passes, let it produce nothing." Timeout ⇒ silence, logged.

5 · Deviations from the v2 draft and PENDING-152, each with its reason

# filed text what the substrate says disposition
1 PENDING-152: the tick is "a counter over refreshes rather than over events" refreshInterval re-runs "in addition to event-driven updates", so invocations burst with activity Mechanism replaced, conclusion kept. The tick consults the clock. AMENDMENT 8
2 PENDING-152: "whether Claude Code re-renders on a timer… not verified" 14 consecutive 60 s gaps with no input, in the body's own log Settled, by the experiment the item itself specified
3 §8 table: voice at seams "guaranteed" — Narrowed by steward ruling to guaranteed occasion
4 §8a: "whether a status line is already in use" no statusLine in any settings file Confirmed free; nothing displaced

6 · The steward's four rulings, carried

  1. The net carries to tier 3 unchanged — imported, never reimplemented. A second copy is a second, quietly divergent standard.
  2. Never relax silence-on-violation. Widening the seam's word cap is licensed if evidence warrants, explicitly and stated; the cap is left as filed on one near-miss, because widening on n=1 is tuning to taste.
  3. The rejection log is the diagnostic — true-versus-drawn rate and what was rejected. ⚠ Its first use exposed that it recorded the verdict and discarded the line. Fixed.
  4. Check any new periodicity for commensurability. A seam is aperiodic and adds no period — but the check found that last-tick persists across sessions, so a gap longer than the interval left a mumble already due at the moment of waking. The seam now resets the clock. Second invisible collision found by an explicitly mandated check.

7 · Verification

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.

⚠ source_lacks() exists because the same bug was written twice in one session — a control whose needle is a literal plants that literal in the file it searches. The second instance was written minutes after the first was fixed, by the party who fixed it, while 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.

7b · The two logs, and why only one of them is a §9 problem

file holds status
tarbuckle-draws.jsonl occurrence only — surface + outcome permanent. Counting is not filing; §8 obliges a rate and a rate needs a denominator
tarbuckle-rejects.jsonl violation reason + up to 200 chars of a suppressed line ⚠ temporary, dies 2026-09-08

Jurist ruling, 2026-08-25 — placed as REVIEWED-128. The rejection log is "a log of my instruction, not of Tarbuckle… the rejected lines were never uttered: he was silent, and the log holds what silence cost." Nothing in it entered the room, so nothing can be cited from it. Three binding conditions:

  1. ⚠ Rejections only — made STRUCTURAL, not intentional. log_rejection() refuses an empty why, and acceptable() returns an empty why exactly when the line passed. So no call site exists from which an accepted line could be written: to log one you would have to invent a violation it does not have. Same guarantee render() takes from its signature. Asserted with both polarities.
  2. ⚠ Temporary. Deleted 2026-09-08 with the report — "a corpus of his suppressed speech is exactly what would let someone reconstruct a register." Tracked as DEFERRED-DECISION: tarbuckle-rejection-log-deleted so it cannot be quietly retained.
  3. ⚠ Not read for content before then. Reading as it accumulates is reading Tarbuckle by the back door and would shape the net toward liked lines. Not clean already: the executor displayed one rejected line to the steward on 2026-08-25, before the condition existed. Disclosed, not buried.

7c · The word caps, and the criterion that decides them

Ordinary surfaces 3–9 words; invoked 180. Both left as filed. The steward licensed widening on evidence, and single near-misses are not evidence.

⚠ The criterion is the SHAPE of the rejections, not their count (jurist, 2026-08-25):

"Scattered rejections mean the net is doing its job; clustered ones at the ceiling mean the ceiling is wrong."

And the asymmetry that raises the stakes at one surface only: the invoked surface is the only place he speaks at length, so a rejection there is a summons answered with silence. Read on 2026-09-08, once, with the deletion.

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 — BUILT 2026-08-25. ! tarbuckle mute silences the utterance and leaves the body in the room; ! tarbuckle off removes him; ! tarbuckle on returns. mute ≠ off: collapsing them would delete the visible silence §8a exists to produce.
  • The wrap seam — BUILT 2026-08-25 on Stop + systemMessage, gated on an actual /wrap-up invocation detected structurally in the transcript. SessionEnd remains unusable and the reason stands in §4.
  • PENDING-159 — CLOSED, REVIEWED-129: option 1. The fool cannot reach the jurist and nothing is marked, nothing built, no flag that could become a channel. Declined as harmful rather than expensive: a marker would have weighted the steward's judgement in the one place it must stay unweighted. ⚠ REJECTED, not deferred — not to be revisited without new steward input.
  • 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.
  • Frequency remains UNKNOWN and is not guessed. The invocation log now measures the base it needs.
  • ~/.claude/settings.json is untracked, and a divergent parallel copy sits at ~/dotfiles/claude/settings/settings.json (2 keys against the live 8). Named, not fixed.

9 · Off

Remove statusLine from ~/.claude/settings.json and the seam line from the SessionStart chain. Nothing else references either. mute/off per §9 is not built — it is owed with the wrap seam.