26 Commits

Author SHA1 Message Date
marfrit e48257bc79 bullpen_config: OC_MODEL auf hossenfelder-proxy, bosch-dspark gibt es nicht mehr
Der Anbieter "bosch-dspark" war in orcas opencode-Konfiguration ein Alias auf
den Gateway. Er ist am 2026-08-05 in "hossenfelder-proxy" umbenannt worden,
weil sein Name nach einem Container klang, den es seit dem Vortag nicht mehr
gibt - und weil ein Name, der auf etwas anderes zeigt als er sagt, beim
naechsten Fehler in die falsche Richtung weist.

Damit lief OC_MODEL ins Leere. Geprueft: von orcas Anbietern fuehrt
hossenfelder-proxy das Modell unter dem schlichten Namen "deepseek-v4-flash";
"hossenfelder" fuehrt daneben die praefixierten Katalogeintraege, darunter die
kostenpflichtige Wolkenvariante.

Im selben Zug in orca aufgeraeumt (nicht Teil dieses Commits, nur zur
Nachvollziehbarkeit): die Anbieter litellm, bosch-ornith und bosch-qwen sind
aus beiden opencode-Konfigurationen entfernt. LiteLLM ist stillgelegt, die
beiden bosch-Ports 8888 und 8086 sind gemessen tot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-08-05 11:04:01 +02:00
marfrit d87189359a bullpen_config: Standardmodell auf deepseek-v4-flash, dspark gibt es nicht mehr
MODEL und OC_MODEL zeigten auf "deepseek-v4-flash-dspark". Dieser Name kam vom
vLLM auf bosch:8888, das mit --served-model-name ...-dspark lief; der Container
ist am 2026-08-04 abgeschaltet worden. An seiner Stelle steht DS4 auf
bosch:8085, und das meldet sich als "deepseek-v4-flash".

Folge war, dass alle sieben Teilnehmerdienste ins Leere liefen. Der Gateway
antwortete zunaechst mit model_not_found; seit der Kostenregler auf 'free'
steht, kommt stattdessen policy_blocked - was in die Irre fuehrt, weil es
nicht der Preis ist, sondern ein Name, den es nicht gibt.

Der Anbieterteil von OC_MODEL bleibt "bosch-dspark": das ist in orcas
opencode-Konfiguration ein Alias, der auf den Gateway zeigt, nicht auf den
abgeschalteten Container. Nur der Modellname war falsch.

Die Vorkommen in tests/ bleiben unberuehrt - das sind aufgezeichnete
Protokollzeilen frueherer Laeufe, also Belege und keine Konfiguration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-08-05 10:36:06 +02:00
marfrit 54106c4210 models-json: CATALOG_PROVIDER aus bullpen_config statt als Literal
tests/test_no_hardcoded_hosts.py verbietet Fleet-Hostnamen in Laufzeit-Strings und
erlaubt genau einen Ort dafuer: lib/bullpen_config.py, wo alle Vorgaben per get() mit
Umgebungsuebersteuerung liegen. bin/models-json trug 'hossenfelder-proxy' als Literal
und riss den Test - eingeschleppt mit Roundhouse-Karte 3 am selben Tag.

Der Wert ist ein Provider-LABEL, kein Hostname, enthaelt aber einen; die Regel prueft
auf Substring, und das ist keine Ueberempfindlichkeit: ihr Zweck ist, dass hiesige
Namen nicht in Code wandern, der veroeffentlicht wird. Uebersteuerbar bleibt er als
BULLPEN_CATALOG_PROVIDER, jetzt nur an einer Stelle statt an zweien.
2026-08-03 12:27:20 +02:00
marfrit a3101bc044 room_read: Offset-Index statt Volltext-Scan; Poll-Takt auf 0,1 Hz
room_read dekodierte bei JEDEM Aufruf jede Zeile der room.jsonl. Bei 2224 Zeilen
und 1,9 MB kostete das 156,51 ms pro Aufruf. Sieben Worker-Daemons pollen den Room
(je Poll zwei TCP-Verbindungen, gemessen ~6 Anfragen/s), also lief lmcp auf hertz
dauerhaft mit 85-90 % eines Kerns: 5 Tage 20 h CPU-Zeit in 7 Tagen Laufzeit, auf
einem Pi mit vier Kernen, der nebenbei Fleet-Cron, SSH-Gateway und das Backup traegt.

Die Datei ist append-only und room_say vergibt die ids counter-first (Luecken
moeglich, Rueckschritte nicht - im Bestand geprueft: 2224 Zeilen, streng steigend).
Damit traegt ein Offset-Index mit Sprungsuche. Aufgebaut wird inkrementell: pro
Aufruf werden nur die neu angehaengten Bytes gelesen. Der Normalfall - "gibt es
etwas neuer als meine letzte id?" - dekodiert jetzt gar nichts mehr.

A/B gegen die echte room.jsonl, 96/96 Kombinationen aus since/limit byte-identisch.
Leer-Poll 156,51 ms -> 0,00 ms (Faktor 32138), mit Treffern Faktor 3013. Nach dem
Ausrollen gemessen: lmcp-CPU 85-90 % -> 0 %, Syscalls je 5 s von 15839 auf 331.

Beide .lua-Dateien tragen dieselbe room_read-Fassung (bullpen.lua auf :8080,
bullpen-room.lua auf :8081). Die Duplizierung ist Entwurfsabsicht, also wird sie
beidseitig gepflegt statt heimlich zusammengelegt.

Der Poll-Takt faellt zusaetzlich von 2,0/2,5 s auf 10 s. Nach dem Index kostet ein
Leer-Poll fast nichts mehr, aber sieben Prozesse, die alle zwei Sekunden aufwachen,
bleiben Grundrauschen. Preis: bis zu 10 s laengere Aufgreifzeit je Anfrage.
Uebersteuerbar per BULLPEN_POLL_SECONDS. Die Teilnehmer erben den Wert aus
bullpen_worker, statt ihn ein zweites Mal aus der Umgebung zu lesen.
2026-08-03 11:45:31 +02:00
marfrit 3d8d28d009 doctor: enforce the orchestrator wake-set invariant instead of only writing it down (#96)
W31 listed "single-session-foreman — cannot be woken by worker replies" as an open gap.
MEASURED against the room log: 233 of 259 replies addressed to @foreman drew a foreman
post within 120 s, median 8 s. That is reply-driven waking, not busy-polling. The gap as
stated was closed on 2026-07-24 by the `.wakereplies` opt-in, three days before the run
reported it.

What is real is the residue. ORCHESTRATION.md §2 states the invariant — "any orchestrator
MUST wake on replies" — and it was written down only after a reply-deaf lurker deadlocked
a campaign. Nothing checks it. The opt-in is a bare flag file in a per-nick lurker dir,
and deploy/install.sh deliberately does not touch those, so a fresh fleet, a restored
home, or a renamed nick brings the herd up looking healthy and quietly unable to finish a
pipeline: the orchestrator ends its turn correctly and waits forever for an event the
lurker discards.

cfg.ORCHESTRATORS names the nicks that drive pipelines (configurable; "foreman" here) and
bullpen-doctor now flags any of them whose lurker lacks the flag. One `test -e` per
orchestrator, read-only. Verified both directions: silent while the flag is present, and
with it moved aside it reports @foreman REPLY-DEAF.
2026-08-02 09:51:48 +02:00
marfrit b03d89770f bullpen-up: honour a deliberate inhibit instead of silently re-arming it (#95)
bullpen-up exists to enable and start its roster, so a unit an operator switched off by
hand came back on the next bring-up — with nothing in the output to say it had. That is
not a bug in bullpen-up; it is doing exactly its job. It does mean an operator's "off"
silently expires, which is the failure class markus has flagged before: re-arming an
inhibit is a decision, not housekeeping.

bullpen-selfimprove.timer was live proof — disabled on request 2026-08-01 while #100 and
#118 ran through the room, and `bullpen-up --dry-run` reported `WOULD … enable+start`.

Mechanism: a file named exactly like the unit in /etc/bullpen/inhibit/ or
~/.config/bullpen/inhibit/, whose CONTENT is the reason. Both paths are read so the same
mechanism works in either install layout, and cfg.inhibit_reason() reads at call time —
lifting an inhibit must not require restarting anything. The reason is mandatory by
construction: an empty file still reports "(no reason recorded)", because an inhibit
nobody can explain is archaeology six weeks later.

Deliberately NOT a tear-down: bullpen-up never stops or disables, so an inhibit that
arrives while the unit runs blocks the re-arm, not the current run. Reporting only
"INHIBIT" there would read as "it is off", so that case says ACTIVE explicitly and tells
the operator to stop it by hand if that is what they meant.

tests/test_bullpen_up_inhibit.py pins all four properties plus non-interference with
ordinary bring-up; with the inhibit check removed, exactly those four fail and the
bring-up test stays green.
2026-08-02 08:44:45 +02:00
marfrit eafa29a074 grinder: report the cost tier that actually served, not the tier asked for (#118)
A worker reported `model qwen3.6-coding` — the rung it ASKED for. That cannot answer
"which rung solved this task", which on 2026-08-01 turned a question that should have
been a glance at the log into a 100-minute measurement.

lib/bullpen_modeltag.py is the pure function, built through the room: contract by
@testdesigner (17 cases, three rounds), implementation ground by @py, reviewed twice by
@reviewer. Each contract round closed a defect the previous one structurally could not
see:
  1. unsatisfiable — one case passed an unprefixed name but demanded the prefixed tier,
     contradicting a sibling. Caught by a reference implementation written from the
     docstring: 11/12, with exactly one impossible failure.
  2. incomplete — a same-model free->paid escalation showed no switch, because names
     were compared after stripping the prefix. In a cost-transparency module that is
     the case that matters. Caught by @reviewer probing the green implementation.
  3. false model of reality — the spec assumed `served` carries a prefix. Measured
     against the live gateway: the catalog prefixes every id (246 [$], 15 [free],
     4 [local], none bare) but the response echoes the model BARE, so every ordinary
     call reported an invented switch to [unknown]. Round 2's fix caused this:
     tightening the comparison removed the looseness the wire format relies on.
Resolution: a missing prefix on `served` means no tier information, not a different
tier — inherit the asked tier and compare names only.

Two more @reviewer findings folded in: the contract's `assert "modeltag.py" in
inspect.getsource(modeltag)` checked the wrong file (the harness needs the name in the
TEST source, for _infer_targets) and the model satisfied it by pasting the filename into
the docstring — a contract that rewards gaming; and a formatting-only difference
(`[free]x` vs `[free] x`) rendered as a switch to itself.

On integration: four if/else pairs producing identical strings collapsed, plus one
branch made unreachable by the inherit assignment above it — behaviour unchanged, the
17 cases still pass. The default gateway name is no longer a fleet hostname (it leaked
into every room message); GATEWAY derives from cfg.PROXY.

PY_GRIND_TIERS now carries prefixed ids, since the bare name has no tier to report.
Verified the gateway accepts the prefixed form and answers 200.
2026-08-02 00:40:10 +02:00
marfrit 05f06ed6a9 portability: fix the two bugs @reviewer found in the portability commit itself (#100)
1. bin/bullpen-up ROSTER collapsed silently. `{cfg.ROOM_HOST: …, cfg.COORD_HOST: …,
   cfg.GRIND_HOST: …}` is a dict literal with CONFIGURABLE keys — a small fleet putting
   two roles on one machine (the plausible target of #100) lost the earlier entry
   without a word. That is the same silent-no-op the commit claimed to fix, one level
   down. Now a list of roles merged per host AND per scope, so a combined
   room+coordinator box brings up both its system and its user units. Verified: split
   roles unchanged (8 system + 12 user), collapsed onto one host keeps all 20.
   Also renamed the local `cfg` in main(), which shadowed the config module.

2. bin/bullpen-selfimprove posted as `from=$BULLPEN_COORD_HOST` — a room NICK confused
   with a HOSTNAME. "noether" is a privileged nick that merely happens to equal the
   coordinator's hostname here; on any other fleet the run would post as an unknown
   nick and be rejected silently, which the R4 comment directly above explains. New
   cfg.POST_NICK.

Test gaps @reviewer named, both closed:
  * shell entrypoints were entirely outside the AST check — and bullpen-selfimprove is
    Bash and was one of the offenders that commit had to fix. Added a grep-based check.
  * `fritz.box` was missing from the host list, though it is the strongest fleet marker
    there is. Adding it immediately surfaced three more real offenders: a hardcoded
    mneme URL in bullpen_participant, another in the lurker, and the legitimate
    last-resort fallback in bullpen_mem (exempted, with the reason).

Documented rather than fixed: %h resolves to the service manager's home regardless of
User=, and a stray /root/.local/bin copy would shadow every system unit — both now in
the units-test docstring. cfg.FLEET_HOSTS lost the `pve\d`/`dcw\d` families the regex
had, so a future pve5 needs adding by hand; noted at the definition. `orca` added.

57/57 across both portability suites. bullpen-up --dry-run on noether unchanged;
selfimprove composes a brief with zero origin-fleet mentions under overridden hosts.
2026-08-02 00:40:10 +02:00
marfrit eb75a30169 lib: adopt hertz's uncommitted comment fixes (stash is retired; mneme needs no successor framing)
These two edits existed only in hertz's working tree since the mneme migration —
install.sh's design says editing the live file IS editing the tracked file, but
they were never committed, so every other host still carried the stale wording
and a git pull on hertz would have refused. Bringing them upstream first makes
hertz's tree a clean discard.
2026-08-02 00:07:39 +02:00
marfrit f385070dbf bullpen: no fleet host in a runtime string; entrypoints run from either layout (#100)
Follows the architecture pass on #100. Three classes of change:

1. sys.path — the ten Python entrypoints used two idioms: seven pinned
   /usr/local/lib/bullpen (system layout only), three resolved ../lib from
   realpath(__file__) first. The second form is a superset: it works in both
   layouts and still falls back to /usr/local/lib. Unified on it. Proven: on
   noether, where /usr/local/lib/bullpen does not exist, `bullpen-dispatcher
   --roster` now runs straight from the checkout — it could not before.

2. Role hosts from config — deus-seize, bullpen-selfimprove, bullpen-up's
   ROSTER keys, and the lurker's opencode host/model now come from
   bullpen_config (env > /etc/bullpen/bullpen.conf > origin-fleet default), so
   behaviour here is unchanged. bullpen-up in particular was a silent no-op on
   any other fleet because its ROSTER was keyed by literal hostnames.

3. Advice the system gives BACK — @dispatcher answered every routing question
   with `sic hertz room-ask …` and @callboy reported artifacts as `sic hertz
   cat …`. On a foreign fleet that is an instruction to talk to a machine that
   does not exist. Now cfg.ROOM_HOST.

tests/test_no_hardcoded_hosts.py enforces (3) by AST, not grep: prose in
docstrings and comments stays (why a thing runs on boltzmann is worth keeping),
but a hostname inside a string the program actually uses fails the test. It
caught three sites I had missed by eye — the dispatcher's proxy default, its
"on <host>" routing regex, and the grinder's timeout message.

The routing regex is the interesting one: those names are real fleet knowledge,
not addresses, so they moved to cfg.FLEET_HOSTS rather than being deleted.
Verified both directions — with the default list "on boltzmann" matches and "on
buildbox" does not; with BULLPEN_FLEET_HOSTS="buildbox chatbox" it is exactly
reversed.

Also verified: DRY_RUN=1 bullpen-selfimprove composes a brief with zero mentions
of the origin fleet under overridden hosts; deus-seize still runs; 52/52 across
both portability suites.

NOT fixed, flagged: deus-seize and bullpen-selfimprove pass the room post-secret
as an lmcp-tool argv value, so it is visible in `ps` on the room host (and in
curl's argv inside lmcp-tool). lmcp-tool has no stdin form for argument values;
closing it means changing lmcp-tool, which is deployed fleet-wide.
2026-08-02 00:06:26 +02:00
marfrit 5d70b7f7b8 report the actual serving model in every LLM-backed reply
Per mneme /ideas id 118 (req markus 2026-07-22): hossenfelder is a
failover/auto-routing proxy, so the model NAME in the request is not
necessarily what served it. Every direct-API caller was parsing
choices[0].message.content and discarding the response's own `model`
field — the room reply echoed the ASK, not the reality.

Applies "asked X, served by Y" (only shown when they differ, so the
common non-failover case stays quiet) to:

- bullpen-grinder: ask_model() captures served_model; save_best/report
  thread it through so GREEN/RED replies show it.
- bullpen-coder, bullpen-researcher (incl. the SSE stream path — model
  is on every chunk, not just a final non-streaming body), bullpen-callboy,
  bullpen-triage: same pattern.
- bullpen-dispatcher: the LLM tie-break path only — the deterministic
  keyword-match path never touched a model and stays untagged.
- lib/bullpen_participant.py (architect/skeptic conversants): tag
  prepended as "> asked X, served by Y" — matches the "> build · model"
  convention opencode-backed workers already show natively, so the room
  now has one consistent visual convention for "which model" instead of
  two (opencode-native vs none).
- lurker/bullpen-lurker (claude -p workers): no proxy failover in this
  path, so just reports the configured --model directly rather than
  parsing a JSON field the CLI may or may not carry.

Skipped: @librarian, @doctor (no LLM in either — nothing to report).
Local syntax-checked; dispatcher's --once mode exercised for both the
keyword path (untagged, correct) and roster-fallback path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-27 08:27:24 +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
marfrit 50009af7d9 bullpen: make @dispatcher + one-shot worker family consumable by orchestrators
The bullpen_worker family (dispatcher/callboy/researcher/librarian/coder) + the triage
supervisor were in cfg.TRUST but absent from the room's PRIVILEGED tables, so their
replies stamped verified=None and an orchestrator's lurker (R4) refused them — @foreman
could consult @dispatcher but never consume the routing (a silent dead-end that stranded
it into a spin). They also addressed replies to a bare nick (no @). Fixed and verified
end-to-end (a @dispatcher reply now lands to=@markus, verified=true):

- lib/bullpen_worker.py: address replies as @<invoker>; read POST_SECRET and attach
  secret= to every room_say (the strict :8080 gate REJECTS a privileged post lacking
  the secret, not merely leaving it unverified). Workers run as root -> secret readable.
- lmcp-tools/bullpen.lua (:8080) + bullpen-room.lua (:8081): add dispatcher/callboy/
  researcher/librarian/coder/triage to PRIVILEGED. Keep the two tables in sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 19:05:41 +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 be0757c20f Route last hardcoded fleet-specifics through bullpen_config (#100)
Rollout-readiness pass: nothing in bin/lurker/lmcp-tools now bakes in the origin
fleet's hostnames, paths, user, or agent roster.

- bullpen_config: new COORD_HOST (default noether), TRUST (the privileged/allowed
  nick roster, space-separated -> set), CLAUDE_BIN (PATH-resolved "claude"),
  CALLBOY_MODEL. bullpen_config.sh + bullpen.conf.example carry the parallels.
- @doctor: literal "boltzmann"/"noether" -> cfg.GRIND_HOST/cfg.COORD_HOST in every
  sh()/vitals()/label/JSON-key; grinds_on_boltzmann->grinds_on_grindhost,
  claude_on_noether->claude_on_coordhost. @grinder: the two remaining [SIC,
  "boltzmann"] exec literals -> GRIND_HOST.
- TRUST DRYed to one cfg.TRUST across @doctor/@grinder/@lurker. This reconciles a
  drift: @doctor trusted `triage` but not `godev`, @grinder the reverse; the shared
  default is now their union (both, plus the base 15). @lurker keeps its per-instance
  ~/<nick>_lurker/.trust override on top.
- @lurker: CLAUDE path -> shutil.which(cfg.CLAUDE_BIN). bullpen.lua: ROOM_DIR +
  SECRET_FILE now read BULLPEN_STATE_DIR/BULLPEN_SECRET_FILE (env), default unchanged.
- Dead code from the mneme migration removed: HUB_CONF in callboy/researcher/librarian
  + librarian's now-unused _boltzmann_token().
- tests: defaults cover the new keys; a TRUST env-override test. 318 green.

All defaults = origin-fleet values, so a host with no config still behaves identically
(except the intended TRUST union reconciliation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 13:25:44 +02:00
Markus Fritsche 0e994c6364 Migrate bullpen shared memory off retired stash onto mneme
The `stash` lmcp tool was retired when mneme replaced the pgvector `memory`
container (2026-07-22); every `lmcp-tool stash ...` call in the agents has
returned "Tool not found" since. Rewire to mneme:

- lib/bullpen_mem.py (new): save() -> POST /save (Bearer, {ns,text}); recall()
  -> GET /query (BM25), filtered to a namespace prefix. Both fail soft.
- bullpen_config: MNEME_URL + MNEME_TOKEN_FILE knobs (defaults = origin fleet).
- @callboy stash_remember -> remember (mem.save); @researcher stash -> remember;
  @librarian _bullpen_recall -> mem.recall(ns="/bullpen"). news-de + README +
  bullpen.conf.example wording updated. No `lmcp-tool stash` calls remain.
- tests/test_bullpen_mem.py: save request shape + Bearer + /query ns-filter (mocked).

Verified: live save/recall round-trip on hertz as root against mneme.fritz.box
(id issued, ns-filtered recall returns it, no cross-ns leak); test entry deleted.
317 tests green. Write token plumbed to hertz:/etc/bullpen/mneme-token (0600 root).

Part of the stash teardown: container already gone, orca's dead stash MCP block
removed, no stash skills/extensions. mneme is the successor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 13:02:08 +02:00
Markus Fritsche 425cd43196 Wire worker layer to bullpen_config (#100)
callboy / coder / researcher + lib/bullpen_worker.py now read PROXY, MODEL and
the state paths (STATE_DIR, ART_DIR, HANDS_DIR, CODER_DIR) from bullpen_config
instead of hardcoded literals. Behavior-neutral: every cfg default equals the
value it replaced, so with no config file the fleet runs identically. 312 tests
green. (callboy keeps its own CALLBOY_MODEL tier; ROOM_HOST/GRIND_HOST in the
coordinator layer remain for a later pass.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 09:58:12 +02:00
Markus Fritsche ce5f1369a3 Add oc-rpc stub + companions doc; BULLPEN_OC_URL config knob (#100)
- contrib/oc-rpc: portable stub of the opencode RPC driver (new/say/wait/read/
  todo/diff/abort/share/list). Sanitized: default OC_URL -> localhost, honors
  OC_URL / BULLPEN_OC_URL. Drives one persistent rich-agent session with
  continuity instead of a fresh amnesiac process per poke.
- README "Companions & execution": recommend sic for remote-exec hygiene
  (netstring argv over ssh, no quoting hell) with both repo links; document
  oc-rpc, mneme, lmcp; note companion hosts resolve via bullpen.conf.
- bullpen_config: add BULLPEN_OC_URL (default http://orca.fritz.box:4096) + test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 09:19:41 +02:00
Markus Fritsche 61a46d651d Add bullpen_config: central config foundation (#100 portability)
First slice of making bullpen deployable to a new fleet. One env-style config
file (resolved: $BULLPEN_CONF -> /etc/bullpen/bullpen.conf -> ~/.config/bullpen/config)
parameterizes host roles, the llm gateway, state paths, and the operator nick.
Built-in defaults equal the current fleet values, so a host with no config file
behaves identically; this commit changes no existing script and cannot alter
live behavior.

- lib/bullpen_config.py  python loader (env > file > default) + derived paths
- lib/bullpen_config.sh   shell source-able equivalent
- bullpen.conf.example    documented template (all defaults shown)
- tests/test_bullpen_config.py  defaults=fleet, env>file>default, derivation (4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-24 09:00:38 +02:00
Claude (noether) 5b5d5ba215 participant: recall own lessons-learned profile too (architect/skeptic) 2026-07-23 09:57:43 +02:00
Markus Fritsche e74b2f3832 @reviewer findings #2–#6: harden lurker + worker reliability
#4 a missing/corrupt .since OR a failed room_read now resumes from the newest id /
   backs off — never resets to 0 (which re-ran the ENTIRE history through the model).
   room_read/_read_since return None on failure to distinguish it from an empty room.
#2 posts are rc-checked; the offset advances past a message only after its reply
   actually lands (3 retries) — a transient lmcp/sic failure retries, never silently drops.
#3 dispatch/run exceptions no longer skip the message (offset is reply-gated, not
   advanced-then-maybe-fail).
#5 offset is persisted per fully-handled message — a restart mid-batch resumes exactly,
   no duplicate ack / re-run.
#6 lurker reply capped at 6000 chars (long Fable reviews no longer blow room limits).
tests/test_worker_reliability.py locks the #4/#2 core (read-fail != empty; rc checks). 6/6.
Also: bp timeouts for testdesigner/py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-21 08:09:15 +02:00
Markus Fritsche f27fb95627 participant: MIN_ROUNDS floor — override early NEXT:human, keep the debate going
dspark politely yields to human on turn ~2 no matter what the prompt says. Enforce
instead of ask: if it targets human before MIN_ROUNDS(9) agent turns, redirect the
stick to the other participant. Hard stop stays at TURN_CAP(12); 42-tripwire still
ends early if the answer surfaces.
2026-07-21 00:58:27 +02:00
Markus Fritsche 5d0e11c54f participant: scope turn context to the current exchange (kill cross-round bleed)
Feeding the last 14 chats flat let a new debate inherit the previous round s turns,
so the model thought it was mid-deep-conversation and yielded to human on turn one.
Scope hist to messages since the last human line = this round only.
2026-07-21 00:53:46 +02:00
Markus Fritsche cc6e20ca8d participant: 42-tripwire (agent-side only) + go-deep tuning + TURN_CAP 10
Overnight meaning-of-life debate. FORBIDDEN=/42|forty-two/ ends the exchange the
moment a PARTICIPANT reaches for it (scans agent turns since last human, so the
human stating the rule does not self-trip — first attempt did exactly that). Nudge
the personas to keep the stick between themselves and go many rounds instead of
dspark politely yielding to human at turn 3. TURN_CAP 6 -> 10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
2026-07-21 00:49:24 +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 3a5f8f9119 refactor: extract shared bullpen_worker harness; workers = nick + dispatch fn
Both @callboy and @librarian duplicated the reactive plumbing (poll/say/read/loop/
addressing/since-offset). Lifted it into lib/bullpen_worker.py (bw.run(nick, dispatch));
a worker is now just a dispatch(msg)->reply function. No behavior change — both verified
live after the swap. Adding a 3rd worker is now ~15 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:25:43 +02:00