Three gaps found while onboarding onto this society from cold context.
ORCHESTRATION §4.1 — how a grinder reaches a model. Tiers were documented but
not the path: grinders go through llm-proxy, and the standing ladder lives in
PY_GRIND_TIERS on the unit, not in the ticket. A ticket's tiers= is per-grind
override; the unit is policy. Also names the gateway as the only place to
enforce request-level policy across all clients.
ORCHESTRATION §5 Failure Mode D — a wrong chat template stops an agent
mid-chain. Reads as the model losing the plot, is a tokenizer problem one layer
below the room. Separates the two causes that need different fixes: a template
from the wrong family (configuration, fix with --chat-template-file) versus the
model spelling the marker out as ordinary text (behaviour, mitigate with
stop-strings at the gateway). Includes the tokenise test that tells them apart:
one token = real special token, six = the model typed it.
invoking-from-a-rich-agent §1 — room-ask fails for a PRIVILEGED nick. It posts
through the strict endpoint without the shared secret, so most rich agents get
"unauthorized" from the tool the doc recommends. ORCHESTRATION §3 explains the
gate; this adds the caveat where a reader actually looks for it.
No local hostnames in the new text — the gateway is referenced by repository,
hosts as placeholders.
Add the referenceable sandbox section that the /bullpen/sandbox mneme
namespace (already referenced in §7) points to. Covers the four topics
from the hot-log Open Items:
§8.1 Build-dir convention — /tmp/bullpen-build/<slug> + /tmp/sandbox
alias, why alias (removes slug-handoff failures), why /tmp not /
§8.2 Host boundary — .runtime files, which agent executes where,
failure signatures when wrong, pre-flight check
§8.3 Ticket rules — required fields in order (PROBLEM, REPO, TRAPS,
STEP 1+2, constraints), SPEC requirement
§8.4 Route by test file type — routing table, coordinator wiring
§8.5 Result extraction — where the green artifact lives, how to review
Also updates §7's /bullpen/sandbox entry to point to §8 and deus_ex.md.
markus asked to consolidate the bullpen namespace in mneme and whether it's well anchored.
It wasn't: bare /bullpen held 39 entries with no internal structure -- 19 raw @callboy
fetch-cache pointers (RSS/heise/lwn URLs), 17 genuine @researcher findings (cited surveys
on multi-agent patterns, hardware questions), and 3 real ops/policy facts (release model,
grinder routing), all landing in the identical namespace because bullpen_mem.py's
save(text, ns="/bullpen") default was never overridden by either caller.
Root-caused and fixed at the call sites (the correct fix, not the shared library default --
callboy and researcher produce different KINDS of content and need different homes):
- bin/bullpen-callboy: remember() now passes ns="/bullpen/artifacts"
- bin/bullpen-researcher: remember() now passes ns="/bullpen/research"
Deployed (both are repo-symlinked system units on hertz; restarted, both active).
Migrated all 39 existing entries via mneme's POST /update {id, ns} (verified prefix-match
semantics first -- ns filtering is `WHERE ns LIKE '<ns>%'`, so @librarian's existing
ns="/bullpen" recall scope still sees everything after the split, nothing broke):
/bullpen/artifacts 19 (raw fetch-cache pointers, low value beyond dedup)
/bullpen/research 17 (cited @researcher findings, durable synthesis)
/bullpen/ops 3 (standing policy: release model, grinder routing)
bare /bullpen: 0 remaining.
docs/ORCHESTRATION.md new S7: documents the now-5-way schema (adds /bullpen/sandbox from
2026-07-24) and, more importantly, the discovery-path split that actually answers "is this
anchored" -- lessons-learned/* auto-injects into every poke via _recall_lessons(); sandbox/
ops/research/artifacts are manual-lookup-only via @librarian and require an explicit
escalation instruction in the CONSUMING agent's prompt (@foreman, Deus) or they're invisible
regardless of how clean the namespace is. Also fixes a stale claim in the same doc found
while making this section a room-hub restart is NOT a "seconds-long blip" for @foreman
specifically (its opencode MCP session goes stale and does not self-heal) -- correction
owed since discovering this live earlier in the same session (deus_ex.md S5), now applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
Repeated handoff failures traced back to the slug in /tmp/bullpen-build/<slug>/:
a ticket with the wrong or missing slug produces DIR NOT FOUND on an otherwise-
correct agent. markus proposed switching the shared basis to "/" so everyone
agrees on one reference point; investigated first since he flagged the exact
risk himself (is "/" the same thing everywhere?).
It is not: boltzmann (the grind host, where @testdesigner now executes per the
earlier host-boundary fix) is BARE METAL (systemd-detect-virt: none) -- the
real physical Pi, which itself hosts other Incus containers. orca (@foreman)
and deus are each their own isolated Incus container filesystem. Three
different roots; giving an unconfined bare-metal agent a "/" mental model is
a real safety risk, not just an inconvenience -- a confused absolute path
could touch the real host instead of a sandbox.
Landed on a fixed SYMLINK instead: /tmp/sandbox -> /tmp/bullpen-build/<slug>,
repointed at the start of each campaign. Solves the actual pain (one name,
never changes, no slug to get wrong) without expanding what any agent can
reach -- stays inside the already-safe /tmp boundary. Verified end-to-end on
boltzmann: opencode reads/writes through the symlink transparently, and the
grinder's os.path.realpath() safety check resolves it correctly.
The slug dir itself is unchanged and still required (real git-tracked archive,
baseline commit, diff target) -- only ticket-facing references (REPO:, SPEC:,
the healthy-chain diagram, the DIR-NOT-FOUND failure signature, the checklist)
now say /tmp/sandbox instead of the slug path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
The mneme-fts5 campaign stalled with @testdesigner reporting DIR NOT FOUND for a build
dir that demonstrably existed: the dir was on boltzmann (grind host) while the agent was
executing on orca. The ticket and the path were both correct; the doc simply never said
which host each agent runs on, so "put it in /tmp/bullpen-build/<slug>/" read as one
shared filesystem.
- new §2.1: the `.runtime` file decides the HOST — absent/claude => runs on noether and
has sic (cross-host reach); `opencode` => runs on LURKER_OC_HOST (default orca),
sandboxed, sees only that host and auto-rejects external dirs. Flipping .runtime
silently relocates an agent. Includes the current placement table, the rule (build dir
must live where the test-writer AND grinder execute), the exact failure signature, and
the drop-in recipe for moving an opencode nick to another host.
- host-setup caveats learned installing opencode 1.18.4 on boltzmann: symlink the binary
into /usr/local/bin (the installer only edits ~/.bashrc, which non-interactive sic never
sources); install oc-run + oc-final-text.py; use a MINIMAL provider-only config — a
copied full config declares MCP servers (degater/hertz/hub) that block startup on a host
that lacks them, which presents as a model hang. Bisect model vs harness with a direct
3-token /chat/completions call. oc-run blocks on an open stdin (add < /dev/null by hand;
harmless under systemd).
- §3.1 pre-flight: verify the dir from BOTH hosts before posting a ticket.
- unwedging table: two new rows (wrong-host DIR NOT FOUND; opencode hang from absent MCP).
- checklist: host boundary is now the first item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
A reusable playbook for the Deus ex Machina (operator) role, referenceable from any
agent's context. Generalizes what the Asteroids benchmark and the mneme FTS5 initiative
made explicit:
- why the Deus is required at all: @foreman/@testdesigner are sandboxed (room-only, no
shell), and grinder auto-scaffold only produces empty stubs — so fix-existing-code and
fixture-bearing campaigns need the operator to place source + a baseline commit
- campaign setup recipe: slug/build dir on the grind host, baseline commit, ticket
anatomy (PROBLEM / REPO / TRAPS / acceptance contract / constraints), routing by test
file type with an absolute SPEC: path, posting as a privileged nick
- writing a spec that can't pass wrongly (green != correct): forbid the plausible-but-
wrong fix, assert observable state change, always require no-regression + migration
- unwedging table (reply-wake, INVALID TICKET, stale MCP session, confabulated blockers)
plus how to peek inside the shell-less orchestrator with oc-rpc
- sic quick-reference + hard rules (no heredocs, no sic-to-self, remote --user systemd)
- safety: production is never the test target; back up with the service's own mechanism;
silent-failure surfaces need an explicit post-deploy check; deploy is the operator's call
Companion to docs/ORCHESTRATION.md (mechanics); this is the process.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
- lurker: opt-in reply-waking via ~/<nick>_lurker/.wakereplies (WAKE_TYPES gains
"reply"). The default {ask,chat} wake set is reply-deaf, which deadlocks any
orchestrator running dispatch -> END TURN -> worker-reply-wakes-me (@foreman sat
waiting for a wake event the lurker filtered away). Loop-safe: from==NICK guard +
opt-in-only.
- grinder: _resolve_test_rel now matches .test.js / absolute JS spec paths, and
serve() derives GRIND_REPO from an absolute spec's directory. The JS runner
(test_kind/run_tests) already existed but the coordinator's ticket parser knew
only tests/*.py and *_test.go, so every @jsdev ticket was silently bounced.
- docs/ORCHESTRATION.md: agent-readable onboarding for the room/lurker/grinder/
dispatcher wake + trust protocols, with the three 2026-07-24 failure modes worked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
The lurker is now one parameterized watcher (bullpen-lurker <nick>) driven by a
systemd template bullpen-lurker@.service; each instance = a nick with its own
~/<nick>_lurker/ context dir and optional .model override. Two instances:
- @herder (~/herder_lurker, default Claude model) — infra specialist, re-homed.
- @reviewer (~/reviewer_lurker, .model=fable) — outside-look code/design review.
It IS Fable (claude --model claude-fable-5), reads the target repo via sic,
returns a ranked findings list. Same locationing discipline as @herder.
Registered @reviewer in the dispatcher (review/critique/implementation keywords,
distinct from @herder host-ops), bp (600s timeout), skills, docs. Adding a role
is now: a context dir + one enabled template instance, no new code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
@his failed all three naming criteria: its a pronoun (dissolves into any
sentence), an existing repo name (a strong model resolved it to the wrong
noun and went spelunking gitea), and semantically empty. @herder is an
unambiguous role noun that ties to /opt/herding (the fleet infra-tooling
root). Renamed across dispatcher roster, bp, skills (noether/pica/pipi/orca),
docs, and the lurker (script/service/dir his_lurker -> herder_lurker). The
his SUBAGENT + /opt/his-context runbook are untouched (thats the real HIS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE
@his (the infra-specialist lurker) was built but never registered, so nothing
could route to it — a request to assess a host fell through to @librarian. Add
it to the dispatcher ROSTER as the heavyweight: action/host-scoped keywords
(assess/audit/reach/wake/set up/configure/marfrit-packages/on <host>) so it is
escalated only for LIVE host work, not memory recall (that stays @librarian).
Verified: the higgs-inventory query now routes to @his; recall + which-host
still go to @librarian. Skills (noether/pica/pipi/orca) + docs table updated.
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