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