Automatischer Push: 45 Datei(en) geändert

- .claude/agents/planer.md
- .claude/agents/quorum-stimme.md
- .claude/skills/bookstack-upload/SKILL.md
- .claude/skills/bookstack-upload/scripts/bookstack/archiv.py
- .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py
- .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py
- .claude/skills/bookstack-upload/scripts/bookstack/bilder.py
- .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py
- .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py
- .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py
- .claude/skills/bookstack-upload/scripts/bookstack/struktur.py
- .claude/skills/bookstack-upload/scripts/bookstack/verweise.py
- … und 33 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-10-01 04:00:02 +02:00
1 parent 60dee0ab68
commit deec79a6ea
45 files changed
+8645 -262

No files matched your search

@@ -5,7 +5,7 @@ status: draft
summary: "waisen.py zählt verwaiste Notizen so, wie Obsidian den Graphen baut, der Sitzungsstart meldet die Zahl, Regeltexte und Planer verlinken künftig den Hub, und ein einmaliger Bestandslauf hängt den Bestand an, nur anhängend und mit Probelauf."
tags: [plan, vault-tools]
created: 2026-09-29
updated: 2026-09-29
updated: 2026-09-30
---
# Waisen im Graphen
@@ -56,6 +56,7 @@ Gelesen am 2026-09-29: sechs gespeicherte Workflows, vier Skills im Vault, vier
| Agent `recherche` | Zeile 15: Hilfsdatei nur mit genanntem Ablageort | nur auf Anweisung | keine, die Regel gilt über den Stamm |
| Skill `anleitung-straffen` | `.claude/skills/anleitung-straffen/SKILL.md` Zeile 22 und 23 | `<name>-hintergrund.md` neben der Anleitung | keine: Zeile 23 verlinkt Anleitung und Hintergrund schon in beide Richtungen |
| Skills `bookstack-upload`, `plane-abgleich` | schreiben nur Frontmatter-Felder bzw. `todo.md` | keine neue Markdown-Datei | keine |
| Skill `plan-executer-pruefung` | verweist auf `codeworkflow.md`, legt selbst nichts an (Plankritik 2026-09-30) | nichts | keine |
| SEO-Audit-Skill (`/seo`, `/seo-audit`) | liegt nicht unter `.claude/skills/` im Vault | Befunddateien unter `findings/` | Regel in `seo.md`: der Bericht listet seine Befunddateien (Task 03) |
| Koordinator selbst | `AI/Regelwerk/codeworkflow.md` Zeile 38 und 113 | Anforderungen, Prüfberichte, Specs, Dumps | allgemeine Regel in `navigation.md` und Stammzeile (Task 03) |
@@ -67,7 +68,7 @@ Frage: Vertragen die Planwerkzeuge eine Zeile `Übersicht: [00-uebersicht.md](00
|---|---|---|---|
| `plan-kopie/plan_materialisiere.py` | Zieldatei nur aus Überschriften `^#{2,4} ` mit Pfad in Backticks; Zaun per `lstrip().startswith("```")`; Marke ist die letzte nicht leere Zeile vor dem Zaun; Löschziele nur unter `## Zieldateien` | Zeile 26 bis 27, 40 bis 44, 57 bis 60, 76 bis 81 | keine: die Zeile ist weder Überschrift noch Zaun und steht nie zwischen Marke und Zaun |
| `plan-kopie/plan_bloecke.py` | dieselbe Logik | Zeile 23 bis 25, 39 bis 42, 54 bis 60 | keine |
| `plan-diff/plan_diff.py` | Pfadzeile nur mit Pfad in Backticks, höchstens zehn Zeilen vor dem Zaun; Zieldateien nur unter `## Zieldateien` | Zeile 32, 44 bis 55, 78 bis 88 | keine: ein Markdown-Link trägt keine Backticks |
| `plan-diff/plan_diff.py` | Pfadzeile nur mit Pfad in Backticks, höchstens zehn Zeilen vor dem Zaun; Zieldateien nur unter `## Zieldateien` | Zeile 32, 44 bis 55, 78 bis 88 | keine: ein Markdown-Link trägt keine Backticks Nur am Text abgelesen: Die Plankritik vom 2026-09-30 maß, dass `plan_diff.py` die Ganzdatei-Blöcke dieses Plans gar nicht liest (Exit 3, `KEIN BLOCK`). |
| `plan-kopie/welle_vergleich.py` | nur `## Zieldateien` | Zeile 40 bis 47 | keine |
| `plan-kopie/rot_nachweis.py` | nur `## Zieldateien` | Zeile 75 bis 81 | keine |
| `plan-kopie/plan_zeichen.py` | nur Inhalte von Codeblöcken | Zeile 35 | keine |
@@ -116,7 +117,8 @@ Ein Nein bei P1 stoppt Task 03 und Task 05, weil die Hub-Regel dann in Projekt-R
| `task-02-sitzungsstart.md` | Zeile im SessionStart-Hook, Treiber, Nachtrag im Entwurf | haiku | Task 01 | 2 |
| `task-03-regeltexte.md` | Hub-Regel in `navigation.md`, Stamm, Planer, Codedoku, SEO | sonnet, effort low | P1 in der App | 1 oder 2 |
| `task-04-bestandsskript.md` | `waisen_bestand.py` und Treiber | haiku | Task 01 | 2 |
| `task-05-bestandslauf.md` | Probelauf, Halt, nach Freigabe echter Lauf | sonnet | Tasks 01, 02, 04; P1 bis P3 | 3 |
| `task-05-bestandslauf.md` | Probelauf, Halt, nach Freigabe echter Lauf | sonnet | Tasks 01, 02, 04, 06; P1 bis P3 | 4 |
| `task-06-nachbesserung-obsidian-regeln.md` | Nachbesserung nach der Prüfung von Welle 1 (2026-09-30): userIgnoreFilters und Linkziele wie Obsidian 1.13.7, Delta gegen den Baum | haiku | Task 01 | 3 |
Die Zieldateien der Tasks 01 bis 04 sind disjunkt. Task 02 und 04 brauchen die Dateien aus Task 01 zur Laufzeit, deshalb Welle 2. Task 05 ändert keine Datei im Plan-Sinn, er fährt Skripte gegen den echten Vault und hält nach Phase A an.
@@ -140,17 +142,23 @@ Der Planer hatte keine Shell. Was gemessen werden muss, steht als Schritt in ein
5. `python D:/Claude/.vault-tools/linkcheck/faelle.py` endet weiter mit Exit 0.
6. `regelwerk_check.py` meldet nach Task 03 keine Befundzeile, die vorher nicht da war.
7. Je Welle sind die Zieldateien byte-gleich mit der Kopie, auf die der Koordinator den Plan vorher angewendet hat (`welle_vergleich.py`).
8. Phase A von Task 05 liegt als `probelauf.txt` vor, der Koordinator hat die Liste gelesen, Pascal hat Phase B freigegeben.
9. Nach Phase B: Probelauf und echter Lauf nennen dieselben Anhänge, die Sicherung liegt außerhalb des Vaults, und Pascals Bildschirmfoto des Graphen ist die Abnahme.
8. Phase A von Task 05 liegt als `probelauf.txt` vor, der Koordinator hat die Liste gelesen und im Sitzungsprotokoll je Gruppe die Zahl und fünf Stichproben notiert, und Phase B ist freigegeben (Pascal hat am 2026-09-30 alles genehmigt).
9. M4: `plan_bloecke.py --projekt D:/Claude vor` über alle Taskdateien endet mit `0 Befund(e).`, und das Materialisieren in der Plankritik lief ohne Befund.
10. `python D:/Claude/.vault-tools/session-hooks/faelle_nach_edit.py` endet weiter mit Exit 0 (Task 02).
11. `linkcheck.py --alle` meldet nach Task 03 keinen neuen toten Link aus einer der Zieldateien.
12. Die Zahl im Sitzungsstart der nächsten Sitzung ist dieselbe wie die von `waisen.py` zur selben Zeit; beide Ausgaben stehen im Sitzungsprotokoll.
13. Nach Phase B: Probelauf und echter Lauf nennen dieselben Anhänge, die Sicherung liegt außerhalb des Vaults, und Pascals Bildschirmfoto des Graphen ist die Abnahme.
## Lauf mit echten Daten
- **Zähler:** `PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/waisen.py --vault D:/Claude --liste`. Die Spec maß am 2026-09-29 1.996 Notizen und 852 Waisen, darunter 10 in `Bookstack/` und 7 unter `data/`. Ohne diese Ausnahmen sind rund 835 zu erwarten, dazu die seitdem entstandenen Dateien. Weicht die Zahl um mehr als etwa 20 ab, vergleicht der Koordinator die Gruppen der Spec mit der Liste, bevor Task 05 startet. Stichprobe: zehn Pfade aus der Liste in Obsidian öffnen, Rückverweise und ausgehende Links müssen leer sein.
- **Zähler:** `PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/waisen.py --vault D:/Claude --liste`. Die Spec maß am 2026-09-29 1.996 Notizen und 852 Waisen, darunter 10 in `Bookstack/` und 7 unter `data/`. Ohne diese Ausnahmen sind rund 835 zu erwarten, dazu die seitdem entstandenen Dateien. Weicht die Zahl um mehr als etwa 20 ab, vergleicht der Koordinator die Gruppen der Spec mit der Liste, bevor Task 05 startet. Stichprobe: zehn Pfade aus der Liste in Obsidian öffnen, Rückverweise und ausgehende Links müssen leer sein. **Neue Basis:** Die Plankritik maß am 2026-09-30 mit `waisen.py` aus der Kopie 2.215 Notizen, 865 Waisen und 17 ausgenommene; 32 der neuen Waisen liegen unter `docs/` an der Vault-Wurzel (Pläne und Specs einer anderen Sitzung, nicht veröffentlicht). Verglichen wird mit dieser Messung, nicht mit der Spec.
- **Sitzungsstart:** nächste Sitzung nach Task 02, die Zeile steht im Kontext, und die Zahl ist dieselbe wie beim Zähler.
- **Bestandslauf:** Task 05. Auffallen müsste: jede Übersicht unter `Code Projekte/*/docs/plans/*/` mit verwaisten Tasks erscheint als `ZIEL`, jede Taskdatei nie. Die Liste für den Koordinator enthält die Gruppe „Einzelne“, die Administrator-Anleitungen und alles ohne Muster.
- **Bestandslauf:** Task 05. Auffallen müsste: jede Übersicht unter `Code Projekte/*/docs/plans/*/` mit verwaisten Tasks erscheint als `ZIEL`, jede Taskdatei nie. Die Liste für den Koordinator enthält die Gruppe „Einzelne“, die Administrator-Anleitungen und alles ohne Muster. **Seit der Nachbesserung vom 2026-09-30** bekommen auch Pläne und Specs unter `docs/` an der Vault-Wurzel einen Anhang (`Vault: [README](…)`); Dateien tiefer als ein Planordner, etwa unter `docs/plans/<ordner>/befunde/`, stehen auf der Liste für den Koordinator und bekommen von Hand eine Zeile auf die Übersicht ihres Planordners.
## Risiken
- **`CLAUDE.md` liegt im Arbeitsbaum mit CRLF**, obwohl `.gitattributes` `eol=lf` vorgibt (Plankritik 2026-09-30). `plan_materialisiere.py` schreibt LF, Task 03 ändert also jedes Zeilenende mit. Das ist gewollt, `git diff` zeigt es nicht, weil der Index LF hat.
- **Obsidian zählt anders als nachgebaut.** Dann weicht die Zahl vom Graphen ab. Erkennbar an P1 bis P3 und an der Stichprobe. Die Auflösung mehrdeutiger Namen ist nachgebaut, das steht als Lücke im Kopf von `waisen.py`.
- **Der Sitzungsstart wird zu langsam.** Über 15 s bricht die Waisenzeile ab und erscheint als Fehlerzeile, die übrigen Zeilen bleiben. Über 30 s gesamt verwirft Claude Code die ganze Ausgabe. M1 und M2 messen das.
- **Ein anderer Agent schreibt während Phase B.** Das Skript liest jede Zieldatei direkt vor dem Anhängen noch einmal und bricht ab, wenn sie sich geändert hat. Was bis dahin angehängt war, nennt die Ausgabe, die Sicherung liegt vor.
@@ -170,6 +178,8 @@ Der Planer hatte keine Shell. Was gemessen werden muss, steht als Schritt in ein
## Offene Punkte
**Entschieden am 2026-09-30:** Pascal hat die Spec samt relativen Markdown-Links in Projekt-Repos abgenommen, P1 bis P3 in der App bestanden und alle Vorschläge dieser Liste angenommen. Task 03 und Phase B von Task 05 sind damit frei.
1. **P1 bis P3 in der App**, vor Task 03 und vor Phase B von Task 05.
2. **Abweichung von der Spec, Workflows:** Die Spec sieht vor, dass gespeicherte Workflows, die in Planordner schreiben, dieselbe Zeile bekommen. Das Inventar zeigt: Nur `planung.js` lässt schreiben, und zwar über `agentType: 'planer'`. Die Regel kommt aus `planer.md`, eine zweite Fassung im Workflow würde driften. Vorschlag: keine Änderung an Workflows.
3. **Abweichung von der Spec, Administrator-Anleitung:** Die Spec nennt sie in der Bestandstabelle bei „eine Zeile mit Link aufs README“. Ohne festen Dateinamen gibt es kein Muster; sie kommt auf die Liste für den Koordinator.
@@ -5,7 +5,7 @@ status: draft
summary: "Legt waisen.py, waisen-ausnahmen.txt, faelle_waisen.py und ENTWURF-waisen.md an; der Treiber fährt die Angriffsliste, einen Rotlauf mit eingebauten Mutanten und eine Laufzeitmessung über den echten Vault."
tags: [plan-task, vault-tools]
created: 2026-09-29
updated: 2026-09-29
updated: 2026-09-30
modell: haiku
modell_begruendung: "Vier neue Dateien stehen wörtlich im Plan und werden per Skript angelegt; die Codeblöcke enthalten keine typografischen Anführungszeichen, Gedankenstriche oder Auslassungspunkte."
---
@@ -599,7 +599,7 @@ FAELLE = [
Fall("frontmatter | Wikilink in einer Blockliste",
neu("A/neu.md", "---\nverwandt:\n - \"[[C/allein]]\"\n---\n"), []),
Fall("frontmatter | ohne Schlusszeile zaehlt als Text",
neu("A/neu.md", "---\nverwandt: \"[[C/allein]]\"\n"), []),
neu("A/neu.md", "---\nquelle: \"[x](../C/allein.md)\"\n"), []),
Fall("frontmatter | Markdown-Link im Frontmatter zaehlt nicht",
neu("A/neu.md", "---\nquelle: \"[x](../C/allein.md)\"\n---\n"),
["A/neu.md", "C/allein.md"]),
@@ -652,6 +652,13 @@ FAELLE = [
Fall("ausblendung | andere Endung ist kein Knoten", neu("C/daten.txt", "[[B/ziel]]\n"), ["C/allein.md"]),
Fall("ausblendung | Endung in Grossbuchstaben ist ein Knoten", neu("C/GROSS.MD", "# G\n"),
["C/GROSS.MD", "C/allein.md"]),
# Dateifilter: Der Ordner davor bleibt stehen, erst die Prüfung je Datei blendet aus.
Fall("ausblendung | Dateifilter blendet eine Notiz aus",
neu(".obsidian/app.json", '{"userIgnoreFilters": ["C/allein.md"]}'), []),
Fall("ausblendung | per Dateifilter ausgeblendete Notiz ist keine Quelle",
lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["D/quelle.md"]}'),
schreibe(v, "D/quelle.md", "[[C/allein]]\n")),
["C/allein.md"]),
Fall("ausnahme | Muster trifft die Waise", nichts, [],
["Ausgenommene Waisen: 1 über 1 Muster"], ausnahmen="C/**\n"),
Fall("ausnahme | ausgenommene Notiz liefert Kanten", neu("D/quelle.md", "[[C/allein]]\n"), [],
@@ -676,6 +683,10 @@ FAELLE = [
ausnahmen="C/../**\n"),
Fall("ausnahme | Laufwerk ist ungueltig", nichts, None, ["ungültiges Muster"],
ausnahmen="D:/C/**\n"),
Fall("ausnahme | Punkt als Pfadteil ist ungueltig", nichts, None, ["ungültiges Muster"],
ausnahmen="./C/**\n"),
Fall("ausnahme | leerer Pfadteil ist ungueltig", nichts, None, ["ungültiges Muster"],
ausnahmen="C//allein.md\n"),
Fall("ausnahme | doppeltes Muster", nichts, None, ["steht doppelt"], ausnahmen="C/**\nc/**\n"),
Fall("ausnahme | Datei fehlt", nichts, None, ["Ausnahmedatei fehlt"], ausnahmen=None),
Fall("ausnahme | ungueltiges UTF-8", nichts, None, ["Ausnahmedatei nicht lesbar"],
@@ -724,14 +735,19 @@ FAELLE = [
Fall("umgebung | ohne PYTHONIOENCODING mit Umlaut und Emoji im Namen",
neu("C/Übersicht 🚀.md", "# U\n"), ["C/allein.md", "C/Übersicht 🚀.md"],
ohne_pythonioencoding=True),
# Mehrdeutig: Die richtige Wahl liegt jeweils alphabetisch hinten, sonst misst der Fall nichts.
Fall("mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt",
lambda v: (schreibe(v, "C/doppelt.md", "# D\n"), schreibe(v, "E/doppelt.md", "# D\n"),
schreibe(v, "C/verweis.md", "[[doppelt]]\n")),
["C/allein.md", "E/doppelt.md"]),
lambda v: (schreibe(v, "Q/X/doppelt.md", "# D\n"), schreibe(v, "A/X/doppelt.md", "# D\n"),
schreibe(v, "Q/X/verweis.md", "[[X/doppelt]]\n")),
["A/X/doppelt.md", "C/allein.md"]),
Fall("mehrdeutig | relativ zur Quelle vor dem Namen",
lambda v: (schreibe(v, "Z/sub/n.md", "# N\n"), schreibe(v, "A/sub/n.md", "# N\n"),
schreibe(v, "Z/verweis.md", "[[sub/n]]\n")),
["A/sub/n.md", "C/allein.md"]),
Fall("mehrdeutig | sonst der kuerzeste Pfad",
lambda v: (schreibe(v, "E/doppelt.md", "# D\n"), schreibe(v, "F/G/doppelt.md", "# D\n"),
schreibe(v, "A/verweis.md", "[[doppelt]]\n")),
["C/allein.md", "F/G/doppelt.md"]),
lambda v: (schreibe(v, "A/B/doppelt.md", "# D\n"), schreibe(v, "Z/doppelt.md", "# D\n"),
schreibe(v, "Q/verweis.md", "[[doppelt]]\n")),
["A/B/doppelt.md", "C/allein.md"]),
Fall("zahl | nur die Zahl", nichts, ["C/allein.md"], zahl=True),
Fall("zahl | null Waisen", neu("A/neu.md", "[[C/allein]]\n"), [], zahl=True),
Fall("zahl | Abbruch ohne Zahl", lambda v: shutil.rmtree(v / ".obsidian"), None,
@@ -968,6 +984,35 @@ MUTANTEN = [
" liste.append((\"regex\", re.compile(eintrag[1:-1])))",
" liste.append((\"praefix\", eintrag))",
["ausblendung | regulaerer Ausdruck in der Tiefe"]),
("Frontmatter ohne Schlusszeile als Kopf",
" return \"\", text",
" return text, \"\"",
["frontmatter | ohne Schlusszeile zaehlt als Text"]),
("Punkt als Pfadteil erlaubt",
" or \"..\" in teile or \".\" in teile or \"\" in teile[:-1]):",
" or \"..\" in teile or \"\" in teile[:-1]):",
["ausnahme | Punkt als Pfadteil ist ungueltig"]),
("leerer Pfadteil erlaubt",
" or \"..\" in teile or \".\" in teile or \"\" in teile[:-1]):",
" or \"..\" in teile or \".\" in teile):",
["ausnahme | leerer Pfadteil ist ungueltig"]),
("Dateifilter ignoriert",
" if not ausgeblendet(praefix + name, filter_liste):",
" if True:",
["ausblendung | Dateifilter blendet eine Notiz aus",
"ausblendung | per Dateifilter ausgeblendete Notiz ist keine Quelle"]),
("gleicher Ordner ignoriert",
" gleich = sorted(r for r in kandidaten if posixpath.dirname(r).lower() == ordner)",
" gleich = []",
["mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt"]),
("kein Kandidat relativ zur Quelle",
" kandidaten = [posixpath.normpath(ziel), posixpath.normpath(posixpath.join(ordner, ziel))]",
" kandidaten = [posixpath.normpath(ziel)]",
["mehrdeutig | relativ zur Quelle vor dem Namen"]),
("mehrdeutig nur alphabetisch",
" return sorted(kandidaten, key=lambda r: (r.count(\"/\"), len(r), r.lower()))[0]",
" return sorted(kandidaten, key=lambda r: r.lower())[0]",
["mehrdeutig | sonst der kuerzeste Pfad"]),
]
@@ -1111,13 +1156,13 @@ Die einzelnen Fälle stehen an genau einer Stelle, in `faelle_waisen.py`. Hier s
| Klasse | Fälle |
|---|---|
| fehlt | `.obsidian`, `app.json`, Schlüssel `userIgnoreFilters`, Ausnahmedatei, `linkcheck.py`, Vault-Ordner |
| fehlt | `.obsidian`, `app.json`, Schlüssel `userIgnoreFilters`, Ausnahmedatei, `linkcheck.py`, Vault-Ordner, Schlusszeile des Frontmatters |
| null | `userIgnoreFilters` ist null |
| falscher Typ | `app.json` als Liste, Filtereintrag als Zahl, `app.json` ohne JSON |
| leer | leere Filterliste, leerer Filtereintrag, leere Ausnahmedatei, nur Kommentare, leere Notiz, Vault ohne Notiz |
| doppelt | doppeltes Muster, gleichnamige Notizen in zwei Ordnern |
| zusätzlich | Punktordner, Punktdatei, andere Endung, Endung in Großbuchstaben |
| Wertebereich | Muster mit Backslash, führendem Schrägstrich, `..`, Laufwerk; Stern über Ordnergrenze |
| doppelt | doppeltes Muster, gleichnamige Notizen in zwei Ordnern: gleicher Ordner, relativ zur Quelle, kürzester Pfad; die richtige Wahl liegt jeweils alphabetisch hinten |
| zusätzlich | Punktordner, Punktdatei, andere Endung, Endung in Großbuchstaben, per Dateifilter ausgeblendete Notiz als Knoten und als Quelle |
| Wertebereich | Muster mit Backslash, führendem Schrägstrich, `..`, `.` als Pfadteil, leerem Pfadteil, Laufwerk; Stern über Ordnergrenze |
| Verweis ins Leere | Wikilink ins Leere, auf Anhang, auf ausgeblendete Notiz, Selbstreferenz, Anker allein |
| Reihenfolge | Waise an erster und letzter Stelle, Muster in erster und letzter Zeile, Filter an erster und letzter Stelle, Link am Dateiende ohne Zeilenumbruch |
| Umgebung | CRLF, BOM, ungültiges UTF-8 in Notiz und Ausnahmedatei, Umlaut und Emoji im Namen ohne `PYTHONIOENCODING` |
@@ -5,7 +5,7 @@ status: draft
summary: "sitzungsstart.py ruft waisen.py --zahl als eigenen Prozess mit 15 s Grenze und meldet die Zahl als Zeile; der Treiber bekommt eine Grundlinie mit .obsidian/app.json, neue Fälle und angepasste Erwartungen; ENTWURF.md bekommt einen Nachtrag."
tags: [plan-task, vault-tools]
created: 2026-09-29
updated: 2026-09-29
updated: 2026-09-30
modell: haiku
modell_begruendung: "Fünfzehn Aus-wird-Blöcke und ein angehängter Abschnitt stehen wörtlich im Plan und werden per Skript angewendet; keine typografischen Zeichen in den Codeblöcken."
---
@@ -421,7 +421,49 @@ Häng diesen Block ans Ende:
**Laufzeit.** Frage 15 peilt unter 5 s an. Ob das mit dem Zähler hält, misst `faelle_sitzungsstart.py --echt`.
````
## Schritt 2 — Angriffsliste
## Schritt 2 — Rot zuerst
Die neuen und geänderten Fälle müssen gegen den Hook ohne die Änderung aus Schritt 1 rot werden. Der Stand ohne Änderung ist `sitzungsstart.py` aus `HEAD` des Vault-Repos; dieser Task committet nicht. Alles in einem einzigen Bash-Aufruf, damit die neue Fassung sicher zurückkommt:
```
H=D:/Claude/.vault-tools/session-hooks
mv "$H/sitzungsstart.py" "$H/sitzungsstart.py.neu" && git -C D:/Claude show HEAD:.vault-tools/session-hooks/sitzungsstart.py > "$H/sitzungsstart.py"
grep -c punkt_waisen "$H/sitzungsstart.py"
PYTHONIOENCODING=utf-8 python -B "$H/faelle_sitzungsstart.py"; echo "exit=$?"
PYTHONIOENCODING=utf-8 python -B -c '
import shutil, sys, tempfile
from pathlib import Path
sys.path.insert(0, "D:/Claude/.vault-tools/session-hooks")
import faelle_sitzungsstart as f
arbeit = Path(tempfile.mkdtemp(prefix="sitzungsstart-rot-")).resolve()
basis = arbeit / "grundlinie"
f.baue_grundlinie(basis)
auswahl = [(x.name, lambda x=x: f.lauf_fall(x, basis, arbeit)) for x in f.FAELLE
if "aisen" in x.name or "ohne jeden Ordner" in x.name or "linkcheck.py fehlt" in x.name]
auswahl += [(n, lambda fn=fn: fn(f.frische_kopie(basis, arbeit))) for n, fn in f.IM_PROZESS
if "aisen" in n or "Datei nicht lesbar" in n]
for name, lauf in auswahl:
try:
ok, text = lauf()
except Exception as exc:
ok, text = None, "ABSTURZ " + type(exc).__name__ + ": " + str(exc)
print({True: "GRUEN", False: "ROT", None: "ABSTURZ"}[ok].ljust(8) + name + "\n" + text)
shutil.rmtree(arbeit, ignore_errors=True)
'; echo "exit=$?"
mv -f "$H/sitzungsstart.py.neu" "$H/sitzungsstart.py"
grep -c punkt_waisen "$H/sitzungsstart.py"
ls "$H"
```
Erwartet, in dieser Reihenfolge:
- Das erste `grep` gibt `0`. Steht dort eine andere Zahl, enthält `HEAD` schon die Änderung, und der Schritt misst nichts: melden.
- Der Treiber endet mit `ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.` und `exit=2`. Unter `erwartet:` steht als letzte Zeile `- Waisen außerhalb der Ausnahmen: 6`, unter `bekommen (stdout):` fehlt sie.
- Die Einzelfälle: je Fall ein Block, der mit `ROT` beginnt, für genau diese Fälle: `leer | Vault ohne jeden Ordner`, `umgebung | linkcheck.py fehlt`, die fünf Fälle, die mit `waisen |` beginnen, `umgebung | ohne PYTHONIOENCODING zaehlt Waisen mit Umlaut im Namen`, `lesen | Datei nicht lesbar` und `waisen | Zeitgrenze ueberschritten`. Jeder Block nennt eine Zeile `fehlt:` mit der Waisenzeile oder eine Zeile `fehlt Zeile mit Anfang: Sitzungsstart-Hook: Waisen:`. Kein Block beginnt mit `GRUEN` oder `ABSTURZ`. Danach `exit=0`.
- Das zweite `grep` gibt `2`: `sitzungsstart.py` ist wieder die Fassung aus Schritt 1.
- `ls` zeigt keine Datei `sitzungsstart.py.neu`.
## Schritt 3 — Angriffsliste
```
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_sitzungsstart.py; echo "exit=$?"
@@ -429,7 +471,7 @@ PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_sitzun
Erwartet: `Grundlinie gruen`, keine Zeile, die mit `FEHLER` beginnt, die Fälle mit `waisen` im Namen als `BESTANDEN`, Schlusszeile `N von N Faellen bestanden.` mit zweimal derselben Zahl, `exit=0`.
## Schritt 3 — Messung M2 über den echten Vault
## Schritt 4 — Messung M2 über den echten Vault
```
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_sitzungsstart.py --echt; echo "exit=$?"
@@ -437,7 +479,7 @@ PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_sitzun
Erwartet: die Kontextzeilen des echten Vaults, darunter eine Zeile `- Waisen außerhalb der Ausnahmen: ` mit einer Zahl und keine Zeile `Sitzungsstart-Hook: Waisen:`. Steht dort `LAUFZEIT UEBER ZIEL` und `exit=1`: nichts ändern, die Laufzeit melden. Das ist Offener Punkt 4 der Übersicht, kein Fehler dieses Tasks.
## Schritt 4 — Nachbarn unverändert grün
## Schritt 5 — Nachbarn unverändert grün
```
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_nach_edit.py; echo "exit=$?"
@@ -447,4 +489,4 @@ Erwartet: `exit=0`.
## Bericht
Melde die vollständige Ausgabe von Schritt 1, 3 und 4, von Schritt 2 die Schlusszeile und jede Zeile, die nicht mit `BESTANDEN` beginnt. Läuft etwas anders als erwartet: nichts anpassen, melden.
Melde die vollständige Ausgabe von Schritt 1, 2, 4 und 5, von Schritt 3 die Schlusszeile und jede Zeile, die nicht mit `BESTANDEN` beginnt. Läuft etwas anders als erwartet: nichts anpassen, melden.
@@ -110,17 +110,20 @@ wird:
| Datei | Hub | Linkform |
|---|---|---|
| Plan-Übersicht `00-uebersicht.md`, Einzeldatei-Plan | Projekt-README | relativer Markdown-Link |
| Plan-Übersicht `00-uebersicht.md`, Einzeldatei-Plan in einem Code-Projekt | Projekt-README | relativer Markdown-Link |
| Plan oder Spec unter `docs/` an der Vault-Wurzel | `README.md` der Vault-Wurzel | relativer Markdown-Link |
| alles andere im Planordner (Task, Anforderung, Prüfbericht, Nachbesserung) | `00-uebersicht.md` desselben Ordners | relativer Markdown-Link |
| Codedoku `docs/code/*.md`, Spec, Quorum-Bericht, CHANGELOG, Administrator-Anleitung | Projekt-README | relativer Markdown-Link |
| SEO-Einzelbefund | Bericht des Laufs | Wikilink mit vollem Pfad |
| Bericht eines SEO-Laufs | SEO-Seed | Wikilink mit vollem Pfad |
| Arbeitsdatei eines Projekts im IT Workspace | Übersicht bzw. README des Projekts | Wikilink mit vollem Pfad |
| Arbeitsdatei oder Plan-Übersicht eines Projekts im IT Workspace, auch unter `_arbeitsstand/` | Übersicht (`_MOC-*.md`) bzw. README des Projekts | Wikilink mit vollem Pfad |
| Dump | Tagesnotiz | Wikilink `[[Inbox/JJJJ-MM-TT]]` |
| Notiz oder Spec in der Inbox | Tagesnotiz des Tages, an dem sie entsteht | Wikilink aus dem Agentenprotokoll der Tagesnotiz, also nach unten |
- **Wo die Zeile steht:** direkt unter dem Frontmatter, ohne Frontmatter in der ersten Zeile. In einer Taskdatei lautet sie `Übersicht: [00-uebersicht.md](00-uebersicht.md)`, in einer Übersicht `Projekt: [README](../../../README.md)`, die Tiefe passend zum Ort.
- **Relative Markdown-Links in Projekt-Repos, entgegen der Vault-Gewohnheit.** Der Projektstatus steht im Ordnerpräfix, und aus `active - X` wird irgendwann `inactive - X`. Wikilinks mit vollem Pfad brechen dabei, wenn nicht in Obsidian umbenannt wird. Gitea zeigt Markdown-Links an, Wikilinks nicht. Leerzeichen und Umlaute im Pfad schreibst du URL-kodiert, ein Leerzeichen als `%20`.
- **Außerhalb der Projekt-Repos** bleibt es beim Wikilink mit vollem Pfad.
- **Relative Markdown-Links in Code-Projekten und unter `docs/` an der Vault-Wurzel, entgegen der Vault-Gewohnheit.** Der Projektstatus steht im Ordnerpräfix, und aus `active - X` wird irgendwann `inactive - X`. Wikilinks mit vollem Pfad brechen dabei, wenn nicht in Obsidian umbenannt wird. Gitea zeigt Markdown-Links an, Wikilinks nicht. Leerzeichen und Umlaute im Pfad schreibst du URL-kodiert, ein Leerzeichen als `%20`.
- **Überall sonst** bleibt es beim Wikilink mit vollem Pfad, auch in Projekten im IT Workspace, deren `_arbeitsstand` ein eigenes Git-Repo ist: Diese Ordner tragen kein Statuspräfix.
- **Notizen in der Inbox verlinkt die Tagesnotiz nach unten**, weil eine Inbox-Notiz noch keinen festen Ort hat. Wird sie einsortiert, gilt die Zeile ihres neuen Orts.
- **SEO-Befunde verlinkt der Bericht nach unten**, weil der Audit-Skill außerhalb des Vaults liegt und seine Befunddateien ohne Link ablegt. Wie, steht in [[AI/Regelwerk/seo|seo.md]], Abschnitt 1, Punkt 5.
- **Bewusste Ausnahmen** sind `Bookstack/` (Warteschlange und Archiv) und Programmausgaben unter `data/` in Code-Projekten. Sie stehen als Muster in `.vault-tools/linkcheck/waisen-ausnahmen.txt` und zählen nicht als Waisen.
@@ -141,7 +144,7 @@ wird:
```
- Neues Projekt in Code Projekte / Security-Compliance → im jeweiligen Oberseed mit Link aufs README eintragen und mit dem passenden Themen-Seed verbinden ([[AI/Regelwerk/projekte|projekte.md]]).
- Jede Datei, die du, ein Subagent, Skill oder Workflow anlegt, verlinkt ihren Hub nach oben: Plan, Codedoku und CHANGELOG das Projekt-README, Task die Plan-Übersicht, Dump die Tagesnotiz. Tabelle und Linkform: [[AI/Regelwerk/navigation|navigation.md]], „Links schreiben und Dateien verschieben“. Der Sitzungsstart meldet die Zahl der Waisen.
- Wer eine Datei anlegt, ob du, ein Subagent, ein Skill oder ein Workflow, verlinkt darin ihren Hub nach oben: Plan, Codedoku und CHANGELOG das Projekt-README, Task die Plan-Übersicht, Dump die Tagesnotiz. Tabelle und Linkform: [[AI/Regelwerk/navigation|navigation.md]], „Links schreiben und Dateien verschieben“. Der Sitzungsstart meldet die Zahl der Waisen.
```
### Hub-Zeile in `.claude/agents/planer.md`
@@ -156,7 +159,7 @@ wird:
```
- **Ablageform hängt am Umfang.** Bis zwei Tasks eine Datei `docs/plans/JJJJ-MM-TT-<thema>.md`. **Ab etwa drei Tasks ein Ordner** `docs/plans/JJJJ-MM-TT-<thema>/` mit `00-uebersicht.md` (Ziel, Architektur, globale Vorgaben, Dateiübersicht, Tasktabelle, Abnahme) und je Task einer Datei `task-NN-<thema>.md`. Jede Taskdatei verweist im Kopf auf die globalen Vorgaben der Übersicht, statt sie zu wiederholen — zwei Fassungen derselben Vorgabe laufen auseinander.
- **Jede Plandatei verlinkt ihren Hub direkt unter dem Frontmatter**, als relativen Markdown-Link, nicht nur als Pfad in Backticks. Die Übersicht und ein Einzeldatei-Plan verlinken das Projekt-README (`Projekt: [README](../../../README.md)`, die Tiefe passend zum Ort), jede andere Datei im Planordner die Übersicht (`Übersicht: [00-uebersicht.md](00-uebersicht.md)`). Leerzeichen und Umlaute im Pfad schreibst du URL-kodiert. Hat das Projekt kein `README.md`, entfällt die README-Zeile. Die Planwerkzeuge vertragen die Zeile an dieser Stelle, weil sie nur Überschriften, Codezäune und den Abschnitt `## Zieldateien` auswerten (Befund im Plan `.vault-tools/linkcheck/docs/plans/2026-09-29-waisen/`). Regel und Begründung: `AI/Regelwerk/navigation.md`, Abschnitt „Links schreiben und Dateien verschieben“.
- **Jede Plandatei verlinkt ihren Hub direkt unter dem Frontmatter**, als relativen Markdown-Link, nicht nur als Pfad in Backticks. Die Übersicht und ein Einzeldatei-Plan verlinken das Projekt-README (`Projekt: [README](../../../README.md)`, die Tiefe passend zum Ort), jede andere Datei im Planordner die Übersicht (`Übersicht: [00-uebersicht.md](00-uebersicht.md)`). Leerzeichen und Umlaute im Pfad schreibst du URL-kodiert. Liegt der Plan unter `docs/` an der Vault-Wurzel, ist der Hub die `README.md` dort, und die Zeile beginnt mit `Vault:` statt `Projekt:`. Liegt er in einem Projekt im IT Workspace, etwa unter `_arbeitsstand/`, verlinkt die Übersicht stattdessen die Projektübersicht (`_MOC-*.md` im Projektordner) als Wikilink mit vollem Pfad. Die Planwerkzeuge vertragen die Zeile an dieser Stelle, weil sie nur Überschriften, Codezäune und den Abschnitt `## Zieldateien` auswerten (Befund im Plan `.vault-tools/linkcheck/docs/plans/2026-09-29-waisen/`). Regel und Begründung: `AI/Regelwerk/navigation.md`, Abschnitt „Links schreiben und Dateien verschieben“.
```
### README-Zeile in `AI/Regelwerk/codedoku.md`
@@ -194,13 +197,13 @@ wird:
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/regelwerk-check/regelwerk_check.py --vault D:/Claude > /tmp/regelwerk-nachher.txt; echo "exit=$?"; diff <(grep '^\[' /tmp/regelwerk-vorher.txt) <(grep '^\[' /tmp/regelwerk-nachher.txt) && echo "keine neue Befundzeile"
```
Erwartet: `keine neue Befundzeile`. Zeigt `diff` eine Zeile, die mit `>` beginnt, ist das ein neuer Befund: melden.
Erwartet: `keine neue Befundzeile`. Zeigt `diff` eine Zeile, die mit `>` beginnt, meldest du sie wörtlich. Ein Befund dieses Tasks ist sie nur, wenn sie eine der fünf Zieldateien nennt. Zwei andere Sitzungen können parallel an `CLAUDE.md` und `AI/Regelwerk/` arbeiten, eine Zeile zu einer anderen Datei stammt deshalb vermutlich von dort. Das entscheidet der Koordinator, nicht du.
```
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/linkcheck.py --vault D:/Claude --alle > /tmp/linkcheck-nachher.txt; echo "exit=$?"; diff <(grep '^ - ' /tmp/linkcheck-vorher.txt) <(grep '^ - ' /tmp/linkcheck-nachher.txt) && echo "keine neuen toten Links"
```
Erwartet: `keine neuen toten Links`.
Erwartet: `keine neuen toten Links`. Für eine neue Zeile gilt dasselbe wie oben: wörtlich melden, ein Befund dieses Tasks nur, wenn sie aus einer der fünf Zieldateien stammt.
## Schritt 3 — Planablage-Prüfung
@@ -5,7 +5,7 @@ status: draft
summary: "Legt waisen_bestand.py an, das verwaiste Notizen über Muster ihrem Hub zuordnet, im Probelauf nur auflistet und mit --anwenden erst sichert und dann anhängt, und faelle_waisen_bestand.py mit Git-Fällen und Rotlauf."
tags: [plan-task, vault-tools]
created: 2026-09-29
updated: 2026-09-29
updated: 2026-09-30
modell: haiku
modell_begruendung: "Zwei neue Dateien stehen wörtlich im Plan und werden per Skript angelegt; keine typografischen Zeichen in den Codeblöcken."
---
@@ -62,6 +62,11 @@ REGELN, alle über Muster, ohne Namen einzelner Projekte oder Kunden
Code Projekte/<p>/CHANGELOG.md und jede Datei unter Code Projekte/<p>/docs/
außerhalb von Planordnern
eine Zeile mit relativem Link aufs Projekt-README.
docs/plans/<ordner>/<datei>.md an der Vault-Wurzel
wie ein Planordner in Code Projekte, aber die Zeile heißt "Vault:" und
verlinkt README.md der Vault-Wurzel.
jede andere Datei unter docs/ an der Vault-Wurzel
eine Zeile mit relativem Link auf README.md der Vault-Wurzel.
IT Workspace/**/_arbeitsstand/**
Planordner wie oben, aber der Link nach oben ist ein Wikilink auf die
einzige _MOC-*.md im Ordner über _arbeitsstand; jede andere Datei
@@ -76,9 +81,10 @@ REGELN, alle über Muster, ohne Namen einzelner Projekte oder Kunden
Alles andere kommt auf die Liste für den Koordinator. Geraten wird nichts.
ÜBERSPRUNGEN UND AUFGELISTET wird eine Zieldatei, die
* in einem Git-Repo unterhalb des Vaults liegt und dort in irgendeinem
Arbeitsbaum uncommittet geändert oder neu ist oder auf einem anderen
lokalen Zweig seit dem gemeinsamen Stand geändert wurde,
* in einem Git-Repo im Vault liegt, auch dem der Vault-Wurzel, und dort in
irgendeinem Arbeitsbaum uncommittet geändert oder neu ist oder auf einem
anderen lokalen Zweig seit dem gemeinsamen Stand geändert wurde; was das
Repo ignoriert, zählt nicht als geändert,
* nicht als UTF-8 lesbar ist oder gemischte Zeilenenden hat,
* die anzuhängende Überschrift oder Linkzeile schon enthält.
Ein Git-Repo, das sich nicht prüfen lässt, sperrt alle seine Zieldateien.
@@ -218,6 +224,21 @@ def _dump(rel: str, teile: list, vault: Path) -> tuple:
return ("zeile", rel, f"Tagesnotiz: [[Inbox/{tag}]]", f"Inbox/{tag}.md")
def _wurzel_docs(rel: str, teile: list, vault: Path) -> tuple:
# Pläne und Specs unter docs/ an der Vault-Wurzel: Hub ist README.md dort.
hub = "README.md"
if not existiert(vault, hub):
return ("rest", "", "README.md der Vault-Wurzel fehlt", "")
if teile[1] == "plans" and len(teile) == 4:
kopf = "/".join(teile[:-1]) + "/00-uebersicht.md"
if not existiert(vault, kopf):
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
return ("ordner", kopf, f"Vault: [README]({rel_link(kopf, hub)})", hub)
if teile[1] == "plans" and len(teile) > 4:
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
return ("zeile", rel, f"Vault: [README]({rel_link(rel, hub)})", hub)
def klassifiziere(rel: str, vault: Path) -> tuple:
"""Liefert (art, ziel, wert, hub). art ist zeile, ordner, befund oder rest."""
teile = rel.split("/")
@@ -229,6 +250,8 @@ def klassifiziere(rel: str, vault: Path) -> tuple:
return _befund(rel, teile, vault)
if teile[:2] == ["AI", "secondbrain"] and len(teile) == 4:
return _dump(rel, teile, vault)
if teile[0] == "docs" and len(teile) >= 2:
return _wurzel_docs(rel, teile, vault)
return ("rest", "", "keine Regel passt", "")
@@ -311,9 +334,12 @@ def anhang_bytes(alt: bytes, zeilen: list, ende: str) -> bytes:
def repo_von(vault: Path, rel: str) -> Optional[Path]:
ordner = (vault / rel).parent
while ordner != vault and vault in ordner.parents:
# Auch die Vault-Wurzel kann ein Repo sein; über ihr wird nicht gesucht.
while ordner == vault or vault in ordner.parents:
if (ordner / ".git").exists():
return ordner
if ordner == vault:
return None
ordner = ordner.parent
return None
@@ -527,6 +553,11 @@ eigener Prozess. Die Git-Fälle legen im Wegwerf-Vault ein Repo mit Zweig oder
Arbeitsbaum an und brauchen git im PATH. Verglichen wird jede Datei des
Wegwerf-Vaults vor und nach dem Lauf, Byte für Byte.
Zwei Fälle laufen über HUELLE: Sie lädt waisen_bestand.py in einen eigenen
Prozess und setzt entweder einen fremden Schreiber ein, der nach der Sicherung
an CHANGELOG.md anhängt, oder schreibt je git-Aufruf den Wert von
GIT_OPTIONAL_LOCKS mit.
--rotlauf baut jeden Mutanten aus MUTANTEN in eine Kopie des Skripts und fährt
die genannten Fälle dagegen. Getötet heißt: jeder genannte Fall schlägt fehl.
@@ -555,6 +586,9 @@ E = P + "/docs/plans/2026-01-01-eins"
S = "IT Workspace/SEO/kunde-audit/15012026"
X = "IT Workspace/Thema/Projekt X"
XP = X + "/_arbeitsstand/docs/plans/2026-01-03-p"
W = "docs/plans/2026-01-04-w"
SPEC = "docs/specs/2026-01-04-spec.md"
NEU = P + "/docs/plans/2026-01-09-neu"
DUMP = "AI/secondbrain/alpha-memory/2026-01-01-thema.md"
DUMP_OHNE = "AI/secondbrain/alpha-memory/2026-01-05-ohne.md"
UMLAUT = "task-09-prüfung x.md"
@@ -565,6 +599,7 @@ BEFUND_KOPF = "## Befunddateien dieses Laufs"
ZIELE = {
P + "/CHANGELOG.md": ["Projekt: [README](README.md)"],
P + "/docs/code/modul.md": ["Projekt: [README](../../README.md)"],
P + "/docs/lose.md": ["Projekt: [README](../README.md)"],
P + "/docs/plans/2026-01-02-einzel.md": ["Projekt: [README](../../README.md)"],
E + "/00-uebersicht.md": ["Projekt: [README](../../../README.md)", "", ORDNER_KOPF, "",
"- [task-01-a.md](task-01-a.md)", f"- [{UMLAUT}]({UMLAUT_LINK})"],
@@ -573,12 +608,64 @@ ZIELE = {
X + "/_arbeitsstand/notiz.md": [f"Projekt: [[{X}/_MOC-projekt-x]]"],
XP + "/00-uebersicht.md": [f"Projekt: [[{X}/_MOC-projekt-x]]", "", ORDNER_KOPF, "",
"- [task-01-t.md](task-01-t.md)"],
W + "/00-uebersicht.md": ["Vault: [README](../../../README.md)", "", ORDNER_KOPF, "",
"- [task-01-w.md](task-01-w.md)"],
SPEC: ["Vault: [README](../../README.md)"],
}
REST = {
P + "/notizen.md": "keine Regel passt",
DUMP_OHNE: "Tagesnotiz Inbox/2026-01-05.md fehlt",
W + "/befunde/b1.md": "Plandatei tiefer als ein Ordner unter docs/plans",
}
HUELLE = '''"""Huelle aus faelle_waisen_bestand.py: laedt waisen_bestand.py und biegt zwei Stellen um."""
import importlib.util
import os
import subprocess
import sys
from pathlib import Path
skript, art, *rest = sys.argv[1:]
# Die Variable darf nicht schon von aussen kommen, sonst misst der Fall die Shell.
os.environ.pop("GIT_OPTIONAL_LOCKS", None)
spec = importlib.util.spec_from_file_location("waisen_bestand_huelle", skript)
modul = importlib.util.module_from_spec(spec)
# Vor exec_module eintragen: dataclasses sucht das Modul in sys.modules.
sys.modules["waisen_bestand_huelle"] = modul
spec.loader.exec_module(modul)
aufrufe = []
echt_run = subprocess.run
echt_copy2 = modul.shutil.copy2
def run(befehl, *a, **k):
# Git-Double: merkt sich je git-Aufruf den Wert von GIT_OPTIONAL_LOCKS.
if befehl and befehl[0] == "git":
aufrufe.append((k.get("env") or os.environ).get("GIT_OPTIONAL_LOCKS"))
return echt_run(befehl, *a, **k)
def copy2(quelle, ziel, *a, **k):
# Fremder Schreiber: haengt nach der Sicherung von CHANGELOG.md etwas an.
ergebnis = echt_copy2(quelle, ziel, *a, **k)
if art == "fremd" and Path(quelle).name == "CHANGELOG.md":
with open(quelle, "ab") as datei:
datei.write(b"fremd")
return ergebnis
subprocess.run = run
modul.shutil.copy2 = copy2
sys.stdout.reconfigure(encoding="utf-8")
code = modul.main(rest)
ohne = [wert for wert in aufrufe if wert != "0"]
if aufrufe and not ohne:
print("HUELLE git ok")
else:
print(f"HUELLE git: {len(aufrufe)} Aufrufe, {len(ohne)} ohne GIT_OPTIONAL_LOCKS=0")
raise SystemExit(code)
'''
def schreibe(v: Path, rel: str, inhalt) -> None:
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
@@ -614,12 +701,14 @@ def nichts(v: Path) -> None:
def baue_grundlinie(v: Path) -> None:
schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["_index/"]}')
schreibe(v, "Wissen/start.md",
f"[[{P}/README]]\n[[IT Workspace/SEO/_MOC-seo]]\n[[{X}/_MOC-projekt-x]]\n")
f"[[{P}/README]]\n[[IT Workspace/SEO/_MOC-seo]]\n[[{X}/_MOC-projekt-x]]\n[[README]]\n")
schreibe(v, "README.md", "# Vault\n")
schreibe(v, P + "/README.md", "# Alpha\n")
schreibe(v, P + "/CHANGELOG.md", "# Changelog")
schreibe(v, P + "/notizen.md", "# Notizen\n")
schreibe(v, P + "/data/2026-01-01.md", "# Auswertung\n")
schreibe(v, P + "/docs/code/modul.md", b"# Modul\r\n\r\nText\r\n")
schreibe(v, P + "/docs/lose.md", "# Lose\n")
schreibe(v, P + "/docs/plans/2026-01-02-einzel.md", "# Einzel\n")
schreibe(v, E + "/00-uebersicht.md", "# Eins\n")
schreibe(v, E + "/task-01-a.md", "# A\n")
@@ -635,6 +724,10 @@ def baue_grundlinie(v: Path) -> None:
schreibe(v, X + "/_arbeitsstand/notiz.md", "# N\n")
schreibe(v, XP + "/00-uebersicht.md", "# P\n")
schreibe(v, XP + "/task-01-t.md", "# T\n")
schreibe(v, W + "/00-uebersicht.md", "# W\n")
schreibe(v, W + "/task-01-w.md", "# T\n")
schreibe(v, W + "/befunde/b1.md", "# B\n")
schreibe(v, SPEC, "# Spec\n")
schreibe(v, "Bookstack/archiv/seite.md", "# S\n")
schreibe(v, "_index/INDEX.md", "# I\n")
@@ -689,6 +782,43 @@ def sauberes_repo(v: Path) -> None:
repo_anlegen(v)
def neuer_planordner(v: Path) -> None:
# Ganz ungetrackter Ordner: im Modus normal nennt git nur den Ordner.
repo_anlegen(v)
schreibe(v, NEU + "/00-uebersicht.md", "# Neu\n")
schreibe(v, NEU + "/task-01-x.md", "# X\n")
def umbenannt_im_index(v: Path) -> None:
# Der alte Pfad ab/CHANGELOG.md ohne seine ersten drei Zeichen wäre CHANGELOG.md.
schreibe(v, P + "/ab/CHANGELOG.md", "# Alt\n")
repo = repo_anlegen(v)
git(repo, "mv", "ab/CHANGELOG.md", "ab/neu.md")
# Ohne erkannte Umbenennung misst der Fall nichts.
status = subprocess.run(["git", "-C", str(repo), "status", "--porcelain"],
capture_output=True, stdin=subprocess.DEVNULL).stdout
if not status.startswith(b"R "):
raise RuntimeError(f"git meldet keine Umbenennung: {status!r}")
def wurzel_repo(v: Path, ignorieren: str) -> None:
schreibe(v, ".gitignore", ignorieren)
git(v, "init", "-q", "-b", "main")
git(v, "config", "core.autocrlf", "false")
git(v, "add", ".gitignore")
git(v, "commit", "-q", "-m", "Stand")
def wurzel_ignoriert_alles(v: Path) -> None:
# Wie die Positivliste der Vault-Wurzel: alles ignoriert außer .gitignore.
wurzel_repo(v, "/*\n!/.gitignore\n")
def wurzel_docs_offen(v: Path) -> None:
# docs/ ist nicht ignoriert, seine Dateien sind also neu im Repo der Wurzel.
wurzel_repo(v, "/*\n!/.gitignore\n!/docs/\n")
def abbild(v: Path) -> dict:
ergebnis = {}
for pfad in sorted(v.rglob("*")):
@@ -703,7 +833,7 @@ def abbild(v: Path) -> dict:
class Fall:
name: str
aendern: Callable = nichts
# probe, anwenden, zweimal, ohne_sicherung, sicherung_da, sicherung_im_vault, anwenden_abbruch
# probe, anwenden, zweimal, ohne_sicherung, sicherung_da, sicherung_im_vault, anwenden_abbruch, anwenden_fremd
modus: str = "probe"
code: int = 0
ziele: Optional[dict] = None
@@ -715,15 +845,17 @@ class Fall:
ueber: dict = field(default_factory=dict)
texte: list = field(default_factory=list)
nachher: dict = field(default_factory=dict)
# leer: Skript direkt; fremd oder sperre: über HUELLE
huelle: str = ""
GRUNDLINIE = Fall("GRUNDLINIE", ziele=ZIELE, rest=REST,
texte=["Waisen außerhalb der Ausnahmen vorher: 15",
texte=["Waisen außerhalb der Ausnahmen vorher: 20",
"Durch Anhänge mitverbunden, ohne eigene Regel: 1"])
FAELLE = [
Fall("anwenden | schreibt genau die erwarteten Bytes", modus="anwenden", ziele=ZIELE, rest=REST,
texte=["Waisen außerhalb der Ausnahmen nachher: 2"],
texte=["Waisen außerhalb der Ausnahmen nachher: 3"],
nachher={
P + "/CHANGELOG.md": b"# Changelog\n\nProjekt: [README](README.md)\n",
P + "/docs/code/modul.md": b"# Modul\r\n\r\nText\r\n\r\nProjekt: [README](../../README.md)\r\n",
@@ -742,6 +874,13 @@ FAELLE = [
Fall("anwenden | Befund des Zaehlers bricht ohne Schreiben ab",
lambda v: entferne(v / ".obsidian"), modus="anwenden_abbruch", code=2,
texte=["Bestandslauf - ABGEBROCHEN", "Ordner .obsidian fehlt"]),
Fall("anwenden | fremder Schreiber zwischen Lesen und Anhaengen bricht ab", modus="anwenden_fremd",
huelle="fremd", code=2,
texte=["hat sich seit dem Lesen geändert", "Vor dem Abbruch schon angehängt (1)"],
nachher={
DUMP: b"# Dump\n\nTagesnotiz: [[Inbox/2026-01-01]]\n",
P + "/CHANGELOG.md": b"# Changelogfremd",
}),
Fall("zeilenende | gemischte Zeilenenden werden uebersprungen",
lambda v: schreibe(v, P + "/docs/code/modul.md", b"# Modul\r\nText\n"),
ueber={P + "/docs/code/modul.md": "gemischte Zeilenenden"}, ohne_ziel=[P + "/docs/code/modul.md"]),
@@ -757,6 +896,17 @@ FAELLE = [
Fall("git | nie committete Datei zaehlt als geaendert", nie_committet,
ueber={P + "/CHANGELOG.md": "Arbeitsbaum"}),
Fall("git | sauberes Repo ohne Zweig wird angehaengt", sauberes_repo, ziele=ZIELE, rest=REST),
Fall("git | ungetrackter Planordner zaehlt je Datei", neuer_planordner,
ueber={NEU + "/00-uebersicht.md": "Arbeitsbaum"}, mit_ziel=[P + "/CHANGELOG.md"]),
Fall("git | Umbenennung: alter Pfad ist kein eigener Eintrag", umbenannt_im_index,
mit_ziel=[P + "/CHANGELOG.md"]),
Fall("git | jeder git-Aufruf mit GIT_OPTIONAL_LOCKS=0", sauberes_repo, huelle="sperre",
ziele=ZIELE, rest=REST, texte=["HUELLE git ok"]),
Fall("git | Vault-Wurzel als Repo, ignorierte Dateien zaehlen nicht", wurzel_ignoriert_alles,
ziele=ZIELE, rest=REST),
Fall("git | Vault-Wurzel als Repo, docs/ nicht ignoriert", wurzel_docs_offen,
ueber={W + "/00-uebersicht.md": "Arbeitsbaum", SPEC: "Arbeitsbaum"},
mit_ziel=[DUMP, P + "/CHANGELOG.md"]),
Fall("regel | Planordner ohne 00-uebersicht.md",
lambda v: (v / E / "00-uebersicht.md").unlink(),
mit_rest={E + "/task-01-a.md": "Planordner ohne 00-uebersicht.md",
@@ -789,6 +939,14 @@ FAELLE = [
Fall("regel | Dump tiefer als ein Ordner",
lambda v: schreibe(v, "AI/secondbrain/alpha-memory/drehbuecher/2026-01-01-x.md", "# D\n"),
mit_rest={"AI/secondbrain/alpha-memory/drehbuecher/2026-01-01-x.md": "keine Regel passt"}),
Fall("regel | Vault-Wurzel ohne README",
lambda v: (v / "README.md").unlink(),
mit_rest={W + "/task-01-w.md": "README.md der Vault-Wurzel fehlt",
SPEC: "README.md der Vault-Wurzel fehlt"},
ohne_ziel=[W + "/00-uebersicht.md", SPEC]),
Fall("regel | Wurzel-Planordner ohne 00-uebersicht.md",
lambda v: (v / W / "00-uebersicht.md").unlink(),
mit_rest={W + "/task-01-w.md": "Planordner ohne 00-uebersicht.md"}),
]
ALLE = [GRUNDLINIE] + FAELLE
@@ -822,6 +980,54 @@ MUTANTEN = [
" if sicherung_pfad == vault or vault in sicherung_pfad.parents:",
" if False:",
["anwenden | Sicherungsordner im Vault"]),
("fremde Aenderung vor dem Anhaengen nicht erkannt",
" if pfad.read_bytes() != alt:",
" if False:",
["anwenden | fremder Schreiber zwischen Lesen und Anhaengen bricht ab"]),
("GEAENDERT.txt unter anderem Namen",
r' (sicherung / "GEAENDERT.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")',
r' (sicherung / "ANDERS.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")',
["anwenden | schreibt genau die erwarteten Bytes"]),
("ungetrackte Ordner nur als Ordner",
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")',
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=normal")',
["git | ungetrackter Planordner zaehlt je Datei"]),
("ignorierte Dateien zaehlen als geaendert",
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")',
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all", "--ignored")',
["git | Vault-Wurzel als Repo, ignorierte Dateien zaehlen nicht"]),
("Umbenennung als zwei Eintraege gelesen",
' if "R" in eintrag[:2] or "C" in eintrag[:2]:',
" if False:",
["git | Umbenennung: alter Pfad ist kein eigener Eintrag"]),
("git ohne GIT_OPTIONAL_LOCKS",
' umgebung["GIT_OPTIONAL_LOCKS"] = "0"',
" pass",
["git | jeder git-Aufruf mit GIT_OPTIONAL_LOCKS=0"]),
("Vault-Wurzel nicht als Repo geprueft",
" while ordner == vault or vault in ordner.parents:",
" while ordner != vault and vault in ordner.parents:",
["git | Vault-Wurzel als Repo, docs/ nicht ignoriert"]),
("docs/ an der Wurzel ohne Regel",
' if teile[0] == "docs" and len(teile) >= 2:',
" if False:",
["GRUNDLINIE"]),
("Wurzel-README nicht geprueft",
" if not existiert(vault, hub):",
" if False:",
["regel | Vault-Wurzel ohne README"]),
("Wurzel-Planordner ohne Uebersicht nicht erkannt",
" if not existiert(vault, kopf):",
" if False:",
["regel | Wurzel-Planordner ohne 00-uebersicht.md"]),
("Wurzel-Plandatei zu tief angehaengt",
' if teile[1] == "plans" and len(teile) > 4:',
" if False:",
["GRUNDLINIE"]),
("Datei direkt unter docs/ ohne Regel",
' if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 2):',
' if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 3):',
["GRUNDLINIE"]),
]
@@ -905,6 +1111,9 @@ def bewerte(fall: Fall, code: int, out: str, v: Path, vorher: dict, sicherung: P
erwartet = sorted(fall.ziele or {})
if geaendert != erwartet:
probleme.append(f"geaendert {geaendert} statt {erwartet}")
liste = sicherung / "GEAENDERT.txt"
if not liste.is_file() or liste.read_bytes() != ("\n".join(erwartet) + "\n").encode("utf-8"):
probleme.append(f"GEAENDERT.txt fehlt oder nennt nicht genau {erwartet}")
for rel in erwartet:
kopie = sicherung / rel
if not kopie.is_file() or kopie.read_bytes() != vorher.get(rel):
@@ -918,6 +1127,13 @@ def bewerte(fall: Fall, code: int, out: str, v: Path, vorher: dict, sicherung: P
for rel, soll in fall.nachher.items():
if nachher.get(rel) != soll:
probleme.append(f"{rel}: {nachher.get(rel)!r} statt {soll!r}")
elif fall.modus == "anwenden_fremd":
erwartet = sorted(fall.nachher)
if geaendert != erwartet:
probleme.append(f"geaendert {geaendert} statt {erwartet}")
for rel, soll in fall.nachher.items():
if nachher.get(rel) != soll:
probleme.append(f"{rel}: {nachher.get(rel)!r} statt {soll!r}")
elif geaendert:
probleme.append(f"hat Dateien geaendert: {geaendert}")
if fall.modus == "anwenden_abbruch" and sicherung.exists():
@@ -935,7 +1151,7 @@ def lauf_fall(fall: Fall, basis: Path, arbeit: Path, skript: Path) -> tuple:
vorher = abbild(v)
sicherung = arbeit / "sicherung"
argumente = ["--vault", str(v)]
if fall.modus in ("anwenden", "zweimal", "anwenden_abbruch"):
if fall.modus in ("anwenden", "zweimal", "anwenden_abbruch", "anwenden_fremd"):
argumente += ["--anwenden", "--sicherung", str(sicherung)]
elif fall.modus == "ohne_sicherung":
argumente += ["--anwenden"]
@@ -944,7 +1160,12 @@ def lauf_fall(fall: Fall, basis: Path, arbeit: Path, skript: Path) -> tuple:
argumente += ["--anwenden", "--sicherung", str(sicherung)]
elif fall.modus == "sicherung_im_vault":
argumente += ["--anwenden", "--sicherung", str(v / "Sicherung")]
code, out, err, dauer = fahre(skript, argumente)
ausfuehren = skript
if fall.huelle:
ausfuehren = arbeit / "huelle.py"
ausfuehren.write_bytes(HUELLE.encode("utf-8"))
argumente = [str(skript), fall.huelle] + argumente
code, out, err, dauer = fahre(ausfuehren, argumente)
probleme = bewerte(fall, code, out, v, vorher, sicherung)
if fall.modus == "zweimal" and not probleme:
erster = abbild(v)
@@ -1070,7 +1291,7 @@ Erwartet: `ABBRUCH: D:\Claude\.vault-tools\linkcheck\waisen_bestand.py fehlt` un
PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/faelle_waisen_bestand.py; echo "exit=$?"
```
Erwartet: erste Zeile `BESTANDEN GRUNDLINIE`, keine Zeile, die mit `FEHLER` beginnt, Schlusszeile `N von N Faellen bestanden.` mit zweimal derselben Zahl, `exit=0`. Der Lauf legt Git-Repos im Temp-Ordner an und dauert deshalb länger als der Treiber aus Task 01.
Erwartet: erste Zeile `BESTANDEN GRUNDLINIE`, keine Zeile, die mit `FEHLER` beginnt, Schlusszeile `N von N Faellen bestanden.` mit zweimal derselben Zahl, `exit=0`. Der Lauf legt Git-Repos im Temp-Ordner an und dauert deshalb länger als der Treiber aus Task 01. Meldet der Fall `git | Umbenennung: alter Pfad ist kein eigener Eintrag` die Zeile `ABSTURZ im Treiber: RuntimeError: git meldet keine Umbenennung`, erkennt das installierte git die Umbenennung nicht: nichts anpassen, melden.
## Schritt 4 — Rotlauf
@@ -46,10 +46,10 @@ Erwartet: zweimal `exit=0`. Sonst Halt und melden.
### A2 — Protokollordner
```
test ! -e D:/Claude-waisen-protokoll && mkdir D:/Claude-waisen-protokoll && echo "angelegt"
mkdir -p D:/Claude-waisen-protokoll && test ! -e D:/Claude-waisen-protokoll/probelauf.txt && echo "angelegt"
```
Erwartet: `angelegt`. Gibt es den Ordner schon, Halt und melden.
Erwartet: `angelegt`. Den Ordner kann Task 06 schon angelegt haben (Unterordner `nachbesserung-06/`); liegt dort aber schon eine `probelauf.txt`, Halt und melden.
### A3 — Zählung vorher
@@ -95,6 +95,16 @@ PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/waisen_bestand.py
Erwartet: `exit=0` und `GLEICH`. Zeigt `diff` Unterschiede, hat sich der Vault seit Phase A geändert: Halt, die `diff`-Ausgabe melden, nichts anwenden.
### B1b — Git-Stand der berührten Repos vorher
Jedes Ziel liegt in einem Git-Repo: in einem Code-Projekt, im `_arbeitsstand` eines Projekts im IT Workspace oder im Repo der Vault-Wurzel. Manche Repos tragen schon vorher fremde Änderungen. Deshalb wird der Stand vorher festgehalten und nachher verglichen. `GIT_OPTIONAL_LOCKS=0` verhindert, dass `git status` den Index auffrischt.
```
D=D:/Claude-waisen-protokoll; sed -nE 's/^ZIEL (.*) \[.*$/\1/p' "$D/probelauf-2.txt" | while IFS= read -r f; do git -C "D:/Claude/$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null; done | sort -u > "$D/repos.txt"; echo "Repos: $(wc -l < "$D/repos.txt")"; cat "$D/repos.txt"; while IFS= read -r r; do echo "== $r"; GIT_OPTIONAL_LOCKS=0 git -C "$r" status --short --untracked-files=all; done < "$D/repos.txt" > "$D/git-vorher.txt"; echo "exit=$?"
```
Erwartet: eine Zeile `Repos: <Zahl>`, die Liste der Repos und `exit=0`.
### B2 — Anwenden
```
@@ -124,10 +134,10 @@ Erwartet: dieselbe Zahl wie in der Zeile „nachher“ aus B3.
Setze `S` auf den Pfad aus B2, Zeile `Sicherung:`, und fahre:
```
cut -d/ -f1-2 "$S/GEAENDERT.txt" | grep '^Code Projekte/' | sort -u | while IFS= read -r r; do echo "== $r"; git -C "D:/Claude/$r" status --short; done
D=D:/Claude-waisen-protokoll; while IFS= read -r r; do echo "== $r"; GIT_OPTIONAL_LOCKS=0 git -C "$r" status --short --untracked-files=all; done < "$D/repos.txt" > "$D/git-nachher.txt"; diff "$D/git-vorher.txt" "$D/git-nachher.txt"; echo "diff-exit=$?"; echo "Zeilen in GEAENDERT.txt: $(wc -l < "$S/GEAENDERT.txt")"
```
Erwartet: je Repo eine Überschrift `== Code Projekte/<projekt>` und darunter nur Zeilen mit ` M` für Dateien aus `GEAENDERT.txt`. Committen tut der Koordinator nach Pascals Ja, je Repo einzeln.
Erwartet: `diff` zeigt nur Zeilen, die mit `> ` und danach ` M` beginnen, eine je geänderter Datei eines Code-Projekts oder eines `_arbeitsstand`. Die Pfade darin sind relativ zum Repo, in `GEAENDERT.txt` relativ zum Vault. Dateien unter `AI/`, `Inbox/` und `IT Workspace/` außerhalb eines `_arbeitsstand` ignoriert das Repo der Vault-Wurzel, sie erscheinen nicht. Eine Zeile, die mit `< ` beginnt, oder eine neue Zeile zu einer Datei, die nicht in `GEAENDERT.txt` steht, meldest du wörtlich: Sie kann von einer parallelen Sitzung stammen. Committen tut der Koordinator, je Repo einzeln und nur die Dateien aus `GEAENDERT.txt`.
### B6 — Bericht