From 7f6b8ebe07c9318bc061b5cc116ba97d45ee4383 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B6rn=20Runden?=
Date: Thu, 29 Oct 2026 21:56:53 +0100
Subject: [PATCH] =?UTF-8?q?Basis=20aus=202nd-brain=20f=C3=BCr=20LPIC-Pr?=
=?UTF-8?q?=C3=BCfungsvorbereitung=20=C3=BCbernommen?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Regeln, Standards, Kontextstruktur und Werkzeuge übernommen; 2nd-brain-
Inhalte (Stacks, Ansible-Inventar, Architektur, Knowledge) entfernt.
Co-Authored-By: Claude Opus 5.5
---
.continue/rules/00_agents.md | 5 +
.editorconfig | 15 +
.github/copilot-instructions.md | 4 +
.gitignore | 11 +
.shellcheckrc | 3 +
AGENTS.md | 61 +++
CLAUDE.md | 4 +
GEMINI.md | 4 +
README.md | 28 ++
ai/README.md | 25 ++
ai/common/README.md | 24 +
ai/common/agents.md | 62 +++
ai/common/ansible.md | 91 ++++
ai/common/architecture.md | 34 ++
ai/common/bash.md | 161 +++++++
ai/common/coding.md | 61 +++
ai/common/docker.md | 189 ++++++++
ai/common/markdown.md | 24 +
ai/common/templates/ansible/.env.sh | 35 ++
ai/common/templates/ansible/ansible.cfg | 24 +
.../ansible/files/example/example.conf.j2 | 10 +
.../ansible/inv/group_vars/all/vars.yml | 10 +
ai/common/templates/ansible/inv/lab | 3 +
.../templates/ansible/playbooks/example.yml | 16 +
.../ansible/playbooks/mini_check.yml | 28 ++
.../ansible/roles/example/defaults/main.yml | 10 +
.../ansible/roles/example/handlers/main.yml | 13 +
.../ansible/roles/example/meta/main.yml | 11 +
.../ansible/roles/example/tasks/config.yml | 13 +
.../ansible/roles/example/tasks/install.yml | 9 +
.../ansible/roles/example/tasks/main.yml | 39 ++
.../ansible/roles/example/tasks/secrets.yml | 14 +
.../ansible/roles/example/tasks/verify.yml | 9 +
ai/common/templates/bash/.env.example | 21 +
ai/common/templates/bash/_functions.sh | 415 ++++++++++++++++++
ai/common/templates/bash/script_template.sh | 58 +++
ai/common/templates/docker/.env.example | 23 +
ai/common/templates/docker/README.md | 29 ++
ai/common/templates/docker/docker-compose.yml | 48 ++
ai/common/templates/markdown/adr.md | 35 ++
.../templates/markdown/knowledge_entry.md | 34 ++
ai/context/README.md | 16 +
ai/context/environment.md | 33 ++
ai/context/knowledge/INDEX.md | 10 +
ai/context/knowledge/README.md | 43 ++
ai/context/knowledge/decisions/README.md | 4 +
ai/context/knowledge/howto/README.md | 4 +
ai/context/knowledge/network/README.md | 4 +
ai/context/knowledge/research/README.md | 4 +
ai/context/knowledge/services/README.md | 4 +
ai/context/knowledge/services/sultan.md | 49 +++
ai/context/knowledge/systems/README.md | 4 +
ai/context/knowledge/topics/README.md | 5 +
ai/context/responsible.md | 17 +
ai/context/style.md | 18 +
ai/inbox/README.md | 13 +
ai/rules/README.md | 26 ++
ai/rules/file_safety.md | 9 +
ai/rules/headers.md | 35 ++
ai/rules/language.md | 17 +
ai/rules/privacy.md | 38 ++
ai/rules/secrets.md | 12 +
ai/rules/testing.md | 17 +
ai/rules/web_search.md | 16 +
bin/.env.example | 21 +
bin/README.md | 11 +
bin/_functions.sh | 415 ++++++++++++++++++
bin/bashdoc.sh | 104 +++++
bin/kb_index.sh | 107 +++++
context.md | 47 ++
70 files changed, 2816 insertions(+)
create mode 100644 .continue/rules/00_agents.md
create mode 100644 .editorconfig
create mode 100644 .github/copilot-instructions.md
create mode 100644 .gitignore
create mode 100644 .shellcheckrc
create mode 100644 AGENTS.md
create mode 100644 CLAUDE.md
create mode 100644 GEMINI.md
create mode 100644 README.md
create mode 100644 ai/README.md
create mode 100644 ai/common/README.md
create mode 100644 ai/common/agents.md
create mode 100644 ai/common/ansible.md
create mode 100644 ai/common/architecture.md
create mode 100644 ai/common/bash.md
create mode 100644 ai/common/coding.md
create mode 100644 ai/common/docker.md
create mode 100644 ai/common/markdown.md
create mode 100644 ai/common/templates/ansible/.env.sh
create mode 100644 ai/common/templates/ansible/ansible.cfg
create mode 100644 ai/common/templates/ansible/files/example/example.conf.j2
create mode 100644 ai/common/templates/ansible/inv/group_vars/all/vars.yml
create mode 100644 ai/common/templates/ansible/inv/lab
create mode 100644 ai/common/templates/ansible/playbooks/example.yml
create mode 100644 ai/common/templates/ansible/playbooks/mini_check.yml
create mode 100644 ai/common/templates/ansible/roles/example/defaults/main.yml
create mode 100644 ai/common/templates/ansible/roles/example/handlers/main.yml
create mode 100644 ai/common/templates/ansible/roles/example/meta/main.yml
create mode 100644 ai/common/templates/ansible/roles/example/tasks/config.yml
create mode 100644 ai/common/templates/ansible/roles/example/tasks/install.yml
create mode 100644 ai/common/templates/ansible/roles/example/tasks/main.yml
create mode 100644 ai/common/templates/ansible/roles/example/tasks/secrets.yml
create mode 100644 ai/common/templates/ansible/roles/example/tasks/verify.yml
create mode 100644 ai/common/templates/bash/.env.example
create mode 100644 ai/common/templates/bash/_functions.sh
create mode 100755 ai/common/templates/bash/script_template.sh
create mode 100644 ai/common/templates/docker/.env.example
create mode 100644 ai/common/templates/docker/README.md
create mode 100644 ai/common/templates/docker/docker-compose.yml
create mode 100644 ai/common/templates/markdown/adr.md
create mode 100644 ai/common/templates/markdown/knowledge_entry.md
create mode 100644 ai/context/README.md
create mode 100644 ai/context/environment.md
create mode 100644 ai/context/knowledge/INDEX.md
create mode 100644 ai/context/knowledge/README.md
create mode 100644 ai/context/knowledge/decisions/README.md
create mode 100644 ai/context/knowledge/howto/README.md
create mode 100644 ai/context/knowledge/network/README.md
create mode 100644 ai/context/knowledge/research/README.md
create mode 100644 ai/context/knowledge/services/README.md
create mode 100644 ai/context/knowledge/services/sultan.md
create mode 100644 ai/context/knowledge/systems/README.md
create mode 100644 ai/context/knowledge/topics/README.md
create mode 100644 ai/context/responsible.md
create mode 100644 ai/context/style.md
create mode 100644 ai/inbox/README.md
create mode 100644 ai/rules/README.md
create mode 100644 ai/rules/file_safety.md
create mode 100644 ai/rules/headers.md
create mode 100644 ai/rules/language.md
create mode 100644 ai/rules/privacy.md
create mode 100644 ai/rules/secrets.md
create mode 100644 ai/rules/testing.md
create mode 100644 ai/rules/web_search.md
create mode 100644 bin/.env.example
create mode 100644 bin/README.md
create mode 100644 bin/_functions.sh
create mode 100755 bin/bashdoc.sh
create mode 100755 bin/kb_index.sh
create mode 100644 context.md
diff --git a/.continue/rules/00_agents.md b/.continue/rules/00_agents.md
new file mode 100644
index 0000000..db1d5f5
--- /dev/null
+++ b/.continue/rules/00_agents.md
@@ -0,0 +1,5 @@
+---
+name: AI-Basis
+alwaysApply: true
+---
+Maßgeblich ist `AGENTS.md` im Workspace-Root: vor jeder Aufgabe lesen und den Regeln in `ai/rules/` folgen.
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..00f2c60
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..970af91
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,4 @@
+# Copilot-Anweisungen
+
+Maßgeblich ist **AGENTS.md** im Repository-Root (Sitzungsstart, Arbeitsgedächtnis `context.md`, Regeln in `ai/rules/`, Standards in `ai/common/`).
+Amtssprache und Projekt stehen dort unter „Metadaten“.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..04732c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# Geheimnisse & lokale Werte
+.env
+*.env.local
+.ans-secret*
+# Laufzeit
+*.log
+log/
+fact_caching/
+*.retry
+# Sicherungen beim Überschreiben (bewusst lokal behalten, nicht versionieren)
+*.orig
diff --git a/.shellcheckrc b/.shellcheckrc
new file mode 100644
index 0000000..620bf28
--- /dev/null
+++ b/.shellcheckrc
@@ -0,0 +1,3 @@
+# _functions.sh wird per Pfad aus SCRIPT_DIR gesourct
+external-sources=true
+source-path=SCRIPTDIR
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f7054ca
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,61 @@
+# AGENTS.md – Einstieg für KI-Agenten
+
+> **Maßgebliche Datei** für alle Agenten (Claude Code, Codex, Gemini CLI, VSCodium/Continue, Copilot, Chat-Dialoge).
+> `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` verweisen nur hierher – Änderungen **nur hier**.
+
+## Metadaten
+- **Projekt:** LPIC-Prüfungsvorbereitung (`lpic`) – Arbeitsgedächtnis: [`context.md`](context.md)
+- **Betreiber:** `br` (siehe [`ai/context/responsible.md`](ai/context/responsible.md))
+- **Amtssprache:** `de`
+- **Basis:** skel-ai, reduziert auf ein einzelnes Projekt (kein Mandant, keine Teilprojekte)
+- **Stand:** 29.10.2026
+
+## Amtssprache
+Alle Antworten, Dokumente, Kommentare, Log-Meldungen, Ansible-Task-Namen und Commit-Texte in der **Amtssprache** (oben).
+Fachbegriffe und Code-Bezeichner im Original. Bei `de`: korrekte Umlaute (ä ö ü ß), nie ae/oe/ue.
+Details und Ausnahmen: [`ai/rules/language.md`](ai/rules/language.md).
+
+## Sitzungsstart (Pflicht, in dieser Reihenfolge)
+1. **Regeln lesen:** alle `ai/rules/*.md`. Sie haben **Vorrang vor allem anderen** (außer Sicherheits-/Datenschutzgrenzen des Agenten).
+2. **Verabredungen lesen:** `ai/common/agents.md`, dann aufgabenbezogen die passende Datei (`bash.md`, `ansible.md`, `docker.md`, `coding.md`, `markdown.md`, `architecture.md`).
+3. **Kontext lesen:** `ai/context/*.md`; Wissen aus `ai/context/knowledge/INDEX.md` gezielt nachladen.
+4. **Arbeitsgedächtnis lesen:** [`context.md`](context.md) im Root.
+5. Kurz bestätigen: gelesene Dateien, nächster Schritt laut `context.md`.
+
+## Verzeichnisstruktur
+| Pfad | Inhalt | Wer pflegt |
+|---|---|---|
+| `AGENTS.md` | diese Datei – Einstieg, Metadaten, Sitzungsablauf | Bediener + Agent |
+| `context.md` | **Arbeitsgedächtnis** des Projekts: Ziel, Stand, Entscheidungen, offene Fragen, nächster Schritt | Agent |
+| `ai/rules/` | **verbindliche** Erlaubnisse/Verbote, eine Regel pro Datei | Bediener (Agent nur auf Anweisung) |
+| `ai/common/` | Verabredungen & Standards (Bash, Ansible, Docker, Coding, Doku, Architektur) + `templates/` | Bediener + Agent |
+| `ai/context/` | Kontext (Verantwortliche, Stil, Umgebung) | Bediener + Agent |
+| `ai/context/knowledge/` | Knowledge-DB (Markdown + Frontmatter, Index per Script) | Agent + Bediener |
+| `ai/inbox/` | Eingang: vom Bediener abgelegte `.md`-Dateien, Agent ordnet ein und verschiebt nach `done/` | Bediener → Agent |
+| `bash/` | Shell-Scripts, Übungsscripts (`ai/common/bash.md`) – bei Bedarf anlegen | Agent + Bediener |
+| `ansible/` | Plays, Rollen, Inventar für Lab-VMs (`ai/common/ansible.md`) – bei Bedarf anlegen | Agent + Bediener |
+| `stacks/` | Compose-Stacks, je Stack ein Ordner (`ai/common/docker.md`) – bei Bedarf anlegen | Agent + Bediener |
+| `bin/` | Werkzeuge der Basis (nach `ai/common/bash.md` gebaut) | Bediener + Agent |
+
+Jeder Ordner hat eine `README.md`, die Zweck, Inhalt und Pflege beschreibt.
+
+## Werkzeuge
+```bash
+bin/kb_index.sh [--check] # Knowledge-INDEX.md erzeugen / prüfen
+bin/bashdoc.sh [-o doku.md] # Doku aus '##'-Kommentaren
+```
+Alle Scripts: `-h` zeigt die Hilfe, `-n` Trockenlauf, `-d` Debug.
+
+## Feste Grundsätze (Kurzfassung – Details in `ai/rules/`)
+- **Datenschutz zuerst** – keine personenbezogenen/identifizierenden Daten an externe Dienste, auch nicht in Suchanfragen ([privacy.md](ai/rules/privacy.md)).
+- **Geheimnisse** nie in Dateien/Git/Chat; Secret-Store zuerst ([secrets.md](ai/rules/secrets.md)).
+- **Verantwortliche im Header** jeder Code-Datei, namentlich aus `ai/context/responsible.md` ([headers.md](ai/rules/headers.md)).
+- **Kein Code in Cloud-VMs oder gegen Produktion** testen ohne ausdrückliche Freigabe ([testing.md](ai/rules/testing.md)).
+- **Vor Löschen/Überschreiben** Ziel ansehen; `*.orig` statt Löschen ([file_safety.md](ai/rules/file_safety.md)).
+- **Keine Erfindungen** – Unsicheres kennzeichnen, Quellen mit Datum nennen.
+
+## Ergebnisse ablegen
+- Projektstand → `context.md` (Sitzungsprotokoll, Entscheidungen, offene Fragen, nächster Schritt).
+- Dauerhaftes Wissen (Prüfungsthemen, Systeme, Dienste, Entscheidungen, Anleitungen) → Knowledge-DB, danach `bin/kb_index.sh`.
+- Verbesserungen an der Basis → siehe „AI-Basis verbessern“ in [`ai/common/agents.md`](ai/common/agents.md).
+- Datumsangaben im Text absolut (TT.MM.JJJJ), in Frontmatter/Dateinamen ISO (JJJJ-MM-TT).
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..635c228
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,4 @@
+# Agenten-Einstieg
+
+Maßgeblich ist **[AGENTS.md](AGENTS.md)** – zu Sitzungsbeginn vollständig lesen und dem dort beschriebenen Sitzungsstart folgen.
+Diese Datei enthält bewusst nichts weiter, damit es nur eine Quelle der Wahrheit gibt.
diff --git a/GEMINI.md b/GEMINI.md
new file mode 100644
index 0000000..635c228
--- /dev/null
+++ b/GEMINI.md
@@ -0,0 +1,4 @@
+# Agenten-Einstieg
+
+Maßgeblich ist **[AGENTS.md](AGENTS.md)** – zu Sitzungsbeginn vollständig lesen und dem dort beschriebenen Sitzungsstart folgen.
+Diese Datei enthält bewusst nichts weiter, damit es nur eine Quelle der Wahrheit gibt.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e244fdc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# lpic – LPIC-Prüfungsvorbereitung
+
+Lernprojekt zur Vorbereitung auf die LPIC-Zertifizierung.
+Aufgebaut auf der skel-ai-Basis: Regeln, Code-Standards (Bash, Ansible, Docker) und eine Markdown-Knowledge-DB,
+damit KI-Agenten (agentisch oder im Chat) konsistent arbeiten.
+
+## Für Agenten
+Einstieg ist **[AGENTS.md](AGENTS.md)**. `CLAUDE.md`, `GEMINI.md`, `.github/copilot-instructions.md` und `.continue/rules/` zeigen nur dorthin.
+Arbeitsstand: [context.md](context.md).
+
+## Aufbau
+```
+lpic/
+├── AGENTS.md # maßgeblicher Einstieg (Metadaten, Sitzungsstart)
+├── context.md # Arbeitsgedächtnis des Projekts
+├── CLAUDE.md · GEMINI.md # Zeiger auf AGENTS.md
+├── ai/
+│ ├── rules/ # verbindliche Regeln (Vorrang)
+│ ├── common/ # Standards & Verabredungen + templates/
+│ └── context/ # Verantwortliche, Stil, Umgebung + knowledge/ (Knowledge-DB, Lerninhalte unter topics/)
+└── bin/ # Werkzeuge der Basis
+```
+Code für Übungen und Lab (`bash/`, `ansible/`, `stacks/`) wird bei Bedarf angelegt.
+
+## Einrichten
+```bash
+cp bin/.env.example bin/.env # optional, lokale Einstellungen der Basis-Werkzeuge
+```
diff --git a/ai/README.md b/ai/README.md
new file mode 100644
index 0000000..a38561d
--- /dev/null
+++ b/ai/README.md
@@ -0,0 +1,25 @@
+# ai/ – Regeln, Standards und Kontext für KI-Agenten
+
+| Ordner | Frage, die er beantwortet | Verbindlichkeit | Wer ändert |
+|---|---|---|---|
+| [`rules/`](rules/README.md) | **Was darf/muss ich (nicht)?** | verbindlich, Vorrang | Bediener; Agent nur auf ausdrückliche Anweisung |
+| [`common/`](common/README.md) | **Wie machen wir Dinge?** (Code, Doku, Architektur, Arbeitsweise) | Standard; Abweichung nur begründet | Bediener + Agent (mit Änderungsvermerk) |
+| [`context/`](context/README.md) | **Für wen, wo, womit?** (Personen, Stil, Umgebung) + Knowledge-DB | Faktenbasis | Bediener + Agent |
+| [`inbox/`](inbox/README.md) | **Was ist neu hereingekommen?** (vom Bediener abgelegte `.md`) | temporär | Bediener legt ab, Agent ordnet ein |
+
+## Lesereihenfolge
+`rules/` → `common/agents.md` → aufgabenbezogener Standard aus `common/` → `context/*.md` → `context/knowledge/INDEX.md` (gezielt) → `../context.md`.
+
+## Abgrenzung (wohin gehört eine neue Information?)
+| Information | Ziel |
+|---|---|
+| Verbot/Erlaubnis, Grenze, Pflicht | `rules/.md` (eine Regel pro Datei) |
+| Konvention, Vorgehen, Vorlage | `common/.md`, Vorlage in `common/templates/` |
+| Wer/wo/womit (Überblick) | `context/.md` |
+| Fakt, Entscheidung, Anleitung, Recherche mit Bestand | `context/knowledge//…` |
+| Projektstand, Ziel, offene Fragen | `../context.md` |
+| Code (Übungen, Lab) | `../bash/`, `../ansible/`, `../stacks/` – Ordner erst bei Bedarf anlegen |
+
+## Namenskonventionen der Basis
+- Datei- und Ordnernamen: Englisch, Kleinbuchstaben, `_` als Trenner, ASCII → stabil bei Klonen mit anderer Amtssprache.
+- Inhalte: Amtssprache (siehe `AGENTS.md`).
diff --git a/ai/common/README.md b/ai/common/README.md
new file mode 100644
index 0000000..605c198
--- /dev/null
+++ b/ai/common/README.md
@@ -0,0 +1,24 @@
+# ai/common/ – Standards & Verabredungen
+
+> **Wie** wir Dinge tun. Verbindlicher Standard; Abweichungen nur begründet (im Ergebnis bzw. Stack-/Projekt-README).
+> Verbesserungen sind erwünscht – Ablauf: `agents.md` § 5 „AI-Basis verbessern“. Jede Datei hat unten ein Änderungsprotokoll.
+
+| Datei | Inhalt |
+|---|---|
+| [agents.md](agents.md) | Arbeitsweise der Agenten, Qualitätscheckliste, Basis verbessern, Rollen |
+| [bash.md](bash.md) | Bash-Standard: `.env` + `_functions.sh`, Optionen, Logging (`becho`…), Trap, Präambel = Hilfe |
+| [ansible.md](ansible.md) | Ansible: Layout, `.env.sh`, Präambel, `hostlist`, Rollen, Facts, Secrets, Tags |
+| [docker.md](docker.md) | Docker-Compose-Standard (GELF, Traefik, Bind-Mounts, `.env`, README-Pflicht) |
+| [coding.md](coding.md) | Allgemeine Code-Regeln, Namen, Konfiguration, Python, Lint/Test |
+| [markdown.md](markdown.md) | Markdown-/Doku-Konventionen, READMEs, Projekt-`context.md` |
+| [architecture.md](architecture.md) | Architektur-Skizze (arc42-light), ADRs, Lösungsgrundsätze |
+
+## templates/
+| Pfad | Inhalt |
+|---|---|
+| `templates/bash/` | `script_template.sh`, `_functions.sh` (Referenz, Version in `B_FUNCTIONS_VERSION`), `.env.example` |
+| `templates/ansible/` | `.env.sh`, `ansible.cfg`, `inv/`, `playbooks/{example,mini_check}.yml`, `roles/example/`, `files/example/` |
+| `templates/docker/` | `docker-compose.yml`, `.env.example`, Stack-`README.md` |
+| `templates/markdown/` | `knowledge_entry.md`, `adr.md` |
+
+Vorlagen werden **kopiert**, nicht verlinkt. `bin/_functions.sh` ist eine Kopie von `templates/bash/_functions.sh` – beide gleich halten.
diff --git a/ai/common/agents.md b/ai/common/agents.md
new file mode 100644
index 0000000..a488376
--- /dev/null
+++ b/ai/common/agents.md
@@ -0,0 +1,62 @@
+# Agenten – Arbeitsweise (projektübergreifend)
+
+> Gilt für alle KI-Agenten, agentisch (Claude Code, Codex, Gemini CLI, VSCodium/Continue) wie im Chat.
+> `ai/rules/*.md` haben Vorrang.
+
+## 1. Arbeitsweise
+- **Sitzungsstart** gemäß `AGENTS.md`.
+- **Erst verstehen, dann bauen:** vorhandenen Code, Knowledge-Einträge und Projektstand lesen, bevor etwas Neues entsteht. Bestehendes wiederverwenden.
+- **Empfehlung statt Optionsliste.** Alternativen nur, wenn die Entscheidung wirklich beim Bediener liegt – dann knapp mit Empfehlung.
+- **Nachfragen**, wenn: Ziel/Projekt unklar, Regel berührt (Produktion, Löschen, extern), Verantwortliche für den Header fehlen, Annahmen das Ergebnis wesentlich ändern.
+- **Selbstständig handeln** bei: Formalien, Konventionen aus `common/`, offensichtlichen Defaults – im Ergebnis kurz erwähnen.
+- **Keine Erfindungen:** Hostnamen, IPs, Versionen, Optionen nur aus Knowledge/Projekt/Quelle. Unsicheres als „unsicher“ markieren.
+- **Ergebnisse ablegen, nicht nur im Chat:** Projektstand → `context.md`; Dauerhaftes → Knowledge-DB.
+
+## 2. Qualitätsmaßstab („nah am Optimum“)
+Vor Abgabe von Code/Doku/Architektur selbst prüfen:
+- [ ] Passender Standard aus `common/` eingehalten, Vorlage aus `common/templates/` genutzt
+- [ ] Header mit Verantwortlichen (`rules/headers.md`), Funktionen mit Kurz-Doku
+- [ ] Keine Geheimnisse, keine internen Daten in Beispielen für Externe
+- [ ] Lokal getestet (Syntax, Lint, Trockenlauf) – oder ausdrücklich gesagt, was **nicht** getestet wurde
+- [ ] Doku/Knowledge aktualisiert, `bin/kb_index.sh` gelaufen
+- [ ] Einfachste Lösung, die das Ziel erfüllt – keine Vorratsfunktionen
+
+## 3. Formales
+- Sprache, Datum: `rules/language.md`.
+- Markdown: `common/markdown.md`.
+- Commit-Messages (falls Git): Amtssprache, Imperativ, erste Zeile ≤ 72 Zeichen, Bezug zum Bereich (`ai/rules: Regel Prüfungsfragen ergänzt`).
+
+## 4. Standards (Verweise)
+| Thema | Datei |
+|---|---|
+| Bash-Scripts | [bash.md](bash.md) |
+| Ansible | [ansible.md](ansible.md) |
+| Docker-Compose | [docker.md](docker.md) |
+| Coding allgemein, Python | [coding.md](coding.md) |
+| Markdown, Knowledge-Einträge | [markdown.md](markdown.md) |
+| Architektur-Doku, Entscheidungen (ADR) | [architecture.md](architecture.md) |
+
+## 5. AI-Basis verbessern
+Agenten **sollen** die Basis besser machen, wenn sie bei der Arbeit Lücken, Widersprüche oder wiederkehrende Muster bemerken.
+
+| Was | Wie |
+|---|---|
+| Regel fehlt/ist unklar (`rules/`) | **Nicht selbst ändern.** Vorschlag im Chat + Eintrag „Offene Fragen“ in `context.md` |
+| Standard verbessern (`common/`) | Ändern, wenn klar besser und rückwärtskompatibel; Eintrag im Änderungsprotokoll der Datei (unten, `TT.MM.JJJJ – was – warum`). Größeres vorher vorschlagen |
+| Vorlage/Script (`common/templates/`, `bin/`) | Ändern + lokal testen + `@version` erhöhen; `_functions.sh` in `bin/` und `templates/bash/` synchron halten |
+| Kontext/Knowledge | Jederzeit ergänzen/korrigieren, mit `updated` und Quelle |
+| Wiederkehrende Frage des Bedieners | Antwort als Knowledge-Eintrag (`howto/`) ablegen |
+
+Verbesserungen, die **allgemein** gelten (nicht nur für dieses Projekt), dem Bediener zur Übernahme ins Skeleton (`~/skel-ai`) vorschlagen.
+
+## 6. Rollen der Agenten
+| Agent | Einsatz | Grenzen |
+|---|---|---|
+| Claude Code | agentisches Coding, Doku | Datenklasse laut `rules/privacy.md` |
+| Chat-Dialog | Konzepte, Reviews | kein Dateizugriff → relevante Dateien einfügen lassen |
+
+**Chat-Dialog ohne Dateizugriff:** Der Bediener fügt `AGENTS.md`, die betroffenen `rules/`, den passenden Standard und `context.md` ein. Der Agent liefert vollständige Dateien bzw. Diffs und am Ende einen Block „Für context.md“ zum Einpflegen.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt (aus skel-ai).
+- 23.10.2026 – auf Einzelprojekt reduziert (`context.md` im Root, Rollen festgelegt).
diff --git a/ai/common/ansible.md b/ai/common/ansible.md
new file mode 100644
index 0000000..a908fb6
--- /dev/null
+++ b/ai/common/ansible.md
@@ -0,0 +1,91 @@
+# Ansible-Standard (verbindlich)
+
+> Gilt für alle Plays und Rollen in diesem Projekt. Verallgemeinert aus einem erprobten Homelab-Setup.
+> Vorlagen: [`templates/ansible/`](templates/ansible/). Geheimnisse: `rules/secrets.md`.
+
+## 1. Verzeichnislayout
+```
+/ansible/
+├── .env.sh # setzt alle Pfade + Secret-Store-Variablen → source (bash/zsh)
+├── ansible.cfg # Fact-Cache, become, Log – Pfade aus .env.sh
+├── inv/
+│ ├── # Inventar (INI oder YAML), z. B. prod, lab
+│ ├── group_vars/all/ # globale Variablen (ansible_user, ans_files, Secret-Store-Pfade)
+│ ├── group_vars/.yml
+│ └── host_vars/.yml
+├── playbooks/ # ein Dienst = ein Play; Präambel = Doku
+├── roles// # Logik: tasks/ handlers/ defaults/ meta/ (keine templates/!)
+└── files// # alle Templates (*.j2) und Dateien – Rollen holen sie aus $ANS_FILES
+```
+`.env.sh` (interaktiv in der Shell gesourct, setzt `ANSIBLE_*`) ist bewusst etwas anderes als die `.env` der Bash-Scripts.
+
+## 2. Aufruf & Umgebung
+- Vor jedem Aufruf: `source ansible/.env.sh`. Pfade **nie** hart codieren: `$ANSIBLE_BASE_PATH`, `$ANS_PLAYDIR`, `$ANS_FILES` (in YAML: `ans_files`).
+- Aufrufe immer: `ansible-playbook $ANS_PLAYDIR/.yml [-e hostlist=…] [--check --diff]`.
+- `.env.sh` setzt fremde `ANSIBLE_*`-Variablen zurück (`unset`), damit kein Kontext eines anderen Repos durchschlägt.
+
+| Variable | Zweck |
+|---|---|
+| `ANSIBLE_BASE_PATH` | Basis für alles |
+| `ANSIBLE_CONFIG`, `ANSIBLE_INVENTORY`, `ANSIBLE_ROLES_PATH` | Standard-Ansible-Variablen |
+| `ANS_PLAYDIR` | Playbooks |
+| `ANS_FILES` | Templates/Dateien (YAML: `ans_files`) |
+| `ANS_DEFAULT_HOSTS` | Dokumentation des Standardziels (s. § 3) |
+| `VAULT_ADDR`, `ANSIBLE_HASHI_VAULT_*`, `ANS_VAULT_KV_MOUNT/PATH` | Secret-Store |
+| `ANSIBLE_VAULT_PASSWORD_FILE` | nur wenn Datei existiert (Fallback Ansible Vault) |
+
+## 3. Playbooks
+- **Präambel max. 12 Zeilen** im Header-Format (`rules/headers.md`): `@file`, `@brief`, `@author`, `@owner`, `@version`, `@usage` mit nötigen/optionalen `-e`-Variablen, Tags und Voraussetzungen.
+- Ziel immer `hosts: "{{ hostlist | default('') }}"` – ohne `-e` genau **ein** bekannter, ungefährlicher Host
+ (Lab/Test). **Nie** `all` oder eine Produktivgruppe als Default.
+- Ein Dienst = ein Play (`radius.yml`, `dns.yml`). Logik liegt in `roles//`; das Play bindet nur die Rolle ein.
+- Dateinamen: Kleinbuchstaben, `_` statt `-` (`vault_check.yml`).
+- Standard-Hilfsplays je Repo: `mini_check.yml` (Erreichbarkeit, Facts), `vault_check.yml` (Secret-Store erreichbar, nur Schlüsselnamen).
+
+## 4. Rollen
+- Rollenname = Dienst. Tasks thematisch teilen: `main.yml` (Ablauf) → `secrets.yml`, `install.yml`, `config.yml`, `verify.yml`.
+- `main.yml` beginnt mit einem **Plattform-Check** (`ansible.builtin.assert` auf `ansible_facts`), Tag `always`.
+- **Templates liegen nicht in der Rolle**, sondern in `files//`; Zugriff über `{{ ans_files }}//…`.
+- Nur Dateien templaten, die wir „besitzen“. Paket-Defaults nicht komplett überschreiben.
+- Konfiguration **vor** dem Restart prüfen: Handler-Kette Check → Restart (`listen`/`notify`), `meta: flush_handlers` vor `service: started`.
+ Bei Fehler bleibt der laufende Dienst unberührt.
+- Jede Rolle hat einen **Smoke-Test** (`verify.yml`, Tag `verify`, `check_mode: false`, `changed_when: false`).
+- **Idempotent:** zweiter Lauf `changed=0`. `command`/`shell` nur mit `creates`/`changed_when`.
+- Module mit FQCN (`ansible.builtin.…`, `community.…`). **Task-Namen in der Amtssprache**, als Satz („Paket installieren“).
+- Variablen der Rolle mit Rollenpräfix (`radius_clients`), Defaults in `defaults/main.yml` mit Kommentar je Variable.
+
+## 5. SSH-Performance
+- `pipelining=True` (Standard in `templates/ansible/ansible.cfg`): Module laufen per stdin statt Kopie nach `/tmp` – spart je Task
+ mehrere SSH-Roundtrips, typisch der größte Einzelgewinn. Voraussetzung: kein `Defaults requiretty` in `sudoers` (aktuelle Distros: Standard).
+- Dazu `ControlMaster=auto` / `ControlPersist=60s`: eine SSH-Verbindung je Host wird wiederverwendet.
+- Bei Fehlern wie „sudo: sorry, you must have a tty“: `requiretty` auf dem Ziel entfernen, nicht `pipelining` abschalten.
+
+## 6. Facts
+- `gathering=smart` + Fact-Cache (`jsonfile`, 86400 s) aus `ansible.cfg`. Facts nutzen statt Shell-Abfragen
+ (`ansible_facts['distribution_major_version']`), Zugriff immer über `ansible_facts[...]`, nicht über `ansible_*`-Kurzformen.
+- Facts neu sammeln: `ansible-playbook $ANS_PLAYDIR/mini_check.yml -e check_mode=setup`.
+
+## 7. Geheimnisse
+- Secret-Store per `community.hashi_vault` (Lookup in `secrets.yml`), Fallback Ansible Vault (`vault_*`-Variablen).
+- Tasks mit Geheimnissen `no_log: true`, Templates mit Geheimnissen `diff: false`. Nicht entfernen.
+- Testbetrieb ohne Secret-Store: `-e _secrets_source=vars -e @/pfad/außerhalb/testsecrets.yml`.
+
+## 8. Tags
+`install`, `config`, `verify`; Plattform-Check und Secrets `always`.
+
+## 9. Kommentare & Stil
+- Deaktivierte, absichtlich behaltene Blöcke mit `#-#` markieren.
+- YAML: 2 Leerzeichen, `---` am Anfang, `true`/`false` statt `yes`/`no`, Strings mit Jinja in Anführungszeichen.
+- Prüfen vor Abgabe: `ansible-lint`, `yamllint`, `ansible-playbook --syntax-check`, `--check --diff`, Lauf gegen lokalen Testcontainer, Idempotenz (`rules/testing.md`).
+
+## 10. Neue Rolle in 5 Schritten
+1. Original-Doku des Dienstes holen (`rules/web_search.md`), Ergebnisse in `knowledge/services/.md`.
+2. `cp -r ai/common/templates/ansible/roles/example roles/`, `files/example` → `files/`, Play aus `playbooks/example.yml`.
+3. Präfix `example_` → `_`, Header ausfüllen.
+4. Lokal testen (Container), Idempotenz prüfen.
+5. Knowledge/`context.md` aktualisieren.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt, verallgemeinert aus infra-devices/ansible.
+- 08.10.2026 – § 5 SSH-Performance (pipelining, ControlPersist) als Vorgabe – Wunsch des Bedieners.
+- 23.10.2026 – Mandantenbezug entfernt (Einzelprojekt).
diff --git a/ai/common/architecture.md b/ai/common/architecture.md
new file mode 100644
index 0000000..1ba355d
--- /dev/null
+++ b/ai/common/architecture.md
@@ -0,0 +1,34 @@
+# Architektur-Dokumentation & Entscheidungen
+
+> Ziel: Jede nicht triviale Lösung ist nachvollziehbar – was, warum, mit welchen Alternativen, und was sie voraussetzt.
+
+## 1. Wann welche Form
+| Umfang | Form | Ort |
+|---|---|---|
+| Einzelne Entscheidung mit Tragweite (Produktwahl, Protokoll, Netzsegmentierung) | **ADR** | `context/knowledge/decisions/adr_NNNN_.md` |
+| Lösung des Projekts | **Architektur-Skizze** (arc42-light, § 2) | `architecture.md` (Root) |
+| Dauerhafter Systembestand (Host, Dienst, Netz) | Knowledge-Eintrag | `context/knowledge/{systems,services,network}/` |
+
+## 2. Architektur-Skizze (arc42-light)
+1. **Ziel & Rahmen** – Anforderungen, Randbedingungen (Budget, Hardware, Datenschutzklasse).
+2. **Kontext** – Diagramm: System, Nutzer, Nachbarsysteme, Schnittstellen (Ports/Protokolle).
+3. **Bausteine** – Komponenten, wo sie laufen, wer sie pflegt (Ansible-Rolle, Docker-Stack, Script).
+4. **Abläufe** – wichtigste 1–3 Szenarien (z. B. Anmeldung, Backup, Ausfall).
+5. **Entscheidungen** – Verweise auf ADRs.
+6. **Risiken & offene Punkte**.
+7. **Betrieb** – Monitoring, Logging (GELF), Backup/Restore, Update-Weg.
+
+## 3. ADR-Format
+Vorlage: [`templates/markdown/adr.md`](templates/markdown/adr.md). Nummern fortlaufend, nie wiederverwenden.
+Überholte ADRs nicht löschen: `status: superseded`, `supersedes`/`superseded_by` pflegen.
+
+## 4. Grundsätze für Lösungen im Netz/IT-Umfeld
+- **Infrastructure as Code:** Neuaufbau = Playbook/Stack + Secret-Store, nicht Handarbeit. Handgriffe, die bleiben, als `howto/` dokumentieren.
+- **Ein Weg pro Aufgabe:** Ansible für Hostkonfiguration, Docker-Compose für Dienste, Bash für Werkzeuge/Glue.
+- **Sicher per Default:** minimale Rechte, Default-Ziel ungefährlich, Trockenlauf vor Änderung, Rückfallweg bei Netzänderungen.
+- **Beobachtbar:** jeder Dienst mit Healthcheck und zentralem Logging.
+- **Wiederherstellbar:** Backup-Pfad und Restore-Test sind Teil der Lösung, nicht Nacharbeit.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt.
+- 23.10.2026 – Architektur-Skizze im Root (Einzelprojekt).
diff --git a/ai/common/bash.md b/ai/common/bash.md
new file mode 100644
index 0000000..3f3dca8
--- /dev/null
+++ b/ai/common/bash.md
@@ -0,0 +1,161 @@
+# Bash-Standard (verbindlich)
+
+> Gilt für jedes Bash-Script dieser Basis und ihrer Projekte. Referenzimplementierung: [`templates/bash/`](templates/bash/).
+> Die Scripts in `bin/` sind nach diesem Standard gebaut und dienen als Beispiele.
+
+## 1. Bausteine – drei Dateien, mehr nicht
+```
+/
+├── mein_script.sh # Präambel (= Hilfe) + Bootstrap + Funktionen + main
+├── _functions.sh # gemeinsame Basis – Kopie aus templates/bash/, NICHT lokal ändern
+├── _functions.local.sh # optional: projektspezifische Funktionen (lädt _functions.sh automatisch)
+├── .env # optional: Einstellungen (nie im Git)
+└── .env.example # Pflicht, wenn .env genutzt wird: gleiche Schlüssel, Platzhalter
+```
+Mehrere Scripts in einem Verzeichnis teilen sich `_functions.sh` und `.env`.
+Das frühere Modell (`_global_functions.sh`, `_project_functions.sh`, `_project_settings.sh`) entfällt:
+Settings → `.env`, Projektfunktionen → `_functions.local.sh`.
+
+## 2. Aufbau eines Scripts
+```bash
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file backup_db.sh
+## @brief Sichert die Datenbank nach $DIR_BACKUP
+## @author
+## @owner
+## @assist KI-unterstützt (), geprüft von
+## @version 1.0.0 (07.10.2026)
+##
+## @usage backup_db.sh [Optionen]
+## @options
+## @OPTIONS@
+## @env .env neben dem Script (Vorlage: .env.example)
+## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler
+#-------------------------------------------------------------------------------
+set -Eeuo pipefail
+
+SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")" # Bootstrap – unverändert übernehmen
+SCRIPT_DIR="${SCRIPT_PATH%/*}"
+SCRIPT_NAME="${SCRIPT_PATH##*/}"
+source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
+
+B_OPTS+=( "k|keep|value:TAGE|14|Sicherungen so lange behalten" )
+b_init "$@"
+set -- "${ARGS[@]}"
+
+## @brief Hauptablauf
+## @param $1 Datenbankname
+## @return 0 ok, 1 Fehler
+main() { … }
+
+main "$@"
+```
+
+### Präambel = Hilfe = Doku
+- Der Header zwischen den ersten beiden `#-----`-Linien ist **gleichzeitig** die Hilfe (`-h`): `b_usage` gibt alle `##`-Zeilen ohne `## ` aus.
+- `## @OPTIONS@` wird durch die Optionsliste aus `B_OPTS` ersetzt → Hilfe und Optionen können nicht auseinanderlaufen.
+- `##` ist der Doku-Marker (wie `/**` bei javadoc). `bin/bashdoc.sh script.sh` erzeugt daraus Markdown. Normale Kommentare mit `#`.
+- Header-Pflichtfelder: `rules/headers.md`.
+
+## 3. Pfadsicherheit
+| Variable | Inhalt |
+|---|---|
+| `SCRIPT_PATH` | absoluter, aufgelöster Pfad des Scripts (Symlinks aufgelöst) |
+| `SCRIPT_DIR` | Verzeichnis des Scripts – Basis für `.env`, `_functions.sh`, relative Pfade aus `.env` |
+| `SCRIPT_NAME` | Dateiname des Scripts |
+| `B_CALL_DIR` | `$PWD` beim Aufruf – Basis für relative Pfade **von der Kommandozeile** |
+| `B_LIB_DIR` | Verzeichnis von `_functions.sh` |
+| `B_TMP_DIR` | Temp-Verzeichnis nach `b_tmpdir`, wird beim Exit gelöscht |
+
+- Nie `cd` als Voraussetzung, nie relative Pfade ohne Basis. Helfer: `b_abspath [basis]` (Default-Basis `SCRIPT_DIR`).
+- Faustregel: Pfade aus `.env`/Defaults → relativ zu `SCRIPT_DIR`; Pfade, die der Benutzer tippt → relativ zu `B_CALL_DIR`.
+
+## 4. Optionen (`B_OPTS`)
+Format je Eintrag: `"kurz|lang|typ[:META]|default|Beschreibung"` – `typ` = `flag` (0/1) oder `value`.
+
+| Quelle | Wo | Hinweis |
+|---|---|---|
+| Standard (`-h -d -q -n -l`) | `_functions.sh` (`B_OPTS_STD`) | immer da |
+| Script-eigen | im Script vor `b_init`: `B_OPTS+=(…)` | Regelfall |
+| Verzeichnisweit | `.env`: `B_OPTS+=(…)` | gilt für alle Scripts des Verzeichnisses |
+
+- Ergebnis: Variable `OPTION_` (Großbuchstaben, `-` → `_`): `--dry-run` → `OPTION_DRY_RUN`, `--keep 7` → `OPTION_KEEP=7`.
+- Unterstützt: `-d`, `-dn` (gebündelt), `-l datei`, `-ldatei`, `--logfile datei`, `--logfile=datei`, `--debug=0`, `--` (Ende der Optionen). Positionsargumente landen in `ARGS` (auch zwischen Optionen).
+- Unbekannte Option / fehlender Wert → Meldung, Hinweis auf `--help`, Exit 2.
+- Immer `+=` verwenden, nie `B_OPTS=(…)` (würde Einträge aus `.env`/`_functions.local.sh` löschen). Kein `|` in Default/Text.
+
+### Vorrang (und Gültigkeit von Umgebungsvariablen)
+**Default < `.env` < Umgebung < Kommandozeile**
+- Exportierte `B_*`/`OPTION_*`-Variablen aus der Umgebung werden vor dem Laden von `.env` gesichert und danach wiederhergestellt
+ (`B_LOG_LEVEL=debug ./script.sh` gewinnt gegen `.env`). Nicht exportierte Shell-Variablen des Aufrufers sieht das Script ohnehin nicht.
+- Für eigene Projektwerte in `.env` dasselbe Verhalten per `VAR="${VAR:-default}"`.
+- `.env` wird **nicht** automatisch exportiert. Werte, die Kindprozesse brauchen, explizit `export`ieren (Geheimnisse möglichst nicht).
+- Wirkung der Standardoptionen: `-d` → `B_LOG_LEVEL=debug`, `-q` → `B_LOG_CONSOLE=0`, `-l` → `B_LOG_FILE`, `-n` → `brun` führt nichts aus.
+
+## 5. Ausgabe & Logging – nie `echo`
+| Funktion | Level | Ziel Konsole | Zweck |
+|---|---|---|---|
+| `becho "…"` | info | stdout, ohne Präfix | Ersatz für `echo` |
+| `bdebug "…"` | debug | stderr, `[DEBUG]` | Details, nur bei `-d` |
+| `bwarn "…"` | warn | stderr, `[WARN]` | Auffälliges, Lauf geht weiter |
+| `berror "…"` | error | stderr, `[FEHLER]` | Fehler melden (Funktion entscheidet über `return`) |
+| `bdie "…" [rc]` | error | stderr | melden + beenden (rc 2 = Aufruffehler mit Hilfe-Hinweis) |
+| `blog "…"` | – | – | Basis aller obigen |
+
+Steuerung (`.env`, Umgebung oder Option):
+| Variable | Default | Bedeutung |
+|---|---|---|
+| `B_LOG_LEVEL` | `info` | Mindestlevel Konsole (`debug`/`info`/`warn`/`error`) |
+| `B_LOG_CONSOLE` | `1` | Konsole an/aus |
+| `B_LOG_FILE` | leer | Logdatei; leer = keine |
+| `B_LOG_FILE_LEVEL` | = `B_LOG_LEVEL` | eigenes Mindestlevel für die Datei |
+| `B_LOG_COLOR` | `auto` | Farben nur auf Terminal |
+
+Logdatei-Format: `JJJJ-MM-TT hh:mm:ss LEVEL script.sh[PID] Text`.
+**Ausnahme vom echo-Verbot:** echte Daten-Ausgabe für Pipes (`printf '%s\n' "$ergebnis"`) – Meldungen gehen nie auf stdout außer über `becho`.
+
+## 6. Fehler, Return-Codes, Aufräumen
+- `set -Eeuo pipefail` ist Pflicht. `b_init` setzt Traps: `EXIT` (Aufräumen), `ERR` (meldet Befehl, Funktion, Zeile), `INT`/`TERM` (Exit 130/143 → EXIT-Trap).
+- **Return-Code-Prinzip:** Jede Funktion endet mit `return 0` (ok), `1` (Fehler) oder `2` (falscher Aufruf); dokumentiert unter `@return`.
+ Fehler meldet die Funktion selbst (`berror`) und gibt den Code zurück – der Aufrufer entscheidet (`if f; then`, `f || return 1`).
+- Ergebnisse über stdout (`x="$(f)"`) oder globale Variablen mit Präfix – nie über Exit-Codes > 2.
+- Aufräumen: Script definiert optional `cleanup() { … }` – wird vom EXIT-Trap mit dem Exit-Code aufgerufen, auch bei Fehler/Abbruch.
+ Temporäres immer über `b_tmpdir` (`$B_TMP_DIR`), wird automatisch entfernt.
+
+## 7. Funktionsdoku („briefly!“)
+```bash
+## @brief Was die Funktion tut – ein Satz
+## @param $1 Bedeutung
+## @param $@ restliche Argumente
+## @return 0 ok, 1 Fehler; Ausgabe auf stdout (falls zutreffend)
+name() {
+```
+- Direkt über der Funktion, keine Leerzeile dazwischen. Interne Helfer mit `_` beginnen (`bashdoc.sh` blendet sie ohne `-p` aus).
+- `local` für alle Variablen in Funktionen.
+
+## 8. Weitere Helfer aus `_functions.sh`
+| Funktion | Zweck |
+|---|---|
+| `brun cmd args…` | ausführen bzw. bei `-n` nur anzeigen (kein `eval`, Argumente als Array) |
+| `b_require prog…` | prüft, ob Programme vorhanden sind |
+| `b_require_vars VAR…` | prüft, ob Variablen gesetzt sind |
+| `b_tmpdir` | legt `$B_TMP_DIR` an |
+| `b_abspath pfad [basis]` | absoluter Pfad |
+| `b_usage` | Hilfe ausgeben |
+
+## 9. Stil
+- Bash ≥ 4.4. Shebang `#!/usr/bin/env bash`. 2 Leerzeichen Einrückung.
+- Immer quoten (`"$var"`, `"${arr[@]}"`), `[[ ]]` statt `[ ]`, `$(…)` statt Backticks, `printf` statt `echo -e`.
+- Dateinamen: Kleinbuchstaben, `_` als Trenner, Endung `.sh`; Hilfsdateien mit `_`-Präfix.
+- Variablen: Konstanten/Globale `GROSS`, lokale `klein`; Präfix `B_`/`_B_`/`OPTION_` ist für `_functions.sh` reserviert.
+- Prüfen vor Abgabe: `bash -n`, `shellcheck -x`, `-h`, `-n`, ein echter Lauf (lokal, `rules/testing.md`).
+
+## 10. Neues Script in 4 Schritten
+1. `cp ai/common/templates/bash/{script_template.sh,_functions.sh,.env.example} /` (`_functions.sh` nur, wenn noch nicht vorhanden).
+2. Umbenennen, Header ausfüllen (Verantwortliche aus `responsible.md`), `B_OPTS` anpassen.
+3. Funktionen mit Kurz-Doku + `main` schreiben.
+4. Testen (§ 9), ggf. `bin/bashdoc.sh` für die Projektdoku.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt; Stack auf `.env` + `_functions.sh` (+ optional `_functions.local.sh`) reduziert.
diff --git a/ai/common/coding.md b/ai/common/coding.md
new file mode 100644
index 0000000..76c28ef
--- /dev/null
+++ b/ai/common/coding.md
@@ -0,0 +1,61 @@
+# Coding – allgemeine Verabredungen (alle Sprachen) + Python
+
+> Sprachspezifisch gehen vor: [bash.md](bash.md), [ansible.md](ansible.md), [docker.md](docker.md).
+
+## 1. Für jede Code-Datei
+- **Header** mit Verantwortlichen (`rules/headers.md`) – ohne Ausnahme, auch für Templates und Configs.
+- **Kurz-Doku je Funktion/Klasse:** Zweck, Parameter, Rückgabe – knapp („briefly!“).
+- **Keine Geheimnisse und keine Umgebungsdaten im Code:** Hosts, IPs, Pfade, Domains, Ports → `.env`/Inventar/Konfiguration.
+- **Fehler nie verschlucken:** melden (Log/stderr) und definierten Code zurückgeben. Exit-Codes: 0 ok · 1 Fehler · 2 Aufruffehler.
+- **Logging statt print/echo** für Meldungen; stdout nur für echte Daten.
+- **Dry-Run** (`-n`/`--dry-run`) für alles, was Systeme verändert.
+- **Idempotent**, wo möglich: zweiter Lauf ändert nichts.
+- **Einfach:** kleinste Lösung, die das Ziel erfüllt; keine Vorratsfunktionen, keine unnötigen Abhängigkeiten.
+
+## 2. Namen
+| Was | Konvention |
+|---|---|
+| Dateien, Verzeichnisse | Englisch, klein, `_` als Trenner (Ausnahme: Konventionen des Werkzeugs, z. B. `docker-compose.yml`, `README.md`) |
+| Variablen/Funktionen | Englisch, sprechend; Bash `snake_case`, Python PEP 8 |
+| Konstanten/Umgebung | `GROSS_MIT_UNTERSTRICH` |
+| Docker/Compose-Namen | `_` statt `-` (`docker.md`) |
+| Kommentare, Meldungen | Amtssprache |
+
+## 3. Konfiguration
+- Reihenfolge überall gleich: **Default < Konfigurationsdatei/.env < Umgebung < Kommandozeile**.
+- `.env` nie im Git, `.env.example` immer mit allen Schlüsseln.
+- Relative Pfade immer zu einer definierten Basis (Script-/Projektverzeichnis), nie zum zufälligen `$PWD`.
+
+## 4. Python
+- Python ≥ 3.11, nur Standardbibliothek, wenn vertretbar. Abhängigkeiten in `requirements.txt` (gepinnt) bzw. `pyproject.toml`.
+- Struktur: `main()` + `if __name__ == "__main__": sys.exit(main())`; Optionen mit `argparse` (Standardoptionen analog Bash: `-d/--debug`, `-q/--quiet`, `-n/--dry-run`, `-l/--logfile`).
+- Logging mit `logging` (Konsole + optional Datei, Level aus Umgebung/Option); kein `print` für Meldungen.
+- Header als Modul-Docstring mit denselben Tags:
+ ```python
+ #!/usr/bin/env python3
+ """
+ @file sync_dns.py
+ @brief Gleicht DNS-Einträge mit dem Inventar ab
+ @author
+ @owner
+ @version 0.1.0 (TT.MM.JJJJ)
+ @usage sync_dns.py [-d] [-n]
+ """
+ ```
+- Funktionen mit Typannotationen und Kurz-Docstring (eine Zeile Zweck, `Args:`/`Returns:` nur wenn nicht offensichtlich).
+- Pfade mit `pathlib`, Basis `Path(__file__).resolve().parent`.
+- Prüfen: `ruff check`, `ruff format --check` (oder `python -m py_compile`), Tests mit `pytest`, lokal.
+
+## 5. Lint & Test (Übersicht)
+| Sprache | Mindestens |
+|---|---|
+| Bash | `bash -n`, `shellcheck -x`, `-h`, `-n`, echter Lauf |
+| Ansible/YAML | `yamllint`, `ansible-lint`, `--syntax-check`, `--check --diff`, Idempotenz |
+| Docker | `docker compose config -q`, Healthcheck `healthy` |
+| Python | `ruff`, `pytest` |
+| Markdown | Links prüfen, `bin/kb_index.sh --check` |
+Was nicht getestet werden konnte, steht ausdrücklich im Ergebnis und in `context.md`.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt.
+- 23.10.2026 – `$PROJ/context.md` → `context.md` (Einzelprojekt).
diff --git a/ai/common/docker.md b/ai/common/docker.md
new file mode 100644
index 0000000..6b04e2a
--- /dev/null
+++ b/ai/common/docker.md
@@ -0,0 +1,189 @@
+# Docker-Compose-Standard (verbindlich)
+
+> Gilt für **jeden** Docker-Stack, der hier entsteht. Nicht zur Diskussion stellen – vollständig „durchdeklinieren“.
+> Abweichungen nur, wenn das Image es technisch erzwingt; dann im Stack-README begründen.
+> Vorlagen: [`templates/docker/`](templates/docker/). Umgebungswerte (`<…>`) stehen in `ai/context/environment.md`.
+
+## 1. Pflichtbestandteile je Service
+| # | Vorgabe | Warum |
+|---|---|---|
+| 1 | `restart: unless-stopped` | stabiler Betrieb, manuelles Stoppen bleibt respektiert |
+| 2 | `logging: *default-logging` (GELF → Graylog) | zentrales Logging |
+| 3 | `container_name: ${COMPOSE_PROJECT_NAME}` (mehrere Services: `${COMPOSE_PROJECT_NAME}_`) | eindeutige Namen, GELF-Tag passt |
+| 4 | `env_file: .env` statt aufgeblähtem `environment:` | Stackfile bleibt transportabel |
+| 5 | Persistenz nur per **Bind-Mount** aus `.env` (`${DIR_*}`), keine Named Volumes | Stack-Verzeichnis = komplettes Backup, umziehbar |
+| 6 | `healthcheck` | „guter Ton“; Test passend zum Image (s. 4.) |
+| 7 | Traefik-Labels (s. 3.) für jeden Service mit Web-Oberfläche/API | Exposition über Reverse Proxy |
+| 8 | Image-Tag aus `.env` (`${_TAG}`), **nie** `latest` | reproduzierbar, Updates bewusst |
+| 9 | **Alle** host-/umgebungsspezifischen Werte in `.env` (Hosts, IPs, Ports, Pfade, Domains, `extra_hosts`) | Stackfile enthält keine Umgebungsdaten |
+| 10 | **Namen ohne Bindestrich – Unterstrich statt `-`:** `COMPOSE_PROJECT_NAME`, Stack-Verzeichnis, Service-, Container- und Router-Namen (z. B. `hashi_vault`, nicht `hashi-vault`) | GELF-Tag = Projektname; `_` läuft in der Elasticsearch-Suche sauber (`-` wird als Worttrenner zerlegt) |
+| 11 | **Header** mit Verantwortlichen im `docker-compose.yml` (`rules/headers.md`) | Zuständigkeit sofort sichtbar |
+
+## 2. Verzeichnislayout je Stack
+```
+// # = COMPOSE_PROJECT_NAME
+├── docker-compose.yml
+├── .env # echte Werte inkl. Secrets – nie in Git/extern
+├── .env.example # gleiche Schlüssel, Platzhalter statt Secrets
+├── README.md # Stack-Doku (s. 5.)
+└── data/ config/ … # Bind-Mount-Ziele (über DIR_* referenziert)
+```
+
+## 3. Vorlagen
+
+### 3.1 `docker-compose.yml`
+```yaml
+x-logging: &default-logging
+ driver: gelf
+ options:
+ gelf-address: "udp://${GELF_HOST}:${GELF_PORT}"
+ tag: "${COMPOSE_PROJECT_NAME}"
+ labels: "com.docker.compose.project,com.docker.compose.service"
+
+services:
+ app:
+ image: /:${APP_TAG}
+ container_name: ${COMPOSE_PROJECT_NAME}
+ restart: unless-stopped
+ env_file: .env
+ # ports: nur wenn Zugriff ohne Traefik nötig ist
+ # - "${APP_PORT}:"
+ extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
+ - "${EXTRA_HOST_1}"
+ volumes:
+ - ${DIR_DATA}:/
+ - ${DIR_CONFIG}:/:ro
+ networks:
+ - proxy
+ labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true (s. 3.2)
+ - "traefik.enable=${TRAEFIK_ENABLE:-false}"
+ - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
+ - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}"
+ # bei Bedarf manuell ergänzen: entrypoints, middlewares …
+ logging: *default-logging
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:/"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 30s
+
+networks:
+ proxy:
+ name: ${TRAEFIK_NETWORK:-traefik-net}
+ external: true
+```
+
+### 3.2 Traefik-Labels: Dummy zuerst, Freischaltung per Schalter
+- Labels stehen **immer** direkt im Stackfile und werden je Stack manuell angepasst (kein `extends`, keine Auslagerung).
+- `traefik.enable=${TRAEFIK_ENABLE:-false}` → solange in `.env` nicht `TRAEFIK_ENABLE=true` steht, ignoriert Traefik den Container: **kein Router, keine Let's-Encrypt-Anfrage.**
+- Ablauf: Stack ohne Traefik hochfahren → Healthcheck `healthy`, Funktion geprüft, FQDN/DNS stimmt → erst dann `TRAEFIK_ENABLE=true` + `docker compose up -d`.
+- Dummy-FQDN `.invalid` (reservierte TLD) verhindert, dass ein vergessener Wert je eine echte Domain anfragt.
+- Mehrere Web-Services: Router-/Service-Namen eindeutig halten (`${COMPOSE_PROJECT_NAME}_`).
+
+### 3.3 `.env` / `.env.example`
+```dotenv
+# --- Stack ---
+COMPOSE_PROJECT_NAME= # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
+APP_TAG=
+
+# --- Logging (Graylog) ---
+GELF_HOST=
+GELF_PORT=12201
+
+# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) ---
+TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!)
+APP_FQDN=
+APP_INTERNAL_PORT=
+#TRAEFIK_NETWORK=traefik-net
+#TRAEFIK_CERTRESOLVER=le
+
+# --- Hosts ---
+EXTRA_HOST_1=:
+
+# --- Pfade (Bind-Mounts, relativ zum Stack) ---
+DIR_DATA=./data
+DIR_CONFIG=./config
+
+# --- Image-spezifische Variablen (laut Orig-Doku, s. README) ---
+```
+
+### 3.4 Geprüfte, aber verworfene Wege (Compose v5.0.2, 06.10.2026)
+- **`label_file:`** – keine Variablen im Schlüssel möglich (Router-Name!) und Werte werden nicht interpoliert. Unbrauchbar für Traefik.
+- **`extends` aus gemeinsamer `traefik.yml`** – technisch ok, vom Bediener abgelehnt: Labels gehören sichtbar und manuell anpassbar ins Stackfile.
+- **Separate `.traefik.env` per `COMPOSE_ENV_FILES` in `.env`** – wirkt nicht; nur als Shell-Variable oder per `--env-file` bei jedem Aufruf → fehleranfällig.
+
+## 4. Hinweise zur Umsetzung
+- **`extra_hosts` in `.env`:** ja, möglich – Compose interpoliert Listenwerte. Ein Eintrag je Variable (`EXTRA_HOST_1=host01.example.org:192.0.2.10`). Schlüssel in Map-Syntax werden *nicht* interpoliert → Listen-Syntax verwenden. Leere Variable ergibt einen ungültigen Eintrag → Zeile nur aufnehmen, wenn genutzt.
+- **Healthcheck:** vorher prüfen, ob `wget`/`curl` im Image existieren (`docker run --rm --entrypoint sh -c 'command -v wget curl'`). Endpunkt wählen, der im Normalbetrieb nicht fälschlich failt (z. B. Vault: `/ui/` statt `/v1/sys/health`, weil versiegelt = 503).
+- **`env_file: .env`** reicht auch Stack-Variablen (GELF_*, DIR_*, TRAEFIK_*) in den Container durch – ist so gewollt und in der Regel harmlos.
+- Kommentare im Stackfile nur für Nicht-Offensichtliches (wie im Vault-Healthcheck).
+
+## 5. Stack-README (Pflicht)
+Jeder Stack bekommt ein `README.md` mit:
+1. **Zweck** und Zugriff (FQDN, ggf. Port).
+2. **Orig-Doku:** Links zu Image-Seite (Docker Hub / GHCR) und Hersteller-Doku, Abrufdatum.
+3. **ENV-Variablen laut Orig-Doku:** Tabelle `Variable | Bedeutung | Default | gesetzt in .env?` – vollständig aus der Herstellerdoku übernommen, nicht geraten.
+4. **Volumes/Pfade** laut Orig-Doku (welcher Container-Pfad persistiert was).
+5. **Ports** intern/extern, Healthcheck-Endpunkt und Begründung.
+6. **Besonderheiten** (Capabilities wie `IPC_LOCK`, Init-/Unseal-Schritte, Update-Hinweise, Abweichungen vom Standard).
+
+Vor dem Schreiben eines Stacks die Orig-Doku per Web-Recherche holen (erlaubt laut `rules/web_search.md`).
+
+## 6. Abnahme-Checkliste
+- [ ] Punkte 1–11 aus Abschnitt 1 erfüllt
+- [ ] Kein `-` und kein `__` in Projekt-, Service-, Container- und Router-Namen
+- [ ] sultan-Host: Port veröffentlicht, kein externes Traefik-Netz, `entrypoints`-Label (7.1)
+- [ ] Keine Hosts/IPs/Domains/Pfade/Secrets im Stackfile
+- [ ] `.env.example` vollständig, ohne Secrets
+- [ ] README mit Orig-Doku und ENV-Tabelle
+- [ ] `docker compose config` läuft fehlerfrei
+- [ ] Healthcheck wird nach Start `healthy` – **erst danach** `TRAEFIK_ENABLE=true`
+
+## 7. Ergänzungen (25.10.2026)
+
+### 7.1 sultan-Variante (Traefik auf anderer VM)
+Gilt für Hosts, deren Traefik woanders läuft und per [sultan](../context/knowledge/services/sultan.md) angebunden ist.
+- **Kein** externes Netz `proxy`/`traefik-net`, **kein** Label `traefik.docker.network`.
+- Web-Service **veröffentlicht** seinen Port: `ports: ["${APP_PORT}:"]` (Bindung `0.0.0.0`; `127.0.0.1` verwirft der sultan-Agent).
+- Label `traefik.http.routers..entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}` gehört dazu; sultan prüft Entrypoint-/Resolver-Namen nicht.
+- `loadbalancer.server.port` = Port **im** Container (der Agent rechnet auf den Host-Port um).
+- Namen: kein doppelter Unterstrich `__` (Trenner bei sultan).
+- Wartung: `docker compose stop` → Wartungsseite; `down` → 404. Für Updates `up -d` (ersetzt Container) ist unkritisch.
+
+### 7.2 Stacks mit mehreren Services / Datenbank
+- Datenbanken als **eigener Stack** (`postgres`), nie veröffentlicht. Der Stack legt ein benanntes Netz an (`postgres_net`);
+ Anwendungen hängen es als `external: true` an. Je Anwendung eigene Datenbank + eigener User.
+- Innerhalb eines Stacks: `depends_on` mit `condition: service_healthy`.
+- Anwendung braucht Internet/Port → zusätzlich Netz `default`.
+
+### 7.3 `.env`: Erzeugung und Geheimnisse
+- `.env.example` ist die **einzige Quelle** der Schlüssel. Platzhalter:
+ `@@SECRET@@` → beim ersten Deployment zufällig erzeugt (je Schlüssel eigener Wert); `<…>` → vom Betreiber zu setzen.
+- Die echte `.env` erzeugt Ansible **nur, wenn sie fehlt**; danach gehört sie dem Betreiber (kein Überschreiben).
+ Später ersetzen Vault-Einträge die Werte (`rules/secrets.md`).
+- Werte, die mehrere Stacks brauchen (DB-Passwort), stehen in der `.env` der **Anwendung**; Ansible liest sie dort und legt damit DB/User an (über stdin, nie als Argument).
+
+### 7.4 Deployment per Ansible
+- Rolle `compose_stack` (`ansible/roles/compose_stack`): Stack-Verzeichnis `//`, Dateien aus `stacks//`,
+ Bind-Mount-Ziele mit Eigentümer/Rechten, `.env` (s. 7.3), `community.docker.docker_compose_v2` mit `wait`.
+- Datenbank je Anwendung: Rolle `pg_app_db`.
+
+### 7.5 Rechte, Dateisystem
+- Bind-Mount-Ziele mit dem Eigentümer des Containers anlegen (z. B. `1000:1000`); im Stack-README dokumentieren.
+- Auf btrfs: Datenbank-Verzeichnisse vor dem ersten Start `chattr +C` (kein Copy-on-Write, weniger Fragmentierung; Prüfsummen entfallen dort).
+
+### 7.6 Backup & Update
+- Datenbanken per Dump sichern (`pg_dumpall`/`pg_dump` vor dem Borgmatic-Lauf) – das reine Kopieren des Datenverzeichnisses im Betrieb ist kein konsistentes Backup.
+- Update: neuen Tag in `.env` → Backup → `docker compose pull && docker compose up -d`. Rückweg: alter Tag + Restore.
+ **Kein automatisches `docker system prune -a`** – das alte Image bleibt für den schnellen Rückweg.
+
+## Festgelegte Werte
+``, `TRAEFIK_NETWORK`, `TRAEFIK_CERTRESOLVER`, `GELF_HOST`/`GELF_PORT` stehen in [`ai/context/environment.md`](../context/environment.md).
+
+## Änderungsprotokoll
+- 07.10.2026 – übernommen und verallgemeinert (Mandantenwerte → Platzhalter, Header-Pflicht ergänzt).
+- 23.10.2026 – Mandantenbezug entfernt, Werte nur noch in `environment.md`; Verweis auf `web_search.md` korrigiert.
+- 25.10.2026 – § 7: sultan-Variante, Mehrfach-Services/DB, `.env`-Erzeugung (`@@SECRET@@`), Deployment per Ansible, Rechte/btrfs, Backup/Update ohne Prune – Entscheidungen des Betreibers vom 23.–25.10.2026.
diff --git a/ai/common/markdown.md b/ai/common/markdown.md
new file mode 100644
index 0000000..77629d6
--- /dev/null
+++ b/ai/common/markdown.md
@@ -0,0 +1,24 @@
+# Markdown & Dokumentation
+
+## 1. Allgemein
+- Eine `#`-Überschrift pro Datei (= Titel), darunter Gliederung mit `##`/`###`. Optional direkt unter dem Titel ein Zitatblock `> Stand: TT.MM.JJJJ · …`.
+- Kurz, konkret, prüfbar: Tabellen für Fakten, Checklisten (`- [ ]`) für Abläufe/Offenes, Code-Blöcke mit Sprache (` ```bash `).
+- Datumsangaben im Text absolut `TT.MM.JJJJ`; in Frontmatter/Dateinamen ISO `JJJJ-MM-TT`.
+- Relative Links innerhalb der Basis (`[bash.md](../common/bash.md)`), keine absoluten Pfade.
+- Quellen als Link + Abrufdatum. Unbelegtes als „Einschätzung“ kennzeichnen.
+- Platzhalter: `` (auszufüllen), `` (offener Punkt); Vorlagen-Variablen für Scripts: `@@NAME@@`.
+- Diagramme: Mermaid oder ASCII im Code-Block (lesbar ohne Renderer); keine Binärbilder ohne Not.
+
+## 2. READMEs
+Jeder Ordner der Basis hat eine `README.md`: Zweck, Inhalt (Tabelle Datei → Inhalt), Pflege (wer, wann, wie).
+
+## 3. Knowledge-Einträge
+Format und Ablage: [`../context/knowledge/README.md`](../context/knowledge/README.md); Vorlage: [`templates/markdown/knowledge_entry.md`](templates/markdown/knowledge_entry.md).
+
+## 4. `context.md` (Arbeitsgedächtnis im Root)
+Abschnitte nicht umbenennen (Agenten suchen danach):
+Metadaten · Ziel · Stand · Entscheidungen · Offene Fragen · Nächster Schritt · Sitzungsprotokoll.
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt.
+- 23.10.2026 – `context.md` liegt im Root (Einzelprojekt).
diff --git a/ai/common/templates/ansible/.env.sh b/ai/common/templates/ansible/.env.sh
new file mode 100644
index 0000000..1bf56ae
--- /dev/null
+++ b/ai/common/templates/ansible/.env.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file .env.sh
+## @brief Ansible-Kontext für dieses Repo setzen (Pfade + Secret-Store)
+## @author
+## @owner
+## @version 1.0.0 (07.10.2026)
+## @usage source ansible/.env.sh (bash und zsh, aus beliebigem Verzeichnis)
+#-------------------------------------------------------------------------------
+# Pfad dieser Datei – bash: BASH_SOURCE, zsh: %x
+_ANS_SRC="${BASH_SOURCE[0]:-${(%):-%x}}"
+_ANS_DIR="$(cd "$(dirname "$_ANS_SRC")" && pwd -P)"
+
+# --- Ansible-Pfade (fremden Kontext zurücksetzen) ---
+unset ANSIBLE_CONFIG ANSIBLE_INVENTORY ANSIBLE_ROLES_PATH ANSIBLE_VAULT_PASSWORD_FILE
+export ANSIBLE_BASE_PATH="$_ANS_DIR"
+export ANSIBLE_CONFIG="$ANSIBLE_BASE_PATH/ansible.cfg"
+export ANSIBLE_INVENTORY="$ANSIBLE_BASE_PATH/inv"
+export ANSIBLE_ROLES_PATH="$ANSIBLE_BASE_PATH/roles"
+export ANS_PLAYDIR="$ANSIBLE_BASE_PATH/playbooks"
+export ANS_FILES="$ANSIBLE_BASE_PATH/files"
+export ANS_DEFAULT_HOSTS="" # nur Doku – Default steht im Play
+
+# --- Secret-Store (HashiCorp Vault, bevorzugt) – Token aus ~/.vault-token ---
+export VAULT_ADDR=""
+export ANSIBLE_HASHI_VAULT_ADDR="$VAULT_ADDR"
+export ANSIBLE_HASHI_VAULT_AUTH_METHOD="token"
+export ANS_VAULT_KV_MOUNT="secret"
+export ANS_VAULT_KV_PATH=""
+
+# --- Ansible Vault (nur Fallback, Passwortdatei außerhalb des Repos) ---
+[ -f "$HOME/.ans-secret-" ] && export ANSIBLE_VAULT_PASSWORD_FILE="$HOME/.ans-secret-"
+
+unset _ANS_DIR _ANS_SRC
+echo "Ansible-Kontext aktiv: ANSIBLE_BASE_PATH=$ANSIBLE_BASE_PATH"
diff --git a/ai/common/templates/ansible/ansible.cfg b/ai/common/templates/ansible/ansible.cfg
new file mode 100644
index 0000000..063af13
--- /dev/null
+++ b/ai/common/templates/ansible/ansible.cfg
@@ -0,0 +1,24 @@
+## @file ansible.cfg – Pfade kommen aus .env.sh (source ansible/.env.sh)
+[defaults]
+inventory=$ANSIBLE_BASE_PATH/inv
+roles_path=$ANSIBLE_BASE_PATH/roles
+log_path=$ANSIBLE_BASE_PATH/ansible.log
+fact_caching=jsonfile
+fact_caching_connection=$ANSIBLE_BASE_PATH/fact_caching
+fact_caching_prefix=facts_
+fact_caching_timeout=86400
+gathering=smart
+interpreter_python=auto_silent
+retry_files_enabled=False
+stdout_callback=default
+callback_result_format=yaml
+
+[privilege_escalation]
+become=True
+become_method=sudo
+become_user=root
+become_ask_pass=False
+
+[ssh_connection]
+ssh_args=-o ControlMaster=auto -o ControlPersist=60s
+pipelining=True
diff --git a/ai/common/templates/ansible/files/example/example.conf.j2 b/ai/common/templates/ansible/files/example/example.conf.j2
new file mode 100644
index 0000000..8b5fa94
--- /dev/null
+++ b/ai/common/templates/ansible/files/example/example.conf.j2
@@ -0,0 +1,10 @@
+{#-
+## @file example.conf.j2
+## @brief Konfiguration für – verwaltet von Ansible (Rolle example)
+## @author
+## @owner
+## @version 0.1.0 (TT.MM.JJJJ)
+-#}
+# {{ ansible_managed }}
+# Nicht von Hand ändern – Änderungen über Ansible.
+password = {{ example_secrets.password | default('') }}
diff --git a/ai/common/templates/ansible/inv/group_vars/all/vars.yml b/ai/common/templates/ansible/inv/group_vars/all/vars.yml
new file mode 100644
index 0000000..2738880
--- /dev/null
+++ b/ai/common/templates/ansible/inv/group_vars/all/vars.yml
@@ -0,0 +1,10 @@
+---
+## @brief Globale Variablen für alle Hosts
+ansible_user: ""
+
+# Ablage für Templates/Dateien (aus .env.sh, Fallback relativ zum Repo)
+ans_files: "{{ lookup('env', 'ANS_FILES') | default(playbook_dir ~ '/../files', true) }}"
+
+# Secret-Store (KV v2) – Werte aus .env.sh
+ans_vault_kv_mount: "{{ lookup('env', 'ANS_VAULT_KV_MOUNT') | default('secret', true) }}"
+ans_vault_kv_path: "{{ lookup('env', 'ANS_VAULT_KV_PATH') | default('', true) }}"
diff --git a/ai/common/templates/ansible/inv/lab b/ai/common/templates/ansible/inv/lab
new file mode 100644
index 0000000..606a46c
--- /dev/null
+++ b/ai/common/templates/ansible/inv/lab
@@ -0,0 +1,3 @@
+## @file inv/lab – Inventar Testumgebung (Hosts aus ai/context/knowledge/systems/)
+[example]
+
diff --git a/ai/common/templates/ansible/playbooks/example.yml b/ai/common/templates/ansible/playbooks/example.yml
new file mode 100644
index 0000000..541c797
--- /dev/null
+++ b/ai/common/templates/ansible/playbooks/example.yml
@@ -0,0 +1,16 @@
+#-------------------------------------------------------------------------------
+## @file example.yml
+## @brief installieren + konfigurieren (Rolle example, Templates aus $ANS_FILES/example)
+## @author
+## @owner
+## @version 0.1.0 (TT.MM.JJJJ)
+## @usage ansible-playbook $ANS_PLAYDIR/example.yml [-e hostlist=host1,gruppe]
+## Tags: install | config | verify Trockenlauf: --check --diff
+## ohne Secret-Store: -e example_secrets_source=vars -e @
+## @requires source ansible/.env.sh und vault login
+#-------------------------------------------------------------------------------
+---
+- name: Example
+ hosts: "{{ hostlist | default('') }}"
+ roles:
+ - example
diff --git a/ai/common/templates/ansible/playbooks/mini_check.yml b/ai/common/templates/ansible/playbooks/mini_check.yml
new file mode 100644
index 0000000..a78e9e2
--- /dev/null
+++ b/ai/common/templates/ansible/playbooks/mini_check.yml
@@ -0,0 +1,28 @@
+#-------------------------------------------------------------------------------
+## @file mini_check.yml
+## @brief Erreichbarkeit prüfen und Kernfakten anzeigen; -e check_mode=setup sammelt Facts neu
+## @author
+## @owner
+## @version 1.0.0 (07.10.2026)
+## @usage ansible-playbook $ANS_PLAYDIR/mini_check.yml [-e hostlist=…] [-e check_mode=setup]
+#-------------------------------------------------------------------------------
+---
+- name: Mini-Check
+ hosts: "{{ hostlist | default('') }}"
+ gather_facts: false
+ tasks:
+ - name: Erreichbarkeit prüfen
+ ansible.builtin.ping:
+
+ - name: Facts neu sammeln
+ ansible.builtin.setup:
+ when: (check_mode | default('')) == 'setup'
+
+ - name: Facts aus Cache/Host laden
+ ansible.builtin.setup:
+ gather_subset: [min]
+ when: ansible_facts['distribution'] is not defined
+
+ - name: Kernfakten anzeigen
+ ansible.builtin.debug:
+ msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}, Kernel {{ ansible_facts['kernel'] }}"
diff --git a/ai/common/templates/ansible/roles/example/defaults/main.yml b/ai/common/templates/ansible/roles/example/defaults/main.yml
new file mode 100644
index 0000000..3f1f55f
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/defaults/main.yml
@@ -0,0 +1,10 @@
+---
+## @brief Defaults der Rolle example – alle Variablen mit Präfix example_
+
+example_packages: [example] # zu installierende Pakete
+example_service: example # systemd-Unit
+example_config_file: /etc/example/example.conf # Ziel der eigenen Konfiguration
+example_config_check_cmd: "example --check-config" # Prüfbefehl vor jedem Restart
+example_smoke_test: true # verify.yml ausführen
+example_secrets_source: vault # vault | vars (Test)
+example_test_secrets: {} # nur mit secrets_source=vars, Datei außerhalb des Repos
diff --git a/ai/common/templates/ansible/roles/example/handlers/main.yml b/ai/common/templates/ansible/roles/example/handlers/main.yml
new file mode 100644
index 0000000..e767d43
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/handlers/main.yml
@@ -0,0 +1,13 @@
+---
+## @brief Handler-Kette: erst Konfiguration prüfen, dann neu starten
+
+- name: Konfiguration prüfen
+ ansible.builtin.command: "{{ example_config_check_cmd }}"
+ changed_when: false
+ listen: example config changed
+
+- name: Dienst neu starten
+ ansible.builtin.service:
+ name: "{{ example_service }}"
+ state: restarted
+ listen: example config changed
diff --git a/ai/common/templates/ansible/roles/example/meta/main.yml b/ai/common/templates/ansible/roles/example/meta/main.yml
new file mode 100644
index 0000000..acd342b
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ role_name: example
+ author: ""
+ description: " installieren und konfigurieren"
+ license: proprietary
+ min_ansible_version: "2.15"
+ platforms:
+ - name: Debian
+ versions: [trixie]
+dependencies: []
diff --git a/ai/common/templates/ansible/roles/example/tasks/config.yml b/ai/common/templates/ansible/roles/example/tasks/config.yml
new file mode 100644
index 0000000..3ea4546
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/tasks/config.yml
@@ -0,0 +1,13 @@
+---
+## @brief Eigene Konfigurationsdateien schreiben (Templates aus files/example/)
+
+- name: Konfiguration schreiben
+ ansible.builtin.template:
+ src: "{{ ans_files }}/example/example.conf.j2"
+ dest: "{{ example_config_file }}"
+ owner: root
+ group: root
+ mode: "0640"
+ backup: true
+ diff: false # Template enthält Geheimnisse – nicht entfernen
+ notify: example config changed
diff --git a/ai/common/templates/ansible/roles/example/tasks/install.yml b/ai/common/templates/ansible/roles/example/tasks/install.yml
new file mode 100644
index 0000000..413aaf9
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/tasks/install.yml
@@ -0,0 +1,9 @@
+---
+## @brief Pakete installieren
+
+- name: Pakete installieren
+ ansible.builtin.apt:
+ name: "{{ example_packages }}"
+ state: present
+ update_cache: true
+ cache_valid_time: 3600
diff --git a/ai/common/templates/ansible/roles/example/tasks/main.yml b/ai/common/templates/ansible/roles/example/tasks/main.yml
new file mode 100644
index 0000000..ddae127
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/tasks/main.yml
@@ -0,0 +1,39 @@
+---
+## @brief Rolle example – Ablauf: prüfen → Geheimnisse → installieren → konfigurieren → testen
+
+- name: Plattform prüfen (Debian >= 13 erwartet)
+ ansible.builtin.assert:
+ that:
+ - ansible_facts['os_family'] == 'Debian'
+ - ansible_facts['distribution_major_version'] | int >= 13
+ fail_msg: "{{ inventory_hostname }}: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }} – Rolle ist für Debian >= 13 gebaut."
+ quiet: true
+ tags: [always]
+
+- name: Geheimnisse laden
+ ansible.builtin.import_tasks: secrets.yml
+ tags: [always]
+
+- name: Installieren
+ ansible.builtin.import_tasks: install.yml
+ tags: [install]
+
+- name: Konfigurieren
+ ansible.builtin.import_tasks: config.yml
+ tags: [config]
+
+- name: Handler jetzt ausführen (Config-Check vor Restart)
+ ansible.builtin.meta: flush_handlers
+ tags: [always]
+
+- name: Dienst aktiv und gestartet
+ ansible.builtin.service:
+ name: "{{ example_service }}"
+ state: started
+ enabled: true
+ tags: [install, config]
+
+- name: Smoke-Test
+ ansible.builtin.import_tasks: verify.yml
+ when: example_smoke_test | bool
+ tags: [verify]
diff --git a/ai/common/templates/ansible/roles/example/tasks/secrets.yml b/ai/common/templates/ansible/roles/example/tasks/secrets.yml
new file mode 100644
index 0000000..5685ecc
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/tasks/secrets.yml
@@ -0,0 +1,14 @@
+---
+## @brief Geheimnisse aus dem Secret-Store (Default) oder aus Testvariablen laden
+
+- name: Geheimnisse aus dem Secret-Store lesen
+ ansible.builtin.set_fact:
+ example_secrets: "{{ lookup('community.hashi_vault.vault_kv2_get', ans_vault_kv_path ~ '/example', engine_mount_point=ans_vault_kv_mount).secret }}"
+ when: example_secrets_source == 'vault'
+ no_log: true
+
+- name: Geheimnisse aus Testvariablen übernehmen
+ ansible.builtin.set_fact:
+ example_secrets: "{{ example_test_secrets }}"
+ when: example_secrets_source == 'vars'
+ no_log: true
diff --git a/ai/common/templates/ansible/roles/example/tasks/verify.yml b/ai/common/templates/ansible/roles/example/tasks/verify.yml
new file mode 100644
index 0000000..d11bdd7
--- /dev/null
+++ b/ai/common/templates/ansible/roles/example/tasks/verify.yml
@@ -0,0 +1,9 @@
+---
+## @brief Smoke-Test: Dienst läuft und antwortet
+
+- name: Dienststatus abfragen
+ ansible.builtin.command: systemctl is-active {{ example_service }}
+ register: example_active
+ check_mode: false
+ changed_when: false
+ failed_when: example_active.stdout != 'active'
diff --git a/ai/common/templates/bash/.env.example b/ai/common/templates/bash/.env.example
new file mode 100644
index 0000000..5f7c353
--- /dev/null
+++ b/ai/common/templates/bash/.env.example
@@ -0,0 +1,21 @@
+# .env – Einstellungen für die Scripts in diesem Verzeichnis
+# Wird von b_init gesourct (Bash-Syntax). Echte .env nie ins Git – nur .env.example.
+# Vorrang: Default < .env < Umgebung (export / VAR=… ./script.sh) < Kommandozeile
+# Relative Pfade gelten relativ zum Script-Verzeichnis, nicht zum aufrufenden $PWD.
+
+## Logging ---------------------------------------------------------------------
+B_LOG_LEVEL=info # debug | info | warn | error (-d setzt debug)
+B_LOG_CONSOLE=1 # 0 = still (-q)
+B_LOG_FILE= # z. B. "log/${SCRIPT_NAME%.sh}.log"; leer = keine Datei (-l DATEI)
+B_LOG_FILE_LEVEL= # leer = wie B_LOG_LEVEL; z. B. debug für ausführliche Logdatei
+B_LOG_COLOR=auto # auto | 1 | 0
+
+## Optionen vorbelegen (überschreibt den Default aus der Definition) ------------
+#OPTION_DRY_RUN=1
+
+## Zusätzliche Optionen für alle Scripts dieses Verzeichnisses – immer mit += ---
+# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
+#B_OPTS+=( "k|keep|flag|0|Zwischendateien behalten" )
+
+## Projektwerte – mit ${VAR:-default}, damit die Umgebung gewinnt ---------------
+#DIR_BACKUP="${DIR_BACKUP:-/srv/backup}"
diff --git a/ai/common/templates/bash/_functions.sh b/ai/common/templates/bash/_functions.sh
new file mode 100644
index 0000000..247ce0a
--- /dev/null
+++ b/ai/common/templates/bash/_functions.sh
@@ -0,0 +1,415 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file _functions.sh
+## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap
+## @author
+## @owner
+## @version 1.0.0 (07.10.2026)
+##
+## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}"
+## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4.
+## Kopien NICHT lokal ändern – Projektspezifisches gehört in
+## _functions.local.sh (wird automatisch geladen, falls vorhanden).
+## Standard: ai/common/bash.md
+#-------------------------------------------------------------------------------
+
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ echo "_functions.sh wird gesourct, nicht ausgeführt." >&2
+ exit 2
+fi
+[[ -n "${_B_LOADED:-}" ]] && return 0
+if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
+ echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2
+ return 1
+fi
+_B_LOADED=1
+B_FUNCTIONS_VERSION="1.0.0"
+
+# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ----------------------
+# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback.
+: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}"
+: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}"
+: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}"
+B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")"
+B_LIB_DIR="${B_LIB_DIR%/*}"
+B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde
+B_TMP_DIR="" # wird von b_tmpdir gesetzt
+
+# --- Standard-Optionen (gelten für jedes Script) ------------------------------
+# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
+# Variable im Script: OPTION_ (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN
+B_OPTS_STD=(
+ "h|help|flag|0|diese Hilfe anzeigen"
+ "d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)"
+ "q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)"
+ "n|dry-run|flag|0|nur anzeigen, nichts ändern"
+ "l|logfile|value:DATEI||zusätzlich in DATEI loggen"
+)
+declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an
+declare -ga ARGS=() # Positionsargumente nach b_init
+
+declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40)
+declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=()
+declare -ga _B_OPT_ORDER=()
+
+#===============================================================================
+# Ausgabe / Logging
+#===============================================================================
+
+## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level
+## @param $1 Level: debug | info | warn | error
+## @param $@ Text
+## @return 0 ok, 2 unbekanntes Level
+blog() {
+ local level="${1:-}"; shift || true
+ local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r=""
+ [[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; }
+
+ if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then
+ if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then
+ r=$'\e[0m'
+ case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac
+ fi
+ case "$level" in
+ info) printf '%s\n' "$msg" ;;
+ debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ esac
+ fi
+
+ if [[ -n "${B_LOG_FILE:-}" ]] \
+ && (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then
+ printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \
+ >> "$B_LOG_FILE" 2>/dev/null || true
+ fi
+ return 0
+}
+
+## @brief Ersatz für echo: Info-Meldung über blog
+## @param $@ Text
+## @return 0
+becho() { blog info "$@"; }
+
+## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug)
+## @param $@ Text
+## @return 0
+bdebug() { blog debug "$@"; }
+
+## @brief Warnung (stderr)
+## @param $@ Text
+## @return 0
+bwarn() { blog warn "$@"; }
+
+## @brief Fehlermeldung (stderr)
+## @param $@ Text
+## @return 0
+berror() { blog error "$@"; }
+
+## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf)
+## @param $1 Text
+## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis)
+## @return – (beendet das Script)
+bdie() {
+ local rc="${2:-1}"
+ berror "${1:-Abbruch}"
+ (( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help"
+ exit "$rc"
+}
+
+#===============================================================================
+# Hilfen für den Script-Alltag
+#===============================================================================
+
+## @brief Befehl ausführen – oder bei --dry-run nur anzeigen
+## @param $@ Befehl mit Argumenten (kein String, kein eval)
+## @return Exit-Code des Befehls, 0 im Dry-Run
+brun() {
+ if (( ${OPTION_DRY_RUN:-0} )); then
+ becho "[dry-run] $(printf '%q ' "$@")"
+ return 0
+ fi
+ bdebug "run: $(printf '%q ' "$@")"
+ "$@"
+}
+
+## @brief Prüft, ob Programme vorhanden sind
+## @param $@ Programmnamen
+## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet)
+b_require() {
+ local cmd rc=0
+ for cmd in "$@"; do
+ command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; }
+ done
+ return "$rc"
+}
+
+## @brief Prüft, ob Variablen gesetzt und nicht leer sind
+## @param $@ Variablennamen
+## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet)
+b_require_vars() {
+ local v rc=0
+ for v in "$@"; do
+ [[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; }
+ done
+ return "$rc"
+}
+
+## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht
+## @return 0 ok, 1 mktemp fehlgeschlagen
+b_tmpdir() {
+ [[ -n "$B_TMP_DIR" ]] && return 0
+ B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1
+ bdebug "Temp-Verzeichnis: $B_TMP_DIR"
+}
+
+## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2
+## @param $1 Pfad
+## @param $2 Basisverzeichnis (Default: SCRIPT_DIR)
+## @return 0; Ergebnis auf stdout
+b_abspath() {
+ local p="$1" base="${2:-$SCRIPT_DIR}"
+ [[ "$p" == /* ]] || p="${base%/}/${p}"
+ printf '%s\n' "$p"
+}
+
+#===============================================================================
+# Usage aus der Präambel
+#===============================================================================
+
+## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt,
+## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt)
+## @return 0
+b_usage() {
+ local line part=0
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ^#-{10,} ]]; then
+ (( ++part >= 2 )) && break
+ continue
+ fi
+ (( part == 1 )) && [[ "$line" == '##'* ]] || continue
+ line="${line#'##'}"; line="${line# }"
+ if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi
+ done < "$SCRIPT_PATH"
+ return 0
+}
+
+## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS)
+## @return 0
+_b_opts_help() {
+ local long s left d
+ for long in "${_B_OPT_ORDER[@]}"; do
+ s="${_B_OPT_SHORT[$long]}"
+ left="${s:+-$s | }--${long}"
+ [[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}"
+ d="${_B_OPT_DEFAULT[$long]}"
+ if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then
+ printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d"
+ else
+ printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}"
+ fi
+ done
+ return 0
+}
+
+#===============================================================================
+# Optionen
+#===============================================================================
+
+## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN
+## @param $1 Langname
+## @return 0; Name auf stdout
+_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; }
+
+## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie
+## @return 0 ok, 2 Definitionsfehler
+_b_opts_build() {
+ local def s long type meta deflt desc rest
+ local -A seen_short=()
+ _B_OPT_ORDER=()
+ for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do
+ IFS='|' read -r s long type deflt desc rest <<< "$def"
+ meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT"
+ if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then
+ berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)"
+ return 2
+ fi
+ if [[ -n "$s" ]]; then
+ [[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && {
+ berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; }
+ seen_short[$s]="$long"
+ fi
+ [[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long")
+ _B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta"
+ _B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc"
+ done
+ return 0
+}
+
+## @brief Setzt OPTION_ für eine erkannte Option
+## @param $1 Langname
+## @param $2 Wert
+## @return 0 ok, 2 ungültiger Wert
+_b_opt_set() {
+ local long="$1" val="$2" var
+ var="$(_b_optvar "$long")"
+ if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then
+ berror "Option --$long erwartet 0 oder 1, nicht \"$val\""
+ return 2
+ fi
+ printf -v "$var" '%s' "$val"
+ _B_CLI_SET[$long]=1
+ return 0
+}
+
+## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente)
+## @param $@ Argumente des Scripts
+## @return 0 ok, 2 Aufruffehler
+_b_opts_parse() {
+ local arg name val s i long
+ ARGS=()
+ while (( $# )); do
+ arg="$1"; shift
+ case "$arg" in
+ --) ARGS+=("$@"); break ;;
+ --?*)
+ name="${arg#--}"; val=""
+ if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi
+ [[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; }
+ if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then
+ (( $# )) || { berror "Option --$name braucht einen Wert"; return 2; }
+ val="$1"; shift
+ elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then
+ val=1
+ fi
+ _b_opt_set "$name" "$val" || return 2
+ ;;
+ -?*)
+ for (( i = 1; i < ${#arg}; i++ )); do
+ s="${arg:i:1}"; long=""
+ for name in "${_B_OPT_ORDER[@]}"; do
+ [[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; }
+ done
+ [[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; }
+ if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then
+ val="${arg:i+1}" # -lDATEI oder -l DATEI
+ if [[ -z "$val" ]]; then
+ (( $# )) || { berror "Option -$s braucht einen Wert"; return 2; }
+ val="$1"; shift
+ fi
+ _b_opt_set "$long" "$val" || return 2
+ break
+ fi
+ _b_opt_set "$long" 1 || return 2
+ done
+ ;;
+ *) ARGS+=("$arg") ;;
+ esac
+ done
+ return 0
+}
+
+#===============================================================================
+# Trap / Aufräumen
+#===============================================================================
+
+## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR
+## @return – (beendet mit dem ursprünglichen Exit-Code)
+_b_on_exit() {
+ local rc=$?
+ trap - EXIT
+ set +e
+ if declare -F cleanup &>/dev/null; then
+ cleanup "$rc" || bwarn "cleanup() meldete Fehler"
+ fi
+ [[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR"
+ bdebug "Ende ${SCRIPT_NAME} (rc=${rc})"
+ exit "$rc"
+}
+
+## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile
+## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug)
+## @param $1 Zeilennummer
+## @param $2 Befehl
+## @return 0 (Abbruch übernimmt set -e)
+_b_on_err() {
+ local rc=$? lvl=error
+ [[ "$2" == return* ]] && lvl=debug
+ blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}"
+ return 0
+}
+
+#===============================================================================
+# Einstieg
+#===============================================================================
+
+## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe
+## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile
+## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}")
+## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2)
+b_init() {
+ local v long var env_file
+ local -A env_keep=()
+ declare -gA _B_CLI_SET=()
+
+ trap _b_on_exit EXIT
+ trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR
+ trap 'exit 130' INT
+ trap 'exit 143' TERM
+
+ # 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env)
+ for v in $(compgen -e); do
+ [[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}"
+ done
+
+ # 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD)
+ env_file="$(b_abspath "${B_ENV_FILE:-.env}")"
+ if [[ -f "$env_file" ]]; then
+ # shellcheck source=/dev/null
+ source "$env_file" || bdie "Fehler beim Laden von $env_file"
+ fi
+ for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done
+
+ # 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat
+ : "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \
+ "${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}"
+ [[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; }
+ [[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")"
+
+ _b_opts_build || exit 2
+ for long in "${_B_OPT_ORDER[@]}"; do
+ var="$(_b_optvar "$long")"
+ [[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}"
+ done
+
+ # 4. Kommandozeile
+ _b_opts_parse "$@" || bdie "Aufruffehler" 2
+
+ # 5. Standard-Optionen anwenden
+ (( OPTION_DEBUG )) && B_LOG_LEVEL=debug
+ (( OPTION_QUIET )) && B_LOG_CONSOLE=0
+ if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then
+ B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD
+ elif [[ -n "$OPTION_LOGFILE" ]]; then
+ B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")"
+ fi
+ if [[ -n "$B_LOG_FILE" ]]; then
+ mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \
+ || { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; }
+ fi
+
+ (( OPTION_HELP )) && { b_usage; exit 0; }
+
+ bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}"
+ bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}"
+ for long in "${_B_OPT_ORDER[@]}"; do
+ var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}"
+ done
+ return 0
+}
+
+# --- projektspezifische Erweiterungen -----------------------------------------
+if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then
+ # shellcheck source=/dev/null
+ source "${B_LIB_DIR}/_functions.local.sh"
+fi
diff --git a/ai/common/templates/bash/script_template.sh b/ai/common/templates/bash/script_template.sh
new file mode 100755
index 0000000..35498e5
--- /dev/null
+++ b/ai/common/templates/bash/script_template.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file script_template.sh
+## @brief
+## @author
+## @owner
+## @assist
+## @version 0.1.0 (TT.MM.JJJJ)
+##
+## @usage script_template.sh [Optionen]
+## @options
+## @OPTIONS@
+## @env .env neben dem Script (Vorlage: .env.example)
+## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler
+#-------------------------------------------------------------------------------
+set -Eeuo pipefail
+
+# --- Bootstrap (unverändert lassen) -------------------------------------------
+SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
+SCRIPT_DIR="${SCRIPT_PATH%/*}"
+SCRIPT_NAME="${SCRIPT_PATH##*/}"
+# shellcheck source=_functions.sh
+source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
+
+# Script-eigene Optionen; -h -d -q -n -l kommen automatisch aus _functions.sh
+B_OPTS+=(
+ "f|force|flag|0|vorhandene Ziele überschreiben"
+ "o|outdir|value:DIR|out|Zielverzeichnis (relativ = zum Script)"
+)
+b_init "$@"
+set -- "${ARGS[@]}"
+
+#===============================================================================
+# Funktionen
+#===============================================================================
+
+## @brief Aufräumen beim Beenden (vom EXIT-Trap aufgerufen, auch bei Fehlern)
+## @param $1 Exit-Code des Scripts
+## @return 0
+cleanup() {
+ return 0
+}
+
+## @brief Hauptablauf
+## @param $1 Ziel
+## @return 0 ok, 1 Fehler
+main() {
+ (( $# == 1 )) || bdie "Genau ein erwartet" 2
+ local target="$1" outdir
+ outdir="$(b_abspath "$OPTION_OUTDIR")"
+
+ becho "Ziel: ${target} → ${outdir}"
+ [[ -e "$outdir" && "$OPTION_FORCE" != 1 ]] && { berror "$outdir existiert (--force zum Überschreiben)"; return 1; }
+ brun mkdir -p -- "$outdir"
+ return 0
+}
+
+main "$@"
diff --git a/ai/common/templates/docker/.env.example b/ai/common/templates/docker/.env.example
new file mode 100644
index 0000000..5de01f8
--- /dev/null
+++ b/ai/common/templates/docker/.env.example
@@ -0,0 +1,23 @@
+# --- Stack ---
+COMPOSE_PROJECT_NAME= # nur a-z, 0-9, _ – kein Bindestrich (GELF-Tag)
+APP_TAG=
+
+# --- Logging (Graylog) ---
+GELF_HOST=
+GELF_PORT=12201
+
+# --- Traefik (Defaults: traefik-net / le – nur bei Abweichung setzen) ---
+TRAEFIK_ENABLE=false # erst auf true, wenn Stack sauber läuft (LE-Limits!)
+APP_FQDN=
+APP_INTERNAL_PORT=
+#TRAEFIK_NETWORK=traefik-net
+#TRAEFIK_CERTRESOLVER=le
+
+# --- Hosts ---
+EXTRA_HOST_1=:
+
+# --- Pfade (Bind-Mounts, relativ zum Stack) ---
+DIR_DATA=./data
+DIR_CONFIG=./config
+
+# --- Image-spezifische Variablen (laut Orig-Doku, s. README) ---
diff --git a/ai/common/templates/docker/README.md b/ai/common/templates/docker/README.md
new file mode 100644
index 0000000..185e97f
--- /dev/null
+++ b/ai/common/templates/docker/README.md
@@ -0,0 +1,29 @@
+#
+
+> @author · @owner · Stand TT.MM.JJJJ
+
+## 1. Zweck und Zugriff
+ · FQDN: `` · Port: <…>
+
+## 2. Original-Doku
+| Quelle | URL | Abgerufen |
+|---|---|---|
+| Image | | TT.MM.JJJJ |
+| Hersteller | | TT.MM.JJJJ |
+
+## 3. ENV-Variablen laut Original-Doku
+| Variable | Bedeutung | Default | gesetzt in .env? |
+|---|---|---|---|
+| | | | |
+
+## 4. Volumes/Pfade
+| Container-Pfad | Zweck | Bind-Mount (`.env`) |
+|---|---|---|
+| | | `DIR_DATA` |
+
+## 5. Ports und Healthcheck
+- intern: <…>, extern: <… oder nur Traefik>
+- Healthcheck: `` – Begründung: <…>
+
+## 6. Besonderheiten
+-
diff --git a/ai/common/templates/docker/docker-compose.yml b/ai/common/templates/docker/docker-compose.yml
new file mode 100644
index 0000000..e41eccf
--- /dev/null
+++ b/ai/common/templates/docker/docker-compose.yml
@@ -0,0 +1,48 @@
+#-------------------------------------------------------------------------------
+## @file docker-compose.yml
+## @brief
+## @author
+## @owner
+## @version 0.1.0 (TT.MM.JJJJ)
+## @usage docker compose up -d (im Stack-Verzeichnis, .env aus .env.example)
+#-------------------------------------------------------------------------------
+x-logging: &default-logging
+ driver: gelf
+ options:
+ gelf-address: "udp://${GELF_HOST}:${GELF_PORT}"
+ tag: "${COMPOSE_PROJECT_NAME}"
+ labels: "com.docker.compose.project,com.docker.compose.service"
+
+services:
+ app:
+ image: /:${APP_TAG}
+ container_name: ${COMPOSE_PROJECT_NAME}
+ restart: unless-stopped
+ env_file: .env
+ # ports: nur wenn Zugriff ohne Traefik nötig ist
+ # - "${APP_PORT}:"
+ extra_hosts: # nur wenn benötigt; ein Eintrag je Variable
+ - "${EXTRA_HOST_1}"
+ volumes:
+ - ${DIR_DATA}:/
+ - ${DIR_CONFIG}:/:ro
+ networks:
+ - proxy
+ labels: # Traefik bleibt aus, bis TRAEFIK_ENABLE=true
+ - "traefik.enable=${TRAEFIK_ENABLE:-false}"
+ - "traefik.docker.network=${TRAEFIK_NETWORK:-traefik-net}"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${APP_FQDN:-${COMPOSE_PROJECT_NAME}.invalid}`)"
+ - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
+ - "traefik.http.services.${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=${APP_INTERNAL_PORT:-80}"
+ logging: *default-logging
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:/"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+ start_period: 30s
+
+networks:
+ proxy:
+ name: ${TRAEFIK_NETWORK:-traefik-net}
+ external: true
diff --git a/ai/common/templates/markdown/adr.md b/ai/common/templates/markdown/adr.md
new file mode 100644
index 0000000..0f110dd
--- /dev/null
+++ b/ai/common/templates/markdown/adr.md
@@ -0,0 +1,35 @@
+---
+id: adr__
+title:
+type: decision
+status: draft # draft|active|superseded
+updated:
+owner:
+tags: []
+related: []
+supersedes:
+classification: intern
+---
+# ADR :
+
+## Kontext
+
+
+## Entscheidung
+
+
+## Alternativen
+| Option | Pro | Contra | Warum nicht |
+|---|---|---|---|
+| | | | |
+
+## Konsequenzen
+- positiv: <…>
+- negativ/Aufwand: <…>
+- Folgearbeiten: <…>
+
+## Quellen
+- (abgerufen TT.MM.JJJJ)
+
+## Änderungen
+- TT.MM.JJJJ – vorgeschlagen ()
diff --git a/ai/common/templates/markdown/knowledge_entry.md b/ai/common/templates/markdown/knowledge_entry.md
new file mode 100644
index 0000000..a8acaed
--- /dev/null
+++ b/ai/common/templates/markdown/knowledge_entry.md
@@ -0,0 +1,34 @@
+---
+id: # = Dateiname ohne .md
+title:
+type:
+status: draft # draft|active|deprecated|superseded
+updated:
+owner:
+tags: []
+sources: []
+related: []
+classification: intern
+---
+#
+
+> Kurzfassung in 1–2 Sätzen: Was ist das, wozu dient es?
+
+## Fakten
+| Merkmal | Wert |
+|---|---|
+| | |
+
+## Details
+
+
+## Betrieb
+- Bereitstellung:
+- Prüfung/Healthcheck: <…>
+- Backup/Restore: <…>
+
+## Quellen
+- (abgerufen TT.MM.JJJJ)
+
+## Änderungen
+- TT.MM.JJJJ – angelegt ()
diff --git a/ai/context/README.md b/ai/context/README.md
new file mode 100644
index 0000000..4587b05
--- /dev/null
+++ b/ai/context/README.md
@@ -0,0 +1,16 @@
+# ai/context/ – Kontext & Knowledge-DB
+
+> **Für wen, wo, womit.** Datenklassen beachten: hier höchstens „intern“ (`rules/privacy.md`). Geheimnisse nie.
+
+| Datei | Inhalt | Lesen |
+|---|---|---|
+| [responsible.md](responsible.md) | Verantwortliche – **Quelle für `@author`/`@owner`** in Headern | immer |
+| [style.md](style.md) | Zielgruppe, Ansprache, Ton, Ausführlichkeit | immer |
+| [environment.md](environment.md) | Infrastruktur-Überblick + zentrale Werte (Secret-Store, Logging, Domains) | immer |
+| [knowledge/](knowledge/README.md) | Knowledge-DB: Systeme, Netz, Dienste, Entscheidungen, Anleitungen, Recherchen | gezielt über `INDEX.md` |
+
+Ziel und Stand des Projekts: [`../../context.md`](../../context.md).
+
+## Pflege
+- Überblicksdateien (`*.md` hier) kurz halten (≤ 1 Bildschirmseite); Details gehören in die Knowledge-DB und werden verlinkt.
+- Änderungen mit Datum (`> Stand: TT.MM.JJJJ`). Widersprüche zwischen Überblick und Knowledge: Knowledge gilt, Überblick korrigieren.
diff --git a/ai/context/environment.md b/ai/context/environment.md
new file mode 100644
index 0000000..d9cd581
--- /dev/null
+++ b/ai/context/environment.md
@@ -0,0 +1,33 @@
+# Umgebung – Überblick & zentrale Werte
+
+> Stand: 29.10.2026 · Klasse: intern · Details je System/Dienst in der Knowledge-DB (`knowledge/`).
+> Werte hier sind die **Platzhalter-Auflösung** für die Standards in `ai/common/` (``, `` …).
+
+## Zentrale Werte
+| Platzhalter | Bedeutung | Wert | Knowledge |
+|---|---|---|---|
+| `` | Secret-Store (HashiCorp Vault o. ä.) | | |
+| `` (KV-Pfad) | Pfadpräfix im Secret-Store | | |
+| `` / Port | zentrales Logging (Graylog, GELF/UDP) | `192.168.218.122` / `12201` | |
+| `` | Basisverzeichnis Docker-Stacks | `/docker` (eigenes LV, btrfs, Label `DOCKER`) | |
+| `TRAEFIK_NETWORK` / `TRAEFIK_CERTRESOLVER` | Reverse Proxy | | |
+| `` | Default-Ziel für Ansible (Lab-VM) | | |
+| `` | SSH-Benutzer für Ansible | `nafvoyr` (sudo) | |
+| Domains intern/extern | DNS | | |
+
+## Topologie (Kurzfassung)
+- **Standort A – Keller:** `kellerbeast` – GPU-Compute (Ollama, faster-whisper), TrueNAS ZFS.
+- **Standort B – Cloud-RZ:** Docker-VMs (Compose, Ansible).
+- **Verbindung:** NetBird-Mesh zwischen Standorten und Clients.
+
+## Werkzeuge
+| Bereich | Werkzeug | Ort | Status |
+|---|---|---|---|
+| Git-Server | Gitea (self-hosted, **vertrauenswürdig**) – Remote dieses Repos: | eigener Server | in Betrieb |
+| Secret-Store | HashiCorp Vault (Auto-Unseal) | `vp.chmln.de` | in Betrieb |
+| Logging | Graylog (GELF) | `192.168.218.122:12201/udp` | in Betrieb |
+| Reverse Proxy | Traefik | | in Betrieb |
+| Monitoring | Checkmk | | |
+| Backup | Borgmatic → ZFS / Proxmox Backup Server | `kellerbeast` | geplant |
+| VPN | NetBird | Mesh | geplant/in Betrieb? |
+| Messenger | Mattermost | | |
diff --git a/ai/context/knowledge/INDEX.md b/ai/context/knowledge/INDEX.md
new file mode 100644
index 0000000..70a90da
--- /dev/null
+++ b/ai/context/knowledge/INDEX.md
@@ -0,0 +1,10 @@
+# Knowledge-Index
+
+> Automatisch erzeugt von `bin/kb_index.sh` – nicht von Hand bearbeiten.
+> Format der Einträge: [README.md](README.md)
+
+## services
+
+| id | Titel | Status | Stand | Tags |
+|---|---|---|---|---|
+| sultan | [sultan – verteiltes Ingress-Routing für Traefik (Eigenentwicklung)](services/sultan.md) | active | 2026-10-23 | traefik, ingress, docker, netbird, checkmk, eigenentwicklung |
diff --git a/ai/context/knowledge/README.md b/ai/context/knowledge/README.md
new file mode 100644
index 0000000..3a80279
--- /dev/null
+++ b/ai/context/knowledge/README.md
@@ -0,0 +1,43 @@
+# Knowledge-DB (Markdown)
+
+> Dauerhaftes Wissen über Umgebung und Projekt – eine Datei pro Eintrag, Metadaten im YAML-Frontmatter,
+> Übersicht in [`INDEX.md`](INDEX.md) (erzeugt von `bin/kb_index.sh`, nicht von Hand bearbeiten).
+
+## Ablage
+| Ordner | `type` | Inhalt | Beispiel |
+|---|---|---|---|
+| `systems/` | `system` | Hosts, VMs, Geräte (Server, Switch, AP, Firewall) | `systems/host01.md` |
+| `network/` | `network` | Netze, VLANs, IP-Pläne, DNS-Zonen, VPN, Routing | `network/ip_plan.md` |
+| `services/` | `service` | Dienste/Anwendungen (DNS, RADIUS, Backup, Mail) | `services/dns_resolver.md` |
+| `decisions/` | `decision` | ADRs – Entscheidungen mit Begründung | `decisions/adr_0001_secret_store.md` |
+| `howto/` | `howto` | Anleitungen, Runbooks, wiederkehrende Handgriffe | `howto/restore_backup.md` |
+| `research/` | `research` | Recherchen mit Quellen und Datum | `research/wifi_controller_vergleich.md` |
+| `topics/` | `topic` | Lerninhalte je LPIC-Prüfungsthema (Objective-Nummer im `id`) | `topics/lpic1_103_1_command_line.md` |
+
+Dateiname = `id` + `.md` (Englisch/ASCII, klein, `_`). ADRs: `adr_NNNN_`.
+
+## Frontmatter (Schlüssel englisch, Werte in Amtssprache)
+```yaml
+---
+id: host01 # Pflicht, eindeutig, = Dateiname ohne .md
+title: Virtualisierungshost 1 # Pflicht
+type: system # Pflicht: system|network|service|decision|howto|research|topic
+status: active # Pflicht: draft|active|deprecated|superseded
+updated: 2026-10-07 # Pflicht, ISO
+owner: br # Pflicht: Kürzel aus ../responsible.md
+tags: [proxmox, server] # optional
+sources: [] # optional: URLs (mit Abrufdatum im Text)
+related: [dns_resolver] # optional: ids verwandter Einträge
+supersedes: # optional (v. a. ADRs)
+classification: intern # optional, Default intern; nie höher (rules/privacy.md)
+---
+```
+Vorlage: [`../../common/templates/markdown/knowledge_entry.md`](../../common/templates/markdown/knowledge_entry.md), ADR: [`adr.md`](../../common/templates/markdown/adr.md).
+
+## Regeln für Agenten
+- **Lesen:** zuerst `INDEX.md`, dann gezielt Einträge. Suche: `grep -rl "tags:.*dns" ai/context/knowledge/`.
+- **Schreiben:** bestehenden Eintrag aktualisieren statt neuen anlegen; `updated` setzen; Quellen mit Datum.
+- **Fakten statt Erzählung:** Tabellen (Adressen, Ports, Versionen), Abschnitt „Stand/Änderungen“ unten mit Datum.
+- **Nach jeder Änderung:** `bin/kb_index.sh` (prüft Pflichtfelder, doppelte ids, aktualisiert `INDEX.md`). `--check` für reine Prüfung.
+- Veraltetes nicht löschen: `status: deprecated`/`superseded`, Verweis auf Nachfolger.
+- Keine Geheimnisse, keine Personennamen außer Kürzeln aus `responsible.md`.
diff --git a/ai/context/knowledge/decisions/README.md b/ai/context/knowledge/decisions/README.md
new file mode 100644
index 0000000..bb5a6aa
--- /dev/null
+++ b/ai/context/knowledge/decisions/README.md
@@ -0,0 +1,4 @@
+# decisions/
+
+Architekturentscheidungen als ADR (`type: decision`), Dateiname `adr_NNNN_.md`, fortlaufend nummeriert.
+Vorlage: [../../../common/templates/markdown/adr.md](../../../common/templates/markdown/adr.md). Überholte ADRs bleiben (`status: superseded`).
diff --git a/ai/context/knowledge/howto/README.md b/ai/context/knowledge/howto/README.md
new file mode 100644
index 0000000..e9c47af
--- /dev/null
+++ b/ai/context/knowledge/howto/README.md
@@ -0,0 +1,4 @@
+# howto/
+
+Anleitungen und Runbooks (`type: howto`): Voraussetzungen → Schritte (kopierbare Befehle) → Prüfung → Rückweg.
+Wiederkehrende Fragen des Bedieners landen hier.
diff --git a/ai/context/knowledge/network/README.md b/ai/context/knowledge/network/README.md
new file mode 100644
index 0000000..1d725c7
--- /dev/null
+++ b/ai/context/knowledge/network/README.md
@@ -0,0 +1,4 @@
+# network/
+
+Netze, VLANs, IP-Pläne, DNS-Zonen, VPN, Routing (`type: network`). Format: [../README.md](../README.md).
+IP-Pläne als Tabelle (Netz | VLAN | Zweck | Gateway | DHCP-Bereich). Diagramme als Mermaid/ASCII.
diff --git a/ai/context/knowledge/research/README.md b/ai/context/knowledge/research/README.md
new file mode 100644
index 0000000..e91f7ec
--- /dev/null
+++ b/ai/context/knowledge/research/README.md
@@ -0,0 +1,4 @@
+# research/
+
+Recherchen mit Bestand (`type: research`): Fragestellung, Ergebnis/Empfehlung, Quellen (URL + Abrufdatum), Einschätzung vs. Beleg getrennt.
+Keine internen Daten in Suchanfragen (`rules/web_search.md`).
diff --git a/ai/context/knowledge/services/README.md b/ai/context/knowledge/services/README.md
new file mode 100644
index 0000000..b801953
--- /dev/null
+++ b/ai/context/knowledge/services/README.md
@@ -0,0 +1,4 @@
+# services/
+
+Dienste und Anwendungen (`type: service`). Format: [../README.md](../README.md).
+Mindestinhalt: Zweck, Host(s), Version, Bereitstellung (Ansible-Rolle / Docker-Stack), Ports, Abhängigkeiten, Secret-Store-Pfad (nur Pfad), Healthcheck, Logging, Backup/Restore, Original-Doku (URL + Datum).
diff --git a/ai/context/knowledge/services/sultan.md b/ai/context/knowledge/services/sultan.md
new file mode 100644
index 0000000..b2f8a61
--- /dev/null
+++ b/ai/context/knowledge/services/sultan.md
@@ -0,0 +1,49 @@
+---
+id: sultan
+title: sultan – verteiltes Ingress-Routing für Traefik (Eigenentwicklung)
+type: service
+status: active
+updated: 2026-10-23
+owner: br
+tags: [traefik, ingress, docker, netbird, checkmk, eigenentwicklung]
+sources: [https://git.everdc.eu/chmln-app-dev/sultan]
+related: []
+classification: intern
+---
+# sultan – verteiltes Ingress-Routing für Traefik
+
+> Eigenentwicklung des Betreibers (Go), Ersatz für HashiCorp Consul im eigenen Bedarf: Dienste deklarieren ihr Routing
+> per Traefik-Labels, ein Agent meldet es an den sultan-Server, Traefik holt die Konfiguration per HTTP-Provider.
+
+## Fakten
+| Merkmal | Wert |
+|---|---|
+| Repo | `ssh://git@git.everdc.eu:10022/chmln-app-dev/sultan.git`, lokal `~/git/sultan` |
+| Image | `zot.chmln.de/sultan:` (distroless, nonroot UID 65532) – feste Version in `.env` |
+| Rollen | `sultan server` (neben Traefik), `sultan agent` (je Docker-Host), `sultan accountant` (Inventar) |
+| Agent → Server | TCP 8443, TLS, nur ausgehend |
+| Traefik → Backend | HTTP auf den **veröffentlichten** Host-Port des Containers |
+| Monitoring | Checkmk Local Checks (`/metrics/checkmk`) |
+
+## Details – was das für unsere Compose-Stacks heißt
+| Punkt | Folge |
+|---|---|
+| Traefik läuft auf anderer VM | kein externes Netz `traefik-net` und kein Label `traefik.docker.network` auf dem Docker-Host |
+| Port muss veröffentlicht sein | `ports: "${APP_PORT}:"` Pflicht für Web-Services; Bindung an `127.0.0.1` wird verworfen. Bindung an eine andere IP (z. B. NetBird-IP) wird akzeptiert (Code: `pkg/agent/workloads.go`, `reachablePorts`). Festlegung je Host in dessen Knowledge-Eintrag |
+| Port-Label | `loadbalancer.server.port` = Port **im Container**; der Agent rechnet auf den Host-Port um |
+| Entrypoints/Certresolver | Labels müssen Namen aus der Traefik-Konfiguration nutzen (`websecure`, `le`); sultan prüft das nicht |
+| Domains | nur Domains, die dem Mandanten im sultan-Server erlaubt sind (sonst „Policy-Verstoß“ im Server-Log) |
+| Namen | IDs: `a-z0-9`, `-`, `_`; **`__` ist reserviert** (Trenner `____`) – unser `_`-Schema passt, doppelter Unterstrich verboten |
+| Backend-Adresse | Agent meldet die Host-IP, über die er den Server erreicht; mit `SULTAN_BACKEND_INTERFACE=wt0` die NetBird-IP |
+| Wartung | gestoppter Container → Wartungsseite; **entfernter** Container (`down`) → 404 (siehe `docs/wartung.md` im Repo) |
+
+## Betrieb
+- Bereitstellung Agent: Compose mit `network_mode: host`, Docker-Socket `:ro`, `group_add: DOCKER_GID`, `suldata/` (enthält `node.key`) persistent und im Backup
+- Einladung: `sultan token invite --tenant=…` auf dem Server, als `SULTAN_TOKEN` in die `.env` des Agents (nur beim ersten Start)
+- Prüfung: Agent-Log „Mit Server verbunden / Lease erhalten“; vom Ingress `curl -sI http://: -H 'Host: …'`
+
+## Quellen
+- `~/git/sultan/README.md`, `docs/community.md`, `deploy/compose/agent/` (gelesen 23.10.2026, Commit `dd93277`)
+
+## Änderungen
+- 23.10.2026 – angelegt (br)
diff --git a/ai/context/knowledge/systems/README.md b/ai/context/knowledge/systems/README.md
new file mode 100644
index 0000000..dafb1e9
--- /dev/null
+++ b/ai/context/knowledge/systems/README.md
@@ -0,0 +1,4 @@
+# systems/
+
+Hosts, VMs und Geräte – ein Eintrag pro System (`type: system`). Format: [../README.md](../README.md).
+Mindestinhalt: Rolle, Standort (grob), Hardware/VM, OS + Version, Adressen, Dienste darauf, Zugriff (ohne Geheimnisse), Verantwortlich, Backup.
diff --git a/ai/context/knowledge/topics/README.md b/ai/context/knowledge/topics/README.md
new file mode 100644
index 0000000..06ef215
--- /dev/null
+++ b/ai/context/knowledge/topics/README.md
@@ -0,0 +1,5 @@
+# topics/
+
+Lerninhalte je LPIC-Prüfungsthema (`type: topic`), ein Eintrag pro Objective.
+`id` enthält Stufe und Objective-Nummer, z. B. `lpic1_103_1_command_line` für 103.1.
+Aufbau: Lernziel (Wortlaut/Version der Objectives mit Abrufdatum) → Kernbefehle und Dateien → Übungen → typische Fallen.
diff --git a/ai/context/responsible.md b/ai/context/responsible.md
new file mode 100644
index 0000000..d3bc876
--- /dev/null
+++ b/ai/context/responsible.md
@@ -0,0 +1,17 @@
+# Verantwortliche
+
+> Stand: 23.10.2026 · **Quelle für die Header-Felder** `@author` / `@owner` (`rules/headers.md`).
+> Nur hier stehen Personennamen. Weitere Personen nur als Rolle (`rules/privacy.md`).
+
+| Kürzel | Header-String (`Name `) | Rolle | Header-Feld | Zuständig für |
+|---|---|---|---|---|
+| br | Björn Runden
| Bediener, Autor und Verantwortlicher | `@author` (Standard) | Scripts, Ansible, Doku |
+| br | Björn Runden
| Bediener, Autor und Verantwortlicher | `@owner` (Standard) | fachliche Freigabe |
+
+## Regeln für Agenten
+- **Standard:** `@author` = Eintrag mit „`@author` (Standard)“, `@owner` = Eintrag mit „`@owner` (Standard)“.
+- Weitere `@author` (Team): nur wer an der Datei tatsächlich mitarbeitet; je Person eine `@author`-Zeile, Hauptautor zuerst.
+- Header-String exakt übernehmen (`Name `), nichts kürzen oder ändern.
+- Abweichende Zuständigkeit für einzelne Bereiche: in `context.md` → Metadaten festlegen; das gilt dann vorrangig.
+- Tabelle leer/Platzhalter → Platzhalter ``/`` im Header lassen und den Bediener darauf hinweisen. Keine Namen erfinden.
+- KI-Beteiligung: `@assist KI-unterstützt (), geprüft von `.
diff --git a/ai/context/style.md b/ai/context/style.md
new file mode 100644
index 0000000..ebe355e
--- /dev/null
+++ b/ai/context/style.md
@@ -0,0 +1,18 @@
+# Stil & Ansprache
+
+> Stand: 23.10.2026
+
+## Zielgruppe
+Der Betreiber (`br`) selbst: knapp, technisch, Empfehlung statt Optionsliste; Markdown und Code.
+
+## Standard
+- Amtssprache laut `AGENTS.md`; direkt, knapp, sachlich. Empfehlung statt Optionsliste.
+- Ansprache des Betreibers: du.
+- Quellen nennen, Unsicheres kennzeichnen.
+
+## Ausführlichkeit
+| Stufe | Wann |
+|---|---|
+| kurz | Rückfragen, Statusmeldungen, Chat |
+| normal | Lösungsbeschreibung, Projektdoku |
+| ausführlich | Konzepte, Entscheidungsvorlagen, Anleitungen |
diff --git a/ai/inbox/README.md b/ai/inbox/README.md
new file mode 100644
index 0000000..1b08b3d
--- /dev/null
+++ b/ai/inbox/README.md
@@ -0,0 +1,13 @@
+# ai/inbox/ – Eingang für Markdown-Dateien
+
+> Temporäre `.md`-Dateien, die noch eingeordnet werden müssen.
+
+## Ablauf
+1. Betreiber legt die Datei hier ab.
+2. Agent liest sie, ordnet den Inhalt ein (`context.md`, `architecture.md`, Knowledge-DB, `ai/context/*.md`) und vermerkt das in `context.md` → Sitzungsprotokoll.
+3. Agent committet das Original vorher (Historie), dann Verschieben nach `done/.md.orig`.
+ `*.orig` ist in `.gitignore` → in `done/` nur lokal, das Original bleibt im Git-Verlauf.
+
+| Datei | Inhalt |
+|---|---|
+| `done/` | eingeordnete Originale (`*.orig`, lokal) |
diff --git a/ai/rules/README.md b/ai/rules/README.md
new file mode 100644
index 0000000..8960cf2
--- /dev/null
+++ b/ai/rules/README.md
@@ -0,0 +1,26 @@
+# ai/rules/ – verbindliche Regeln
+
+> Regeln haben **Vorrang** vor allem anderen in dieser Basis (außer den Sicherheits-/Datenschutzgrenzen des Agenten selbst).
+> Eine Regel pro Datei. Ändern nur durch den Bediener oder auf seine ausdrückliche Anweisung.
+
+| Datei | Regel in einem Satz |
+|---|---|
+| [language.md](language.md) | Alles in der Amtssprache aus `AGENTS.md`; Struktur sprachneutral englisch. |
+| [privacy.md](privacy.md) | Keine identifizierenden Personen-/Umgebungsdaten an externe Dienste; Daten nach Klasse behandeln. |
+| [secrets.md](secrets.md) | Geheimnisse nie in Dateien/Git/Chat; Secret-Store zuerst, Ansible Vault als Fallback. |
+| [headers.md](headers.md) | Jede Code-Datei trägt einen Header mit namentlich Verantwortlichen. |
+| [testing.md](testing.md) | Lokal testen; Cloud-VMs und Produktion nur mit ausdrücklicher Freigabe. |
+| [file_safety.md](file_safety.md) | Vor Löschen/Überschreiben ansehen, `*.orig` statt Löschen. |
+| [web_search.md](web_search.md) | Web-Recherche erlaubt, aber ohne interne Daten; Quellen mit Datum. |
+
+## Aufbau einer Regeldatei
+```markdown
+# Regel:
+
+> Stand: TT.MM.JJJJ · gilt für: alle Agenten (bis auf Widerruf)
+
+## Erlaubt / Pflicht
+## Verboten / Einschränkungen
+## Ausnahmen (wer darf freigeben, wie wird es dokumentiert)
+```
+Ausnahmen, die der Bediener erteilt, in `context.md` mit Datum festhalten – sie gelten nur für genau diesen Fall.
diff --git a/ai/rules/file_safety.md b/ai/rules/file_safety.md
new file mode 100644
index 0000000..2f71007
--- /dev/null
+++ b/ai/rules/file_safety.md
@@ -0,0 +1,9 @@
+# Regel: Löschen und Überschreiben
+
+> Stand: 07.10.2026 · gilt für: alle Agenten
+
+- Vor dem Löschen/Überschreiben: **Ziel ansehen** (Inhalt, Größe, Änderungsdatum) und im Zweifel nachfragen.
+- Statt Löschen: Sicherung `.orig` (bei Kollision `.orig.JJJJMMTT`). `*.orig` steht in `.gitignore`.
+- Massenoperationen (`rm -r`, `find -delete`, `rsync --delete`, `git clean`, `git reset --hard`) nur mit Freigabe und vorherigem Trockenlauf.
+- Fremde Dateien (nicht in dieser Basis/diesem Projekt entstanden) nicht ohne Auftrag umbauen oder „aufräumen“.
+- Ansible: `backup: true` bei Templates auf Zielsystemen, sofern die Datei keine Geheimnisse enthält (sonst Kopien mit Geheimnissen → bewusst entscheiden).
diff --git a/ai/rules/headers.md b/ai/rules/headers.md
new file mode 100644
index 0000000..13ad2c1
--- /dev/null
+++ b/ai/rules/headers.md
@@ -0,0 +1,35 @@
+# Regel: Header mit Verantwortlichen
+
+> Stand: 08.10.2026 · gilt für: jede Code-, Konfigurations- und Template-Datei, die in diesem Repo entsteht
+
+## Pflicht
+Jede Datei beginnt (nach Shebang bzw. `---`) mit einem Header im **bashdoc-Stil** (`## @tag`), eingerahmt von `#-----`-Linien:
+
+| Tag | Pflicht | Inhalt |
+|---|---|---|
+| `@file` | ja | Dateiname |
+| `@brief` | ja | Zweck in einem Satz |
+| `@author` | ja | `Name ` des verantwortlichen Menschen (aus `ai/context/responsible.md`) – nie die KI. Mehrere Autoren: Zeile `@author` wiederholen, Hauptautor zuerst |
+| `@owner` | ja | `Name ` oder Rolle des fachlich Verantwortlichen (aus `responsible.md`) |
+| `@assist` | wenn KI beteiligt | `KI-unterstützt (), geprüft von ` |
+| `@version` | ja | SemVer + Datum `x.y.z (TT.MM.JJJJ)` |
+| `@usage` | Scripts/Playbooks | Aufruf; Optionen bei Bash über `## @OPTIONS@` (generiert) |
+
+- Agenten tragen Namen und Mail exakt wie in `responsible.md` (Spalte „Header-String“) ein. Fehlt dort ein Eintrag: Platzhalter `` stehen lassen und den Bediener darauf hinweisen – **keine Namen erfinden**.
+- Bei inhaltlicher Änderung: `@version` erhöhen, Datum aktualisieren; `@author` nur ändern, wenn die Verantwortung wechselt.
+- Kommentar-Präfix je Sprache: Bash/YAML/Python `##`, Jinja `{#- … -#}`, SQL `--`. Format der Tags bleibt gleich.
+
+## Vorlagen
+`ai/common/templates/` – Bash (`bash/script_template.sh`), Ansible (`ansible/playbooks/example.yml`), Docker (`docker/docker-compose.yml`).
+
+## Beispiel (mehrere Autoren)
+```bash
+## @author Erika Muster
+## @author Max Beispiel
+## @owner Clara Fach
+```
+
+## Änderungsprotokoll
+- 07.10.2026 – angelegt.
+- 08.10.2026 – `Name `-Format, mehrere `@author` erlaubt.
+- 23.10.2026 – Mandantenbezug entfernt (Einzelprojekt).
diff --git a/ai/rules/language.md b/ai/rules/language.md
new file mode 100644
index 0000000..b417e58
--- /dev/null
+++ b/ai/rules/language.md
@@ -0,0 +1,17 @@
+# Regel: Amtssprache
+
+> Stand: 07.10.2026 · gilt für: alle Agenten, alle Ausgaben
+
+## Pflicht
+- Die **Amtssprache** steht in `AGENTS.md` → „Metadaten“ (Skeleton: `de`). Sie gilt für:
+ Antworten, Markdown-Dokumente, Code-Kommentare, `@brief`-Texte, Log-/Konsolenmeldungen, Ansible-Task-Namen, Commit-Messages.
+- Fachbegriffe, Produktnamen und Code-Bezeichner bleiben im Original.
+- Bei `de`: korrekte Orthografie mit Umlauten und ß – nie `ae`/`oe`/`ue`/`ss` als Ersatz (außer in Bezeichnern, s. u.).
+
+## Sprachneutral (immer Englisch, ASCII)
+- Datei- und Ordnernamen, Variablen, Funktionen, Rollen, Tags, YAML-/Frontmatter-Schlüssel.
+- Grund: Übernahmen aus/in skel-ai mit anderer Amtssprache behalten Struktur, Verweise und Scripts.
+
+## Datum & Zahlen
+- Fließtext: TT.MM.JJJJ (bei `de`), immer absolut – nie „gestern“, „nächste Woche“.
+- Maschinenlesbar (Frontmatter, Dateinamen, Logs): ISO 8601 `JJJJ-MM-TT`.
diff --git a/ai/rules/privacy.md b/ai/rules/privacy.md
new file mode 100644
index 0000000..ac30c2c
--- /dev/null
+++ b/ai/rules/privacy.md
@@ -0,0 +1,38 @@
+# Regel: Datenschutz
+
+> Stand: 23.10.2026 · gilt für: alle Agenten · **Priorität 1**
+
+## Grundsatz
+Nichts, was die Basis verlässt (Web-Suche, externe APIs, Cloud-KI außerhalb der Freigabe, Pastebins, Issue-Tracker Dritter),
+darf auf den Betreiber, seine Umgebung oder Dritte (Kontakte, Kunden) rückführbar sein.
+
+## Datenklassen
+| Klasse | Beispiele | In der AI-Basis ablegen? | An externe Dienste? |
+|---|---|---|---|
+| öffentlich | Herstellerdoku, Paketversionen, RFCs | ja | ja |
+| intern | Hostnamen, IP-Pläne, Topologie, Dienste, Rollen | ja (Knowledge-DB, Projekte) | **nein** (nicht in Suchen/URLs) |
+| vertraulich | Personennamen über die Verantwortlichen hinaus, Kundendaten, Verträge, Logs mit Personenbezug | nein – nur Verweis auf Ablageort | nein |
+| geheim | Passwörter, Schlüssel, Tokens, Zertifikats-Privatschlüssel | **nie** (→ `secrets.md`) | nie |
+
+## Pflicht
+- Namentlich genannt werden **nur** die Verantwortlichen aus `ai/context/responsible.md` (Header-Pflicht, `headers.md`).
+- Weitere Personen nur als Rolle („Leitung Buchhaltung“, „Azubi IT“), nie mit Namen/Kontaktdaten.
+- Beispiele, Testdaten, Doku-Snippets: pseudonymisiert (`host01`, `192.0.2.0/24`, `example.org`, `user_a`).
+- Logs/Dumps aus Kundensystemen vor dem Einfügen in einen Chat bereinigen (Namen, Mailadressen, IPs öffentlicher Netze).
+
+## Leitsatz: so lokal wie möglich, so auswärts wie nötig
+Verarbeitung (KI, Speicherung, Dienste) zuerst auf eigener Infrastruktur. Ein externer Dienst nur, wenn lokal nicht
+sinnvoll machbar – und nur bis zur unten freigegebenen Klasse.
+
+## Freigegebene KI-Dienste
+| Dienst | Erlaubt für Klasse bis | Grundlage (z. B. AVV, Self-Hosting) | Stand |
+|---|---|---|---|
+| Lokale Modelle (Ollama, STT auf `kellerbeast`) | vertraulich (inkl. persönlicher Inhalte) | Self-Hosting | 23.10.2026 |
+| Claude Code (Anthropic) | intern (Lerninhalte, Lab-Konfiguration, Code, Doku – **keine** persönlichen Inhalte) | Entscheidung Betreiber | 23.10.2026 |
+
+## Vertrauenswürdige Ablagen
+| Ablage | Erlaubt für Klasse bis | Grundlage | Stand |
+|---|---|---|---|
+| Gitea (self-hosted, Remote dieses Repos) | vertraulich | Self-Hosting | 23.10.2026 |
+
+> Ohne Eintrag gilt: nur Klasse „öffentlich“ darf an externe KI-Dienste. Rechtliche Bewertung (DSGVO, AVV) liegt beim Betreiber – der Agent gibt keine Rechtsberatung.
diff --git a/ai/rules/secrets.md b/ai/rules/secrets.md
new file mode 100644
index 0000000..00752e9
--- /dev/null
+++ b/ai/rules/secrets.md
@@ -0,0 +1,12 @@
+# Regel: Geheimnisse
+
+> Stand: 07.10.2026 · gilt für: alle Agenten, alle Projekte
+
+1. **Nie** Passwörter, Shared Secrets, private Schlüssel oder Tokens in Dateien der Basis, ins Git, in Chats oder Logs – auch nicht in Beispielen mit echten Werten.
+2. **Quelle Nr. 1: Secret-Store** (Platzhalter: HashiCorp Vault ``, KV v2, Mount `secret`, Pfad `//…`). Konkrete Werte in `ai/context/environment.md`.
+3. **Fallback: Ansible Vault.** Passwortdatei **außerhalb** des Repos (`~/.ans-secret-`), Variablennamen beginnen mit `vault_`.
+4. `.env`-Dateien: nie im Git, nur `.env.example` mit gleichen Schlüsseln und Platzhaltern. Secrets in `.env` nur, wenn das Werkzeug es erzwingt (z. B. Docker-Stack).
+5. **Ansible:** Tasks mit Geheimnissen `no_log: true`, Templates mit Geheimnissen `diff: false` – sonst landen Werte in Terminal und `ansible.log`.
+6. **Bash:** Geheimnisse nie als Kommandozeilenargument (sichtbar in `ps`), nie mit `becho`/`bdebug` ausgeben; per Datei-Deskriptor, stdin oder Umgebungsvariable übergeben.
+7. Geheimnisse **erzeugen**, nicht ausdenken: `openssl rand -hex 24` direkt im Schreibbefehl (`vault kv put … key="$(openssl rand -hex 24)"`). Bei KV v2 `patch` statt `put`, wenn nur ein Schlüssel geändert wird.
+8. **Agenten** lesen keine `.env`/Vault-Inhalte aus und geben keine Geheimnis-Werte aus – nur Schlüsselnamen. Benötigt ein Test ein Geheimnis, fragt der Agent den Bediener, ihn selbst auszuführen.
diff --git a/ai/rules/testing.md b/ai/rules/testing.md
new file mode 100644
index 0000000..796fcd8
--- /dev/null
+++ b/ai/rules/testing.md
@@ -0,0 +1,17 @@
+# Regel: Testen
+
+> Stand: 07.10.2026 · gilt für: alle Agenten
+
+## Pflicht
+- **Lokal testen:** Syntax-/Lint-Checks (`bash -n`, `shellcheck`, `ansible-lint`, `yamllint`, `docker compose config`) und Läufe in lokalen Containern/VMs (z. B. `debian:` per Docker/Podman).
+- **Trockenlauf zuerst:** Bash `-n`, Ansible `--check --diff`, dann echter Lauf.
+- **Idempotenz** prüfen: zweiter Lauf ohne Änderungen (`changed=0`).
+- Ergebnis des Tests (Datum, was, wogegen, Ergebnis) in `context.md` festhalten.
+
+## Verboten ohne ausdrückliche Freigabe des Bedieners für genau diesen Fall
+- Code in **Cloud-VMs/Sandboxes Dritter** ausführen oder testen.
+- Läufe gegen **Produktivsysteme** (auch `--check` nicht, wenn Zugangsdaten oder Last im Spiel sind).
+- Änderungen an Netzwerkgeräten, die die Verbindung des Agenten/Bedieners kappen können (Firewall, Routing, SSH) – immer mit Rückfallweg (z. B. zeitgesteuertes Rollback) und Freigabe.
+
+## Ausnahmen
+Freigabe im Chat genügt; der Agent vermerkt sie mit Datum in `context.md` („Freigabe: Lauf gegen host01, 07.10.2026“).
diff --git a/ai/rules/web_search.md b/ai/rules/web_search.md
new file mode 100644
index 0000000..458c325
--- /dev/null
+++ b/ai/rules/web_search.md
@@ -0,0 +1,16 @@
+# Regel: Web-Recherche
+
+> Stand: 07.10.2026 · gilt für: alle Agenten mit Web-Zugriff
+
+## Erlaubt
+- Herstellerdoku, Paketversionen, Kompatibilität, Sicherheitshinweise (CVE), Best Practices nachschlagen.
+- Vor jedem neuen Docker-Stack oder jeder neuen Ansible-Rolle: Original-Doku holen (Pflicht laut `ai/common/docker.md` / `ansible.md`).
+
+## Einschränkungen
+- **Keine internen Daten** in Suchanfragen oder URLs: keine IPs, Hostnamen, MACs, Domains, Projekt-/Firmennamen, Personennamen (→ `privacy.md`).
+- Keine Inhalte aus lokalen Dateien/Code an Web-Dienste übergeben (keine Pastebins, keine Online-Validatoren mit internem Inhalt).
+- Ergebnisse mit **Quelle (URL) und Abrufdatum** ablegen; Unbelegtes als Einschätzung kennzeichnen.
+- Dauerhaft wertvolle Recherche → Knowledge-DB (`context/knowledge/research/`).
+
+## Gesperrte Seiten
+- (derzeit keine)
diff --git a/bin/.env.example b/bin/.env.example
new file mode 100644
index 0000000..5f7c353
--- /dev/null
+++ b/bin/.env.example
@@ -0,0 +1,21 @@
+# .env – Einstellungen für die Scripts in diesem Verzeichnis
+# Wird von b_init gesourct (Bash-Syntax). Echte .env nie ins Git – nur .env.example.
+# Vorrang: Default < .env < Umgebung (export / VAR=… ./script.sh) < Kommandozeile
+# Relative Pfade gelten relativ zum Script-Verzeichnis, nicht zum aufrufenden $PWD.
+
+## Logging ---------------------------------------------------------------------
+B_LOG_LEVEL=info # debug | info | warn | error (-d setzt debug)
+B_LOG_CONSOLE=1 # 0 = still (-q)
+B_LOG_FILE= # z. B. "log/${SCRIPT_NAME%.sh}.log"; leer = keine Datei (-l DATEI)
+B_LOG_FILE_LEVEL= # leer = wie B_LOG_LEVEL; z. B. debug für ausführliche Logdatei
+B_LOG_COLOR=auto # auto | 1 | 0
+
+## Optionen vorbelegen (überschreibt den Default aus der Definition) ------------
+#OPTION_DRY_RUN=1
+
+## Zusätzliche Optionen für alle Scripts dieses Verzeichnisses – immer mit += ---
+# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
+#B_OPTS+=( "k|keep|flag|0|Zwischendateien behalten" )
+
+## Projektwerte – mit ${VAR:-default}, damit die Umgebung gewinnt ---------------
+#DIR_BACKUP="${DIR_BACKUP:-/srv/backup}"
diff --git a/bin/README.md b/bin/README.md
new file mode 100644
index 0000000..c74dc59
--- /dev/null
+++ b/bin/README.md
@@ -0,0 +1,11 @@
+# bin/ – Werkzeuge der AI-Basis
+
+> Selbst nach [`ai/common/bash.md`](../ai/common/bash.md) gebaut – zugleich Referenzbeispiele für den Standard.
+> Alle: `-h` Hilfe · `-n` Trockenlauf · `-d` Debug · `-q` still · `-l DATEI` Logdatei.
+
+| Script | Zweck |
+|---|---|
+| `kb_index.sh [--check]` | Knowledge-`INDEX.md` erzeugen, Pflichtfelder/doppelte ids prüfen |
+| `bashdoc.sh … [-o datei] [-p]` | Markdown-Doku aus `##`-Kommentaren |
+| `_functions.sh` | gemeinsame Basis – **Kopie** von `ai/common/templates/bash/_functions.sh`, nicht lokal ändern |
+| `.env.example` | Vorlage für lokale Einstellungen (`cp .env.example .env`) |
diff --git a/bin/_functions.sh b/bin/_functions.sh
new file mode 100644
index 0000000..247ce0a
--- /dev/null
+++ b/bin/_functions.sh
@@ -0,0 +1,415 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file _functions.sh
+## @brief Gemeinsame Bash-Basis: .env laden, Optionen, Logging, Usage, Trap
+## @author
+## @owner
+## @version 1.0.0 (07.10.2026)
+##
+## @usage source "${SCRIPT_DIR}/_functions.sh"; B_OPTS+=(…); b_init "$@"; set -- "${ARGS[@]}"
+## @note Nur sourcen, nicht ausführen. Benötigt bash >= 4.4.
+## Kopien NICHT lokal ändern – Projektspezifisches gehört in
+## _functions.local.sh (wird automatisch geladen, falls vorhanden).
+## Standard: ai/common/bash.md
+#-------------------------------------------------------------------------------
+
+if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
+ echo "_functions.sh wird gesourct, nicht ausgeführt." >&2
+ exit 2
+fi
+[[ -n "${_B_LOADED:-}" ]] && return 0
+if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
+ echo "FEHLER: bash >= 4.4 nötig (gefunden: ${BASH_VERSION})." >&2
+ return 1
+fi
+_B_LOADED=1
+B_FUNCTIONS_VERSION="1.0.0"
+
+# --- Pfade (pfadsicher, unabhängig vom aufrufenden $PWD) ----------------------
+# Das Script setzt SCRIPT_PATH/DIR/NAME selbst; hier nur Fallback.
+: "${SCRIPT_PATH:=$(readlink -f -- "${BASH_SOURCE[1]:-$0}")}"
+: "${SCRIPT_DIR:=${SCRIPT_PATH%/*}}"
+: "${SCRIPT_NAME:=${SCRIPT_PATH##*/}}"
+B_LIB_DIR="$(readlink -f -- "${BASH_SOURCE[0]}")"
+B_LIB_DIR="${B_LIB_DIR%/*}"
+B_CALL_DIR="${PWD}" # Verzeichnis, aus dem aufgerufen wurde
+B_TMP_DIR="" # wird von b_tmpdir gesetzt
+
+# --- Standard-Optionen (gelten für jedes Script) ------------------------------
+# Format: "kurz|lang|typ[:META]|default|Beschreibung" typ = flag | value
+# Variable im Script: OPTION_ (Großbuchstaben, '-' → '_'), z. B. OPTION_DRY_RUN
+B_OPTS_STD=(
+ "h|help|flag|0|diese Hilfe anzeigen"
+ "d|debug|flag|0|ausführliche Ausgabe (Log-Level debug)"
+ "q|quiet|flag|0|keine Konsolenausgabe (Logdatei bleibt)"
+ "n|dry-run|flag|0|nur anzeigen, nichts ändern"
+ "l|logfile|value:DATEI||zusätzlich in DATEI loggen"
+)
+declare -p B_OPTS &>/dev/null || B_OPTS=() # Script/.env hängen mit B_OPTS+=(…) an
+declare -ga ARGS=() # Positionsargumente nach b_init
+
+declare -grA _B_LEVELS=([debug]=10 [info]=20 [warn]=30 [error]=40)
+declare -gA _B_OPT_SHORT=() _B_OPT_TYPE=() _B_OPT_META=() _B_OPT_DEFAULT=() _B_OPT_DESC=()
+declare -ga _B_OPT_ORDER=()
+
+#===============================================================================
+# Ausgabe / Logging
+#===============================================================================
+
+## @brief Zentrale Ausgabe auf Konsole und/oder Logdatei, gefiltert nach Level
+## @param $1 Level: debug | info | warn | error
+## @param $@ Text
+## @return 0 ok, 2 unbekanntes Level
+blog() {
+ local level="${1:-}"; shift || true
+ local n="${_B_LEVELS[$level]:-}" msg="$*" c="" r=""
+ [[ -n "$n" ]] || { printf 'blog: unbekanntes Level "%s"\n' "$level" >&2; return 2; }
+
+ if (( ${B_LOG_CONSOLE:-1} )) && (( n >= ${_B_LEVELS[${B_LOG_LEVEL:-info}]:-20} )); then
+ if [[ "${B_LOG_COLOR:-auto}" == 1 || ( "${B_LOG_COLOR:-auto}" == auto && -t 2 ) ]]; then
+ r=$'\e[0m'
+ case "$level" in debug) c=$'\e[2m' ;; warn) c=$'\e[33m' ;; error) c=$'\e[31m' ;; esac
+ fi
+ case "$level" in
+ info) printf '%s\n' "$msg" ;;
+ debug) printf '%s[DEBUG] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ warn) printf '%s[WARN] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ error) printf '%s[FEHLER] %s%s\n' "$c" "$msg" "$r" >&2 ;;
+ esac
+ fi
+
+ if [[ -n "${B_LOG_FILE:-}" ]] \
+ && (( n >= ${_B_LEVELS[${B_LOG_FILE_LEVEL:-${B_LOG_LEVEL:-info}}]:-20} )); then
+ printf '%(%Y-%m-%d %H:%M:%S)T %-5s %s[%d] %s\n' -1 "${level^^}" "$SCRIPT_NAME" "$$" "$msg" \
+ >> "$B_LOG_FILE" 2>/dev/null || true
+ fi
+ return 0
+}
+
+## @brief Ersatz für echo: Info-Meldung über blog
+## @param $@ Text
+## @return 0
+becho() { blog info "$@"; }
+
+## @brief Debug-Meldung (nur bei -d bzw. B_LOG_LEVEL=debug)
+## @param $@ Text
+## @return 0
+bdebug() { blog debug "$@"; }
+
+## @brief Warnung (stderr)
+## @param $@ Text
+## @return 0
+bwarn() { blog warn "$@"; }
+
+## @brief Fehlermeldung (stderr)
+## @param $@ Text
+## @return 0
+berror() { blog error "$@"; }
+
+## @brief Fehlermeldung ausgeben und Script beenden (EXIT-Trap räumt auf)
+## @param $1 Text
+## @param $2 Exit-Code (Default 1; 2 = Aufruffehler, zeigt Hilfe-Hinweis)
+## @return – (beendet das Script)
+bdie() {
+ local rc="${2:-1}"
+ berror "${1:-Abbruch}"
+ (( rc == 2 )) && blog info "Hilfe: ${SCRIPT_NAME} --help"
+ exit "$rc"
+}
+
+#===============================================================================
+# Hilfen für den Script-Alltag
+#===============================================================================
+
+## @brief Befehl ausführen – oder bei --dry-run nur anzeigen
+## @param $@ Befehl mit Argumenten (kein String, kein eval)
+## @return Exit-Code des Befehls, 0 im Dry-Run
+brun() {
+ if (( ${OPTION_DRY_RUN:-0} )); then
+ becho "[dry-run] $(printf '%q ' "$@")"
+ return 0
+ fi
+ bdebug "run: $(printf '%q ' "$@")"
+ "$@"
+}
+
+## @brief Prüft, ob Programme vorhanden sind
+## @param $@ Programmnamen
+## @return 0 alle da, 1 mindestens eins fehlt (wird gemeldet)
+b_require() {
+ local cmd rc=0
+ for cmd in "$@"; do
+ command -v "$cmd" &>/dev/null || { berror "Programm fehlt: $cmd"; rc=1; }
+ done
+ return "$rc"
+}
+
+## @brief Prüft, ob Variablen gesetzt und nicht leer sind
+## @param $@ Variablennamen
+## @return 0 alle gesetzt, 1 mindestens eine fehlt (wird gemeldet)
+b_require_vars() {
+ local v rc=0
+ for v in "$@"; do
+ [[ -n "${!v:-}" ]] || { berror "Variable nicht gesetzt: $v (in .env oder Umgebung)"; rc=1; }
+ done
+ return "$rc"
+}
+
+## @brief Legt ein temporäres Verzeichnis an (B_TMP_DIR), wird beim Exit gelöscht
+## @return 0 ok, 1 mktemp fehlgeschlagen
+b_tmpdir() {
+ [[ -n "$B_TMP_DIR" ]] && return 0
+ B_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${SCRIPT_NAME%.sh}.XXXXXX")" || return 1
+ bdebug "Temp-Verzeichnis: $B_TMP_DIR"
+}
+
+## @brief Macht einen Pfad absolut; relative Pfade gelten relativ zu $2
+## @param $1 Pfad
+## @param $2 Basisverzeichnis (Default: SCRIPT_DIR)
+## @return 0; Ergebnis auf stdout
+b_abspath() {
+ local p="$1" base="${2:-$SCRIPT_DIR}"
+ [[ "$p" == /* ]] || p="${base%/}/${p}"
+ printf '%s\n' "$p"
+}
+
+#===============================================================================
+# Usage aus der Präambel
+#===============================================================================
+
+## @brief Gibt die Präambel des Scripts als Hilfe aus ('## ' wird entfernt,
+## die Zeile '## @OPTIONS@' wird durch die Optionsliste ersetzt)
+## @return 0
+b_usage() {
+ local line part=0
+ while IFS= read -r line || [[ -n "$line" ]]; do
+ if [[ "$line" =~ ^#-{10,} ]]; then
+ (( ++part >= 2 )) && break
+ continue
+ fi
+ (( part == 1 )) && [[ "$line" == '##'* ]] || continue
+ line="${line#'##'}"; line="${line# }"
+ if [[ "$line" == *'@OPTIONS@'* ]]; then _b_opts_help; else printf '%s\n' "$line"; fi
+ done < "$SCRIPT_PATH"
+ return 0
+}
+
+## @brief Optionsliste für die Hilfe (aus B_OPTS_STD + B_OPTS)
+## @return 0
+_b_opts_help() {
+ local long s left d
+ for long in "${_B_OPT_ORDER[@]}"; do
+ s="${_B_OPT_SHORT[$long]}"
+ left="${s:+-$s | }--${long}"
+ [[ "${_B_OPT_TYPE[$long]}" == value ]] && left+=" ${_B_OPT_META[$long]}"
+ d="${_B_OPT_DEFAULT[$long]}"
+ if [[ "${_B_OPT_TYPE[$long]}" == value && -n "$d" ]]; then
+ printf ' %-28s %s (Default: %s)\n' "$left" "${_B_OPT_DESC[$long]}" "$d"
+ else
+ printf ' %-28s %s\n' "$left" "${_B_OPT_DESC[$long]}"
+ fi
+ done
+ return 0
+}
+
+#===============================================================================
+# Optionen
+#===============================================================================
+
+## @brief Variablenname zu einer Langoption: dry-run → OPTION_DRY_RUN
+## @param $1 Langname
+## @return 0; Name auf stdout
+_b_optvar() { local n="${1^^}"; printf 'OPTION_%s\n' "${n//-/_}"; }
+
+## @brief Baut die Optionstabellen aus B_OPTS_STD + B_OPTS und prüft sie
+## @return 0 ok, 2 Definitionsfehler
+_b_opts_build() {
+ local def s long type meta deflt desc rest
+ local -A seen_short=()
+ _B_OPT_ORDER=()
+ for def in "${B_OPTS_STD[@]}" "${B_OPTS[@]}"; do
+ IFS='|' read -r s long type deflt desc rest <<< "$def"
+ meta="${type#*:}"; type="${type%%:*}"; [[ "$meta" == "$type" ]] && meta="WERT"
+ if [[ -z "$long" || ! "$long" =~ ^[a-z][a-z0-9-]*$ || ! "$type" =~ ^(flag|value)$ || -n "$rest" ]]; then
+ berror "Ungültige Optionsdefinition: \"$def\" (kurz|lang|flag|value[:META]|default|Text)"
+ return 2
+ fi
+ if [[ -n "$s" ]]; then
+ [[ -n "${seen_short[$s]:-}" && "${seen_short[$s]}" != "$long" ]] && {
+ berror "Kurzoption -$s doppelt vergeben (${seen_short[$s]}, $long)"; return 2; }
+ seen_short[$s]="$long"
+ fi
+ [[ -n "${_B_OPT_TYPE[$long]:-}" ]] || _B_OPT_ORDER+=("$long")
+ _B_OPT_SHORT[$long]="$s"; _B_OPT_TYPE[$long]="$type"; _B_OPT_META[$long]="$meta"
+ _B_OPT_DEFAULT[$long]="$deflt"; _B_OPT_DESC[$long]="$desc"
+ done
+ return 0
+}
+
+## @brief Setzt OPTION_ für eine erkannte Option
+## @param $1 Langname
+## @param $2 Wert
+## @return 0 ok, 2 ungültiger Wert
+_b_opt_set() {
+ local long="$1" val="$2" var
+ var="$(_b_optvar "$long")"
+ if [[ "${_B_OPT_TYPE[$long]}" == flag && ! "$val" =~ ^[01]$ ]]; then
+ berror "Option --$long erwartet 0 oder 1, nicht \"$val\""
+ return 2
+ fi
+ printf -v "$var" '%s' "$val"
+ _B_CLI_SET[$long]=1
+ return 0
+}
+
+## @brief Parst die Kommandozeile → OPTION_* und ARGS (Positionsargumente)
+## @param $@ Argumente des Scripts
+## @return 0 ok, 2 Aufruffehler
+_b_opts_parse() {
+ local arg name val s i long
+ ARGS=()
+ while (( $# )); do
+ arg="$1"; shift
+ case "$arg" in
+ --) ARGS+=("$@"); break ;;
+ --?*)
+ name="${arg#--}"; val=""
+ if [[ "$name" == *=* ]]; then val="${name#*=}"; name="${name%%=*}"; fi
+ [[ -n "${_B_OPT_TYPE[$name]:-}" ]] || { berror "Unbekannte Option: --$name"; return 2; }
+ if [[ "${_B_OPT_TYPE[$name]}" == value && "$arg" != *=* ]]; then
+ (( $# )) || { berror "Option --$name braucht einen Wert"; return 2; }
+ val="$1"; shift
+ elif [[ "${_B_OPT_TYPE[$name]}" == flag && "$arg" != *=* ]]; then
+ val=1
+ fi
+ _b_opt_set "$name" "$val" || return 2
+ ;;
+ -?*)
+ for (( i = 1; i < ${#arg}; i++ )); do
+ s="${arg:i:1}"; long=""
+ for name in "${_B_OPT_ORDER[@]}"; do
+ [[ "${_B_OPT_SHORT[$name]}" == "$s" ]] && { long="$name"; break; }
+ done
+ [[ -n "$long" ]] || { berror "Unbekannte Option: -$s"; return 2; }
+ if [[ "${_B_OPT_TYPE[$long]}" == value ]]; then
+ val="${arg:i+1}" # -lDATEI oder -l DATEI
+ if [[ -z "$val" ]]; then
+ (( $# )) || { berror "Option -$s braucht einen Wert"; return 2; }
+ val="$1"; shift
+ fi
+ _b_opt_set "$long" "$val" || return 2
+ break
+ fi
+ _b_opt_set "$long" 1 || return 2
+ done
+ ;;
+ *) ARGS+=("$arg") ;;
+ esac
+ done
+ return 0
+}
+
+#===============================================================================
+# Trap / Aufräumen
+#===============================================================================
+
+## @brief EXIT-Trap: ruft cleanup() des Scripts (falls definiert), löscht B_TMP_DIR
+## @return – (beendet mit dem ursprünglichen Exit-Code)
+_b_on_exit() {
+ local rc=$?
+ trap - EXIT
+ set +e
+ if declare -F cleanup &>/dev/null; then
+ cleanup "$rc" || bwarn "cleanup() meldete Fehler"
+ fi
+ [[ -n "$B_TMP_DIR" && -d "$B_TMP_DIR" ]] && rm -rf -- "$B_TMP_DIR"
+ bdebug "Ende ${SCRIPT_NAME} (rc=${rc})"
+ exit "$rc"
+}
+
+## @brief ERR-Trap: meldet fehlgeschlagenen Befehl mit Funktion und Zeile
+## (bewusstes 'return N' hat die Funktion schon selbst gemeldet → nur debug)
+## @param $1 Zeilennummer
+## @param $2 Befehl
+## @return 0 (Abbruch übernimmt set -e)
+_b_on_err() {
+ local rc=$? lvl=error
+ [[ "$2" == return* ]] && lvl=debug
+ blog "$lvl" "rc=${rc} in ${FUNCNAME[1]:-main}() Zeile ${1}: ${2}"
+ return 0
+}
+
+#===============================================================================
+# Einstieg
+#===============================================================================
+
+## @brief Initialisiert das Script: Trap, .env, Defaults, Optionen, Logging, Hilfe
+## Vorrang: Default < .env < Umgebung (B_*, OPTION_*) < Kommandozeile
+## @param $@ Argumente des Scripts (danach: set -- "${ARGS[@]}")
+## @return 0 ok; beendet bei --help (0) oder Aufruffehler (2)
+b_init() {
+ local v long var env_file
+ local -A env_keep=()
+ declare -gA _B_CLI_SET=()
+
+ trap _b_on_exit EXIT
+ trap '_b_on_err "$LINENO" "$BASH_COMMAND"' ERR
+ trap 'exit 130' INT
+ trap 'exit 143' TERM
+
+ # 1. exportierte Steuer-Variablen aus der Umgebung merken (gewinnen gegen .env)
+ for v in $(compgen -e); do
+ [[ "$v" =~ ^(B_[A-Z0-9_]+|OPTION_[A-Z0-9_]+)$ ]] && env_keep[$v]="${!v}"
+ done
+
+ # 2. .env laden (Bash-Syntax; relativ zum Script, nicht zum $PWD)
+ env_file="$(b_abspath "${B_ENV_FILE:-.env}")"
+ if [[ -f "$env_file" ]]; then
+ # shellcheck source=/dev/null
+ source "$env_file" || bdie "Fehler beim Laden von $env_file"
+ fi
+ for v in "${!env_keep[@]}"; do printf -v "$v" '%s' "${env_keep[$v]}"; done
+
+ # 3. Defaults für alles, was weder .env noch Umgebung gesetzt hat
+ : "${B_LOG_LEVEL:=info}" "${B_LOG_CONSOLE:=1}" "${B_LOG_FILE:=}" \
+ "${B_LOG_FILE_LEVEL:=}" "${B_LOG_COLOR:=auto}"
+ [[ -n "${_B_LEVELS[$B_LOG_LEVEL]:-}" ]] || { bwarn "B_LOG_LEVEL=$B_LOG_LEVEL ungültig → info"; B_LOG_LEVEL=info; }
+ [[ -n "$B_LOG_FILE" ]] && B_LOG_FILE="$(b_abspath "$B_LOG_FILE" "$SCRIPT_DIR")"
+
+ _b_opts_build || exit 2
+ for long in "${_B_OPT_ORDER[@]}"; do
+ var="$(_b_optvar "$long")"
+ [[ -v "$var" ]] || printf -v "$var" '%s' "${_B_OPT_DEFAULT[$long]}"
+ done
+
+ # 4. Kommandozeile
+ _b_opts_parse "$@" || bdie "Aufruffehler" 2
+
+ # 5. Standard-Optionen anwenden
+ (( OPTION_DEBUG )) && B_LOG_LEVEL=debug
+ (( OPTION_QUIET )) && B_LOG_CONSOLE=0
+ if [[ -n "${_B_CLI_SET[logfile]:-}" ]]; then
+ B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$B_CALL_DIR")" # CLI: relativ zum $PWD
+ elif [[ -n "$OPTION_LOGFILE" ]]; then
+ B_LOG_FILE="$(b_abspath "$OPTION_LOGFILE" "$SCRIPT_DIR")"
+ fi
+ if [[ -n "$B_LOG_FILE" ]]; then
+ mkdir -p -- "${B_LOG_FILE%/*}" 2>/dev/null && touch -- "$B_LOG_FILE" 2>/dev/null \
+ || { B_LOG_FILE=""; bwarn "Logdatei nicht beschreibbar – nur Konsole"; }
+ fi
+
+ (( OPTION_HELP )) && { b_usage; exit 0; }
+
+ bdebug "Start ${SCRIPT_NAME} (_functions ${B_FUNCTIONS_VERSION}) in ${SCRIPT_DIR}, Aufruf aus ${B_CALL_DIR}"
+ bdebug ".env: $( [[ -f "$env_file" ]] && echo "$env_file" || echo "keine" ) · Log: ${B_LOG_LEVEL}/${B_LOG_FILE:-ohne Datei}"
+ for long in "${_B_OPT_ORDER[@]}"; do
+ var="$(_b_optvar "$long")"; bdebug " ${var}=${!var}"
+ done
+ return 0
+}
+
+# --- projektspezifische Erweiterungen -----------------------------------------
+if [[ -f "${B_LIB_DIR}/_functions.local.sh" ]]; then
+ # shellcheck source=/dev/null
+ source "${B_LIB_DIR}/_functions.local.sh"
+fi
diff --git a/bin/bashdoc.sh b/bin/bashdoc.sh
new file mode 100755
index 0000000..0ed8bf5
--- /dev/null
+++ b/bin/bashdoc.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file bashdoc.sh
+## @brief Erzeugt Markdown-Doku aus den '##'-Kommentaren von Bash-Scripts
+## (Präambel + Funktionsköpfe, angelehnt an javadoc)
+## @author
+## @owner
+## @assist KI-unterstützt (Claude), geprüft von
+## @version 1.0.0 (07.10.2026)
+##
+## @usage bashdoc.sh [Optionen] [...]
+## Ausgabe auf stdout oder mit -o in eine Datei
+## @options
+## @OPTIONS@
+## @exit 0 = ok · 1 = Fehler · 2 = Aufruffehler
+#-------------------------------------------------------------------------------
+set -Eeuo pipefail
+
+SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
+SCRIPT_DIR="${SCRIPT_PATH%/*}"
+SCRIPT_NAME="${SCRIPT_PATH##*/}"
+# shellcheck source=_functions.sh
+source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
+
+B_OPTS+=(
+ "o|output|value:DATEI||Markdown in DATEI schreiben (relativ = zum Aufrufverzeichnis)"
+ "p|private|flag|0|auch interne Funktionen (_name) dokumentieren"
+)
+b_init "$@"
+set -- "${ARGS[@]}"
+
+## @brief Wandelt ein Script in Markdown um
+## @param $1 Script-Datei
+## @return 0 ok, 1 Datei nicht lesbar; Markdown auf stdout
+doc_file() {
+ local file="$1"
+ [[ -r "$file" ]] || { berror "Nicht lesbar: $file"; return 1; }
+ awk -v fname="${file##*/}" -v private="$OPTION_PRIVATE" '
+ function flush_block(name, i, l, tag) {
+ if (!private && name ~ /^_/) { n = 0; return }
+ printf "\n### `%s()`\n\n", name
+ for (i = 1; i <= n; i++) {
+ l = blk[i]
+ if (l ~ /^@brief/) { sub(/^@brief[ \t]*/, "", l); print l; last = "brief" }
+ else if (l ~ /^@param/) { sub(/^@param[ \t]*/, "", l); print "- **Param** " l; last = "param" }
+ else if (l ~ /^@return/) { sub(/^@return[ \t]*/, "", l); print "- **Return** " l; last = "return" }
+ else if (l ~ /^@/) { tag = l; sub(/[ \t].*/, "", tag); sub(/^@[a-z]+[ \t]*/, "", l); print "- **" substr(tag, 2) "** " l }
+ else if (l != "") { print (last == "brief" ? l : " " l) }
+ }
+ n = 0
+ }
+ function print_opts( i, f, left) {
+ print " -h -d -q -n -l Standardoptionen (Hilfe, Debug, still, Trockenlauf, Logdatei)"
+ for (i = 1; i <= nopt; i++) {
+ split(opt[i], f, "|"); sub(/:.*/, "", f[3])
+ left = (f[1] != "" ? "-" f[1] " | " : "") "--" f[2]
+ if (opt_meta[i] != "") left = left " " opt_meta[i]
+ printf " %-26s %s%s\n", left, f[5], (f[3] == "value" && f[4] != "" ? " (Default: " f[4] ")" : "")
+ }
+ }
+ # 1. Durchlauf: Script-Optionen aus B_OPTS+=( "…" ) sammeln
+ FNR == NR {
+ if ($0 ~ /^[ \t]*B_OPTS\+=\(/) inopts = 1
+ if (inopts) {
+ l = $0
+ while (match(l, /"[^"]*"/)) {
+ o = substr(l, RSTART + 1, RLENGTH - 2); l = substr(l, RSTART + RLENGTH)
+ opt[++nopt] = o; split(o, f, "|"); opt_meta[nopt] = (f[3] ~ /:/ ? substr(f[3], index(f[3], ":") + 1) : (f[3] == "value" ? "WERT" : ""))
+ }
+ if ($0 ~ /\)[ \t]*$/) inopts = 0
+ }
+ next
+ }
+ # 2. Durchlauf: Präambel + Funktionsköpfe
+ /^#-{10,}/ { part++; if (part == 1) printf "## %s\n\n```text\n", fname; if (part == 2) print "```"; next }
+ part == 1 && /^##/ { l = $0; sub(/^## ?/, "", l); if (l !~ /@OPTIONS@/) print l; else print_opts(); next }
+ part >= 2 && /^[ \t]*##/ { l = $0; sub(/^[ \t]*## ?/, "", l); blk[++n] = l; next }
+ part >= 2 && n > 0 && /^[ \t]*(function[ \t]+)?[A-Za-z_][A-Za-z0-9_]*[ \t]*\(\)/ {
+ name = $0; sub(/^[ \t]*(function[ \t]+)?/, "", name); sub(/[ \t]*\(.*/, "", name)
+ flush_block(name); next
+ }
+ part >= 2 && n > 0 && !/^[ \t]*#/ { n = 0 }
+ ' "$file" "$file"
+}
+
+## @brief Hauptablauf
+## @param $@ Script-Dateien
+## @return 0 ok, 1 Fehler
+main() {
+ (( $# )) || bdie "Mindestens ein Script angeben" 2
+ local f out="" rc=0
+ [[ -n "$OPTION_OUTPUT" ]] && out="$(b_abspath "$OPTION_OUTPUT" "$B_CALL_DIR")"
+ {
+ printf '# Script-Doku\n\n> Erzeugt mit `bin/bashdoc.sh` am %(%d.%m.%Y)T – Quelle sind die `##`-Kommentare.\n\n' -1
+ for f in "$@"; do
+ doc_file "$(b_abspath "$f" "$B_CALL_DIR")" || rc=1
+ printf '\n'
+ done
+ } > "${out:-/dev/stdout}"
+ [[ -n "$out" ]] && becho "Doku geschrieben: $out"
+ return "$rc"
+}
+
+main "$@"
diff --git a/bin/kb_index.sh b/bin/kb_index.sh
new file mode 100755
index 0000000..65c9123
--- /dev/null
+++ b/bin/kb_index.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+## @file kb_index.sh
+## @brief Erzeugt ai/context/knowledge/INDEX.md aus dem Frontmatter aller Einträge
+## und prüft die Pflichtfelder (id, title, type, status, updated, owner)
+## @author
+## @owner
+## @assist KI-unterstützt (Claude), geprüft von
+## @version 1.0.0 (07.10.2026)
+##
+## @usage kb_index.sh [Optionen]
+## @options
+## @OPTIONS@
+## @exit 0 = ok · 1 = Fehler / Index veraltet (--check) · 2 = Aufruffehler
+#-------------------------------------------------------------------------------
+set -Eeuo pipefail
+
+SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
+SCRIPT_DIR="${SCRIPT_PATH%/*}"
+SCRIPT_NAME="${SCRIPT_PATH##*/}"
+# shellcheck source=_functions.sh
+source "${SCRIPT_DIR}/_functions.sh" || { echo "FEHLER: ${SCRIPT_DIR}/_functions.sh fehlt" >&2; exit 1; }
+
+B_OPTS+=(
+ "c|check|flag|0|nur prüfen: Pflichtfelder ok und INDEX.md aktuell?"
+ "k|kbdir|value:DIR|../ai/context/knowledge|Wurzel der Knowledge-DB (relativ = zum Script)"
+)
+b_init "$@"
+set -- "${ARGS[@]}"
+
+REQUIRED=(id title type status updated owner)
+
+## @brief Liest einen Frontmatter-Wert (einzeilig) aus einer Markdown-Datei
+## @param $1 Datei
+## @param $2 Schlüssel
+## @return 0; Wert auf stdout (leer, wenn nicht vorhanden)
+fm_get() {
+ awk -v key="$2" '
+ NR == 1 && $0 != "---" { exit }
+ NR > 1 && $0 == "---" { exit }
+ NR > 1 {
+ k = $0; sub(/:.*/, "", k)
+ if (k == key) { v = $0; sub(/^[^:]*:[ \t]*/, "", v); sub(/[ \t]+#.*$/, "", v); gsub(/^"|"$/, "", v); print v; exit }
+ }' "$1"
+}
+
+## @brief Baut den Index-Inhalt und meldet fehlende Pflichtfelder
+## @param $1 KB-Wurzel
+## @return 0 ok, 1 mindestens ein Eintrag unvollständig; Markdown auf stdout
+build_index() {
+ local kb="$1" f rel dir last_dir="" key val rc=0 n=0
+ local -A ids=()
+ printf '# Knowledge-Index\n\n'
+ printf '> Automatisch erzeugt von `bin/kb_index.sh` – nicht von Hand bearbeiten.\n'
+ printf '> Format der Einträge: [README.md](README.md)\n'
+ while IFS= read -r f; do
+ rel="${f#"$kb"/}"; dir="${rel%%/*}"
+ [[ "$dir" == "$rel" ]] && dir="."
+ for key in "${REQUIRED[@]}"; do
+ [[ -n "$(fm_get "$f" "$key")" ]] || { berror "${rel}: Pflichtfeld '${key}' fehlt"; rc=1; }
+ done
+ val="$(fm_get "$f" id)"
+ if [[ -n "$val" ]]; then
+ [[ -n "${ids[$val]:-}" ]] && { berror "${rel}: id '${val}' doppelt (auch ${ids[$val]})"; rc=1; }
+ ids[$val]="$rel"
+ fi
+ if [[ "$dir" != "$last_dir" ]]; then
+ printf '\n## %s\n\n| id | Titel | Status | Stand | Tags |\n|---|---|---|---|---|\n' "$dir"
+ last_dir="$dir"
+ fi
+ printf '| %s | [%s](%s) | %s | %s | %s |\n' "$val" "$(fm_get "$f" title)" "$rel" \
+ "$(fm_get "$f" status)" "$(fm_get "$f" updated)" "$(fm_get "$f" tags | tr -d '[]')"
+ (( ++n ))
+ done < <(find "$kb" -type f -name '*.md' ! -name 'README.md' ! -name 'INDEX.md' ! -name '_*' | LC_ALL=C sort)
+ (( n )) || printf '\n_(noch keine Einträge)_\n'
+ bdebug "${n} Einträge gelesen"
+ return "$rc"
+}
+
+## @brief Hauptablauf
+## @return 0 ok, 1 Fehler / veraltet
+main() {
+ (( $# == 0 )) || bdie "Keine Positionsargumente erwartet" 2
+ local kb rc=0 new
+ kb="$(readlink -f -- "$(b_abspath "$OPTION_KBDIR")")"
+ [[ -d "$kb" ]] || { berror "KB-Verzeichnis fehlt: $kb"; return 1; }
+ b_tmpdir
+ new="${B_TMP_DIR}/INDEX.md"
+ build_index "$kb" > "$new" || rc=1
+
+ if (( OPTION_CHECK )); then
+ if ! cmp -s "$new" "${kb}/INDEX.md"; then
+ berror "INDEX.md ist veraltet – bin/kb_index.sh ausführen"; rc=1
+ fi
+ (( rc )) || becho "Knowledge-DB ok"
+ return "$rc"
+ fi
+ if cmp -s "$new" "${kb}/INDEX.md"; then
+ becho "INDEX.md unverändert"
+ else
+ brun cp -- "$new" "${kb}/INDEX.md"
+ becho "INDEX.md aktualisiert"
+ fi
+ return "$rc"
+}
+
+main "$@"
diff --git a/context.md b/context.md
new file mode 100644
index 0000000..093ebc2
--- /dev/null
+++ b/context.md
@@ -0,0 +1,47 @@
+# Projekt-Kontext: LPIC-Prüfungsvorbereitung
+
+> Arbeitsgedächtnis für Agenten. Wird zu Sitzungsbeginn gelesen und am Ende jeder Sitzung mit relevantem Ergebnis aktualisiert.
+> Abschnitte nicht umbenennen.
+
+## Metadaten
+- **Projekt:** `lpic` – Vorbereitung auf die LPIC-Zertifizierung
+- **Status:** Idee
+- **Angelegt:** 29.10.2026 · **Letzte Aktualisierung:** 29.10.2026
+- **Verantwortlich:** `@author` br · `@owner` br (siehe `ai/context/responsible.md`)
+- **Code:** in diesem Repo – Übungsscripts unter `bash/`, Lab-Aufbau unter `ansible/` bzw. `stacks/` (Ordner bei Bedarf anlegen) · Remote:
+
+## Ziel
+
+Messbar am Ende:
+1.
+2. Jedes Prüfungsthema (Objective) hat einen Eintrag unter `ai/context/knowledge/topics/` mit Kernbefehlen, Dateien und Übungen.
+
+## Rahmen
+- Prüfungsgrundlage: offizielle Lernziele (Objectives) des LPI in der jeweils gültigen Version – Version und Abrufdatum im Knowledge-Eintrag nennen.
+- Übungen lokal (Container/VMs, `ai/rules/testing.md`); keine Produktivsysteme.
+- Leitsatz: **so lokal wie möglich, so auswärts wie nötig** (`ai/rules/privacy.md`).
+
+## Fahrplan
+- [ ] Ziel festlegen (Stufe, Prüfungen, Termin)
+- [ ] Objectives als Gliederung in `ai/context/knowledge/topics/` anlegen
+- [ ] Lab-Umgebung festlegen (lokale Container/VMs)
+
+## Stand
+- 29.10.2026: Repo aus der 2nd-brain-Basis abgeleitet (Regeln, Standards, Kontextstruktur übernommen; 2nd-brain-Inhalte entfernt, ohne Git-Historie).
+
+## Entscheidungen
+| Datum | Entscheidung | Warum | ADR |
+|---|---|---|---|
+| 29.10.2026 | Neues Repo ohne Historie aus 2nd-brain; Regeln, `ai/common/`, `bin/`, Umgebung und KI-Freigaben übernommen | Basis wiederverwenden, keine 2nd-brain-Inhalte in der Historie | – |
+| 29.10.2026 | Knowledge-Typ `topic` (`topics/`) für Lerninhalte je Prüfungsthema | LPIC-Inhalte von Umgebungswissen trennen | – |
+
+## Offene Fragen
+1. [ ] **Ziel:** Welche Stufe (LPIC-1/-2/-3) und welche Prüfungen, bis wann?
+2. [ ] **Remote:** Gitea-Repo anlegen (z. B. `br/lpic`) und als `origin` eintragen.
+3. [ ] **Lab:** Welche Umgebung für Übungen (lokale Container, VMs, Distributionen)?
+
+## Nächster Schritt
+Betreiber legt Ziel und Prüfungen fest; danach Objectives als Gliederung unter `topics/` anlegen.
+
+## Sitzungsprotokoll
+- 29.10.2026 – Repo aus `2ndbrain` abgeleitet: Stacks, Ansible-Inventar, `architecture.md`, 2nd-brain-Knowledge (`dockweb112`, `kellerbeast`, LUKS-Recherche) entfernt; `services/sultan` (Verweis aus `docker.md`) bereinigt behalten; `environment.md` und `privacy.md` auf Projekt angepasst; Knowledge-Typ `topic` ergänzt.