The root refusal (--dangerously-skip-permissions as root) is gone: both
containers have a `claude` user at uid 1000, matching the ownership the lurker
dirs already carried from the host they were copied from. The lurker runs as it,
WorkingDirectory moved out of /root (0700, untraversable for a non-root
process), the per-nick post-secret is group-readable by claude, and .runtime is
back to `claude` per the roster's `motor: claude`.
What was NOT the problem, though it looked like it: authentication. The
credentials file existed in both containers -- and was a SHELL. accessToken and
refreshToken were empty strings, expiresAt 0, only the metadata survived. So
`claude` reported "OAuth session expired and could not be refreshed", which
sounds like expiry and was not: there was never a token to expire. Copied
noether's working credential in; both now answer.
The crutch, and it says so in its own header: three instances now share one
refresh token. If the token rotates on refresh, whoever refreshes first
invalidates the others. This timer is the REPAIR, not the prevention -- it puts a
working credential back after a rotation has broken it. The interval therefore
sets the worst-case outage: weekly means an agent can be mute for up to seven
days. OnCalendar is the only thing to change if that bites. The clean fix
remains one /login per container.
Two things the script does on purpose:
* it verifies the SOURCE carries a real token before copying. An empty shell
overwriting a working credential is worse than doing nothing, and that is
exactly the state the containers were found in.
* it reports "claude answers", not "file copied". A file in the right place is
not a working login -- that was the whole mistake being repaired.
The token never touches a command line: file to stdin to file, so it does not
appear in the process list on any of the three hosts it passes through.
Why it matters beyond convenience: while both fell back to opencode, @architect
and @reviewer ran the same model as @foreman and @testdesigner. Phase 5's
"Zweitmodell-Review" was a same-model review -- the design and its review came
from one model. That is finding 1 of today's implementation-vs-spec audit.
Everything in this script was done BY HAND today: run the preflight, read the
deviations, notice nine containers six commits behind, a network-facing surface
outside the roster, a stale .model making a lurker report a model it does not
run. None of it needs judgement; all of it needs someone to look, regularly.
@doctor is the right owner precisely because it has no model. A monitor that
costs an LLM call stops when the quota does -- and the operator asked for the
micromanagement to organise itself, which rules out anything that needs a
session.
Split of duties, forced by where things live: hertz supplies the VIEW (incus,
checkouts, units, roster are only visible from the host), the room supplies the
VOICE. The report is piped into the room container, which posts as @doctor with
the secret it already holds. hertz has no /etc/bullpen and must not get one -- a
per-nick secret on the host is a secret outside the pen.
It posts on CHANGE, not on schedule. A monitor that repeats itself hourly trains
everyone to ignore it. Comparison is by deviation SUBJECT, not text, so "6
commits behind" and "7 commits behind" stay one open item.
Two things learned the hard way, both now in the file:
* It MUST run as mfritsche. bullpen-verify reaches the host through `sic`, and
root on hertz has no SSH key -- as root every probe returns empty and the
whole pen reads as absent. The first run posted 44 phantom deviations
("anzahl kinder soll=9 ist=0") into the room before I measured the observer
instead of believing it. Corrected in the room as #408; incus itself works
fine as root, which is what made it convincing.
* On a failed post the state file is NOT advanced. Otherwise a report that
reached nobody counts as delivered and the deviation vanishes silently --
the failure mode this whole system specialises in.
First honest run: 10 deviations of 113 checks. @doctor had never posted in the
room's entire history before today.
Acht der vierzehn Units starteten ihr Programm ueber
/usr/local/bin/bullpen-*. Dorthin darf ein .deb nicht schreiben -
/usr/local gehoert dem Administrator (Debian Policy 9.1.2), und das
bullpen-Paket legt seine Einstiegspunkte folgerichtig nach /usr/bin.
Wer das Paket installiert und eine dieser acht Units aktiviert, bekam
203/EXEC. Sechs Units machten es schon richtig; jetzt alle vierzehn:
ExecStart=/usr/bin/env bullpen-<rolle>
`env` sucht ueber PATH und traegt damit BEIDE Aufstellungen - Paket
unter /usr/bin und Arbeitskopie mit Verweisen unter /usr/local/bin. Ein
fester /usr/bin-Pfad haette die Arbeitskopie gebrochen. Die
PATH=-Zeilen, die /usr/local/bin ENTHALTEN, bleiben genau deshalb
stehen: dort ist der Pfad eine Suchreihenfolge, keine Festlegung.
Dazu systemd/bullseye-server.service. Die Weboberflaeche lief seit dem
Umbau auf eine Unit, die es nur im Container gab - nicht im Repo, nicht
im Paket, also nirgends wiederherstellbar. Sie loest dasselbe
Zwei-Aufstellungs-Problem ueber PYTHONPATH statt ExecStart, weil
`python3 -m bullseye.server` das Verzeichnis UEBER bullseye/ braucht:
/usr/share/bullpen (Paket), /opt/bullpen-src (Container), %h/src/bullpen
(Arbeitskopie). Nicht vorhandene Eintraege ueberspringt Python still,
also darf alles dreifach dastehen. Nachgemessen am ausgepackten Paket:
`PYTHONPATH=/usr/share/bullpen python3 -c "import bullseye.server"` traegt.
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.
Both pointed at /usr/local/bin/<name>, which no longer exists (removed at
some point, likely the same de-shadowing pass that took out the sic shadow
copies) — the correct path, matching every other noether-scope unit
(doctor, grinder-{py,godev,jsdev}), is the ~/.local/bin symlink into this
repo. Neither unit was ever actually re-exec'd since the file went away, so
this was silent: a running process from before the removal kept going, and
only a fresh restart/timer-fire would hit ENOENT (systemd 203/EXEC).
Found by restarting bullpen-triage.service after the escalation-dedup fix
(previous commit) and hitting exactly that. Checked selfimprove too since it
has the identical ~/.local/bin-symlink-but-unit-says-/usr/local/bin shape --
same bug, same fix. Both verified live: triage active+running, selfimprove
oneshot exits 0/SUCCESS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
triage.service has crash-looped since it was enabled (27k+ restarts, never
ran once) because its ExecStart pointed at the user-layout bin path while
the unit was deployed via install.sh --system (symlinks live in
/usr/local/bin). selfimprove.service had the same latent bug, dormant until
its weekly timer next fired.
bullpen_participant.py's _read_chats() only ever collected type=="chat"
messages, so a direct type=="ask" ping (or any one-shot question) to
@skeptic or @architect was invisible to the turn loop. @architect appeared
to work anyway because it has a second, redundant responder
(architect_lurker, opencode-backed) that @skeptic never got — so @skeptic
was silently unreachable outside an active debate turn.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
The society was Python (@py) + Go (@godev) + Lua (@coder) only; a real JS/web need
arrived (the Asteroids HTML task had no home). @jsdev closes the JS-language gap:
- grinder: test_kind recognizes .test.js/.mjs -> js; run_tests runs node --test
--test-reporter=tap in the bwrap jail; _js_scores parses TAP (mirrors _go_scores);
per-kind timeouts + prompt suite name. Verified end-to-end: a failing add() spec
ground to GREEN in one iteration.
- jsdev added to cfg.TRUST + the room PRIVILEGED set (both lua variants).
- systemd/bullpen-grinder-jsdev.service (--serve jsdev).
Node --test grades JS LOGIC; a single-file HTML *game* still needs a jsdom/browser
smoke contract (follow-on). Also: boltzmann is now deploy-from-repo (its grinder was
a stale standalone copy that silently ran old code).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
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
A recurring cadence where the room turns its research on ITSELF. A systemd weekly
timer on noether (Sun ~04:00, off-peak) pokes @foreman with a campaign: survey the
SOTA on multi-agent teamups / personas / single rich-agent skill-teams / scientific
prompt engineering (explicitly incl. Chinese/Asian labs — Qwen/DeepSeek/Kimi/GLM/
Baidu/Tencent/Tsinghua), then find GAPS in bullpen's own design (grounded in the live
README + dispatcher roster, not guesses), @reviewer-critique them, and land ranked
findings in the idea keeper (stash /ideas canonical + DokuWiki ideas mirror).
The room does the thinking; the timer + driver only schedule and hand off (release
principle: orchestration/scheduling lives outside the chat). Focus rotates by week so
/ideas accumulates full coverage monthly. DRY_RUN=1 composes the brief without posting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deterministic (no LLM) checkup surfaced in the room: host vitals (load/temp/mem
for boltzmann+noether) + active-job liveness (grinder runs via bracketed pgrep;
agent claude sessions filtered to *_lurker cwds so the primary session is not
mis-flagged) + age-based stall flag (>25min). Verdict: BUSY & HEALTHY / IDLE /
NEEDS-A-LOOK. bullpen-doctor --once prints it; --serve doctor is the room worker
(bp doctor). Registered in dispatcher (health/busy/stalled/checkup keywords).
Complements @triage (post-failure) — the doctor watches DURING the run, so the
herd can answer busy-or-dead itself instead of a human SSHing in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
- @callboy gains write_file(name, content): jailed to /var/lib/bullpen/hands/,
returned as a fetchable "sic hertz cat" handle + /bullpen stash index (never
the raw content). Small content only — big verbatim text should not round-trip
the 4B. Mechanical allowlist still enforced (fetch_url + write_file).
- bin/bp: REPL shorthand for room-ask (@/# optional, --from default, researcher
auto-bumps timeout; no-arg = roster).
- bin/bullpen-gc + timer: daily retention, artifacts >14d / hands >7d, scoped to
the two scratch dirs (never room.jsonl/COUNTER).
- @dispatcher roster: callboy re-described as tool-hands + write/save-file keywords
so the new capability is discoverable; dropped librarian s collision-prone
"notes" keyword (matched filenames like notes.md).
- Docs/skill/README updated; bp shorthand + tool-hands noted across surfaces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
Reads a request and names the right worker + the exact room-ask to run;
never answers or forwards it, so a misroute costs one wasted hop. Keyword
match first (no LLM slot burned), one LLM tie-break through the gated proxy
only on ambiguity, full roster on weak signal. Strong debate markers route
to @architect/@skeptic and win over an incidental worker-keyword hit. Roster
manifest in the script is the single source of truth. Docs + skill updated
with a start-here-if-unsure pointer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
bullpen_participant harness: a chatty agent holds the stick when the latest chat is
addressed to it, replies (persona LLM), and hands the floor on. Talking-stick enforced:
parse-fail/unknown/offline -> yield to human; >=TURN_CAP consecutive agent turns -> forced
yield (storm-guard). Two example conversants (architect/skeptic) verified debating a real
design question with clean stick-passing + default-to-human. Wired only when you want the
society to converse; on-demand visit/ask/leave needs none of this.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First multi-step worker: web_search (Brave) -> fetch top 3 (degater) -> synthesize a
cited digest (dspark, big context) -> stash to /bullpen. Honest about source gaps (won't
fabricate a metric the sources don't give). dspark-only (3 full pages overflow the 4B).
Verified live on the SSD->RAM homelab cost/perf question.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coder: reactive worker on the shared harness; dspark (via gated proxy) generates Lua,
writes to a sandbox, runs it as 'nobody' with a timeout, returns code+output. Verified a
full 3-way chain: pipi (dspark, research-constrained) -> @callboy (fetch lua-users CsvUtils)
-> @coder (write+run csvtest.lua) -> pipi saves ~/csvtest.lua & reports. dspark never
self-implemented; the no-Lua-knowledge constraint forced delegation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proves the worker pattern generalizes: @callboy writes knowledge, @librarian reads it
back. Reactive-only, reply-to-asker, coexists with @callboy (each responds only to its
own @nick). Search = apropos (curated fleet namespaces) + stash recall of /bullpen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-made multi-agent chatroom. Append-only JSONL room on lmcp; reactive thin worker
with a mechanically-enforced tool allowlist; human window via room_tail. Engine = a
small local tool-model. Verified end-to-end (ask->ack->fetch_url reply incl. artifact
spill; non-tool ask -> graceful decline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>