Das Architekturmanifest nennt als staerkstes Argument fuer einen
deklarativen Erbauer nicht Bequemlichkeit, sondern SICHTBARKEIT: zwei
stille Drifts blieben monatelang unbemerkt, weil nichts sie angezeigt
hat — der Kollaps der Zwei-Endpunkt-Architektur und das abgeklemmte
Lease-Tor. Beide wurden per Zufall gefunden, beim Suchen nach etwas
anderem.
pen-verify haelt den laufenden Aufbau gegen deploy/roster.yaml und
AENDERT NICHTS. Rueckgabe 0 wenn Soll == Ist, sonst 1 — damit ist es in
einer Unit oder einem Timer benutzbar, ohne dass jemand die Ausgabe
liest. Geprueft werden Eltern (Speicher, nesting, idmap, autostart,
Pool), je Kind Existenz/Zustand/Speicher/autostart, die Geheimnisse
(genau die Nicks, die das Roster diesem Container gibt) und ob die
erwarteten Units AKTIVIERT sind — nicht bloss vorhanden.
Dazu eine Invariante, die sich aus keiner Konfiguration ergibt:
LMCP_TOOL_ALLOW im Raum. Ohne diese Liste bringt lmcp seinen vollen
Grundstock mit, und jeder Token-Halter hat eine Wurzelschale im
Raum-Container. Genau das war B6.
ERSTER LAUF: 52 Pruefungen, 4 Abweichungen — und alle vier waren echt:
* foreman, architect, reviewer standen auf boot.autostart=false und
waeren nach einem Wirtsneustart nicht wiedergekommen;
* reviewers bullpen-lurker war nie aktiviert — ein Container mit
angemeldetem Claude Code und nichts verdrahtet.
Behoben, nachgemessen: 0 Abweichungen bei 52 Pruefungen, Rueckgabe 0.
reviewers Lurker ist aktiviert, aber NICHT gestartet — der Raum ruht,
und ihn stillschweigend aufzuwecken waere ein anderer Eingriff.
Die `abweichung:`-Vermerke im Roster sind damit erledigt und entfernt.
Sie waren ein Merkzettel, kein Dauerzustand.
Was hier NOCH NICHT steht: das Erzeugen. bullpen-up ist weiterhin der
alte Einzel-Container-Starter. Aber die Reihenfolge ist Absicht — ein
Erbauer ohne Abnahme baut nur schneller das Falsche, und pen-verify IST
die Abnahme fuer jeden kuenftigen Erbauer-Lauf.
Das Architekturmanifest nennt den Erbauer als offenes Tor 1 und
roster.yaml als "die Quelle, aus der sowohl die Container als auch das
Dispatcher-Roster entstehen. Eine Liste, zwei Verbraucher." Bisher gab
es sie nicht; die neun Container sind von Hand angelegt.
Diese Fassung ist AUFGENOMMEN, nicht entworfen: jede Zahl kommt aus dem
laufenden Aufbau. Drei Abweichungen sind dabei sichtbar geworden und
stehen jetzt als `abweichung:` in der Datei statt in niemandes Kopf:
* foreman, architect, reviewer haben boot.autostart=false — sie kaemen
nach einem Wirtsneustart nicht wieder.
* reviewer hat ausserdem KEINE einzige aktivierte Unit: ein Container
mit angemeldetem Claude Code und nichts verdrahtet.
Ebenfalls festgehalten, weil man es sonst wieder herausmessen muss: neun
Kinder sind heute die Decke (ein zehnter scheitert am Start mit
"newuidmap failed to write mapping", trotz idmap.size=1e9); die Summe
der Kinder-Deckel uebersteigt den Eltern um Faktor 2,62; die
Erlaubnisliste des Raums; dass grinds Ausgang GEFILTERT und nicht dicht
ist (eine Nullsperre baut einen toten Container, die Grinder-Schleife
braucht Proxy und Raum); und dass deus bewusst keine Zugangsdaten haelt.
secrets.example enthaelt keine Werte, sondern beschreibt, was existieren
muss und woher es kommt. Der Erbauer soll ABBRECHEN, wenn etwas fehlt,
statt sich Geheimnisse auszudenken — ein Aufbau, der das tut, ist beim
naechsten Lauf ein anderer Aufbau.
Der Erbauer selbst (deploy/bullpen-up, das die neun daraus erzeugt und
die Container AUS DEM PAKET aufsetzt) fehlt weiterhin. Das ist der
naechste Schritt; ohne die Quelle waere er nicht schreibbar gewesen.
install.sh promises "the running fleet == the repo, always; editing the live file IS
editing the tracked file". Its bin/ loop globbed `bin/bullpen-*`, so five entrypoints were
never deployed by it at all: `bp`, `room-ask`, `room_tail`, `news-de` and `deus-seize` —
the commands a human types, plus the emergency stop. They had been hand-copied once and
then drifted silently. Measured on 2026-08-02: the room host was running a `bp` from
2026-07-21 (61 lines behind the repo, which is how `bp --route` came back as "no reply
from @--route"), and deus-seize had never reached any host, so tonight's portability work
on it was live nowhere.
The hole is invisible to inspection: the glob reads as "the bullpen commands", and every
file it misses is sitting right beside the ones it catches. tests/test_install_covers_bin.py
compares the two sets instead of reading the pattern — red under the old glob, green now.
Directories and non-executables are skipped explicitly, so bin/__pycache__ (which appears
the moment anything in bin/ is imported) cannot be symlinked into /usr/local/bin.
deploy/install.sh symlinks the SAME tracked unit into ~/.config/systemd/user OR
/etc/systemd/system, so any literal ExecStart path is wrong in one of them. That
is why the path flip-flopped: 6905f4e moved %h/.local/bin -> /usr/local/bin ("--
system needs it"), 933dd86 moved it back but as /home/mfritsche/.local/bin,
dropping the specifier. bullpen-triage crash-looped 27000+ times on 203/EXEC.
Use a pair that resolves correctly in both scopes instead:
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env <prog>
Measured on hertz, not recalled: in a system unit %h expands to /root, so
/root/.local/bin misses and /usr/local/bin wins; in a user unit it expands to the
user's home and ~/.local/bin wins first. /usr/bin/env resolves the program
through the unit's own PATH in both scopes.
The two layouts stay as they are — DEPLOY.md justifies them with a trust
boundary (agent tier without fleet credentials vs. coordinator tier with sic
reach), so collapsing them would trade a security property for deploy comfort.
tests/test_units_portable.py fails on the pre-fix tree (exactly these 7 units)
and passes after. Verified live: daemon-reload + restart of every affected unit
on noether, all active/running, NRestarts=0.
Extends install.sh to symlink systemd/ + lurker/ unit files into place (/etc/systemd/system
for --system, ~/.config/systemd/user otherwise) + daemon-reload, so unit files are also
git-pull-updatable rather than hand-copied. Captures the live-only bullpen-grinder-godev
unit into the repo (sibling of grinder-py, --serve godev). Verified: every live unit on
hertz + noether is byte-identical to repo, so converting the copies to symlinks is a no-op
for the running services.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
The road to `bullpen up`: launch a bridged Debian system container, install lmcp
from the marfrit apt repo + this repo, seed config + a room lmcp, then round-trip
a room_say/room_read INSIDE the container so you get a proven box, not a hopeful
one. Env-overridable; defaults target the origin fleet's public repo + apt repo.
--verify re-runs just the probe; --destroy tears a throwaway down.
Verified end-to-end on a throwaway Incus container (boltzmann): launch -> apt+lmcp
-> install.sh -> seed -> room service -> probe round-tripped (real room.jsonl
entry), idempotent on re-run, clean --destroy.
Also:
- install.sh --system: /usr/local layout for the room/worker host (bin ->
/usr/local/bin, lib -> /usr/local/lib/bullpen, contrib/lmcp-tool onto PATH) —
matches how hertz is wired; the plain user mode is unchanged.
- contrib/lmcp-tool: vendor the lmcp client the coordinators call by name. It was
a loose unpackaged script on the room host (portability gap for a new fleet).
- DEPLOY.md: bullpen-up as the fastest path; manual recipe fixed to actually
install lmcp + note --system.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
Captures the packaging design: bullpen.conf carries fleet-specifics (defaults =
origin fleet). Recommended path = an Incus system container (bridged, systemd,
LAN IP — behaves like metal, no snap/NAT/PID-1 confusion for agents), with the
untrusted-input agent + code-exec tier isolated (no fleet creds, egress-filtered)
per tonights
Deployment now flows FROM the repo: install.sh replaces the loose hand-edited
~/.local/bin/bullpen-* copies with symlinks into the checked-out tree, so the
running fleet == the repo and editing a "live copy" is editing the tracked file.
Idempotent, reversible (pre-existing files -> .predeploy-bak). To update a host:
git pull in the working copy. Closes the drift vector that hid a whole session
of edits on one host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE