Lessons Learned

1. tsxesbuild.scss kann tsx nicht laden

Problem: CI-Workflow quartz.yml scheiterte mit ERR_UNKNOWN_FILE_EXTENSION .scss.

Ursache: npm run install-plugins verwendete tsx, um Plugin-Indizes zu generieren. tsx importiert Module nativ und kann .scss-Dateien nicht interpretieren.

Lösung: Ersetzt durch npx quartz plugin install. Die Quartz CLI nutzt esbuild mit sassPlugin und verarbeitet .scss korrekt.

Prävention: Nie eine eigene install-plugins-Alternative schreiben — die upstream-empfohlene Methode (npx quartz plugin install) verwenden.


2. Flüchtige node_modules/-Persistenz maskiert Fehler

Problem: Lokal funktionierte npx quartz build, im CI nicht.

Ursache: Lokal war .quartz/plugins/ von einem früheren Durchlauf liegengeblieben. Ein frisches Clone (wie im CI) hat dieses Verzeichnis nie, also fehlte der Import ../../.quartz/plugins.

Lösung: CI muss die exakte Sequenz des Dockerfiles abbilden: npm ci && npx quartz plugin install && npx quartz build.

Prävention: Vor lokalem Testen immer in einem frischen Clone arbeiten oder rm -rf .quartz node_modules && npm ci && npx quartz plugin install ausführen.


3. Dockerfile als Single Source of Truth für Build-Schritte

Problem: CI-Workflow und Dockerfile hatten unterschiedliche Build-Sequenzen.

Ursache: Der CI-Workflow verwendete npm run install-plugins (eigenes Skript), während das Dockerfile npx quartz plugin install nutzte.

Lösung: CI-Workflow auf die Sequenz des Dockerfiles abgestimmt.

Prävention: Wenn ein Dockerfile existiert, ist es die autoritative Quelle für die Build-Reihenfolge. CI-Workflows müssen diese spiegeln, nicht eigene Varianten erfinden.


4. Docker build context ≠ Dockerfile-Pfad

Problem: docker compose build scheiterte mit "/pyproject.toml": not found.

Ursache: Build-Kontext war . (Projekt-Root), aber COPY pyproject.toml . erwartet die Datei relativ zum Kontext. pyproject.toml liegt in server/, nicht im Root.

Lösung: Build-Kontext auf ./server geändert: context: ./server; dockerfile: Dockerfile.

Prävention: COPY-Pfade sind immer relativ zum context:, nicht zum Dockerfile. Dockerfile kann in einem Unterverzeichnis liegen, dann müssen die Quellpfade zum Kontext passen — oder den Kontext auf das Verzeichnis setzen, in dem die zu kopierenden Dateien liegen.


5. Kein doppelter Event-Loop (anyio + uvicorn)

Problem: Container startete, aber MCP-Server stürzte mit RuntimeError: asyncio.run() cannot be called from a running event loop ab.

Ursache: anyio.run(main() startete einen Event-Loop, dann startete uvicorn.run() innerhlb von main() einen zweiten — das ist nicht erlaubt.

Lösung: main() synchron machen. SSE-Pfad ruft uvicorn.run() direkt auf (eigener Loop). Stdio-Pfad ruft anyio.run() auf (der einzige Loop). Struktur:

def main():
    if transport == "sse":
        uvicorn.run(app, ...)   # uvicorn managed seinen eigenen Loop
    else:
        anyio.run(_serve_stdio)  # anyio managed den Loop für stdio

Prävention: Nie uvicorn.run() oder asyncio.run() innerhalb einer async def aufrufen, die bereits in einem Event-Loop läuft. Entweder alles synchron (uvicorn direkt) oder alles asynchron (anyio ohne uvicorn).


6. GitPython braucht git CLI im Container

Problem: Container startete, aber GitPython warf ImportError: Failed to initialize: Bad git executable.

Ursache: python:3.12-slim enthält kein git. GitPython benötigt das git-Binary zur Laufzeit.

Lösung: apt-get install git im Dockerfile, plus ENV GIT_PYTHON_REFRESH=quiet zur Unterdrückung der Startup-Warnung.

Prävention: Bei Images, die GitPython oder andere Git-Bindings nutzen, immer prüfen, ob git im Image installiert ist. python:X-slim-Images haben es nicht.


7. GPU-Reservierung in docker-compose bricht ohne NVIDIA-Treiber

Problem: docker compose up scheiterte mit could not select device driver "nvidia" with capabilities: [[gpu]].

Ursache: deploy.reservations.devices mit NVIDIA-Treiber-Anforderung, aber die Maschine hat keine NVIDIA-GPU.

Lösung: GPU-Reservierung auskommentiert. Ollama läuft auch auf CPU (nur langsamer).

Prävention: GPU-Konfigurationen in docker-compose.yml optional halten oder per Profil/Override steuern. Für CI und Maschinen ohne GPU muss der Stack ohne GPU laufen.


8. Port-Mapping in docker-compose ist unabhängig vom App-Port

Problem: MCP-Server hörte auf Port 8765 im Container, war aber vom Host nicht erreichbar.

Ursache: Uvicorn band an 0.0.0.0:8765 innerhalb des Containers, aber docker-compose hatte kein ports: für gitlas-mcp.

Lösung: ports: ["8765:8765"] zu gitlas-mcp in docker-compose.yml hinzugefügt.

Prävention: Der App-Port (Uvicorn/Server-Config) und der Docker-Port (ports: in compose) sind zwei separate Einstellungen. Beide müssen gesetzt sein, damit der Service von außen erreichbar ist.


9. Module-Level-Seite-Effekte erschweren Tests

Problem: import gitlas_mcp.server führt sofort repo = GitRepo(...) aus und erwartet GITLAS_REPO_PATH.

Ursache: Die repo-Instanz wird beim Modul-Import erzeugt — vor dem Test-Framework und vor allen Fixtures.

Lösung: In conftest.py auf Modulebene ein temporäres Repo erzeugen und per Umgebungsvariable setzen. Tests die repo brauchen, patchen es mit unittest.mock.patch("gitlas_mcp.server.repo", temp_repo).

Prävention: Module-Level-Seite-Effekte (Repo-Initialisierung, Config-Laden) vermeiden, oder per Lazy-Initialisierung ersetzen. Für bestehenden Code reicht conftest.py + patch.


10. git init verwendet systemabhängigen Default-Branch

Problem: Test test_current_branch_default erwartete "main", auf Systemen mit Git < 2.28 ist der Default "master".

Ursache: Seit Git 2.28 ist init.defaultBranch konfigurierbar; viele Distributionen setzen main, NixOS verwendet noch master.

Lösung: Assertion auf beide Werte: assert branch in ("main", "master").

Prävention: Tests, die den Branch-Namen prüfen, müssen beide Varianten akzeptieren, oder das Repo explizit mit --initial-branch=main initialisieren.


11. yaml.safe_load deserialisiert Datum-Objekte, keine Strings

Problem: _build_frontmatter(created=date(...)) produzierte YAML mit created: 2026-07-21, aber yaml.safe_load gab ein datetime.date-Objekt zurück, keinen String.

Ursache: PyYAML erkennt das ISO-Datum und deserialisiert es als Python-date-Objekt.

Lösung: Test-Assertion mit datetime.date-Vergleich statt str-Vergleich.

Prävention: Beim Testen von YAML-Frontmatter immer den tatsächlichen Typ von deserialisierten Werten prüfen — PyYAML konvertiert Datums- und Zeitstrings automatisch.


12. markdown_files zählt auch Root-Markdown-Dateien (README.md)

Problem: Test erwartete 3 Markdown-Dateien, fand 4 (README.md war auch *.md).

Ursache: rglob("*.md") durchsucht das gesamte Repo, inklusive Wurzel.

Lösung: Erwartung auf 4 korrigiert, README.md in die Assertion aufgenommen.

Prävention: Bei Tests, die Dateien zählen, immer das vollständige Inventar der Test-Repo-Struktur kennen. README.md wird oft übersehen.


13. Alpine env unterstützt kein -Scoreutils notwendig

Problem: Quartz-Container startet nicht mit #!/usr/bin/env -S node --no-deprecation.

Ursache: Alpine nutzt BusyBox env, das -S nicht unterstützt. Quartz verwendet env -S im bootstrap-cli.mjs Shebang.

Lösung: coreutils im Dockerfile installieren (apk add coreutils).

Prävention: Alpine-Images immer auf Shebang-Kompatibilität prüfen, wenn Skripte env -S verwenden.


14. esbuild resolvt .mjs nicht automatisch

Problem: npx quartz build scheitert mit Could not resolve "../../.quartz/plugins".

Ursache: esbuild resolvt standardmäßig .js, .ts, .tsx, .jsx, .css, .json — nicht .mjs. Ein Import von .quartz/plugins findet plugins.mjs nicht.

Lösung: Manuell .quartz/plugins.js anlegen (.js statt .mjs).

Prävention: Bei esbuild-basierten Builds immer die auto-resolve-Extensions prüfen. Dateien mit nicht-standard-Extensions (.mjs, .cjs) müssen explizit importiert oder umbenannt werden.


15. Read-Only Volumes blockieren git clone

Problem: Config-Mounts auf /opt/quartz/quartz.config.ts verhindern git clone (Ziel-Pfad nicht leer).

Ursache: Docker-Volumes binden vor dem git clone, sodass das Zielverzeichnis nicht leer ist. Git verweigert den Clone.

Lösung: Config-Dateien in ein separates Verzeichnis mounten (/opt/quartz-config/) und im Entrypoint kopieren.

Prävention: Docker-Volumes und nachfolgende git-Operationen vertragen sich nicht auf demselben Pfad. Immer separate Mount-Punkte verwenden.


16. mkdir -p /opt/quartz vor WORKDIR /opt/quartz

Problem: Quartz Build scheitert weil /opt/quartz nicht existiert.

Ursache: node:22-alpine Image definiert kein WORKDIR. WORKDIR /opt/quartz erwartet den Pfad implizit.

Lösung: Explizit mkdir -p /opt/quartz vor dem WORKDIR-Wechsel.

Prävention: Alpine-Images immer auf WORKDIR-Existenz prüfen, da sie sehr minimalistisch sind.


17. index.md fehlt für Quartz Landing Page

Problem: gitlas.behbahan.de zeigt 404 für den Root-Pfad (14 Unterseiten funktionieren).

Ursache: Keine index.md im Content-Verzeichnis. Quartz generiert keine Landing Page ohne index.md.

Lösung: index.md im Repo-Wurzelverzeichnis anlegen (Referenz auf alle Kategorien).

Prävention: Bei jedem neuen Quartz-Repository als erste Task eine index.md anlegen.


18. Matrix Bot Setup — Registration Token + SQL-Insert

Problem: Kein Admin-API in Conduit zur Benutzererstellung.

Ursache: Conduit hat kein CLI/API zur Benutzerregistrierung. Der Weg ist: shared registration_token in config + REST-Registrierung oder raw SQL.

Lösung:

  • registration_token im conduit.toml setzen
  • python: Argon2id-Hash generieren → SQL INSERT in users-Tabelle
  • INSERT INTO user_emails für registration_token
  • Bot-Client via matrix-nio registrieren (register_new_user)

Prävention: Dokumentation der Conduit-spezifischen Setup-Schritte im Infrastruktur-Repo festhalten.


19. Bot-Room-Join muss via API erfolgen, nicht via SQL-Insert

Problem: Nach SQL-INSERT in userroomid_joined kann der Bot keine Nachrichten senden (M_FORBIDDEN).

Ursache: Der INSERT umgeht Conduits State-Machine. Der Bot erscheint in der DB als “joined”, aber die Room-State-Events zeigen weiterhin membership: invite. Die Join-API gibt {"room_id":...} zurück, aktualisiert den State aber nicht, weil Conduit den Bot bereits in der DB sieht.

Lösung:

  1. Bot-Einträge aus userroomid_joined und roomuserid_joined löschen
  2. Bot via POST /_matrix/client/v3/join/<room_id> neu joinen

Prävention: Niemals direkt in Conduit-Tabellen schreiben, die den Room-State betreffen.


20. Zu viele MCP-Tools überfordern kleine Modelle

Problem: Bot antwortet mit leerem Text nach Tool-Calls (qwen2.5:7b mit 140 Tools).

Ursache: Das Model bekommt 135 MCP-Tools + 5 Builtins und generiert nur Tool-Calls, keinen Text. Die Antwort an den User bleibt leer.

Lösung: Tools auf ~10 reduzieren (7 gitlas-MCP + 3 Builtins). Kleinere Modelle (3B-7B) können ~10-15 Tools zuverlässig handhaben.

Prävention: Nie mehr als ~15 Tools an ein 3B-7B Model geben, oder ein größeres Model (14B+) verwenden.


21. CPU-Inference: 7B vs 3B Geschwindigkeit

Problem: qwen2.5:7b auf CPU braucht ~3 Minuten pro Antwort bei Tool-Calling.

Ursache: 2050 Tokens Inference auf CPU-only (keine GPU) bei 7B Parametern.

Lösung: qwen2.5:3b verwenden → Antwort in ~4 Sekunden bei vergleichbarer Qualität für Chat-Aufgaben.

Prävention: Für Chat-Bots auf CPU immer das kleinste Modell nehmen, das die Aufgabe erfüllt. Tool-Calling multipliziert die Latenz (mehrere Runden).


22. Builtin-Tools, die das Model verwirren

Problem: get_time wird vom Model auch bei einfachen Fragen wie “Hi, testest du gerade?” aufgerufen.

Ursache: Das Model “denkt” über Tool-Nutzung nach statt direkt zu antworten, wenn irrelevante Builtins verfügbar sind.

Lösung: get_time, get_weather und andere irrelevante Builtins aus dem Schema entfernen.


23. System-Prompt: “Always use tools” verhindert Smalltalk

Problem: Das Prompt “Always use tools instead of fabricating information” veranlasst das Model, auch bei Grüßen Tools aufzurufen.

Lösung: Prompt präzisieren:

For simple greetings or casual conversation, respond directly without using tools.
Only use tools when the user asks for information from the knowledge atlas or
wants to create/update pages.

24. Env-Override in ollamarama überschreibt Config

Problem: Nach Model-Wechsel in der Config startet der Bot mit Configuration errors: ollama.default_model must match a key or id in ollama.models.

Ursache: Der entrypoint.py von ollamarama liest OLLAMA_MODEL aus der Umgebung und überschreibt ollama.default_model in der Config. Wenn das Age-Secret noch den alten Wert enthält, stimmt default_model nicht mehr mit ollama.models überein.

Lösung: Beide Stellen aktualisieren:

  1. cfg.ollamaModel im Nix-Modul (Default-Wert für Config-JSON)
  2. OLLAMA_MODEL im Age-Secret gitlas-matrix-env.age

Prävention: Bei Config-Änderungen immer prüfen, ob ein Env-Override im entrypoint.py existiert und das zugehörige Secret aktualisieren.


25. Lokale Re-Imports in Tests verwirren Ruff (F401/F811)

Problem: ruff check meldete 12 Errors — F401 (unused import) und F811 (redefinition) in test_server.py.

Ursache: Top-Level-Import von _read_page, _search, repo + lokale Re-Importe in Test-Methoden. Ruff sieht den Top-Level-Import als “unused” weil er lokal überschrieben (F811) wird.

Lösung:

  • Unbenutzte Top-Level-Imports entfernt (Path, Repo, repo)
  • Lokale Re-Imports von _read_page/_search entfernt — die Top-Level-Imports reichen, da patch("gitlas_mcp.server.repo", ...) das Modul-Attribut patcht, nicht den lokalen Namen

Prävention: patch-basierte Tests brauchen keine lokalen Imports. Einmal importieren, dann patchen. Lokale Re-Imports sind immer ein Flag für eine fehlbereinigte Test-Struktur.


26. Quartz-Doc-Referenzen sparen eigene Doku

Problem: Gitlas-Dokumentation wiederholt Quartz-Features (FlexSearch, Graph View, Authoring), statt auf die offizielle Doku zu verlinken.

Ursache: Eigenständig erklären statt upstream verlinken.

Lösung: Quartz-Docs-Links in README, SEARCH.md, ARCHITECTURE.md ergänzt — verweisen auf https://quartz.jzhao.xyz/features/, /authoring-content, /configuration, /features/graph-view, /features/full-text-search.

Prävention: Wenn ein externes Tool ein Feature bereitstellt, mit einem Link auf dessen Doku verweisen statt es selbst zu erklären. Das reduziert Wartungskosten bei Upstream-Änderungen.


27. npm exec triggert Alpine Shebang-Problem (coreutils)

Problem: RUN npm exec quartz plugin install --from-config im Dockerfile scheitert mit /usr/bin/env: unrecognized option: S.

Ursache: npm exec ruft die Quartz CLI direkt auf. Deren Shebang (#!/usr/bin/env -S node --no-deprecation) benötigt GNU coreutils env, aber Alpine verwendet BusyBox env, das -S nicht unterstützt. (Gleicher Root Cause wie Lesson #13, aber dort trat es beim Container-Start auf, hier beim Image-Build via npm exec.)

Lösung: coreutils im Dockerfile installieren: apk add --no-cache git coreutils.

Prävention: Jeder RUN-Befehl, der indirekt die Quartz CLI ausführt (npm exec quartz, npx quartz, direkter Aufruf) braucht coreutils im Image. Die einfachste Regel: immer coreutils zu Alpine-Images hinzufügen, die Node.js-Tools mit env -S-Shebangs ausführen.


28. Pre-built Docker Image entkoppelt Infra-Phase von Content-Build

Problem: Der CI-Workflow quartz.yml klonte bei jedem Content-Push Quartz neu, installierte npm-Dependencies und Plugins (~2 Min). Die Content-Build-Phase (npx quartz build) dauerte davon nur ~5s.

Ursache: Infra-Phase (clone + npm + plugin install) und Content-Phase (build) waren ein einziger Workflow-Job ohne Caching. Jeder Push durchlief beide Phasen, obwohl sich Quartz-Version und Plugins kaum ändern.

Lösung:

  1. Image-Build-Workflow (quartz-image.yml) — baut ein Docker-Image mit vorinstalliertem Quartz + node_modules + Plugins. Läuft nur bei Änderungen an quartz/Dockerfile, quartz.config.ts oder quartz.layout.ts. Push das Image in die lokale Registry (localhost:5000/gitlas-quartz:latest).

  2. Validation-Workflow (quartz.yml) — läuft bei jedem Content-Push und validiert mit docker run --rm ... npx quartz build --output /dev/null. Dauert ~5s, bricht bei Syntaxfehlern.

  3. docker-compose.ymlbuild: + image: parallel:

    quartz:
      build: ./quartz          # für lokales `make demo` (docker compose --build)
      image: localhost:5000/... # für CI/Production (docker compose up ohne --build)

    Docker Compose baut lokal, zieht aber in CI das gepushte Image.

Prävention: Neue Dienste, die eine langsame Setup-Phase und eine schnelle Arbeits-Phase haben, immer als zweistufigen Prozess modellieren: pre-built Image für die Infra, schneller Docker-Run für den Content.

See Also


Problem: Quarzs Root-Pfad (/) zeigt 404, weil kein index.md im Repo existiert. README.md enthält die Projektbeschreibung, die als Landing Page ideal wäre.

Lösung: Symlink im Working Tree + Git-Ignore:

ln -sf README.md index.md
echo "index.md" >> .gitignore

Der Symlink lebt auf dem Filesystem, wird aber nicht von Git getrackt. Quartz folgt dem Symlink und rendert README.md als Landing Page auf /.

Vorteile:

  • Kein Content-Duplikat (README.md bleibt Single Source)
  • .gitignore verhindert versehentliches Commit des Symlinks
  • Funktioniert lokal + CI + Docker gleichermaßen
  • Einmalig eingerichtet, null Wartung

Achtung: Diese Lösung setzt ein Dateisystem mit Symlink-Unterstützung voraus. Linux/macOS: kein Problem. Windows: Git Bash oder WSL nötig (sonst echten index.md manuell pflegen).


31. Auto-Deploy via systemctl restart nach Image-Build

Problem: Nach einem CI-Image-Build muss der Container auf dem Server manuell neugestartet werden. Das ist fehleranfällig (vergisst man leicht) und verzögert die Auslieferung.

Lösung: Zwei Änderungen parallel in CI+Infra:

Infra-Seite (forgejo-runner.nix): sudo-Regel für den Service:

{
  command = "/run/current-system/sw/bin/systemctl restart docker-quartz.service";
  options = [ "NOPASSWD" "SETENV" ];
}

CI-Seite (quartz-image.yml): Deploy-Step nach Build+Push:

- name: Deploy quartz container
  run: |
    docker pull localhost:5000/gitlas-quartz:latest
    sudo systemctl restart docker-quartz.service

Vorteile:

  • ~2s extra im CI statt ~10 Min für nixos-rebuild switch oder deploy-rs
  • Gleiches Muster wie teev und temuul (bewährt)
  • Kein SSH-Key, kein deploy-rs, kein full NixOS-Closure nötig

Voraussetzung: Der CI-Runner läuft auf demselben Host wie der Container (hier: nixos:host-Label). Für Remote-Deployment wäre deploy-rs oder ein Webhook nötig.

Prävention: Bei jedem neuen Service, dessen Image in CI gebaut wird, direkt die sudo-Regel in forgejo-runner.nix und den Deploy-Step im Workflow anlegen — gehören in denselben PR.


Problem: Der Quartz-Container mountet /repo als :ro. Der Entrypoint machte ln -sf /repo content — damit war content/ ein Symlink auf das read-only Volume. Ein index.md für die Landing Page (/) konnte nicht angelegt werden, weil /repo nicht beschreibbar ist.

Lösung: Statt ln -sf /repo content eine Symlink-Farm anlegen und index.md bei Bedarf generieren:

rm -rf content
mkdir -p content
find /repo -maxdepth 1 -not -path /repo -exec ln -s {} content/ \;
test -f /repo/index.md || printf '---\ntitle: Gitlas\n---\n...' > content/index.md

Vorteile:

  • content/ ist ein echtes Verzeichnis im Container-Filesystem (beschreibbar)
  • Alle Dateien aus /repo werden via Symlink sichtbar — kein Duplikat
  • index.md wird nur generiert, wenn keins im Repo existiert
  • Symlinks sind transparent für Quartz’ Datei-Leser

Achtung: Die find-Symlink-Farm kann git-basierte Datumsermittlung (Quartz’ CreatedModifiedDate-Plugin) beeinträchtigen, weil .git im Ursprungsverzeichnis /repo liegt und nicht in content/. Quartz findet es aber via Symlink-Traversal.

Alternative: ln -sf /repo content + ln -s README.md /repo/index.md — geht aber nur, wenn /repo read-write gemountet ist (nicht empfohlen).


33. Nix-''-String: Backslash vor Semikolon richtig escapen

Problem: Der find -exec-Terminator \; funktioniert nicht im Nix-''-Mehrzeilen-String.

Ursache: In Nix-''-Strings ist \ kein Escape-Zeichen. \\ bleibt \\. Das \\; im Nix-Code landet als \\; im Shell-Skript. Bash interpretiert \\ als escaped Backslash → \ und ; als Command-Separator. find sieht nie den Terminator.

Lösung: Im Nix-''-String einfach \; schreiben:

Nix-CodeIm Shell-SkriptBash-InterpretationEffekt
\\;\\;\ (literal) + ; (Command-Sep)find kein Terminator
\;\;\;; (literal)find bekommt ;

Prävention: In Nix-''-Strings keine doppelten Backslashes vor Sonderzeichen setzen. Einfach \; (oder \; gefolgt von Newline) reicht.


34. Auto-Update für Quartz via background git pull im Container

Problem: Nach jedem git push musste man manuell cd /srv/gitlas/repo && git pull && docker restart gitlas-quartz ausführen, damit die neue Content-Version live war. Forgejo-Webhooks oder systemd-Timer sind zusätzliche Infrastruktur-Komponenten, die gewartet werden müssen.

Lösung: Der Quartz-Container pullt selbstständig neue Inhalte im Hintergrund:

while true; do cd /repo && git pull -q && sleep 300; done &
exec npx quartz build --serve --port 8080
  • Ein Hintergrund-Loop pullt alle 5 Minuten (git pull -q, keine Ausgabe wenn aktuell)
  • Quartz’ --serve watched das Dateisystem und baut bei Änderungen automatisch neu
  • Kein Container-Restart, kein Webhook, kein systemd-Timer
  • /repo muss :rw gemountet sein (sonst kein git pull)

Vorteile:

  • Container ist autark — keine externen Abhängigkeiten für Content-Updates
  • Maximal ~5 Minuten Verzögerung zwischen Push und Live-Schaltung
  • Funktioniert auch nach Container-Neustart (der Loop startet mit dem Container)
  • Einfacher als jede Hook/Timer-Lösung

Achtung: Bei Konflikten beim git pull (z.B. durch parallele Agent-Änderungen) schlägt der Pull stumm fehl. Der nächste Durchlauf (5 min später) holt es nach, sobald der Konflikt aufgelöst ist. Für kollaborative Szenarien mit mehreren Agenten siehe Collaboration (jj als Workaround).

Prävention: Für statische Content-Container, die Live-Updates brauchen, ist ein background git pull die einfachste Lösung — vorausgesetzt der Container hat Schreibzugriff auf das Repo.


35. Quartz nutzt globby mit gitignore: true.gitignore filtert Content

Problem: Quartz fand 14 Input-Files statt 15 (index.md fehlte). Landing Page / zeigte 404.

Ursache: Quartz verwendet globby zur Dateisuche mit gitignore: true:

// quartz/util/glob.ts
const fps = await globby(pattern, {
  cwd,
  ignore: ignorePatterns,  // Quartz' eigene ignorePatterns
  gitignore: true,         // zusätzlich .gitignore respektieren!
})

Ein Eintrag index.md in .gitignore führt dazu, dass globby die Datei ignoriert — obwohl sie nicht in Quartz’ ignorePatterns steht.

Lösung: index.md aus .gitignore entfernt:

sed -i '/^index.md$/d' .gitignore

Prävention: Bei jedem Quartz-Repo prüfen: steht die Content-Datei in .gitignore? Quartz’ globby-Konfiguration respektiert .gitignore und filtert sie raus. Das betrifft nicht nur index.md, sondern alle Dateien, die per ln -s README.md index.md als Symlink existieren und ignoriert wurden.


36. Docker-Build-Output liegt im Container — cp nach exit greift ins Leere

Problem: CI-Workflow quartz.yml deployte leeres Verzeichnis. Das index.html war im Build-Output enthalten, aber nicht im Webroot.

Ursache: Getrennte Phasen im CI-Step:

# ❌ Falsch: Build läuft im Container, cp auf dem Host
docker run ... sh -c "npx quartz build --output /tmp/public"
rm -rf /var/www/gitlas/* && cp -r /tmp/public/. /var/www/gitlas/

Nach docker run-Exit existiert /tmp/public nur im Container, nicht auf dem Host. Der cp-Befehl auf dem Host kopiert ein leeres/nicht-existierendes Verzeichnis.

Lösung: Output-Verzeichnis per Volume-Mount aus dem Container herausreichen:

docker run --rm \
  -v ${{ github.workspace }}:/repo \
  -v /var/www/gitlas:/out \          # Host:Container
  localhost:5000/gitlas-quartz:latest \
  sh -c "npx quartz build --output /tmp/public && rm -rf /out/* && cp -r /tmp/public/. /out/"

Jetzt liegt der Output direkt auf dem Host, auch nach Container-Exit.

Alternative: docker cp nach dem Run:

docker run --name tmpbuild ... && docker cp tmpbuild:/tmp/public/. /var/www/gitlas/ && docker rm tmpbuild

Prävention: Wenn ein CI-Step Output aus einem Docker-Container auf den Host kopieren muss, immer ein Volume-Mount verwenden. docker cp ist eine Alternative, aber fummeliger.


37. Static Site statt Container — Quartz ohne --serve via nginx

Problem: Der Quartz-Container mit --serve-Modus war die einzige Art, die Site auszuliefern. Das bedeutete: Container muss laufen, Quartz muss laufen, Memory/CPU-Overhead, kein caching, kein nginx.

Architektur-Entscheidung: Static Build + nginx statt containerisiertem --serve:

CI (Build)                  Server (Serve)
┌─────────────┐            ┌──────────────────┐
│ docker run  │──rsync──>  │ nginx            │
│ quartz build│  /var/www/ │ root /var/www/   │
│ → /tmp/     │  gitlas/   │ try_files $uri   │
│ public/     │            │  .html $uri/     │
└─────────────┘            │ =404             │
                           └──────────────────┘
Traefik → nginx:8080 → /var/www/gitlas/

Vorteile:

  • Kein Quartz-Prozess auf dem Server (nur nginx)
  • Kein Node.js-Runtime-Overhead
  • nginx served statische Files direkt (schneller, weniger RAM)
  • try_files $uri.html erlaubt URLs ohne .html-Extension
  • error_page 404 /404.html für benutzerdefinierte 404-Seite
  • CI kann beliebig oft bauen, ohne den Server zu belasten

NixOS-Konfiguration:

{
  name = "gitlas";
  domain = "gitlas.behbahan.de";
  www = false;
  extraConfig = ''
    try_files $uri $uri.html $uri/ =404;
    error_page 404 /404.html;
  '';
}

Prävention: Für rein statische Inhalte (generierte HTML-Seiten) ist ein Static-Site-Setup mit nginx fast immer besser als ein Application-Server. Weniger Angriffsfläche, weniger Ressourcen, einfachere Konfiguration.


38. Konzeptvergleich: Gitlas vs. Cabinet (runcabinet.com)

Kontext: Cabinet (Open Source, “AI workspace your company owns”) ist das nächste Produkt mit derselben Grundphilosophie: Markdown on Disk, Git-History, self-hosted, BYO-AI. Ein Vergleich lohnt sich, weil er zeigt, was Gitlas bewusst weglässt und was ergänzbar wäre.

Gemeinsame Prinzipien (bestätigen Gitlas’ Fundament):

  • Markdown on Disk, keine Datenbank, kein Vendor Lock-in
  • “Git everything” — jede Änderung versioniert (Cabinet: auto-commit pro Save)
  • Self-hosted, Daten gehören dem Nutzer
  • BYO-AI (eigene Modelle, keine Markup-Sperre)

Wo Cabinet weiter ist (Differenzierungsmerkmale):

  • Multi-Agenten-Team mit Rollen (CEO, Editor, Marketer), Skills, Missions/Tasks und Kanban-Zuordnung
  • Scheduled Jobs (Cron): “Reddit-Scout alle 6h”, “Weekly Report” — läuft 24/7, während Gitlas rein auf Nutzer-Initiation über den Agent läuft
  • GUI: WYSIWYG-Editor (Tiptap), Web-Terminal (Claude Code im Browser), eingebettete HTML-Dashboards, interner Team-Chat
  • Connectors (Slack, Gmail, Drive, Notion, …) und Approval-Queue vor externen Aktionen

Wo Gitlas stärker bleibt:

  • Git-Forge-Kollaboration (Forgejo: Branches, PRs, Issues, Releases, Review-Workflow) — Cabinet hat kein PR-Review-Modell
  • Agent als Interface, nicht als App-Feature — keine GUI nötig
  • Vollständig lokalfähig ohne Cloud (Ollama), kein Desktop-App-Zwang

Learnings für Gitlas:

  1. Scheduled Jobs sind die größte Lücke — ein einfacher Cron-Loop im Container (z.B. git pull-Style, aber für Agent-Tasks wie “wöchentliche Zusammenfassung schreiben”) wäre ein kleiner, natürlich-integrierter Zusatz, ohne GUI-Aufwand.
  2. GUI ist optional — Cabinet braucht eine App, weil sein Agent im Produkt eingebettet ist. Gitlas’ “Agent als Layer” macht jede GUI austauschbar; die Quartz-Ansicht deckt die Lese-Schnittstelle ab.
  3. KISS bleibt das Alleinstellungsmerkmal — die Stärke von Gitlas ist die Reduktion: kein App-Layer, kein Kanban, kein Dashboard. Jedes neue Feature muss sich gegen “Agent + Git + Quartz” verteidigen.

Prävention: Vor neuen Features immer den Gegencheck machen: “Braucht Gitlas das wirklich, oder macht es Cabinet nur anders?” Wenn die Antwort “weil Cabinet es auch hat” ist, nicht bauen. Nur wenn es den Kern (Agent → Markdown → Git) stärkt, ist es ein Kandidat.


39. Konzeptvergleich: Gitlas vs. Tolaria (github.com/refactoringhq/tolaria)

Kontext: Tolaria (AGPL-3.0, ~19k Stars, Tauri + React) ist eine Desktop-App zur Verwaltung von Markdown-Wissensbasen, gebaut von einem Einzelentwickler für seinen persönlichen Vault mit 10.000+ Notizen.

Kernbotschaft: Tolaria bestätigt unabhängig dieselbe Architektur-These wie Gitlas — “Jeder Vault ist ein Git-Repo mit Markdown, alles andere ist austauschbar.” Das ist eine starke externe Validierung des Gitlas-Fundaments.

Gemeinsame Prinzipien (validieren Gitlas):

  • Files-first: Markdown on Disk, portabel, keine Export-Sperre
  • Git-first: “Every vault is a git repository, any git remote, zero dependency on app servers”
  • Offline-first, zero lock-in: keine Accounts, keine Subscriptions, keine Cloud-Abhängigkeit
  • Standards-based: Markdown + YAML frontmatter, keine proprietären Formate
  • AI-first but not AI-only: Agenten nutzen den Vault, aber der Nutzer ist frei (Claude Code, Codex, Gemini CLI + AGENTS-File)

Wo Tolaria anders ist:

  • Desktop-GUI (WYSIWYG-Editor, Command Palette, Inbox-Workflow, “Types as lenses, not schemas”) — AI ist eine eingebettete Schicht über einen bundled MCP-Server, nicht das primäre Interface
  • Einzelnutzer, kein Publishing: kein Quartz-Äquivalent, keine Forge/Kollaborationsschicht (PRs, Issues, Review), nur Git-Remote als Sync
  • Keyboard-first Power-User-Tool für den eigenen Arbeitsrechner, statt serverbasiertem 24/7-Stack

Learnings für Gitlas:

  1. Externe Bestätigung des Kern-Designs — ein unabhängiges Projekt mit ~19k Stars setzt exakt “Git als Source of Truth für Markdown-Vaults” um. Bei Diskussionen um die Architektur ist das ein gutes Referenzargument.
  2. AGENTS.md-Muster übernehmen — Tolaria legt ein AGENTS.md ins Vault, damit beliebige Agent-CLIs sich sofort zurechtfinden. Gitlas sollte dasselbe tun (zusätzlich zu Conventions), damit auch externe Agenten (Claude Code, Codex) ohne Einarbeitung produktiv sind.
  3. “Types as lenses, not schemas” — Tolaria verzichtet bewusst auf Pflicht-Frontmatter/Validierung. Gitlas’ CONVENTIONS sollten das als Designentscheidung explizit machen: Navigation, keine Erzwingung.
  4. Quartz + Forgejo sind Gitlas’ echte Differenzierung — Tolaria hat beides nicht (kein Publishing, keine Kollaboration). Diese beiden Komponenten sind der Wert, den eine reine Desktop-App nicht liefert.

Prävention: Konkurrenz-Analysen immer gegen den Kern bewerten (Agent → Markdown → Git). Wenn ein Projekt dieselbe These validiert, bestätigt das die Architektur — und die Differenzierung kommt aus den Komponenten, die Gitlas zusätzlich hat (Quartz, Forgejo), nicht aus mehr App-Funktionen.


40. MCP 2.0-Migration: Server-Decorators → MCPServer + @server.tool()

Problem: lint.yml brach mit AttributeError: 'Server' object has no attribute 'list_tools'. Ursache: mcp>=1.0.0 in pyproject.toml löste auf das neu veröffentlichte mcp 2.0.0 auf, das die gesamte Server-API umbrach.

Änderung in mcp 2.0:

1.x2.0
from mcp.server import Serverfrom mcp.server import MCPServer
server = Server("gitlas")server = MCPServer(name="gitlas")
@server.list_tools() + große Tool(...)-Listeje Funktion @server.tool() mit typisierten Parametern
@server.call_tool() + match name:entfällt — Parameter werden direkt an die Funktion gebunden
server.run(read, write, create_initialization_options())server.run(transport="stdio")
SSE: manuell SseServerTransport + Starletteserver.run(transport="sse", host=..., port=..., sse_path="/sse", message_path="/messages/")

Lösung:

  1. server.py auf MCPServer + 7 @server.tool()-Funktionen umgestellt (Signaturen erzeugen die JSON-Schemas automatisch — kein manuelles inputSchema-Diktat, kein Dispatch-match mehr).
  2. pyproject.toml: mcp>=2.0.0.
  3. main() nutzt server.run(transport=...) — SSE-Pfad bleibt /sse + /messages/ (Kai-Kompatibilität unverändert).

Verifikation:

  • 46 Tests grün, CI-ruff (0.15.20) sauber
  • End-to-End über SSE getestet: list_tools liefert alle 7 Tools, read_page liefert Inhalt (mcp 2.0-Client gibt ListToolsResult mit .tools-Liste zurück, nicht die Tools selbst)

Prävention: MCP-SDK ist jung und bricht Semver beim Major-Bump. Vor Upgrades auf eine neue Major-Version die Decorator/Transport-API im Release-Changelog prüfen. Bei mcp gilt: 1.x Server vs. 2.0 MCPServer sind nicht austauschbar.