# CLAUDE.md — Projektanweisungen **Projektkennung: P4 · Anwendung ohne Datenbank.** Jede Meldung an das Vorlagen-Repo trägt sie als `[P4 · Anwendung ohne Datenbank]`. Sie stand hier bis zum Abgleich auf v1.16.0 **nicht** — mit der Folge, dass Meldungen von hier zeitweise unter P5 liefen und das Register eine eigene Nummer (P7) für sie reservierte. Nachweis und Auflösung: Issue #93. **Prozess-Stand: v1.19.1** — Stand der Vorlage, aus der dieses Projekt stammt. Hochgesetzt am 03.11.2026 (Abgleich #153, Sprung von v1.16.0 über 19 Versionen). **Zwei Tür-Proben sind dabei NICHT gefahren worden, beide vertagt mit Bedingung:** die Ausfall-Echtprobe des Prod-Wächters (Bedingung: erste Instanz, die ein Wächter schützt — hier ist keiner installiert) und der Linux-Zweig von `baum-stoppen.sh` (dieser Rechner setzt `GITBASH`; die Wache ist dort nur gelesen, nicht gemessen). Offene Punkte des Sprungs: #149, #151, #152, #154. Beim Abgleich mit einer neueren Vorlagen-Version hochsetzen; wie das geht, steht in `docs/agents/abgleich.md` im Vorlagen-Repo `Trust1509/agent-projekt-template` (dieses Repo führt selbst keine `abgleich.md`, weil sie nur beim Abgleichen gebraucht wird, nicht im laufenden Betrieb). Diese Zeile bleibt im Projekt stehen. Immich Family Tools ist eine Companion-Web-App für selbst gehostetes Immich: ein FastAPI-Backend und ein React/TS-Frontend in einem gemeinsamen Container, für kontenübergreifendes Gesichts-Matching und geteilte Alben über die Immich-REST-API. Es gibt keine eigene Datenbank — die gesamte Persistenz ist eine einzige JSON-Datei (`accounts.json`) auf einem ZFS-Volume. Das Projekt **erfordert Immich v3.x**. **Profil: Anwendung ohne Datenbank.** Die gesamte Persistenz ist eine JSON-Datei; es gibt kein Migrations-Framework, keinen Datenbank-Dienst und keine öffentlich erreichbare Schnittstelle. Der **Prozess-Kern** (Panel, Bau-Brief, Rot-Beweis, Release-Ritual, Betrieb) gilt vollständig. Die **Stack-Maschine** (Migrationsköpfe, Roundtrips gegen eine echte Zieldatenbank, Schnittstellen-Fuzzing) hat hier keinen Gegenstand — mit einer Ausnahme, die zählt: Die Datenerhalt-Probe gilt sehr wohl, nur framework-frei, als Test gegen das JSON-Alt-Format (`backend/tests/test_config_store.py`). ## Umgebungsaussagen — gemessen, nicht angenommen Jede Aussage dieser Datei über die Umgebung ist eine **Behauptung** und trägt deshalb ein Kommando mit Exit-Code. Der Anlass steht in `docs/agents/lehren.md` §32: „Kein Node/npm lokal auf diesem Rechner" stand in fünf Projektständen der Vorlage — auf dem Rechner lagen Node 24 und npm 11, ein Bauer baute sein Ritual auf die Aussage, und das Ritual vernichtete Daten. Gemessen am 22.09.2026 auf dem Entwicklungsrechner, **nachgemessen am 03.11.2026** beim Abgleich auf Vorlage v1.19.1. Eine Zahl hatte sich bewegt: Docker 29.6.1 → 29.8.2. Node, npm, Python und `gh` standen unverändert, die beiden `grep`-Zeilen ebenfalls. | Behauptung | Kommando | Ergebnis | | ------------------------- | ------------------------------------------------------------------------- | ----------------------- | | Node liegt lokal | `node --version` | Exit 0, `v24.18.0` | | npm liegt lokal | `npm --version` | Exit 0, `11.16.0` | | Python liegt lokal | `python --version` | Exit 0, `Python 3.14.5` | | Docker liegt lokal | `docker --version` | Exit 0, `29.8.2` | | `gh` liegt lokal | `gh --version` | Exit 0, `2.93.0` | | kein Migrations-Framework | `grep -rniE 'alembic\|sqlalchemy\|psycopg' backend/requirements*.txt` | Exit 1 (kein Treffer) | | kein Datenbank-Dienst | `grep -cE 'image: *(postgres\|mysql\|mariadb\|mongo)' docker-compose.yml` | `0`, Exit 1 | **Die eine Stelle, an der lokal und CI auseinandergehen:** Lokal läuft **npm 11** (Node 24), die CI fährt **Node 22** (`ci.yml`, `node-version: "22"`) und damit npm 10. Ein `package-lock.json`, das npm 11 erzeugt, kann npm 10 zurückweisen — der Lauf scheitert dann in der CI und nirgends vorher. Deshalb entstehen Änderungen an der Sperrdatei mit `npx -y npm@10`, nicht mit dem lokalen npm. Die Zahlen hier veralten mit dem nächsten Node-Update; wer sie liest, misst nach, statt sie zu glauben. ## Vor dem ersten Slice **`docs/agents/lehren.md` lesen.** Fehlerklassen, die real getroffen haben — Wächter, die grün sind ohne etwas zu beweisen; Seitenkanäle; Datenmigrationen, deren Fehler kein Update mehr heilt; „weniger Abfragen", das langsamer ist — plus ein eigener Abschnitt mit Funden aus diesem Projekt. Kostet fünf Minuten und hat schon mehrfach einen Prod-Fund verhindert. Für die meisten Schritte gibt es fertige Methoden-Anleitungen — welche, woher und wann welche passt: `docs/agents/skills.md`. ## Arbeitsweise **Issues sind der Arbeitsspeicher.** Jeder Fund, jede Entscheidung, jeder zurückgestellte Punkt wird ein Issue — nicht eine Notiz im Chat. Siehe `docs/agents/issue-tracker.md` und `docs/agents/triage-labels.md`. **Ein Slice nach dem anderen.** Parallel nur, was sich nachweislich nicht überschneidet (verschiedene Dateien, verschiedene Schichten). Sonst arbeiten zwei Bauläufe im selben Arbeitsbaum und niemand weiß mehr, wem welche Änderung gehört. **Der Hauptagent baut nicht selbst, er arbitriert.** Bauen macht ein Subagent mit einem Bau-Brief (`docs/agents/bau-brief.md`), prüfen macht das Panel (`docs/agents/panel.md`), entscheiden macht der Hauptagent — **und reproduziert jeden Blocker am Code**, statt der Konvergenz der Prüfer zu glauben. **Technisches selbst entscheiden, Fachliches fragen.** Bibliothekswahl, Schnittstellen-Zuschnitt, Testform: selbst. Ob eine Zahlung auf eine abgeschlossene Rechnung möglich sein soll: fragen. Im Zweifel: eine Annahme formulieren, weiterbauen, die Annahme sichtbar machen. **Den Herkunftsstempel setzt der Hauptagent, nie der Bauer.** Ein Bau-Subagent kennt seine eigene Modell-Kennung nicht und erfindet sie — gemessen in vier von vier Erstbauten, auch nach ausdrücklicher Auflage. Bau-Briefe geben deshalb den Platzhalter `bau=` vor; ersetzt wird er beim Landen. Form und Begründung: `docs/agents/bau-brief.md`, „Modell-Stempel". **Zeit wird in Bilanzen als DREI Zahlen geführt**, nicht als eine — damit sie über Projekte hinweg summierbar bleibt (Owner-Entscheid 06.09.2026). Jeder Block der Zeitachse trägt genau einen Zustand: | Zustand | Bedeutung | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ACTIVE` | Es wird gearbeitet. **Verworfene Arbeit zählt mit** — Messfehler und Wiederholung sind Reibung des Verfahrens, nicht Ausschuss. | | `PAUSED_EXTERNAL_CAPACITY` | Wartezeit auf eine fremde Kapazitätsgrenze: Sitzungslimit, Guthaben, Dienstausfall. Mit Ursache und Dauer protokollieren; trifft ausdrücklich auch den Orchestrator. | | `MIXED` | Nicht aufgeteilt. Wird **ausgewiesen, nicht geschätzt** — erzeugt eine Untergrenze („≥ …"). | Wartezeit auf ein fremdes Kontingent wird **gezählt, nicht in die Zeit summiert**: Die Vergleichsgröße zwischen Projekten ist die aktive Umsetzungszeit, nicht die Wandzeit. Für dieses Projekt ist das keine Theorie — zwei Subagenten sind an Sitzungslimits gestorben, und der Orchestrator selbst war zuletzt mitten in einem Slice ausgesperrt. **Bilanzen zählen außerdem, welche Funde den ORCHESTRATOR trafen** und nicht den Bauer — die Zahl ist die einzige, die zeigt, ob die Prüfung dort ankommt, wo sie bisher nicht hinsah (`docs/agents/bau-brief.md`, „Den Orchestrator prüft niemand"). ## Risikoklasse je Slice — hier wird entschieden, was geprüft wird Jeder Slice bekommt im Bau-Brief eine Risikoklasse mit Auslöser (Block 0: `Risiko: R — Auslöser: …`). **Die Klasse wird aus Auslösern abgeleitet, nicht frei vergeben** — kleine Diff-Größe stuft einen R3-Auslöser nie herunter (sechs Zeilen an einer Berechtigungsgrenze sind R3). Im Zweifel gilt die höhere Klasse; **die Abstufung nach unten braucht die Begründung, nicht die nach oben.** | Klasse | Auslöser (abschließend) | Mindestprüfung | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **R0** | reine Testinfrastruktur ohne Verhaltensänderung (ein neuer Test ist das NICHT); Doku-Korrektur ohne ausgelieferten Inhalt; Typisierung ohne Verhaltensänderung | lokale Gates | | _R1_ | **keine frei vergebbare Klasse** — seit v1.15.0 in der Zeile „Nacharbeit" aufgegangen, die für JEDE Nacharbeit gilt, nicht nur für mechanische Auflagen | siehe „Nacharbeit" | | **R2** | alles ohne R0-, R3- oder R4-Auslöser (der Normalfall) | Blindprüfer + Fremdprüfer | | **R3** | Datenmigration — unabhängig davon, ob ein Migrations-Framework beteiligt ist: Gemeint ist der VORGANG, bestehende Daten werden unumkehrbar umgewandelt (auch eine selbstheilende JSON-Schema-Wanderung ist eine); Berechtigungs-/Datenschutzlogik; Geld/Steuern; Außenwirkung über eine Schnittstelle; **Fremdcode** — Produktionscode, den ein FREMDES System beigesteuert hat (Patch eines Anbietermodells, zugelieferter Zweig, übernommener Schnipsel) — **und fremde Werkzeug-Mechanik, die in den eigenen Prozess übernommen wird** (Hooks, Agenten-Definitionen, Skills eines anderen Frameworks: je Zusicherung wie „nur lesend" oder „schlägt geschlossen fehl" eine negative Probe); **kritische Dateien** — Wächter, Gate-Probe, Selbstprobe, Hook-Einträge, Gate-, Release- und Prüfskripte (Liste in `docs/agents/panel.md`, „Kritische Dateien"), unabhängig von der Diffgröße — **zwei Ausnahmen, beide nur für das Panel, die Probe durch die echte Tür bleibt in beiden Fällen:** (a) byte-identische Übernahme aus der Vorlage beim Abgleich (`abgleich.md` **im Vorlagen-Repo**, siehe Kopf dieser Datei — hier liegt sie nicht; Absatz „Panel für den Abgleich-Commit"); (b) **reine Kopftext-Änderung** — nur Kommentare oder Docstrings, Syntaxbaum nachweislich gleich; dann R2. **Bedingungen, Verfahren und Beleg stehen in `docs/agents/panel.md`, „Kritische Dateien"** und werden hier nicht wiederholt (Owner-Entscheid 2026-09-21). **NICHT gemeint: der eigene Bau-Subagent** — sonst ist der Auslöser immer erfüllt und R2 verschwindet | volles Panel mit Gegenprüfer + risikospezifische Probe durch die echte Tür | | _Nacharbeit_ | jede Nacharbeitsrunde nach einem Panel, gleich welcher Klasse; **höchstens zwei** (gezählt: Erstrunde, Nacharbeit 1, Nacharbeit 2) | Blindprüfer; Gegenprüfer nur bei neuer Tür; bei R3 und R4 zusätzlich die risikospezifische Probe durch den Arbiter. **Nach Nacharbeit 2 wird nicht weiter gebaut:** offene oder neue BLOCKER → anhalten, Owner fragen; nur noch Nicht-Blocker → landen, die Funde werden ein eigenes Issue mit Beleg | | **R4** | irreversible Daten-/Prod-Wirkung; fachlich nicht rückholbare Entscheidung | R3 + ausdrückliche Owner-Freigabe. **Ein projekteigenes Release-Gate mit breiteren Auslösern bleibt davon unberührt** — diese Spalte sagt, ab wann die VORLAGE eine Freigabe verlangt, nicht, ab wann euer Projekt sie verlangen darf | „Klein" und „gut rückrollbar" sind Urteile, keine Auslöser — und Urteile sind der Punkt, an dem sich der Ausführende unter Druck freispricht. **Hinweis für den Abgleich (Owner-vorlagepflichtig):** Die Tabelle besitzt die PRÜFTIEFE (wer prüft, wie tief). Das RELEASE-GATE — wer vor einem Release freigibt — ist eine eigene Größe mit eigener Zeile im **Schwellen-Inventar** unten: Seine **Untergrenze hat der Owner für alle Projekte gesetzt** (Migrationen, Geld/Steuer — beides R3-Auslöser, also unterhalb von R4), und ein Projekt darf weitere Auslöser haben. Die Tabelle ersetzt so ein Gate NICHT. Wer eigene Auslöser über der Untergrenze aufgeben will, tut das als ausdrückliche, veto-fähige Owner-Entscheidung — nie als Nebenwirkung der Übernahme; **unter die Untergrenze führt kein Entscheid auf Projektebene.** Real passiert: Eine frühere Fassung dieser Zeile erklärte sich zum „Namen für" das bestehende Riskant-Gate und verengte damit in einem Projekt sechs Freigabe-Auslöser stillschweigend auf einen; bemerkt hat es der Abgleich, nicht die Übernahme. **Projektspezifisch — Anwendung des Auslösers „Datenmigration":** Dieses Projekt hat kein Alembic, aber `backend/services/config_store.py` führt beim Start eine selbstheilende JSON-Schema-Migration aus (`SCHEMA_VERSION`, `_migrate()`), die ungeprüft in die einzige persistente Datei des Projekts (`accounts.json`) schreibt — und fällt damit unter den Auslöser „Datenmigration" der Tabelle oben (R3). Einmal hier festgelegt, damit es nicht erneut diskutiert werden muss. Das Verfahren je Klasse beschreibt `docs/agents/panel.md`; diese Tabelle ist der einzige Eigentümer der PRÜFTIEFEN-Auslöser. (Über Release-Freigaben entscheidet sie nur **bei R4** — projekteigene, breitere Gates siehe Hinweis oben.) ## Schwellen-Inventar **Jede Schwelle hat hier eine Zeile, und ihr WERT steht genau einmal:** entweder in dieser Tabelle, oder bei dem Eigentümer, den die Zeile nennt — dann steht hier der Zeiger und nicht die Zahl. Die Spalte „entschieden von" sagt, wer den Wert ändern darf: **Portfolio** heißt, der Owner hat ihn für alle Projekte entschieden; dieses Repo übernimmt ihn und wählt ihn nicht. **Projekt** heißt, der Wert gehört hierher. **Portfolio-Untergrenze** heißt: Der Owner hat eine Mindestschwelle für alle Projekte gesetzt, und dieses Projekt darf strenger sein, nie lockerer. Wer eine Schwelle sucht und sie hier nicht findet, hat eine gefunden, die keinen Eigentümer hat — das ist ein Fund, kein Versehen. Diese Tabelle kam mit dem Abgleich auf Vorlage v1.19.1 (03.11.2026). Vorher gab es sie hier **nicht**: Die Schwellen standen über fünf Abschnitte verteilt, und ob eine Portfolio-Entscheidung hier schon angekommen war, konnte man nur durch Lesen aller fünf herausfinden. **Eine** Zeile unten trägt einen **gemessenen** statt eines gewählten Wertes und ist als solche markiert (Parallelität innerhalb eines Laufs). Das Zeitlimit je Prüfbefehl ist ausdrücklich **gewählt**; alle übrigen Zeilen sind Portfolio-Entscheidungen oder Zeiger. Hier stand zuerst „zwei Zeilen" — nachgezählt vom Fremdprüfer des Abgleich-Panels. | Schwelle | Wert | entschieden von | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Nacharbeitsrunden nach einem Panel | Wert in der Risiko-Tabelle oben, Zeile „Nacharbeit" — hier absichtlich nur der Zeiger, nicht die Zahl (Ein-Eigentümer-Regel; der Fremdprüfer des Abgleich-Panels fand die Zahl hier zuerst doppelt) | **Portfolio** (2026-09-17) | | CI-Läufe je Slice | Wert im Abschnitt „Prüfschritte" unten: **einer**, auf dem gelandeten Endstand; Bau-, Panel- und Nacharbeits-Pushes tragen den Überspring-Marker, lokale Gates sind die Verifikation | **Portfolio** (2026-09-02) | | Kostenschwelle je Panel-Lauf (Ersatzstimme) | **keine** — pauschal freigegeben; das Verfahren bei Guthabenende (HTTP 402) steht im Abschnitt „Review-Panel" unten | **Portfolio** (2026-09-19) | | Panel-Untergrenze beim Verkürzen | Wert im Abschnitt „Review-Panel" unten, Absatz „Verkürzen" | **Portfolio** | | Prüftiefe je Risikoklasse | Tabelle oben, Auslöser abschließend | **Portfolio**; Risiko-Tabelle | | Release-Gate (ab wann eine Freigabe nötig ist) | **Untergrenze:** Migrationen und Geld/Steuer. Geld/Steuer hat hier keinen Gegenstand; „Datenmigration" gilt in diesem Projekt framework-frei und trifft die JSON-Schema-Wanderung in `backend/services/config_store.py`. **Darüber projekteigen:** die Schwelle „gefahrlos", Wert im Abschnitt „Release" unten — sie ist breiter als die Untergrenze und bleibt es | **Portfolio-Untergrenze** (2026-09-02), darüber **Projekt** | | Zeitlimit einzelner Befehle im Prüfauftrag | **15 Minuten.** Was länger braucht, wird zerlegt, nicht verlängert. Gewählt nach dem längsten CI-Job (`timeout-minutes: 15` in `.github/workflows/ci.yml`, gemessen 03.11.2026), damit ein Befehl, der örtlich gerade noch durchläuft, nicht erst in der CI abbricht | **Projekt** (2026-11-03) | | CI-Nebenläufigkeit (`concurrency`, `cancel-in-progress`) | **an** — ein neuer Lauf bricht den alten ab; **Tag-Läufe ausgenommen**. Wert in `.github/workflows/ci.yml`, Block `concurrency:`. Die zwei weiteren Vorlagen-Ausnahmen haben hier keinen Gegenstand (gemessen 03.11.2026): `ci.yml` liefert nicht aus (der Abbild-Job läuft mit `push: false`), und kein Workflow meldet seinen eigenen Abbruch | **Portfolio** (2026-09-02, Notbremse dauerhaft) | | Turnus des Abhängigkeits-Bots | **monatlich, höchstens ein offener Vorschlag** je Ökosystem; Wert in `.github/dependabot.yml`, alle vier Ökosysteme. Sicherheits-Updates bleiben unberührt — die hängen an den Repo-Einstellungen, nicht an dieser Datei | **Portfolio** (2026-09-02) | | Schwere Läufe auf dem Rechner — alles, was eine volle Testsuite, einen Browser oder einen Container startet (Gate-Skript, Container-Bau, Mutationslauf über die ganze Suite); im Zweifel schwer | **höchstens einer gleichzeitig auf dem ganzen Rechner**, über alle Sitzungen, Projekte und Rollen (Bauer, Prüfer, Arbiter); leichte Läufe (eine Testdatei, Lint) sind frei. Eingehalten über **eine** Sperre mit festem Pfad für alle Projekte, `$HOME/.sperre-schwerer-lauf` — Kommandos und Regeln stehen in `docs/agents/bau-brief.md`, Randbedingung „Parallelität", nicht hier. Gate-Skripte nehmen die Sperre **nicht** selbst; wer das Gate startet, nimmt sie im selben Aufruf. Eine Zahl je Sitzung begrenzte die Summe nicht | **Portfolio** (2026-10-18) | | Parallelität innerhalb eines Laufs (Worker je Testlauf, Browser) | **pytest: ein Prozess** (kein `pytest-xdist` im Baum, kein `addopts`). **vitest: kein Deckel gesetzt** — also die Voreinstellung der Bibliothek, auf einem Rechner mit **24 Kernen**. **Kein Browser-Gate.** Alle vier Angaben gemessen am 03.11.2026. Das ist ein **gemessener, kein gewählter** Wert: offener Punkt **Issue #150**. **Was ein Bau-Brief hier einträgt, solange das offen ist:** „pytest 1 Prozess; vitest ohne Deckel (Voreinstellung), Issue #150" — die Randbedingung „Parallelität" des Briefs verlangt eine Zahl, und ein Brief darf sie nicht erfinden. Gefunden hat diese Lücke der Fremdprüfer des Abgleich-Panels | **Projekt** | **Panel-INTERNE Schwellen besitzt `docs/agents/panel.md`**, nicht diese Tabelle: wie oft eine ausgefallene Stimme wiederholt wird und wie groß das Antwortbudget angesetzt wird. Das ist die ausdrückliche Ausnahme von „Schwellen hier" — sie bedeuten nur innerhalb des Panel-Verfahrens etwas. Deshalb steht hier auch keine ihrer Zahlen. ## Review-Panel **Kostenschwelle für die Fremdprüfer-Ersatzregel: keine** — Owner-Entscheid 19.09.2026, pauschal freigegeben; die Größenordnung liegt bei Cent je Lauf. Bei **Guthabenende (HTTP 402)** wird nicht ausgeliefert und nicht still verkürzt, sondern der Owner gefragt. Beim Erreichen der Schwelle gilt dasselbe wie bei erschöpftem Guthaben — Regel und Verfahren stehen nur in `docs/agents/panel.md`, Klumpenrisiko Punkt 4; die Form des geordneten Abbruchs steht dort unter „Ein Kostenstopp steht fest, BEVOR die Serie beginnt". Bis zu drei Stimmen mit festen Namen — wie viele ein Slice bekommt, sagt die Risiko-Tabelle oben; Details und Kommandos in `docs/agents/panel.md`: 1. **Blindprüfer** — frischer Reviewer-Subagent, bekommt **nur Diff und Repo**, nicht den Bau-Brief. Liefert erfahrungsgemäß die schwersten Funde, weil er den Code liest statt der Absicht. 2. **Fremdprüfer** — Modell eines anderen Anbieters über denselben Diff. 3. **Gegenprüfer** — zweite frische Claude-Stimme mit Widerlegungsauftrag; Pflicht bei R3, optional bei R2 (bei R2 alternativ eine günstige diff-only-Fremdstimme). Bis v1.14 hieß er „Zweitblind-Stimme", im Pilot, aus dem er stammt, „Arm C". **Nacharbeit** hat eine eigene Zeile in der Risiko-Tabelle oben; dort steht die Schwelle, in `panel.md`, Schritt 4, stehen Verfahren und Beleg. Der Panel-Kommentar hat eine **feste Form**: eine Überschrift je Stimme, auch bei „keine Funde". Fällt eine Stimme aus, steht unter ihrer Überschrift der **Grund** und der Schritt, nach dem gehandelt wurde. **Die Schritte selbst stehen nur in `docs/agents/panel.md`, „Wenn eine Stimme ausfällt"** — vier Schritte, mit den Sonderfällen Guthabenende und Token-Budget; diese Datei wiederholt sie nicht. Ein Slice ohne vollständiges oder vermerkt-verkürztes Panel gilt als **nicht geprüft**. **Verkürzen** (eine Stimme bewusst weglassen) nur mit Grund unter der Überschrift der weggelassenen Stimme — und nie unter die **Untergrenze**: bei R2 der Blindprüfer; bei R3 und R4 zusätzlich nie ohne Gegenprüfer und Tür-Probe. Die Nacharbeit folgt ihrer eigenen Zeile in der Tabelle. **Ein AUSFALL einer Pflichtstimme ist keine vermerkte Verkürzung:** Verkürzt heißt „bewusst und begründet weggelassen"; eine ausgefallene Pflichtstimme wird vor der Auslieferung nachgeholt, oder der Owner gibt die Auslieferung ohne sie ausdrücklich frei — beides steht im Panel-Kommentar. **Dasselbe gilt für eine teilweise ausgefallene Tür-Probe** bei R3 und R4 (unter der Überschrift „Tür-Probe (R3/R4)" in `docs/agents/panel-kommentar.md` vermerkt): Sie gehört zur Untergrenze, also nachholen oder ausdrückliche Owner-Freigabe. **Modellnamen und Aufrufkommandos der Stimmen für dieses Projekt** stehen in `docs/agents/panel.md`, Abschnitt „Eingetragen: unsere Aufrufe" — hier nicht wiederholt, um Doppelpflege zu vermeiden. ## Bau-Brief Jeder Bau-Auftrag folgt dem Pflicht-Gerüst aus `docs/agents/bau-brief.md` — Block 0 (`Risiko: R — Auslöser: …`) plus neun Themen-Blöcke, keiner leer. **Vor jedem Absenden verbindlich:** `LC_ALL=C.UTF-8 sh scripts/bau-brief-pruefen.sh ` — **das Skript setzt die Locale nicht selbst, und ohne sie meldet es Vorhandenes als fehlend.** Der Grund hat sich mit v1.14.1 geändert, die Anweisung nicht. Die Muster tragen heute keine literalen Umlaute mehr, und seit dem Abgleich benutzen alle sieben Ersatzmuster `..?` statt eines einzelnen Punkts — ein Punkt trifft **ein Byte**, ein Umlaut besteht aus zweien. **Das ist eine bewusste Abweichung von der Vorlagenfassung**, offengelegt im Kopf des Skripts und als Vorlagen-Issue #73 gemeldet. Was weiterhin an der Locale hängt, ist die **Fallfaltung**: `## 9 PRÜFFRAGEN` in Versalien wird ohne sie nicht gefunden — und „kein Treffer" ist genau das Urteil, das dieses Skript belastbar nennt. Details, Begründungen und projektspezifische Prüf-Kommandos stehen in `docs/agents/bau-brief.md`, nicht hier. ## Release **Schwelle „gefahrlos"** (Bedeutung siehe `docs/agents/release-ritual.md`): Für Releases dieser Stufe gilt bei grüner CI eine stehende Owner-Freigabe zum Taggen; jede Migration bleibt Owner-Sache und wird vor dem Tag gefragt, im Zweifel die vorsichtigere Stufe. Diese Schwelle ist das Release-Gate — und sie ist **breiter als R4**: Die Risikoklassen-Tabelle oben hat sie nie ersetzt und ersetzt sie nicht. Dass diese Schwelle hier überlebt hat, obwohl eine ältere Fassung der R4-Zeile sich zum „Namen dafür" erklärte, liegt an der Ein-Eigentümer-Regel: Eine Schwelle hat bei uns genau einen Eigentümer, und der Eigentümer dieser Schwelle ist dieser Abschnitt, nicht die Tabelle. Diese Regel hat hier einen realen Schaden verhindert, den ein anderes Projekt erlitten hat (siehe „Hinweis für den Abgleich" bei der Risikoklassen-Tabelle oben). **Folgerung aus unserer eigenen v1.11.3-Festlegung (dem Owner vorgelegt, veto-fähig):** Weil der Auslöser „Datenmigration" bei uns framework-frei gilt, zählt eine Änderung an der selbstheilenden JSON-Schema-Migration (`backend/services/config_store.py`, `SCHEMA_VERSION`, `_migrate()`) als Migration auch für diese Release-Schwelle — ein Release mit einer solchen Änderung ist nicht „gefahrlos" und wird vor dem Tag gefragt. Dieser Punkt folgt zwar aus einer bereits getroffenen Festlegung, ist aber selbst eine Aussage über das Release-Gate und damit Owner-Gebiet. Ablauf, welche Dateien die Version führen und alle Details stehen in `docs/agents/release-ritual.md` — keine weitere Kurzfassung hier. ## Prüfschritte **Ein CI-Lauf je Slice, nicht je Push.** Bau, Panel und Nacharbeit landen mit `[skip ci]`; nur der **gelandete Endstand** eines Slices läuft durch die CI. Reine Doku-Pushes filtert `paths-ignore` (`.github/workflows/ci.yml`) von selbst weg. Lokale Gates (der Husky-Hook) sind die **Verifikation** und bleiben Pflicht; die CI ist die **Gegenprobe**, nicht der Erstlauf. Warum: Das Portfolio-Tagesbudget für GitHub Actions liegt bei 100 Minuten über alle Repos des Owners (Auslöser: 2701/3000 Portfolio-Minuten am 14.08.2026 verbraucht). Dieses Repo selbst ist PUBLIC, und ob ein Lauf hier abgerechnete Minuten kostet, ist **ungemessen**: Die Abrechnungs-Schnittstelle meldet `billable = 0` — aber sie meldet das auch für PRIVATE Repos (gemessen am 28.09.2026 an zwei privaten Läufen), taugt also nicht als Beleg. Hier stand bis zum Abgleich auf v1.19.1 „ein Lauf hier kostet 0 abgerechnete Minuten" mit genau dieser Schnittstelle als Beleg; die Vorlage hat die Aussage in v1.19.0 zurückgenommen. **Die Regel hing nie daran** und gilt unverändert: als Portfolio-Disziplin (dieselbe Arbeitsweise in allen Repos) und weil sie sofort real wird, sollte dieses Repo je auf privat umgeschaltet werden. Was davon dauerhaft bleibt, weil es Qualität nicht kostet: ein pytest-Job statt zwei, kein `pull_request`-Trigger im Trunk-Workflow, `concurrency` mit Abbruch (Tag-Läufe ausgenommen), `paths-ignore`. Diese Regel steht **hier** und nur hier — `release-ritual.md`, `docs/agents/lehren.md` und `ci.yml` verweisen bei Bedarf, sie wiederholen sie nicht. Ohne `pull_request`-Trigger laufen Dependabot-PRs ohne Vor-Merge-CI; der Lauf auf `main` nach dem Merge ist ihr Gate — wird er rot, den Merge revertieren, nicht reparieren. Ein Prüfschritt, den nur die CI kennt, wird lokal nie gefahren und meldet sich zum ungünstigsten Zeitpunkt — genau so ist `npm audit` in diesem Repo einmal rot geworden, mitten in einem Release, ohne dass sich eine Zeile geändert hatte. Deshalb hier **alle** Kommandos, die die CI fährt: **Push-Pfad** (`.github/workflows/ci.yml`, bei Push auf `main` und `release/`-Zweigen und bei Tags, außer bei reinem Doku-Push — Pfadfilter greifen bei Tag-Pushes nicht): - Backend: `pytest -q backend/tests` - Backend: `python -I -S -m compileall -q backend` - Backend: `sh scripts/release-selbstprobe.sh` — die Selbstprobe des Release-Gates. Sie hängt am Push-Pfad und nicht am Release, weil ein Wächter, dessen Lauf niemand erzwingt, seine Abdeckungszahl zur Beruhigung macht (`docs/agents/lehren.md` §18), und weil ein kaputtes Gate am Release-Tag zu spät auffällt. Braucht kein Netz und kein `gh`. - Backend: `sh scripts/commit-msg-selbstprobe.sh` — die Selbstprobe des `commit-msg`-Hooks, 19 Fälle. Aus demselben Grund hier: Der Hook läuft nur lokal und nur nach einem `npm install` in der Wurzel; ohne diesen Lauf wäre seine Abdeckung reine Behauptung. Sie fährt echte Commits in Wegwerf-Repos und prüft, was git **speichert**, nicht nur, was der Hook sieht. - Backend: `sh scripts/zeilenenden.sh` und `sh scripts/zeilenenden-selbstprobe.sh` — der Wächter für #104: jede versionierte Datei hat `eol=lf` (`git check-attr`, einmal mit und einmal ohne `core.ignorecase`, weil Windows die Muster ohne Rücksicht auf Groß-/Kleinschreibung liest), kein Blob trägt CRLF oder gemischte Zeilenenden (`git ls-files --eol`). Er fragt Attribute und Index ab, nicht den Arbeitsbaum, weil der Linux-Läufer ohnehin LF auscheckt und einen Rückbau sonst nie sähe. Die `check-attr`-Liste trägt schon seit #118 eine eigene Zeilenzahl-Probe gegen `git ls-files`; seit #118 Nacharbeit 1 gilt dieselbe Probe auch für die INDEX-Liste (`git ls-files --eol`, vorher ungeprüft) — eine gekürzte ODER verlängerte Zwischen-Pipe bleibt auf keiner der beiden Seiten mehr unbemerkt. Beide laufen unter `core.quotePath=true` (erzwungen, unabhängig vom lokalen Wert), damit ein Pfad mit Sonderzeichen auf Attribut- und Index-Seite gleich geschrieben ankommt. Absichtlich CRLF tragende Dateien stehen zeilenweise mit Pfad UND erlaubtem Wert (z. B. `crlf`) in `scripts/zeilenenden-ausnahmen.txt` (heute keine) — exakter Pfad- UND Wertvergleich, kein Glob, keine Groß-/Kleinschreibungs-Toleranz; ein eingetragener Pfad, der nicht mehr versioniert ist, ein doppelter Eintrag, ein drittes Feld, eine Zeile ohne TAB, ein leerer Pfad oder ein CR mitten in der Zeile sind selbst ein Befund mit eigener Meldung — ebenso eine Ausnahme, die nie greift (die Datei ist längst sauber): Die Erfolgsmeldung zählt seit #118 Nacharbeit 2 nur tatsächlich ANGEWANDTE Ausnahmen, nicht bloße Einträge. Die Selbstprobe baut die gemessenen Rückbau-Varianten nach (Regel gelöscht, `*.md -eol`, `*.md !eol`, `*.md eol`, `*.md eol=LF`, `*.MD eol=crlf`, verschachtelte `.gitattributes` mit `eol=crlf`, CRLF- und gemischter Blob, Aufruf aus einem Unterverzeichnis) und die Mutanten des Wächters, die das Panel an früheren Fassungen vorbeibrachte (u. a. die Ausnahmeliste als Glob, eine gekürzte oder verlängerte Prüfliste vor `check-attr` oder `ls-files --eol`, ein Vergleich ohne Anker am Zeilenende, ein Pfad- oder Wertvergleich ohne Groß-/Kleinschreibungs-Schärfe). Bekannte Grenzen stehen im Kopf des Wächters. Ein Push, der nur `.md`-Dateien ändert, löst wegen `paths-ignore` keinen Lauf aus — ein CRLF-Blob in einer `.md` fällt dann erst beim nächsten Code-Push auf. - Backend: `python -I -S docs/vorlagen/lehren-verweise-pruefen.py --selbstprobe` und `python -I -S docs/vorlagen/lehren-verweise-pruefen.py --lehren docs/agents/lehren.md docs/agents/*.md CLAUDE.md` — der Verweis-Wächter für die Lehren-Nummern, seit dem Abgleich auf Vorlage v1.19.1. Er fängt die Verweise, die beim Übernehmen einer Lehre in der Vorlagen-Zählung stehen bleiben und dann eine ANDERE, existierende Lehre treffen. **Seine benannte Grenze:** Er prüft nur, DASS ein `§N` einen Abschnitt trifft, nicht ob es den gemeinten. Genau diese Lücke hat im Abgleich-Panel einen Verweis durchgelassen, den derselbe Abgleich erzeugt hatte. - Backend: `python -I -S scripts/faltung-sonde-selbsttest.py` — die Selbstprobe der Faltungs-Sonde (#83). Hier aus demselben Grund wie die beiden darüber, nur schärfer: Die Sonde trägt eine Owner-Entscheidung über eine **unumkehrbare** Datenwanderung, und ihre erste Fassung meldete „folgenlos" für einen Bestand, in dem sich sehr wohl etwas ändert. Gefunden haben das zwei Prüfstimmen, nicht ihre eigenen grünen Proben. - Frontend (in `frontend/`): `npm ci` - **Wurzel: `npm ci --ignore-scripts` und `npx --no-install prettier --check .`** — diese Zeile fehlte hier bis zum 27.09.2026, und genau deshalb ist der Release-Lauf von 1.8.0 rot geworden: zwei Dateien mit Formatierungsfehlern, die kein lokales Tor gesehen hat. Der Husky-Hook prüft nur die **gestageten** Dateien; wer mit `--no-verify` committet oder eine Datei nach dem Formatieren noch anfasst, kommt daran vorbei. **Seit #104 ist der lokale Lauf auch hier verwertbar:** `.gitattributes` setzt `* text=auto eol=lf`, der Arbeitsbaum trägt also LF wie der CI-Läufer, auch bei `core.autocrlf=true`. Gemessen in einem frischen Klon mit `autocrlf=true`: alle Textdateien `w/lf`, `npx prettier --check .` ohne Meldung. **Ein Checkout von vor #104 bleibt CRLF**, bis die Dateien neu ausgecheckt werden. `git status` zeigt das nicht an (gemessen, auch nach `touch`); zu sehen ist es an `git ls-files --eol` (`w/crlf`) und an den Warnungen „CRLF will be replaced by LF", wenn git den Inhalt einer Datei neu liest (`git diff`, `git add`). Wer `core.safecrlf=true` gesetzt hat, kann in einem solchen Checkout CRLF-Dateien nicht mehr hinzufügen (`git add`, `commit -a`: `fatal: CRLF would be replaced by LF`, gemessen) — dann zuerst auffrischen. Auffrischen in Git Bash (PowerShell 5.1 kennt kein `&&`), mit Riegel gegen ungesicherte Änderungen (bricht mit Exit 1 ab, wenn etwas gestaged oder geändert ist; `:/` wirkt vom ganzen Repo aus, egal in welchem Verzeichnis man steht): `git diff --quiet HEAD -- && git rm -r -q --cached :/ && git reset -q --hard`. Unversionierte und ignorierte Dateien bleiben erhalten. Was der Riegel nicht sieht: Änderungen an Dateien mit `assume-unchanged` gehen verloren (wie bei jedem `reset --hard`), und ein angefangener Merge ohne Inhaltsänderung wird verworfen (gemessen vom Gegenprüfer). Vorher war der lokale Lauf wertlos: Beim Release 1.8.0 meldete er rund zwei Dutzend Dateien, die in der CI sauber waren, und zwei echte Funde gingen darin unter (`docs/agents/lehren.md` §45). - Frontend (in `frontend/`): `npm test` — seit #72 unter einem **echten DOM** (`happy-dom`, gewählt gegen `jsdom`: gemessen +9 statt +37 Pakete im Baum — wobei die Paketzahl die Lieferkette misst, nicht die DOM-Treue; die zweite Seite der Abwägung ist ungemessen). Damit prüfen die Tests nicht mehr nur, was eine Funktion zurückgibt, sondern was passiert, wenn jemand klickt. `src/test-setup.ts` räumt nach jedem Test auf — **nur mit `cleanup()`, weil nur das rot-beweisbar ist**: Entfernt man es, fallen zwei Testdateien. Zwei weitere Zeilen standen dort eine Fassung lang (Speicher leeren, `` zurücksetzen) und überlebten den Mutationslauf, weil jeder Test seinen Ausgangszustand selbst setzt; sie sind entfernt. Bewusst **ohne** `globals: true` — der Schalter aktiviert die automatische Aufräumroutine der Testbibliothek, und solange die läuft, ist die eigene wirkungslos und damit unbeweisbar. - Frontend (in `frontend/`): `npm run build` (enthält `tsc`) - Container: `docker build` des Gesamt-Images (`push: false`, reiner Bau-Test) - Secrets: `gitleaks` gegen die Commits des Pushes entlang der ersten Eltern-Linie (`--first-parent --no-merges`: Merge-Commits und Commits eines eingemergten Zweigs werden nicht gescannt — relevant bei Dependabot-Merges) (bei `workflow_dispatch`: voller Verlauf) Zusätzlich lokal vor jedem Commit (Husky-Pre-Commit-Hook, nicht Teil der CI, aber dieselbe Klasse von „muss laufen, sonst meldet es sich zu spät"): **Dieser Hook ist ein schwerer Lauf, und `git commit` ist der Aufruf, der ihn startet.** Er fährt die volle Frontend- und Backend-Suite. Die Zeile „Schwere Läufe auf dem Rechner" im Schwellen-Inventar verlangt dafür die rechnerweite Sperre, und sie verlangt zugleich, dass das Gate sie **nicht selbst** nimmt — also nimmt sie, **wer committet**, im selben Aufruf: ```bash S="$HOME/.sperre-schwerer-lauf"; until mkdir "$S" 2>/dev/null; do sleep 30; done echo "immich-family-tools $(date +%FT%T)" > "$S/wer" git add -A && git commit -F rc=$?; rm -f "$HOME/.sperre-schwerer-lauf/wer"; rmdir "$HOME/.sperre-schwerer-lauf"; exit $rc ``` Dass diese Regel in diesem Repo keinen benannten Angriffspunkt hatte, fand der Blindprüfer des Panels zu v1.19.1: Die Sperre war beschrieben, der einzige schwere Lauf des Repos war beschrieben, und niemand hatte beides verbunden. - `npx lint-staged` (prettier) - Frontend (in `frontend/`): `npm run typecheck` (= `tsc --noEmit`) - Frontend (in `frontend/`): `npm run test` (= `vitest run`, kein Watch-Modus) - Backend: `pytest backend/tests/ --basetemp=/tmp/pytest-immich -q` - Backend: `python -I -S docs/vorlagen/lehren-verweise-pruefen.py --selbstprobe` und daneben der Lauf selbst (`--lehren docs/agents/lehren.md docs/agents/*.md CLAUDE.md`). Hier und nicht nur in der CI, weil `paths-ignore` reine Doku-Pushes wegfiltert und die CI sie nie sieht — genau die Commits, in denen ein Verweis stehen bleibt. Beide Zeilen sind einzeln fahrbar. Dazu der **Commit-Nachrichten-Hook** (`.husky/commit-msg`): Er lehnt die CI-Überspring-Kennung ab Zeile 2 ab — in der Klammer- **und** in der Trailer-Form (`skip-checks:`), nachdem GitHub beide auswertet. Begründung und Vorfall: `docs/agents/lehren.md` §24; Abdeckung und benannte Restlücke: `scripts/commit-msg-selbstprobe.sh`. **Beide Hooks sind nach einem frischen Klon NICHT scharf** — und das ist der Fall, den man übersieht, weil im eingerichteten Arbeitsbaum alles läuft. `core.hooksPath` zeigt auf `.husky/_`, und dieses Verzeichnis ist **nicht versioniert**; es entsteht erst durch ein `npm install` **in der Wurzel** (`"prepare": "husky"` in der Wurzel-`package.json`). Die `npm ci` in der Liste oben laufen in `frontend/` und richten nichts ein. Also: ```bash npm install # in der Wurzel, einmal je Klon — richtet .husky/_ ein git config core.hooksPath # muss .husky/_ ausgeben, sonst laeuft kein Hook ``` Und die verbleibenden Löcher offen benannt, damit niemand den Hook für mehr hält, als er ist: `--no-verify` umgeht ihn, ein Merge über die GitHub-Oberfläche erreicht ihn nie, und es gibt keine serverseitige Entsprechung. Er ist eine Verifikation auf diesem Rechner, kein Tor am Repo. **Wochen-Prüfung (`.github/workflows/wochen-pruefung.yml`, montags 04:00 + `workflow_dispatch`, bewusst **nicht** auf dem Push-Pfad):** - Backend: `pip-audit -r backend/requirements.txt` - Frontend (in `frontend/`): `npm audit --audit-level=high` - Secrets: `gitleaks` gegen den vollen Verlauf, im Wochenrhythmus — schließt die Lücke, die der Push-Scan systembedingt lässt: Pushes ohne CI-Lauf — `[skip ci]`, `paths-ignore` — erreichen den Push-Scan nie; der nächste Lauf holt sie nicht nach ## Produktivinstanz Sonden gegen die laufende Immich-Instanz **nur lesend**. Schreibpfade ausschließlich gegen Testdaten. Das ist ein Minimum, keine Lösung — die strukturelle Lösung ist ein Immich-API-Key mit ausschließlich lesenden Rechten (Immich kann granulare Rechte je Key vergeben, offener Punkt: **Issue #149**). Bis dieser Read-Only-Key im Agenten-Kontext hinterlegt ist, gilt die Regel oben als das, was tatsächlich durchsetzbar ist. Hier stand bis zum Abgleich auf v1.19.1 „siehe Issue #53". Gemessen am 03.11.2026 (`gh issue view 53`): Das ist der **abgeschlossene** Abgleich v1.3.0→v1.7.1 und hat mit einem Read-Only-Key nichts zu tun; eine Suche über alle Issues fand kein passendes. Der Punkt hatte also keinen Eigentümer — der Verweis sah nur so aus. **Prüffrage für jeden Verweis dieser Datei: zeigt er auf einen Vorgang, der den Punkt tatsächlich führt?** **Kein technischer Wächter — und der Grund dafür ist zur Hälfte gemessen.** Gemessen am 03.11.2026 auf dem Entwicklungsrechner: Es sind zwei `PreToolUse`-Hooks eingetragen, **beide mit einem MCP-Matcher** (`mcp____.*`) — keiner trifft Shell-Aufrufe. Gelesen wurden die `hooks`-Blöcke der Agenten-Einstellungen mit `python -I -S` und `json.load`, ausgegeben wurden Ereignis und Matcher je Eintrag. **Heute sieht also kein Hook ein `curl`** — das ist die eine Hälfte. Was daraus NICHT folgt: dass ein Hook das grundsätzlich nicht könnte. Ein `PreToolUse`-Hook mit Shell-Matcher bekommt die Kommandozeile zu sehen; ob er die Zugriffe dieses Projekts tatsächlich sieht, ist **ungemessen**. Eine Echtprobe dafür schaltet einen Wächter scharf und ist damit Owner-Einstellung (Issue #149). Bis dahin steht hier die Prüffrage statt der Behauptung: **Welcher Zugriff auf Immich entsteht, ohne durch ein Werkzeug zu laufen, das ein Hook sieht?** Solange die Antwort „ein Unterprozess, den ein Skript startet" lautet, wäre ein solcher Wächter eine Verifikation auf diesem Rechner und kein Tor am Repo — genau wie die Husky-Hooks. Sollte Immich je als MCP angebunden werden, gilt die Fassung aus Vorlage v1.3.1 — und zwar mit ihren beiden Korrekturen: Der Präfix `mcp____` stammt aus der **Client**-Konfiguration und ist frei gewählt (ein geratener Präfix passt auf nichts), und der Wächter muss in dem Client liegen, der die Aufrufe tatsächlich macht. Vor dem Scharfschalten die **Echtprobe**: einen echten Aufruf auslösen und nachweisen, dass der Wächter ihn überhaupt sieht. Ohne diesen Nachweis prüfen die Testfälle nur selbst erzeugte Eingaben und beweisen nichts über die, die das System wirklich produziert. ## Umgang mit Geheimnissen Der Agent trägt **keine** Schlüssel, Tokens oder Passwörter ein — auch nicht auf Zuruf und auch nicht in eine private Datei. Er darf Platzhalter setzen, das Format erklären und die Stelle vorbereiten; eingetragen wird vom Menschen. `.env.example` zeigt, welche Werte gebraucht werden. **Und die Gegenrichtung, hier real passiert:** Ein Geheimnis, das zum Sondieren in ein Agenten-Gespräch kopiert wird, liegt danach in einem System, das es speichert — es gilt als offengelegt und gehört rotiert, unabhängig davon, ob es je missbraucht wurde. Deshalb gehört in den Kontext nur ein Schlüssel, dessen Offenlegung verkraftbar ist: der Read-Only-Key aus dem Abschnitt oben, nie der schreibberechtigte. ## Dieses Repo ist öffentlich `Trust1509/immich-family-tools` ist ein **öffentliches** Repository. Was hier landet, ist weltweit und dauerhaft lesbar — ein Commit, der ein Geheimnis enthält, ist auch nach dem Zurücknehmen noch im Verlauf. Deshalb: keine echten Schlüssel, keine Immich-Instanz-Adressen mit Zugangsdaten, **keine Personen- oder Gesichtsdaten aus den Fotobeständen** — weder in Code noch in Tests, Fixtures, Issues oder Commit-Nachrichten. Die PII-Grenze für Fremdstimmen im Panel steht in `docs/agents/panel.md` („PII-Grenze") — hier nur der Verweis, nicht die Wiederholung. **Drei Verteidigungslinien, mit unterschiedlicher Reichweite** — das ist der Punkt, den unser 01.09.-Panel teuer gelernt hat, eine Linie allein zu verwechseln heißt, die Lücke der anderen zu übersehen: - **GitHub Push Protection** — blockt bekannte Geheimnis-Muster **vor** dem Landen, GitHub-seitig, kein eigener Lauf. Erkennt nur **Partner-Muster** (bekannte Anbieter-Tokenformate) — bei uns sind Nicht-Anbieter-Muster abgeschaltet (Beleg unten); einen generischen Schlüssel wie unseren Immich-API-Key erkennt diese Linie damit **nicht**. - **GitHub Secret Scanning** — durchsucht den vollen Verlauf, ebenfalls GitHub-seitig, unabhängig davon, ob und wann unsere eigene CI läuft. Derselbe Vorbehalt gilt hier genauso: nur **Partner-Muster**, denselben generischen Schlüssel erkennt auch diese Linie nicht. Beleg für beide, wörtlich zitiert: ``` gh api repos/Trust1509/immich-family-tools \ --jq .security_and_analysis.secret_scanning_non_provider_patterns.status ``` → `disabled`. **Weil beide Linien denselben blinden Fleck haben**, ist `gitleaks` faktisch die einzige, die unseren Immich-API-Key fangen würde. - **Unser `gitleaks`** — der Push-Job (`ci.yml`) scannt nur die Commits des Pushes entlang der ersten Eltern-Linie (Flag und Begründung: Abschnitt „Prüfschritte" oben); erst der Wochenlauf (`wochen-pruefung.yml`) deckt den vollen Verlauf ab. **Und was die Öffentlichkeit NICHT belegt:** Dass CI-Läufe hier nichts kosten. Das stand hier bis zum Abgleich auf v1.19.1 als Nutzen — mit der Abrechnungs-Schnittstelle als Beleg, und die taugt dafür nicht (Begründung im Abschnitt „Prüfschritte" oben, hier nur der Verweis). Was die Öffentlichkeit wirklich kostet, ist der Abschnitt darüber: Was hier landet, ist weltweit und dauerhaft lesbar. ### Gemessen am 06.09.2026: was der eigene Scanner zusätzlich leistet Auftrag aus dem Vorlagen-Abgleich v1.14.0 („doppelte Wächter kosten Budget"). Ergebnis: **beide Plattform-Schalter sind an, und `gitleaks` bleibt trotzdem.** Schalterstand — `gh api repos/Trust1509/immich-family-tools --jq .security_and_analysis`: ``` secret_scanning: enabled secret_scanning_push_protection: enabled secret_scanning_non_provider_patterns: disabled secret_scanning_validity_checks: disabled ``` Deckungs-Messung, Wegwerf-Repo mit sechs **erfundenen** Werten, `gitleaks detect` über den vollen Verlauf. Klasse A ist die Form, in der unser einziges echtes Geheimnis auftreten würde (Immich-Schlüssel: 32 Zeichen, kein Anbieter-Präfix); Klasse B ist die Gegenprobe: | Fixture | Klasse | gitleaks | Plattform | | ----------------------------------- | ------ | ------------------ | --------- | | Immich-Schlüssel in `accounts.json` | A | Fund (generic) | kein Fund | | Immich-Schlüssel in Python-Code | A | Fund (generic) | kein Fund | | Passwort in einer Datenbank-URL | A | **kein Fund** | kein Fund | | privater RSA-Schlüssel | A | Fund (private-key) | kein Fund | | AWS-Schlüsselpaar | B | Fund | Fund | | GitHub-PAT | B | Fund (github-pat) | Fund | **Drei von sechs fängt nur `gitleaks`, darunter beide Formen unseres einzigen echten Geheimnistyps.** Diese zwei bleiben exklusiv, auch wenn die generischen Muster eingeschaltet werden: Ein bloßer Schlüssel ohne Präfix ist kein generisches Muster (das sind private Schlüssel und Verbindungszeichenketten), höchstens ein KI-erkanntes — und die haben laut Plattform-Dokumentation keine Push Protection. **Und das Kostenargument gegen den eigenen Scanner trägt nicht** — aber aus einem anderen Grund, als hier bis zum Abgleich auf v1.19.1 stand. Dort stand: `actions/runs//timing` meldet `billable.UBUNTU.total_ms = 0` für alle Jobs, geprüft über drei Läufe und beide Workflows. Das ist richtig gemessen und **beweist nichts**: Dieselbe Schnittstelle meldet 0 auch für private Repos (gemessen am 28.09.2026 an zwei privaten Läufen). Was trägt, ist die **Laufzeit**: Der `secrets`-Job läuft in 4–10 Sekunden. Bei einem Lauf je Slice ist das in jeder Abrechnungsform unterhalb der Wahrnehmungsgrenze, ohne dass man wissen muss, ob abgerechnet wird. **Zwei offene Punkte** (Issue #77): Die generischen Muster einschalten ist kostenlos und deckt **privaten Schlüssel und Verbindungszeichenkette** plattformseitig ab — und zwar **vor** dem Push, wo unser Scanner erst danach prüft. Owner-Schalter. Und **das Passwort in einer Verbindungszeichenkette fängt heute keiner von beiden**; solange das so ist, ist es kein gedeckter Fall, sondern eine bekannte Lücke. ## Projektspezifisches **Mehrsprachigkeit.** Nutzersichtbare UI-Texte werden in allen Sprachen aus `LANG_LABELS` (`frontend/src/i18n.tsx`) gepflegt — das ist die einzige Quelle, welche Sprachen es gibt; `translations` ist als `satisfies Record>` deklariert, also erzwingt der Compiler über die Typ-Parität jeden Schlüssel in jeder dort gelisteten Sprache und nennt bei einem fehlenden Schlüssel Datei und Zeile der schuldigen Stelle. `LANG_LABELS` bleibt der einzige Eigentümer der Sprachenzahl. Die README-Liste der Sprachen **ist seit v1.6.0 bewacht** — ein Test in `i18n.test.ts` vergleicht sie gegen `LANG_LABELS` und wird rot, wenn eine ausgelieferte Sprache dort fehlt. Vorher stand hier, sie sei „nachrangiger Werbetext, von `tsc` nicht bewacht"; genau so ist sie einmal still veraltet, und gemerkt hat es erst eine Prüfstimme nach dem Merge. Echte Umlaute/Sonderzeichen, kein ASCII-Ersatz. **Sync-Log.** Einträge tragen `message_key` + `message_params`; das deutsche `details`-Feld bleibt als Fallback für Alt-Einträge bestehen, nicht für neue Einträge verwenden. **Fehlermeldungen des Servers.** Kein Router wirft mehr eine nackte `HTTPException` — jede Meldung kommt aus `backend/errors.py` und trägt einen Schlüssel. Die Antwort führt **beides**: `detail` bleibt eine Zeichenkette und bleibt deutsch, `error_key` und `error_params` stehen daneben. Das ist keine Bequemlichkeit, sondern der Rückfall: Kennt das Frontend einen Schlüssel nicht, zeigt es den deutschen Satz statt einer leeren Zeile. Wer eine Meldung hinzufügt, trägt sie in **beide** Stellen ein — `backend/errors.py` und `frontend/src/i18n.tsx`; ein Test vergleicht die Schlüsselmengen, und einer verbietet die nackte `HTTPException`. **JSON-Migration.** Die Migration in `backend/services/config_store.py` (`SCHEMA_VERSION`) ist rein additiv und schreibt in die einzige persistente Datei des Projekts. Sie fällt unter **R3** (siehe „Risikoklasse je Slice" oben). Änderungen dort brauchen einen Test gegen das Alt-Format (vorhandenes Muster: Auffüllen der Felder UND Datenerhalt, siehe `backend/tests/test_config_store.py`). ## Betrieb Sobald echte Menschen echte Daten in der Anwendung haben, gilt `docs/agents/betrieb.md`: Sicherung **mit Rückspiel-Probe**, Totmann-Schalter, Erreichbarkeits-Wächter. Grundsatz: Ein Erfolgssignal, das in der Anwendung selbst lebt, schweigt genau dann, wenn der Rechner stirbt. Offene Punkte laufen als Issue #54. ## Agent skills ### Issue tracker Issues live in GitHub Issues (Trust1509/immich-family-tools). External PRs are a triage surface. See `docs/agents/issue-tracker.md`. ### Triage labels Default label vocabulary (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See `docs/agents/triage-labels.md`. ### Domain docs Single-context — one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.