diff --git a/.empire/AGENTS.md b/.empire/AGENTS.md index f481d25f88..f17d001e3b 100644 --- a/.empire/AGENTS.md +++ b/.empire/AGENTS.md @@ -1795,6 +1795,95 @@ Der größte Rauschposten war nicht GitHub, sondern **Munirs eigene Systeme**, d Der Zahlungs-Block liefert zusätzlich ein 24-Stunden-Fenster (`last24_total`, `last24_paid`, `last24_paid_eur`, `last24_failed_after_method`, `last24_paid_methods`, `payments_window_complete`). Grund: der Morgenbrief fragt „was hat sich seit gestern bewegt", nicht „wie war der Monat" — und ohne eigenes Fenster hätte er das aus `last_payments` raten müssen, also aus den letzten DREI Zahlungen. `payments_window_complete` sagt, ob die 50 abgeholten Zahlungen 24 h überhaupt abdecken. Der Morgenbrief ruft `lagebild.sh` jetzt mit `--amounts` auf (privater Kanal); `--redact` nimmt die Beträge wieder heraus. +--- + +## Der `claude`-Agent erbt Munirs Vollkonfiguration — gemessen, und warum die Trennung (noch) nicht kommt (buzz#84) + +**Kurzfassung:** Ja, er erbt alles. Das kostet **17,8 s pro `session/new`** und schleppt **56 MCP-Server** +mit, 12 davon dauerhaft kaputt. Der Hebel dagegen ist gemessen und wirkt (**2,2 s / 19 Server, 8,1×**) — +er ist trotzdem **nicht verdrahtet**, weil die Auth dabei auseinanderliefe. Das ist kein Zögern, das ist +der Guardrail des Tickets: *kein zweiter Auth-Zustand, bevor irgendetwas getrennt wird.* + +### Warum er erbt: es steht im Adapter, nicht in Buzz + +`@agentclientprotocol/claude-agent-acp@0.64.0` (`dist/acp-agent.js`) baut die SDK-Optionen mit + +```js +const options = { systemPrompt, settingSources: ["user", "project", "local"], ... } +``` + +Das ist eine bewusste Opt-in-Entscheidung des Adapters: das Claude Agent SDK lädt **ohne** diese Zeile +gar keine Filesystem-Settings. Mit ihr bekommt der Agent Munirs kompletten User-Scope — 28 MCP-Server +aus `~/.claude.json`, 39 Plugins aus `~/.claude/settings.json`, dazu Skills und Hooks. Buzz injiziert +dabei nichts; der Agent-Record hatte bis heute keine Env-Overrides. + +### Messung auf dem echten Agentenpfad + +`%APPDATA%\Buzz\node-tools\claude-agent-acp.cmd` direkt über stdio, `initialize` → `session/new` +(`{cwd: ~/.buzz, mcpServers: []}`), **kein `session/prompt`** — kontingentunabhängig, wie bei buzz#40. +Server-Zahlen aus `claude mcp list` im selben `cwd` (verbindet jeden Server wirklich, meldet Fehler). + +| Messgröße | geerbt (Ist-Zustand) | `CLAUDE_CONFIG_DIR=~/.claude-buzz` | +|---|---|---| +| `session/new`, warm, direkt hintereinander | **17 756 ms** | **2 192 ms** | +| Wall bis nutzbarer Session | 18 789 ms | **2 962 ms** | +| erste Session nach Ruhe (kalt) | 25 059 ms | — | +| MCP-Server im `cwd` `~/.buzz` | **56** | **19** | +| davon kaputt / unauthentifiziert | **12** (u. a. `obsidian`, `magic`, `coolify`, `n8n-mcp`, `spaceship-mcp`, `gbp`, `unreal-mcp`, 2 Plugin-Server) | 4 (nur Account-Connectors) | + +Zwei Nebenbefunde aus derselben Messung: + +- **Der Nest-`cwd` kostet fast nichts.** Gleiche Bedingungen, `cwd` = leeres Verzeichnis: 10 972 ms. + Der Unterschied zu `~/.buzz` sind ~1,7 s für die fünf Nest-Server — die Last liegt praktisch + vollständig im User-Scope, nicht im Projekt-Scope. +- **`enableAllProjectMcpServers: true` in `~/.buzz/.claude/settings.local.json` trägt.** Auch mit einem + fremden, schlanken Config-Dir verbinden sich alle fünf Nest-Server (`google-mcp`, `telegram-mcp`, + `espo-mcp`, `obsidian-mcp-tools`, `n8n-api`) sauber. Die Dispatcher-Ausstattung aus buzz#4 hängt + nicht am User-Scope. +- **Scope-Konflikt gemeldet:** `n8n-api` ist in `user` und `project` mit verschiedenen Endpunkten + definiert; OAuth-Token werden pro Endpunkt gespeichert. + +### Warum `CLAUDE_CONFIG_DIR` trotzdem nicht verdrahtet wird + +Bei codex (buzz#40) hielt ein **Hardlink** auf `auth.json` beide Seiten auf einem Token. Für Claude Code +gilt das **nicht** — zweimal reproduziert: + +``` +# frisch verlinkt +inode=4503599632054867 links=2 ~/.claude/.credentials.json +inode=4503599632054867 links=2 ~/.claude-buzz/.credentials.json +# EIN claude-Aufruf spaeter +inode=6755399445748017 links=1 ~/.claude/.credentials.json <- neue Datei +inode=4503599632054867 links=1 ~/.claude-buzz/.credentials.json <- alter Stand, eingefroren +``` + +Claude Code **ersetzt** die Credentials-Datei (neu schreiben + umbenennen) statt sie zu überschreiben. +Ein Hardlink zeigt auf den Inode, nicht auf den Pfad — er überlebt das Umbenennen nicht und zerfällt +lautlos in zwei Dateien. Danach refresht jede Seite ihr eigenes Token auf demselben Konto: genau der +zweite Auth-Zustand, den das Ticket verbietet, und ein Kandidat dafür, dass eine Seite die andere +ausloggt. **Zur Kontrolle nachgemessen:** `~/.codex/auth.json` ↔ `~/.codex-buzz/auth.json` stehen +weiterhin auf `links=2`, gleicher Inode, inhaltsgleich — die Codex-Engine schreibt **in place**. +Der Unterschied liegt in der Schreibweise der jeweiligen Engine, nicht im Mechanismus. + +Ein **Symlink** (der nach Pfad auflöst und das Ersetzen überlebt) wäre die richtige Form, scheitert hier +an Windows: `New-Item -ItemType SymbolicLink` → `NewItemSymbolicLinkElevationRequired`. Junctions gibt +es nur für Verzeichnisse. + +**Entscheidung:** Messung steht, Hebel ist belegt, Verdrahtung bleibt aus. Der Agent-Record wurde +testweise gesetzt und wieder zurückgenommen (`managed-agents.json.bak-buzz84-*` liegt daneben); das +Experiment-Verzeichnis `~/.claude-buzz` wurde restlos entfernt, damit kein eingefrorenes Token +herumliegt. Munirs interaktives Setup wurde nicht beschnitten. + +### Der Weg, der die Auth gar nicht erst anfasst + +Nicht der Config-Dir muss weg, sondern **eine Zeile im Adapter**: wäre `settingSources` konfigurierbar +(z. B. `["project","local"]` per Env), bliebe der Config-Dir — und damit **ein** Auth-Zustand — +unangetastet, und die 28 User-Server plus 39 Plugins fielen trotzdem weg. Das ist eine upstream-fähige +Änderung an `@agentclientprotocol/claude-agent-acp`, kein Buzz-Fork-Feature. Folge-Ticket: buzz#123. + +**Merksatz für den nächsten Agenten:** Ein Hardlink teilt einen *Inode*. Wer ihn als „geteilte Datei" +benutzt, muss vorher wissen, ob der Schreiber in place schreibt oder ersetzt — sonst hat man nach dem +ersten Schreibvorgang zwei Wahrheiten und merkt es nicht. ## buzz#69 — Rechte-Rückfall im CRM stillgelegt (2026-08-01) @@ -1832,3 +1921,23 @@ Die frühere Notiz „Stripe fehlt bewusst — als Lücke benannt statt als 0 | Morgenbrief ohne Key | „Stripe kann ich nicht sehen — dort könnte Geld eingegangen sein, das in dieser Zeile fehlt." | **Noch offen (braucht den Key):** Stichprobe gegen echte Stripe-Daten und die Schreibschutz-Probe (ein Schreibaufruf mit dem Key muss abgelehnt werden). **Derselbe Handgriff hängt an agency-infra#130** (`STRIPE_INVOICE_EXPORT_KEY`, Invoices:read) — ein Restricted Key mit den drei Lese-Rechten Invoices/Charges/Subscriptions bedient beide Tickets. + +### Nachtrag zu #106: drei Fehler, die erst der Datumswechsel und die Rot-Probe zeigten + +Der Brief lief um 20:50 sauber und um 00:35 nicht mehr. Alle drei Befunde sind gemessen, keiner war vorher sichtbar: + +1. **Termine, die vor Mitternacht begonnen haben, standen als „morgen" im Brief.** Google liefert im Fenster auch Termine, die vor `timeMin` begonnen haben und noch laufen. Der Vergleich `start == heute → "Um", sonst "Morgen um"` warf damit Vergangenes in die Zukunft: der gestrige 23:00-Block erschien als „Morgen um 23:00". Jetzt wird **dreifach** unterschieden (`< heute` → „Läuft noch, seit", `== heute` → „Um", `> heute` → „Morgen um") und die Liste ist nach `start` sortiert. **Regel: bei Kalenderfenstern ist „nicht heute" nicht dasselbe wie „morgen".** +2. **Die Kopfzeile brach mitten im Satz ab, sobald zwei Monitore rot waren.** Eine Kopfzeile, die auf „… meldet backup-offsite, mollie-recurring als …." endet, ist schlechter als eine kürzere vollständige. Jetzt: passt die volle Fassung nicht in eine Zeile, wird sie aus den Messdaten neu gebaut („backup-offsite und 1 weitere Überwachung ist ausgefallen"), nicht abgeschnitten. **Regel: eine Zeile, die allein tragen soll, darf nie ein Fragment sein.** +3. **Eine Lücke ohne Abschnitt ist wieder eine stille Null.** Seit #106 hat der Morgenbrief keinen Entscheidungs-Block mehr — Lücken der Kategorie `entscheidungen` wurden also erhoben, kategorisiert und dann **von keinem Renderer ausgegeben**. In der Rot-Probe fiel die Empfehlung sichtbar zurück, aber niemand erfuhr warum. Jetzt rendert der „Zwei-Minuten"-Abschnitt sie mit. **Regel: nach jedem Umbau der Abschnitte prüfen, ob jede Lücken-Kategorie noch einen Abschnitt hat, der sie druckt.** + +Zusätzlich: Lücken-Sätze sind bei 130 Zeichen gedeckelt (Wortgrenze). Eine Repo-Liste mit 20 Namen flutet sonst den Brief, egal wie ehrlich sie ist. + +### Die eine Handlung kommt aus dem Audit, nicht aus dem Label (adas-empire#86) + +„Wenn du zwei Minuten hast" zog vorher das älteste offene `P1-money`-`blocked-munir`-Ticket. Das ist die falsche Quelle: es gibt rund 90 solcher Tickets, viele sind **Dubletten derselben Handlung**, und die Sortierung nach echter Geldwirkung steckt im Audit `adas-empire#86` („Die ersten sieben"), nicht im Label. + +Der Brief liest deshalb **Zeile 1 der Tabelle** aus diesem Issue — ein Satz, den ein Mensch nach Geldwirkung sortiert hat, mitsamt Ticket-Referenz. Quelle umstellbar über `RITUAL_MUNIR_LISTE`. + +**Ist die Quelle nicht lesbar, wird das gesagt** („Die Blockerliste … war nicht lesbar — die Empfehlung oben ist nur das älteste Geld-Ticket, nicht das wichtigste") und der Brief fällt sichtbar auf den alten Weg zurück. Rot-Probe mit `RITUAL_MUNIR_LISTE=munirad7s/adas-empire#999999` gefahren — genau dieser Satz erschien, und **genau diese Probe hat Befund 3 oben aufgedeckt**. + +**Folge für alle Agenten:** Wer `adas-empire#86` pflegt, schreibt direkt in Munirs Morgenbrief. Zeile 1 der Tabelle ist kein Notizzettel mehr. diff --git a/.empire/tools/ritual.sh b/.empire/tools/ritual.sh index b4d6f713db..e9fc37d923 100644 --- a/.empire/tools/ritual.sh +++ b/.empire/tools/ritual.sh @@ -101,7 +101,12 @@ gap() { printf '%s\t%s\n' "$1" "$2" >> "$GAPFILE"; } gaps_for() { awk -F'\t' -v w="$1" '{n=split($1,c," "); for(i=1;i<=n;i++) if(c[i]==w){print $2; break}}' "$GAPFILE"; } gaps_flat() { cut -f2- "$GAPFILE"; } # Im Brief steht kein Stichwort-Fragment, sondern ein Satz — auch die Luecke. -gaps_satz() { gaps_for "$1" | sed 's/[^.…!?]$/&./'; } +# Keine Luecke darf den Brief fluten: eine Repo-Liste mit 20 Namen ist als +# Luecken-Satz genauso unlesbar wie als Block. Deckel bei 130 Zeichen, an der +# Wortgrenze — der Vollstand steht im Log. +gaps_satz() { + gaps_for "$1" | sed 's/[^.…!?]$/&./' | awk '{ if (length($0) > 130) { s = substr($0, 1, 130); sub(/ [^ ]*$/, "", s); print s " …" } else print }' +} has_gap() { [ -n "$(gaps_for "$1")" ]; } gapcount() { wc -l < "$GAPFILE" | tr -d ' '; } @@ -588,6 +593,33 @@ write_snapshot() { return 0 } +# (g) Munir-Blockerliste — adas-empire#86. Die eine Handlung im Morgenbrief +# wird NICHT neu erhoben: der Backlog kennt 93 blocked-munir-Tickets, aber viele +# sind Dubletten derselben Handlung, und die Sortierung nach Geldwirkung steckt +# im Audit, nicht im Label. Deshalb ist Zeile 1 der Tabelle „Die ersten sieben" +# aus #86 die Quelle — ein Satz, den ein Mensch nach echter Geldwirkung sortiert +# hat. Fällt die Quelle aus, steht das als Lücke da und der Brief fällt sichtbar +# auf das ältestes-P1-money-Ticket zurück. Kein erfundener Satz. +MUNIR_LISTE_ISSUE="${RITUAL_MUNIR_LISTE:-munirad7s/adas-empire#86}" +collect_munir_liste() { + local repo nr body + repo="${MUNIR_LISTE_ISSUE%#*}"; nr="${MUNIR_LISTE_ISSUE##*#}" + if ! body="$(gh issue view "$nr" -R "$repo" --json body -q .body 2>"$TMP/ml.err")"; then + gap "entscheidungen" "Die Blockerliste ($MUNIR_LISTE_ISSUE) war nicht lesbar — die Empfehlung oben ist nur das älteste Geld-Ticket, nicht das wichtigste" + return 1 + fi + # Erste Datenzeile der Tabelle: | **1** | **** | | | + printf '%s' "$body" | tr -d "$CR" \ + | awk -F'|' '/^\| *\*\*1\*\* *\|/ { print $3 "\t" $4; exit }' \ + | sed -e 's/\*\*//g' -e 's/`//g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \ + > "$TMP/munir1.txt" + if [ ! -s "$TMP/munir1.txt" ]; then + gap "entscheidungen" "Die Blockerliste ($MUNIR_LISTE_ISSUE) hat keine erste Zeile geliefert — die Empfehlung oben ist nur das älteste Geld-Ticket, nicht das wichtigste" + rm -f "$TMP/munir1.txt"; return 1 + fi + return 0 +} + # (d) Foki — die drei #-Überschriften aus Now.md. Vault ist Kanon. collect_foki() { if [ ! -f "$NOW_MD" ]; then @@ -761,9 +793,32 @@ befunde() { # Die Kopfzeile darf nicht "irgendwas ist kaputt" sagen und den Leser nach # unten schicken — sie muss allein tragen. Also: dieselbe erste Sache, nur # kurz genug für eine Zeile. + # Und sie muss ein GANZER Satz bleiben. Ein abgeschnittenes „… meldet + # backup-offsite, mollie-recurring als …." ist schlechter als eine kürzere, + # aber vollständige Aussage — gemessen, sobald zwei Monitore rot waren. KAPUTT_KURZ="" - [ -n "$KAPUTT" ] && KAPUTT_KURZ="$(kurz "$(printf '%s\n' "$KAPUTT" | head -1 \ - | sed -e 's/^Auf dem Server stimmt etwas nicht: //' -e 's/\.$//' -e 's/;.*//')" 62)" + if [ -n "$KAPUTT" ]; then + local roh; roh="$(printf '%s\n' "$KAPUTT" | head -1 \ + | sed -e 's/^Auf dem Server stimmt etwas nicht: //' -e 's/\.$//' -e 's/;.*//')" + KAPUTT_KURZ="$(kurz "$roh" 62)" + case "$KAPUTT_KURZ" in + *' …') + # Passt nicht in eine Zeile. Statt vage zu werden: EIN Name plus Anzahl. + # Vollständig stehen sie zwei Abschnitte weiter unten. + local kn kd + kn="$(jq -r '(.server.kuma_down // [])|length' "$TMP/lage.json" 2>/dev/null | tr -d "$CR")" + kd="$(jq -r '(.server.kuma_down // [])[0] // ""' "$TMP/lage.json" 2>/dev/null | tr -d "$CR")" + if [ -n "$kd" ] && [ "${kn:-0}" -gt 1 ] 2>/dev/null; then + KAPUTT_KURZ="$kd und $((kn-1)) weitere $([ "$((kn-1))" -eq 1 ] && printf 'Überwachung ist' || printf 'Überwachungen sind') ausgefallen" + elif [ -n "$kd" ]; then + KAPUTT_KURZ="die Überwachung $kd ist ausgefallen" + else + KAPUTT_KURZ="$(printf '%s' "$roh" | sed 's/:.*//')" + [ "${#KAPUTT_KURZ}" -gt 62 ] && KAPUTT_KURZ="auf dem Server stimmt etwas nicht" + fi + ;; + esac + fi GELD_ALARM="" if [ -f "$TMP/lage.json" ]; then @@ -793,6 +848,16 @@ zwei_minuten() { printf 'Antworte %s. Zwei Sätze reichen — Hauptsache, es kommt heute etwas zurück.\n' "$(kurz "$WARTET_NAME" 40)" return fi + # Erste Wahl: der von Hand nach Geldwirkung sortierte Kopf der Blockerliste. + if [ -s "$TMP/munir1.txt" ]; then + local satz ticket + satz="$(cut -f1 "$TMP/munir1.txt")" + ticket="$(cut -f2 "$TMP/munir1.txt" | sed -e 's/[[:space:]]*→.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [ "$REDACT" = "1" ]; then satz="‹Handlung geschwärzt›"; ticket=""; fi + printf '%s\n' "$(kurz "$satz" 150)" + [ -n "$ticket" ] && printf '(%s)\n' "$ticket" + return + fi if [ -f "$TMP/blocked.json" ] && [ "$(jq -r 'length' "$TMP/blocked.json")" -gt 0 ]; then local e t r n e="$(jq -r '([.[] | select(.labels|index("P1-money"))] + .) | .[0] | "\(.title)\t\(.repo)\t\(.number)"' \ @@ -971,9 +1036,16 @@ render_morgenbrief() { jq -r --arg heute "$(date '+%Y-%m-%d')" --argjson redact "$REDACT" ' def was($s): if $redact == 1 then "‹Termin geschwärzt›" else ($s|.[0:44]) end; [.events[]|select(.allDay)] as $a | - [.events[]|select(.allDay|not)] as $t | + # Chronologisch, und die Tagesgrenze wird DREIFACH unterschieden. + # Google liefert im Fenster auch Termine, die vor `timeMin` begonnen + # haben und noch laufen; die wurden vorher als „Morgen" ausgegeben — + # gemessen um 00:35, als der gestrige 23:00-Block als „morgen 23:00" + # im Brief stand. Vergangen ist nicht künftig. + [.events[]|select(.allDay|not)|select(.start != null)] | sort_by(.start) as $t | ([ $a[] | " Den ganzen Tag: " + was(.summary) + "." ] - + [ $t[] | (if (.start|.[0:10]) == $heute then " Um " else " Morgen um " end) + + [ $t[] | (if (.start|.[0:10]) < $heute then " Läuft noch, seit " + elif (.start|.[0:10]) == $heute then " Um " + else " Morgen um " end) + (.start|.[11:16]) + " " + was(.summary) + (if .location and $redact == 0 then " (" + (.location|.[0:22]) + ")" else "" end) + "." ])[0:6] | .[] ' "$TMP/cal.json" | tr -d "$CR" @@ -996,6 +1068,12 @@ render_morgenbrief() { # --- Wenn du zwei Minuten hast printf '\n⏱️ Wenn du zwei Minuten hast\n' zwei_minuten | sed 's/^/ /' + # Ohne diese Zeile wären die Lücken der Kategorie `entscheidungen` im + # Morgenbrief STUMM: der Brief hat seit #106 keinen Entscheidungs-Block + # mehr, in dem sie sonst aufschlügen. Gemessen in der Rot-Probe — die + # Empfehlung fiel sichtbar zurück, aber niemand erfuhr warum. Eine Lücke, + # die keinen Abschnitt hat, ist wieder eine stille Null. + gaps_satz entscheidungen | sed 's/^/ /' } | falte > "$out" printf '%s' "$out" } @@ -1375,6 +1453,7 @@ case "$MODE" in collect_calendar collect_people collect_blocked + collect_munir_liste || true BRIEF="$(render_morgenbrief)" CH="${BUZZ_CHANNEL:-$CH_GENERAL}"; LABEL="🌅 Morgenbrief" ;;