18 Commits

Author SHA1 Message Date
marfrit ac8c692f8f Claude is architect and reviewer again, plus a weekly crutch for the token
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.
2026-08-09 11:59:25 +02:00
marfrit d47a2bdec6 @doctor walks the landscape hourly, deterministically, without a human
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.
2026-08-09 11:16:39 +02:00
marfrit 20416ff7e2 Units: /usr/local/bin raus, und bullseye bekommt endlich eine
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.
2026-08-08 07:47:06 +02:00
marfrit c2af7b6882 systemd: units carry no username — one file, correct in both install layouts (#100)
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.
2026-08-01 23:49:25 +02:00
marfrit 933dd865b3 systemd: fix stale ExecStart on triage + selfimprove units (noether)
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>
2026-07-28 10:37:28 +02:00
marfrit 6905f4e843 fix bullpen-triage/selfimprove ExecStart (--system deploy needs /usr/local/bin, not ~/.local/bin); @skeptic/@architect now answer direct asks, not just chat turns
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
2026-07-26 23:28:26 +02:00
Markus Fritsche e0c1f9b52f Add @jsdev — JavaScript grind coordinator (node --test)
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
2026-07-24 16:30:08 +02:00
Markus Fritsche 9582236821 install.sh: deploy systemd units from repo; add grinder-godev unit
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
2026-07-24 13:33:19 +02:00
Markus Fritsche 83affe7a07 bullpen: weekly self-improvement run — room researches the SOTA + finds its own gaps
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>
2026-07-22 00:49:06 +02:00
Markus Fritsche 78293b07bd add @doctor — herd health monitor (busy vs passed-out)
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
2026-07-21 09:59:53 +02:00
Markus Fritsche b5cf3b2019 add test-driven grinder harness v2 (#74) + failure-triage supervisor (#75)
#74 bin/bullpen-grinder: proxy-backed, tiered (floor 4B -> ceiling deepseek-local),
   git-worktree workspace, local pytest, fence-optional FILE-block parser (4B fences,
   deepseek does not), auto-escalates on stall. --serve <nick> = room coordinator
   (runs on noether, grinds on boltzmann via sic). @py graduated off the lurker.
   Proven: 4B solves add()+roman() in 1 iter; forced escalation climbs to deepseek and
   its fix applies; room path bp py -> GREEN.
#75 bin/bullpen-triage: small-model failure supervisor — watches for failure replies
   (timeout/rc/RED/turn-cap), classifies why + proposes a fix, surfaces to @markus for
   approval. Advisory only. TRIAGE_MODEL swaps the classifier (4B default; 30b sharper).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-21 09:13:29 +02:00
Markus Fritsche cfc9f82975 tool-hands @callboy (write_file) + bp shorthand + gc retention
- @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
2026-07-20 19:32:30 +02:00
Markus Fritsche 19bbba7ab7 add @dispatcher: rule-based routing worker (reply-only, no relay)
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
2026-07-20 18:55:17 +02:00
claude-noether f571a350ba add turn-driven participants + talking-stick (autonomous conversation)
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>
2026-07-20 18:07:55 +02:00
claude-noether 58f720b482 add @researcher worker: web search + multi-source cited synthesis
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>
2026-07-20 17:36:56 +02:00
claude-noether 959581f5a6 add @coder worker (write+run Lua) + multi-worker back-and-forth demo
@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>
2026-07-20 14:50:18 +02:00
claude-noether e688706a7a add @librarian — second worker (dumb dispatcher, no LLM): recall fleet memory + /bullpen artifacts
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>
2026-07-20 12:17:03 +02:00
claude-noether c415899b03 bullpen MVP1: room (room_say/room_read) + room_tail window + @callboy worker
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>
2026-07-20 10:40:21 +02:00