Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .empire/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1715,3 +1715,57 @@ Das gehört genauso ins Protokoll wie die Funde — jeder davon hätte ein plaus
- **`gh run list` ist blind für Repos ohne Workflows.** Ein leeres Ergebnis heißt „kein CI", nicht „CI kaputt". Die Gegenprobe ist `GET /repos/{o}/{r}/actions/workflows` plus `total_count` der Runs.
- **Ein in der Workflow-YAML referenzierter Name beweist nicht, dass der Wert gesetzt ist** — `GET /repos/{o}/{r}/actions/secrets` beweist es. Skripte, die ohne den Wert still zum No-op werden (`[ -n "${TOKEN:-}" ] || return 0`), sind sonst grün und wirkungslos.
- **Der Berichts-Vorbehalt:** Dass eine Zahl irgendwo im Morgenbrief auftaucht, macht sie nicht zu einem Wächter. `lagebild.sh` hätte einen fehlgeschlagenen Einzug als Zähler gezeigt — ohne Kunde, ohne Aktion, ohne Alarm. Beim Bewerten eines Fundes gehört diese Teil-Mitigation benannt, aber sie entkräftet ihn nicht.

## Die Briefe lesen sich wie Briefe (buzz#106 — Morgenbrief + Gate-Batch)

Munirs Befund, wörtlich: „der Struktur von Morgenbrief und so weiter ist nicht so schön, muss für mich übersichtlicher, mit schön menschlich beschrieben, gute Struktur." Der Leser ist einer, er liest um 08:45 auf dem Telefon, mit Neugeborenem und Drittversuch-Klausuren, und er hat zwei Minuten. Ein Brief, der wie ein Systemreport aussieht, wird nicht gelesen — und ein nicht gelesener Brief macht die gesamte Erhebungsmaschinerie (#7, #10, #59, #62, #63) wertlos.

**Morgenbrief — feste Reihenfolge, keine nummerierten Blöcke mehr:** Kopfzeile in ganzen Worten (beurteilt den Tag, ohne Zahl und ohne Status-Code) · 💶 Geld · ✉️ Menschen · 📅 Dein Tag · ⚙️ Läuft · ⏱️ Wenn du zwei Minuten hast.

**Gate-Batch:** Kopfzeile mit Anzahl UND Zeitbedarf („Sechs Entscheidungen, etwa drei Minuten.") · nummerierte Entscheidungen, je drei Zeilen (worum es geht · was ein Ja bewirkt · was ein Nein oder Schweigen bewirkt) · dann höchstens fünf Zeilen „was heute gelaufen ist" · Schluss.

### Was daran mehr ist als Kosmetik

- **Reiner Text, selbst umgebrochen bei 64 Zeichen** (`falte`, `RITUAL_WIDTH`). Kein Markdown, keine Tabellen, keine Codeblöcke: Was auf dem Telefon zerfallen kann, kommt nicht vor. Die Telegram-Markup-Strippe in `post_telegram` ist damit ein No-Op statt einer Rettung.
- **Die Folgen im Gate-Batch werden dem Ticket ENTNOMMEN, nie erfunden.** „Sagst du ja" ist der erste Satz aus `## Mission`, „Sagst du nein oder gar nichts" der Satz aus `## Money-Link`, der mit „Ohne"/„Solange"/„Bis dahin" beginnt. Fehlt er, steht dort ein ehrlicher Platzhalter („es bleibt genau so liegen wie jetzt") — kein erfundener Schaden. Gemessen am 01.08.: 4 von 6 Entscheidungen trugen ihren echten Preis, 2 fielen sichtbar auf den Platzhalter zurück. **Das ist der Grund, warum `## Money-Link` einen „Ohne …"-Satz enthalten sollte: er landet wörtlich in Munirs Abendbatch.**
- **Lücken stehen im betroffenen Abschnitt, nicht in einem Sammelblock am Ende.** `gap()` trägt jetzt Zielabschnitte (`geld menschen tag laeuft entscheidungen sonst`). Eine Lücke im Sammelblock liest niemand; eine Lücke unter „Geld" ersetzt die Zahl, die dort sonst stünde.
- **Der Gate-Batch zeigt sechs statt zwölf Entscheidungen** (`RITUAL_GATE_LINES`). Zwölf sind auf dem Telefon eine Wand; die Sortierung P1-money-zuerst sorgt dafür, dass die sechs teuersten oben stehen, der Rest wird gezählt statt verschwiegen.

### Der Fund, der den Inbox-Block ersetzt hat (gemessen 01.08.)

Die alte Zeile lautete „≥50 neue Nachrichten, davon ≥43 ungelesen". Eine Stichprobe über die **50 neuesten Nachrichten der letzten fünf Tage** ergab: **ausnahmslos Maschinen** — GitHub-CI, das eigene Monitoring, Revolut, Werbung. Kein einziger Mensch. Zwei Konsequenzen:

1. Die Zahl maß reines CI-Rauschen und sah dabei aus wie eine Aussage über Kunden.
2. `gmail_search` deckelt hart bei 50 (zod-Schema). Eine echte Kundenmail wäre **hinter dem Deckel unsichtbar** geblieben — der gefährlichere Teil.

Deshalb: **Der Ausschluss gehört in die Abfrage, nicht hinter sie.** Der Menschen-Block fragt `in:inbox newer_than:7d` mit serverseitigem `-from:`/`-category:`-Ausschluss und filtert danach nochmals lokal (`MACHINE_RE`, `OWN_DOMAINS`). Zwei Netze, weil Gmails `from:`-Matching unscharf ist. Wirkung im selben Postfach: aus 50 Maschinen wurden 3 namentlich genannte Menschen + 5 weitere Absender + 29 gezählte Benachrichtigungen.

Der größte Rauschposten war nicht GitHub, sondern **Munirs eigene Systeme**, die ihm in sein eigenes Postfach schreiben (`mondsamt@`, `autopilot@`, `foerderwerk@`, `kontakt@` auf `adas.team`/`adasgroup.de`). Ein Kunde schreibt nicht von adas.team.

**Bewusst in Kauf genommen:** schriebe ein echter Kunde von `support@seinefirma.de`, landete er in der Benachrichtigungszählung. Deshalb steht die Zahl der Aussortierten IM Brief — sichtbar falsch ist besser als unsichtbar weg.

**Einordnung zahlend/interessiert:** Vault-Kundenordner (`04 Areas/clients/<kunde>/`, Zusagen-Kanon) schlägt CRM (Espo `espo_search` auf Contact/Lead, Pipeline-Wahrheit). Antwortet das CRM nicht, ist das eine benannte Lücke — nie „kein Kunde".

### Fallen, die beim Bau zugeschnappt sind (alle gemessen, alle abgeräumt)

- **`read` mit `IFS=$'\t'` schluckt führende Leerfelder.** Der Tabulator ist ein IFS-*Whitespace*-Zeichen; ein leeres erstes Feld verschwindet und die ganze Zeile rutscht eine Spalte. Im Brief stand daraufhin der Betreff als Absendername. Regel: bei `@tsv` + `read` darf **kein** Feld leer sein — jq setzt Platzhalter.
- **`jq -n` schreibt mehrzeiliges JSON, und mehrzeiliges JSON überlebt die MSYS-argv-Konvertierung nicht.** `mcp-call.mjs` bekam `--args` und meldete „ist kein JSON". Regel: JSON-Argumente für `mcp-call.mjs` immer `jq -cn`.
- **`state: "error"` ist NICHT `state: "fehlt"`.** Der Zahlungs-Block eines toten Lagebilds enthält trotzdem ein `.pay`-Objekt; `// 0` machte daraus „Kein Zahlungseingang" und „kein Abo" — zwei zuversichtliche Falschaussagen aus einer toten Quelle, direkt in der Rot-Probe sichtbar. Nur `ok`/`warn` dürfen Zahlen tragen.
- **Eine Kopfzeile ohne Inhalt ist die stillste stille Null.** Ein jq-Compilefehler in `render_gate_lines` lieferte „Sechs Entscheidungen, etwa drei Minuten." — und danach nichts. Der Abend sah leer aus. Jetzt wird das Renderergebnis geprüft, bevor die Kopfzeile etwas verspricht.
- **jq-Regex im Shell-Script braucht doppelte Backslashes** (`"\\s"`, `"\\["`). Einfache verschluckt jq mit „Invalid escape" und liefert *nichts* — wieder eine stille Null.
- **Ein laufendes Bash-Script darf nicht bearbeitet werden.** Bash liest die Datei inkrementell nach; ein Edit während eines Hintergrundlaufs erzeugte „erselben: command not found" und einen Syntaxfehler in einer Zeile, die syntaktisch einwandfrei war. Bei parallelen Läufen: Kopie laufen lassen — dann aber `$HERE` beachten, sonst findet die Kopie `lagebild.sh` nicht.
- **`date '+%a'` liefert unter MSYS „Sat".** Ein englischer Wochentag in einem deutschen Brief ist genau der Systemgeruch, der hier weg soll — Wochentage werden aus `date '+%w'` selbst gesetzt.

### Rot-Proben (der Brief MUSS falsch aussehen können)

| Probe | Ergebnis |
|---|---|
| `RITUAL_MCP_CONFIG=/gibt/es/nicht.json` | „Ich komme heute nicht ans Postfach: …" und „Deinen Kalender habe ich heute nicht erreicht: …" — **nicht** „Niemand hat geschrieben" / „Keine Termine" |
| `MOLLIE_LIVE_API_KEY=live_kaputt` | „Ich konnte die Zahlungen nicht messen (error): Mollie /subscriptions HTTP 400" — **nicht** „Kein Zahlungseingang" (erst nach dem `state`-Fix, siehe oben) |
| `GH_TOKEN=gho_kaputt` (Gate-Batch) | „Ich konnte heute nicht nachsehen, was auf dich wartet. Das heißt ausdrücklich nicht, dass nichts wartet." — **nicht** „Heute nichts zu entscheiden" |
| Breiten-Probe | längste Zeile 64 Zeichen in beiden Briefen |
| Wochen-Review (#63) unangetastet | vollständiger Lauf nach dem Umbau: 127 geschlossene Issues, 89 offene Entscheidungen, Vorschlagsblock gefüllt |

### Kleine additive Erweiterung an `lagebild.sh`

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.
1 change: 1 addition & 0 deletions .empire/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
2026-08-01 21:30 | 1 Ticket | #15 Empire-Cockpit-Tab (eigener /empire-Tab statt Pulse-Erweiterung; 4 Kacheln aus lagebild.sh + managed_agents-Laufzeit + neuen ritual.sh-Lauf-Quittungen; keine stillen Nullen in 3 Schichten, Rust 8/8 + TS 19/19, Stichproben gegen gh exakt) | PR #105 | Befunde: lib.rs stand exakt auf der 1000-Zeilen-Ratchet -> jede Command-Registrierung kippt pnpm check; routeTree.gen.ts erzeugt Vite, nicht tsc; frischer Worktree hat keine Sidecars | Gardener: #108, #109 | Offen: gerenderter Screenshot-Beweis (#109)
2026-08-01 22:00 | 2 Tickets | #48 Windows-Canary als CI-Gate (Root-Cause: der Upstream-Canary traegt `if: github.repository == 'block/buzz'` -> auf JEDEM Fork wird der Job still uebersprungen, ein Dispatch dort meldet success ohne eine Zeile zu bauen; Fork-Pendant empire-windows-canary.yml mit push-auf-main + paths-ignore, dispatch auf jedem Branch, concurrency, timeout 120; Auto-Trigger beim Merge von PR #102 bewiesen: Run 30712467814 success, 18/18 Steps, 32m32s, Artefakt 52.342.569 Bytes; Rot-Probe Typfehler in buzz-cli -> failure in "Build sidecars" mit E0308/exit 101; Doku-PR #107 loeste dank paths-ignore KEINEN Lauf aus) + #79 CRM-Loeschzeile im Gate-Batch (Quelle action_history_record read-only ueber den bestehenden ssh-Pfad -> kein neuer Espo-User; Access-Log und `deleted:true`-Grabstein begruendet verworfen; Schwelle aus 8 Tagen gemessen: taeglich exakt n8n-agent 1x Lead + 1x CTouchpoint; 5 Proben: Ruhetag gruen, Ausschlag 61/59 aufgeschluesselt, Detektor rot->gruen per echter Testloeschung ueber delete:own, tote Quelle = benannte LUECKE statt "0", ACL-Waechter danach 3/3; Kanal-Beweis Buzz-Event 10f831820480c508) | PRs #102 #107 #111 | Gardener: #113 (Fork-Actions entruempeln: ein main-Push zuendet 6 Upstream-Workflows, Sprig scheitert reproduzierbar an "release not found" -> ein dauerhaft roter Actions-Tab entwertet das neue Gate); #114 als Duplikat zu #69 geschlossen | Befunde: der Gutfall darf nicht formuliert werden — die erste Fassung schrieb die feste Formel "1x Lead + 1x CTouchpoint" und behauptete bei EINER Loeschung einen CTouchpoint, den es nicht gab (erfundene Zahl im Fuehrungsbrief, gefixt); einer der drei Espo-Zugangsnamen im zentralen Secrets-File antwortet 401 (gehoert zu #69); `UID` ist in bash readonly (MSYS) -> die Zuweisung scheitert still und man arbeitet mit der Shell-UID weiter | Blocker: Approval-Gate weiterhin OHNE echte Freigabe (gate-audit nur requested/timeout, auch ein Fremd-Request um 19:57 lief in Timeout) -> #32/#42 bleiben zu; claude-Kontingent (#3), Codex bis 08.08. (#13/#18)
2026-08-01 22:45 | 3 Tickets Betrieb gehaertet | adas-empire#79 Eskalationsweg beweisbar (Kanarie durch denselben empire_notify-Code, Ankunft in der ECHTEN INBOX belegt; 3 Rot-Proben; tote Adresse munirdue@ raus) + buzz#77 Sentry-Liveness (Mention+Token, Kuma sentry-liveness, echter Reboot: Agent ohne Zutun in 29 s zurueck, 87/87 Container) + buzz#86 Token-Waechter mit zwei Alarmwegen (Token-Tod pusht status=down MIT Fix-Befehl, Rechner-aus heilt sich per StartWhenAvailable+Logon-Trigger) | PRs adas-empire#83/#84, buzz#112/#116, google-mcp#5 | BEFUND (systemisch): 9/11 Kuma-Push-Monitore mit maxretries=1 bei retryInterval==interval alarmieren erst nach der DOPPELTEN dokumentierten Zeit, ein status=down erzeugt dort nur PENDING/important=0 -> agency-infra#135 | Widerlegte Praemissen: kein Bounce-Regen im Postfach, Eskalationsmails KAMEN an (nur unbewiesen), buzz#77-Logrotation existierte laengst, Monitor 54 hatte nie Alarm geschlagen | Blocker: keine
2026-08-01 23:10 | 1 Ticket | #106 Morgenbrief + Gate-Batch lesbar (Brief statt Systemreport: Kopfzeile in ganzen Worten, Geld/Menschen/Tag/Laeuft/Zwei-Minuten; Gate-Batch nummeriert mit Ja-/Nein-Folgen AUS dem Ticket-Text; reiner Text, Umbruch bei 64) | Fund: 50 von 50 Inbox-Nachrichten der letzten 5 Tage waren Maschinen -> Ausschluss gehoert IN die Gmail-Abfrage, sonst bleibt eine Kundenmail hinter dem 50er-Deckel unsichtbar | Rot-Proben 3/3 (MCP tot, Mollie tot, gh tot) + Breiten-Probe 64 | Fallen: read/IFS-TAB schluckt Leerfelder; jq -n mehrzeilig stirbt in MSYS-argv; state=error != fehlt (stille 0); laufendes Bash-Script nicht editieren | Blocker: keine
19 changes: 18 additions & 1 deletion .empire/tools/lagebild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,17 @@ block_pay() {
block_error pay "Mollie /payments HTTP $code ohne verwertbare Liste"; return
fi

local since30
local since30 since24
since30="$(date -u -d '-30 days' '+%Y-%m-%d' 2>/dev/null || date -u -v-30d '+%Y-%m-%d')"
# Der Morgenbrief fragt „was hat sich seit gestern bewegt", nicht „wie war der
# Monat". Ohne eigenes 24-h-Fenster müsste er das aus `last_payments` raten —
# und das sind nur die letzten DREI, also eine stille Untergrenze.
since24="$(date -u -d '-24 hours' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -v-24H '+%Y-%m-%dT%H:%M:%SZ')"

jq -n \
--argjson amounts "$SHOW_AMOUNTS" \
--arg since30 "$since30" \
--arg since24 "$since24" \
--slurpfile subs "$TMP/subs.json" \
--slurpfile pays "$TMP/pays.json" '
($subs[0]._embedded.subscriptions) as $s
Expand All @@ -460,6 +465,8 @@ block_pay() {
# Prüfung ausnahmslos nie begonnene Checkouts waren.
| ($p30 | map(select((.status=="failed" or .status=="expired") and (.method != null)))) as $real_fail
| ($p30 | map(select((.status=="failed" or .status=="expired") and (.method == null)))) as $never_started
| ($p | map(select(.createdAt >= $since24))) as $p24
| ($p24 | map(select(.status=="paid"))) as $paid24
| {
state: (if ($act|length) == 0 then "warn"
elif ($real_fail|length) > 0 then "warn"
Expand All @@ -473,6 +480,16 @@ block_pay() {
last30_total: ($p30|length),
last30_failed_after_method: ($real_fail|length),
last30_never_started: ($never_started|length),
# 24-h-Fenster für den Morgenbrief. `payments_window_complete` sagt, ob
# die 50 abgeholten Zahlungen überhaupt 24 h abdecken — sonst wäre
# "1 Zahlung" eine Untergrenze, die wie eine gemessene Zahl aussieht.
last24_total: ($p24|length),
last24_paid: ($paid24|length),
last24_failed_after_method:
($p24 | map(select((.status=="failed" or .status=="expired") and (.method != null))) | length),
last24_paid_eur: (if $amounts == 1 then ($paid24|map(.amount.value|tonumber)|add // 0) else null end),
last24_paid_methods: ($paid24 | map(.method // "unbekannt") | unique),
payments_window_complete: (($p|length) < 50 or (($p|last|.createdAt) < $since24)),
mrr_eur: (if $amounts == 1 then ($act|map(.amount.value|tonumber)|add // 0) else null end),
reason: (if ($act|length) == 0 then "keine aktive Subscription"
elif ($real_fail|length) > 0
Expand Down
Loading
Loading