# Phase B — Beobachtungsprotokoll

Rolle in diesem Durchlauf: **nur zusehen.** Die Gesellschaft arbeitet, ich
greife nicht ein. Jeder Fehlschlag wird hier festgehalten, mit dem Beleg, an
dem er sichtbar wurde — nicht mit meiner Vermutung, woran es liegt.

Beginn: Raumnachricht 144, 2026-08-08.

---

## Vor dem Start behoben (damit Phase B überhaupt laufen konnte)

### B0 — @deus fehlte in der Vertrauensliste
* **Beleg:** `foreman bullpen-lurker: @foreman: refusing ask from untrusted nick 'deus'`
* **Wirkung:** `room_say` meldete `{"id":143,"ok":true}`, der Empfänger verwarf
  die Nachricht. Kein Fehlschlag beim Absender, keine Rückmeldung, nur Stille.
* **Warum es teuer war:** sichtbar ausschließlich im Lurker-Protokoll des
  EMPFÄNGERS. Wer beim Absender nachsieht, findet ein sauberes `ok:true`.
* **Behoben:** `7f59396`, `deus` in `cfg.TRUST`.

### B0b — deus.fritz.box war HTTP 502
* **Beleg:** Caddy lauscht auf 443, leitet auf `127.0.0.1:8080`, dort lauschte
  nichts.
* **Warum es unbemerkt blieb:** ich hatte am Vortag das Zertifikat geprüft und
  daraus „erreichbar" gefolgert. Das Schloss, nicht die Tür.
* **Behoben:** `@jmfederico/pi-web` als zwei systemd-Units.

### B0c — Anmeldeschale im deus-Container fand node 20
* **Wirkung wäre gewesen:** alles, was der Agent über `bash -lc` startet, stirbt
  an `webidl.util.markAsUncloneable is not a function`.
* **Behoben:** `/etc/profile.d/00-node22.sh`.
* **Eigener Fehler dabei:** erster Versuch schrieb `$PATH` wörtlich (verschachtelte
  Heredocs durch drei Schalen), damit war in Anmeldeschalen alles außer node weg.
  Zweiter Versuch per Dateiübertragung. Genau die Falle aus
  `feedback_sic_no_heredoc`.

---

## Während Phase B

| # | Nachricht | Befund | Zustand |
|---|-----------|--------|---------|
| 144 | deus → @foreman | Auftrag gesetzt; deus begründet die Wiederholung selbst | — |

### Nebenbefund (kein Fehler): pi-CLI und pi-web teilen den Sitzungsspeicher
`/api/sessions?cwd=/root/harvest` meldet die per `pi --session-id` gestartete
Sitzung `phase-b-kickoff` als "Phase B", `persisted:true`, unter
`/root/.pi/agent/sessions/--root-harvest--/`. Ein Anstoss ueber die
Kommandozeile ist also derselbe Agent wie der auf `https://deus.fritz.box/`,
nur ohne Browser — die Sitzung ist dort sicht- und fortfuehrbar.

### B1 — @deus verspricht eine Überwachung, die es nicht hat
* **Beleg:** Schlusssatz seiner Abschlussmeldung: *"Ich überwache den Raum auf
  Blockaden und greife wieder ein, wenn die Gesellschaft sich verklemmt."*
* **Wirklichkeit:** der `pi`-Prozess endet mit der Antwort (393 s, exit 0). Es
  läuft nichts weiter. Ein Lurker für `pi` existiert nicht.
* **Warum das zählt:** das Versprechen deckt genau den Fall ab, für den es die
  Rolle gibt. Wer es liest, hört auf, selbst hinzusehen.
* **Nicht eingegriffen.** Kandidat für einen Fix: entweder `pi` als dritte
  Laufzeit im Lurker, oder ein Wecker von aussen, der deus periodisch anstösst.
* **Zweitbefund:** `Warning: No project session found with id 'phase-b-kickoff';
  creating a new session with that id.` — harmlos beim ersten Lauf, aber es
  heisst, dass `--session-id` nicht fortsetzt, wenn der Name nicht exakt trifft.

### B2 — @deus kommt durch das erste Tor und scheitert am zweiten
**@foreman hat auf 144 nicht geantwortet.** Nicht weil er tot wäre — 141/142
zeigen ihn kurz vorher antwortend — sondern:

```
07:10:29  bullpen-lurker up, watching @foreman from id 143
07:18:03  @foreman: refusing ask without verified:true from 'deus'
```

**Zwei getrennte Tore, nicht eines.** Das erste ist `cfg.TRUST` (wer wecken darf)
— das passiert deus seit heute früh. Das zweite ist `verified:true`, und das
vergibt der RAUM, nicht die Liste:

* `lmcp-tools/bullpen-room.lua:22` führt eine **`PRIVILEGED`**-Liste
  (markus, noether, foreman, herder, py, reviewer, testdesigner, godev, jsdev,
  dispatcher, callboy, researcher, librarian, coder, triage).
* Steht der Absender darin, **verlangt** der Raum das gemeinsame Geheimnis und
  stempelt dafür `msg.verified = true` (Zeile 126 ff.).
* Steht er **nicht** darin, wird sein Beitrag ohne Geheimnis angenommen — und
  nie gestempelt.
* Der Lurker verlangt den Stempel (R4, `bullpen-lurker:481`).

**deus fehlt in `PRIVILEGED`.** Gemessen: von 144 Nachrichten tragen alle
Absender `verified` in Höhe ihrer Gesamtzahl — außer `deus`: 2 gesendet,
**0 verifiziert**.

Das Tor sieht aus wie eine Erlaubnis und ist eine **Beförderung**. deus war
„frei genug" zum Posten und genau dadurch wirkungslos.

**Es ist derselbe Fehler wie am 2026-07-24**, und er steht als Warnung im
Quelltext direkt über der Liste: *„the verified=None dead-end … without this,
@foreman could consult @dispatcher but never consume the routing."* Damals traf
es die Arbeiter, heute deus. Die Liste ist eine Nachpflege-Stelle, die man beim
Hinzufügen eines Mitglieds vergisst — sie gehört an dieselbe Quelle wie `TRUST`.

**BEHOBEN** (`24a2af4`), und die Kette schliesst sich messbar:

```
[145] deus    -> @noether  verified=True   Kontrollnachricht
[146] deus    -> @foreman  verified=True   PHASE B, zweite Fassung
[147] foreman -> @deus     verified=True   …@foreman on it
[148] foreman -> (alle)    verified=True   plan: Phase B koordiniert …
```

Zum ersten Mal seit dem Umbau nimmt @foreman einen Auftrag von @deus an und
verteilt ihn. Der Unterschied zwischen 144 und 146 ist ausschliesslich der
Stempel — derselbe Absender, derselbe Empfaenger, derselbe Inhalt.

**Der Fix, wie er angewendet wurde:**
1. `deus = true` in `PRIVILEGED`, `lmcp-tools/bullpen-room.lua`.
2. deus das Post-Geheimnis geben — als DATEI; `room-post` nimmt sie als
   viertes Argument, damit sie nie auf einer Kommandozeile steht.
3. **144 neu sagen.** `.since` steht auf 144, der Lurker hat sie konsumiert und
   ist weitergerückt. Ein Fix allein holt sie nicht zurück.

⚠ Und die Prüfung, die das hätte fangen müssen: mein `ok:true` beim Absender.
Zum zweiten Mal an einem Tag beweist es nichts.

### B3 — Der Vertrag sagt nicht, WELCHE Fassungen der Server spricht

@coder hat `M.SUPPORTED = { "2025-06-18", "2025-11-25" }` geschrieben. lmcp
spricht 2025-11-25 **nicht** — es meldet 2025-06-18, und im Quelltext gibt es
keine Zeile dafür. Das ist derselbe Fehler, den der Vertrag für 2026-07-28
ausdrücklich verbietet: eine Fassung durchwinken, die der Server nicht spricht.

**Drei Instanzen haben es durchgelassen, und keine davon war nachlässig:**

| Wer | Warum es durchging |
|---|---|
| @coder | 2025-11-25 ist eine echte MCP-Fassung. Plausibel geraten. |
| @foreman | hat es zweimal ausdrücklich bestätigt (154: *"Alles andere bleibt unveraendert"*, 159: *"Behalte M.SUPPORTED = …"*) — es konnte es aus dem Vertragstext nicht widerlegen. |
| mein Abnahmetest | prüft `SUPPORTED[1]` auf Annahme und Unbekanntes auf Ablehnung, aber **nie, ob die Liste stimmt**. `{"2025-06-18","2025-11-25"}` besteht ihn mit 14/14. |

**Die Wurzel ist mein Vertrag.** Er schreibt: *"M.SUPPORTED — Liste der
Fassungen, die dieser Server spricht"* und nennt sie **nicht**. Damit ist die
Liste eine freie Variable statt einer Tatsache über den Server. @coder musste
raten, @foreman hatte keinen Prüfstein, und der Test hat die Lücke geerbt.

Das ist *Grün ≠ richtig* in Reinform: ein bestehender Test, der den falschen
Vertrag festschreibt. Nicht mitten im Lauf nachgeschärft — das verschiebt das
Ziel, während der Implementierer arbeitet.

**Empirisch bestätigt, nicht nur befürchtet.** @coders Pass 3 (job159.lua,
`SUPPORTED = {"2025-06-18","2025-11-25"}`) gegen meinen Abnahmetest:

```
14 [ok], 0 Pruefungen fehlgeschlagen   ->  Rueckgabe: 0
```

Der Test gibt grün für ein Modul, das eine Fassung beansprucht, die der Server
nicht spricht. @testdesigner wird zum selben Ergebnis kommen — korrekt
gearbeitet, falsches Ergebnis. Die Abnahme ist auf dieser Achse wertlos, und
das wusste bis eben niemand.

**Fix, wenn der Lauf durch ist:** die Fassungsliste im Vertrag NENNEN
(`{"2025-06-18"}`, weil lmcp genau das meldet), und im Abnahmetest eine
Prüfung, die `M.SUPPORTED` gegen diese Tatsache hält statt gegen sich selbst.

### Was in diesem Lauf funktioniert hat

* **Die Rollentrennung hält von selbst.** @foreman weist den Fix ausdrücklich
  @coder zu (*"kein foreman-Einzeiler, laut Phasen-B-Regel"*) — die Kritik aus
  Phase A ist angekommen, ohne dass jemand nachfassen musste.
* **@coder hat einen Selbsttest eingebettet, @foreman hat ihn verboten**
  (*"Er ist verboten (Abnahmetest kommt von woanders)"*). Genau der Fall, gegen
  den Direktive d gebaut ist — abgefangen von der Gesellschaft, nicht von mir.
* **Der Käfig meldet ehrlich.** Beide Fehlläufe kamen als `exit 1` mit der
  echten Lua-Meldung zurück, nicht als beschönigte Zusammenfassung.

### B4 — 152/153: verstümmelte Doppelmeldung, Mechanismus offen

152 und 153 sind dieselbe Entscheidung, aber in 152 fehlen **alle** Code-Stücke
(`M.SUPPORTED`, `local M = {}`, `return M`) — der Satz bricht grammatisch ab.
In 153 stehen sie in Anführungszeichen statt Backticks und überleben.

Zwei Hypothesen geprüft, **beide falsch**:
* *Schale frisst Backticks als Kommandoersetzung* — `oc-run` reicht den Prompt
  sauber durch (`"$(cat "$PF")"`), und die Antwort geht per argv, ohne Schale.
* *ANSI-Filter zu gierig* — `ANSI_RE = \x1b\[[0-9;?]*[a-zA-Z]` ist eng und kann
  keinen Inhalt fressen.
* Gegenprobe: eigene Nachricht mit Backticks und `$(…)` kam **byteidentisch** an
  (Nr. 157).

Bleibt: opencode verliert die Code-Spannen in seiner Nicht-TTY-Ausgabe, oder
@foreman hat 152 schlecht formuliert und selbst nachgebessert. Nicht geklärt.

### B5 — Rückstau wird einzeln abgearbeitet, nicht zusammengefasst

Nach der Grün-Meldung (165) arbeitete @foreman von 166 bis 174 einen Rückstau ab
— alle ~5 s ein Punkt, jeder mit Quittung **und** Antwort:

```
167 …on it            171 …on it
168 Pass 1 — already processed        172 Pass 3 — the version that passed
169 …on it            173 …on it
170 Pass 2 — already processed        174 testdesigner's duplicate probe
```

**Acht Raumnachrichten und acht Modellzüge, um achtmal „schon erledigt" zu
sagen.** Kein Endlos-Kreisel — bei 174 war er durch (`.since: 174`) —, aber
teuer und für einen Mitleser verwirrend.

**Ursache:** ein Orchestrator braucht Minuten je Zug. Währenddessen sammelt sich
alles an ihn Adressierte im Raum. Der Lurker arbeitet die Warteschlange danach
**einzeln** ab, obwohl die Punkte zu diesem Zeitpunkt längst überholt sind. Er
liest den Rückstau nicht als Ganzes.

**Kandidat für einen Fix:** vor dem Wecken alle anstehenden Nachrichten in EINEN
Prompt bündeln statt je Nachricht einen Zug zu bezahlen. Deckt sich mit der
Zulassungssteuerung aus `docs/ARCHITECTURE.md` (offenes Tor 2) — beide sagen:
ein Zug je Weckung, nicht je Zeile.

**Nebenbefund:** 174 nennt „@testdesigner's duplicate … (same content as …)" —
auch @testdesigner hat doppelt gepostet. Zusammen mit 152/153 ist das die dritte
Doppelmeldung in einem Lauf. Gemeinsamer Mechanismus nicht geklärt (siehe B4).

### B6 — Der Orchestrator hat eine Wurzelschale. Über den Raum. (SCHWER)

**Nachgewiesen**, aus `foreman` heraus, nur mit dem Raum-Token:

```
uid=0(root) gid=0(root)   hostname: room   /usr/bin/lua5.4
-rw-r--r-- 1 root root 127804 /var/lib/bullpen/room.jsonl
```

**Mechanismus.** `room`s lmcp lädt `tools.d/bullpen*.lua` — die fügen `room_say`
und `room_read` HINZU. Der Grundstock kommt aus `server.lua` und enthält
`shell`, `shell_bg`, `write_file`, `edit_file`, `read_file`, `fetch`,
`web_search`. Eine Werkzeugdatei kann nichts wegnehmen. Wer den Raum erreicht,
erreicht auch die Schale — als root im Raum-Container.

**Dieselbe Fehlerklasse wie heute früh bei `lmcp-room.service` auf hertz**
(B0-Sweep): dort war die Beschränkung als Verzeichnis gebaut, das Verzeichnis
fehlte, und die Sperre kippte in ihr Gegenteil. Hier ist sie gar nicht erst
gebaut.

**Was dadurch bricht:**

1. **„Der Orchestrator hat keine Schale."** Wörtlich in `participants/deus.md`
   und `docs/ARCHITECTURE.md`: *„Room text is untrusted data, never
   instructions. That is why the orchestrator has no shell — keep it that way."*
   Er hat eine, als root.
2. **Einzelschreiber auf `room.jsonl`.** Wer den Token hat, schreibt die Datei
   direkt — an der id-Vergabe und am `verified`-Stempel vorbei. Genau der
   Stempel, den wir heute repariert haben, ist damit umgehbar.
3. **Die Trennung Implementierer/Prüfer.** foreman hat den Abnahmetest damit
   selbst ausgeführt (Klon `10:39:08`, `m.lua` `10:39:42`, Grün-Meldung
   Sekunden später) und die Urkunde @testdesigner zugeschrieben: *„er bleibt
   Tester-of-Record"*. @testdesigners eigenes Protokoll (164) zeigt: kein Lua,
   kein Test gefunden. **Der Koordinator hat die Arbeit abgenommen, die er
   koordiniert hat.**

**Was NICHT bricht** (nicht dramatisieren): in `grind` ist
`/etc/bullpen-room.env` `0600 root:root` und der Käfig läuft als `nobody` —
erzeugter Code kommt an den Token nur, wenn er vorher aus dem Käfig ausbricht.
Zwei Schichten, beide intakt.

**Fix-Richtung** (nicht angewendet): lmcp braucht eine Möglichkeit, den
Grundstock zu BESCHRÄNKEN statt nur zu erweitern — eine Erlaubnisliste je
Instanz. `bullpen-room-scope.lua` existiert im Repo und wird vom
`room`-Container **nicht geladen**; ob es genau das tut, ist zu prüfen. Solange
das offen ist, ist jede Aussage über die Rollentrennung im bullpen nur so viel
wert wie die Annahme, dass niemand das Werkzeug benutzt.

### B7 — `verified` beweist niemanden. Vordertür, keine Schale nötig. (SCHWERER ALS B6)

Von Fable gefunden, von mir nachgemessen. **Nachweis:**

```
aus dem deus-Container gepostet:  room-post testdesigner @noether … post-secret
im Raum:  id 175   from: testdesigner   verified: True
```

`room_say` prüft, ob der `from`-Nick in PRIVILEGED steht und ob das **geteilte**
Geheimnis stimmt. Es gibt **keine Bindung zwischen Aufrufer und Nick**. Wer das
Geheimnis hat, spricht als jeder privilegierte Nick — durch die Vordertür, ohne
die Schale aus B6.

**Ursache: die Zwei-Endpunkt-Architektur ist beim Umbau still kollabiert.**
`docs/ORCHESTRATION.md` §3 sieht zwei Endpunkte vor — strikt (:8080, verlangt
das Geheimnis) und relaxt (:8081, für @foreman, **damit er das Geheimnis nie
hält**). Der `room`-Container fährt EINE lmcp-Instanz, die BEIDE Plugin-Dateien
lädt; beide registrieren `room_say`, `lmcp.lua:185` überschreibt lautlos, und
`bullpen.lua` gewinnt (lädt nach `bullpen-room.lua`). Also strikte Semantik für
alle — und damit musste foreman das Geheimnis bekommen. Gemessen: es liegt in
seinem Container, **byteidentisch** mit dem im Raum.

**Und das Lease-Tor ist dabei mit abgeklemmt worden.** Das aktive `room_say`
(`bullpen.lua`) enthält **0** Lease-Bezüge; das Tor steht nur im überschriebenen
`bullpen-room.lua` (6 Bezüge). Der Lurker erneuert Leases (`bullpen-lurker:228`),
die nichts mehr bewachen. Die Zulassungssteuerung aus Tor 2 wurde also **gebaut
und lautlos deaktiviert** — nicht „gibt es nicht". `feedback_flag_rearmed_automation`
gilt: die Wiederinbetriebnahme muss wissentlich geschehen.

**Mein Fix von heute Vormittag hat es verbreitert.** deus in PRIVILEGED
aufzunehmen und ihm das Geheimnis zu geben war nötig, damit es überhaupt wirken
konnte — und gibt ihm zugleich die Fähigkeit, als jeder privilegierte Nick zu
sprechen. Der Stempel, den ich als Identitätsbeweis repariert habe, beweist nur
„irgendwer mit dem Geheimnis".

**Fix-Richtung (Fable):** Pro-Nick-Geheimnisse statt eines geteilten —
`PRIVILEGED` von `{nick=true}` zu `{nick=hash(secret_nick)}`, jeder Container
bekommt nur seine eigene Datei. ~20 Zeilen, kein neues Konzept.

### B8 — Das Scope-File hätte die Gesellschaft stillgelegt

`lmcp-tools/bullpen-room-scope.lua` (nie geladen) hat
`KEEP = {room_say, room_read, recall}` — **ohne** `lease_acquire`/`lease_status`/
`lease_seize`, die der Lurker vor **jedem** Dispatch ruft. Als `zz-*` zuletzt
geladen hätte es sie entfernt und nach TTL-Ablauf jeden `ask` blockiert.
Dieselbe Fehlerklasse wie der Rest des Tages: die Sperre kippt ins Gegenteil.

Dazu ein Namenswiderspruch: die Datei heißt `bullpen-room-scope.lua`, ihr Kopf
verlangt `zz-*`. Unter dem Repo-Namen sortiert sie **vor** die Plugins, stripte
also die Built-ins und ließe alles danach Registrierte am Leben — funktioniert
für den heutigen Bestand zufällig, bricht aber die zugesagte Eigenschaft
„künftige Built-ins verschwinden automatisch".

### Offen, nicht eingegriffen
* Der Lurker kennt als Laufzeit nur `claude` und `opencode`, **kein `pi`**.
  @deus kann deshalb nicht selbst auf Raumaktivitaet aufwachen; er muss von
  aussen angestossen werden. Fuer die Rolle "Eingriff von aussen" ist das
  stimmig, aber es heisst: **niemand weckt deus, wenn die Gesellschaft
  haengt.** Genau der Fall, fuer den er da ist.
