[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
+49
-3
@@ -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.**
|
**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
|
### 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.*
|
*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
|
<!-- DEFERRED-DECISION: mumble-rate-two-week-report
|
||||||
since: 2026-08-25
|
since: 2026-08-25
|
||||||
trigger: manual
|
trigger: date 2026-09-08
|
||||||
owner: executor
|
owner: executor
|
||||||
discriminator: §8's own clause — "Report the observed mumble rate after two weeks." The
|
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
|
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
|
honestly be written yet. `manual` was the correct recording rather than a proxy date, and
|
||||||
this converts to `trigger: date <wiring +14d>` the day the body ships.
|
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.)*
|
*(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
@@ -38,6 +38,7 @@ is filed UNKNOWN — a number derived from an unmeasured base is worse than a bl
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -52,6 +53,19 @@ MARKS = (".", "·", "˙") # period, middle dot, dot above
|
|||||||
|
|
||||||
MUMBLE_INTERVAL_MIN = 20 # §8, determined 2026-08-25
|
MUMBLE_INTERVAL_MIN = 20 # §8, determined 2026-08-25
|
||||||
LOG = os.path.expanduser("~/.claude/state/tarbuckle-invocations.jsonl")
|
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:
|
def render(now_min: int) -> str:
|
||||||
@@ -78,6 +92,76 @@ def log_invocation(payload: dict) -> None:
|
|||||||
pass
|
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:
|
def main() -> int:
|
||||||
payload = {}
|
payload = {}
|
||||||
try:
|
try:
|
||||||
@@ -89,7 +173,17 @@ def main() -> int:
|
|||||||
except Exception:
|
except Exception:
|
||||||
payload = {}
|
payload = {}
|
||||||
log_invocation(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
|
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 single characters", all(len(m) == 1 for m in MARKS))
|
||||||
ck("C6 marks are distinct", len(set(MARKS)) == len(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:
|
for name in checks:
|
||||||
print(f" {'FAIL' if name in failed else 'ok '} {name}")
|
print(f" {'FAIL' if name in failed else 'ok '} {name}")
|
||||||
print(f"{len(checks) - len(failed)}/{len(checks)} controls passed")
|
print(f"{len(checks) - len(failed)}/{len(checks)} controls passed")
|
||||||
|
|||||||
Executable
+229
@@ -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())
|
||||||
Reference in New Issue
Block a user