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 e8472409e9
commit eb48f8822d
45 files changed
+8645 -262

No files matched your search

+14
View File
@@ -106,3 +106,17 @@ Schluss ist, wenn beide Treiber mit grüner Grundlinie und ohne Fehlschlag durch
## Was diese Skripte nicht sind
Ein Fangnetz. `sitzungsstart.py` nimmt das Zählen ab, nicht das Lesen der Inbox. `regelwerk_nach_edit.py` reicht weiter, was `regelwerk_check.py` sieht, und das sind formale Widersprüche, keine inhaltlichen. Der belastbare Nachweis, dass die Hooks in Claude Code wirken, ist eine echte Sitzung nach dem Eintrag in `settings.json`, nicht dieser Entwurf.
## Nachtrag 2026-09-29: Zahl der Waisen
**Anlass.** Die Spec Waisen im Graphen vom 2026-09-29 verlangt beim Sitzungsstart eine Zeile mit der Zahl der Waisen außerhalb der Ausnahmen, nur die Zahl, keine Liste. Plan: `.vault-tools/linkcheck/docs/plans/2026-09-29-waisen/`.
| # | Frage | Antwort | Herkunft |
|---|---|---|---|
| 16 | Woher kommt die Zahl? | `waisen.py --zahl` aus `.vault-tools/linkcheck/`, als eigener Prozess mit `sys.executable`, stdin geschlossen, `PYTHONIOENCODING=utf-8`. Die Regeln des Zählers stehen in `.vault-tools/linkcheck/ENTWURF-waisen.md`. | **Projektvorgabe**: Plan |
| 17 | Wie lange darf sie dauern? | 15 s, dann bricht der Hook den Zähler ab und meldet eine Fehlerzeile. Das Budget des Hooks in `.claude/settings.json` sind 30 s für alles. | **Projektvorgabe** |
| 18 | Was, wenn `waisen.py` fehlt, abbricht oder keine Zahl liefert? | Fehlerzeile `Sitzungsstart-Hook: Waisen:` mit der ersten Ausgabezeile des Zählers, keine Zahl. | **Projektvorgabe**: `pruefwerkzeuge.md` Abschnitt 6 |
**Treiber.** Die Grundlinie trägt jetzt `.obsidian/app.json` und hat sechs Waisen. `kopiere_hook()` kopiert `waisen.py` und `waisen-ausnahmen.txt` mit. Neue Fälle: ein Link senkt die Zahl, Waisen an erster und letzter Stelle, ein Muster aus der Ausnahmedatei greift, `waisen.py` fehlt, kaputte `app.json`, Umlaut und Emoji im Namen ohne `PYTHONIOENCODING`, und im Prozess die überschrittene Zeitgrenze. Zwei bestehende Fälle erwarten zusätzlich die Fehlerzeile des Zählers: Vault ohne jeden Ordner (dann fehlt `.obsidian`) und `linkcheck.py` fehlt. Dass der Hook die Zahl wirklich vom Zähler übernimmt, zeigen die verschiedenen erwarteten Zahlen 5, 6, 7 und 8 in den neuen Fällen.
**Laufzeit.** Frage 15 peilt unter 5 s an. Ob das mit dem Zähler hält, misst `faelle_sitzungsstart.py --echt`.
@@ -55,7 +55,10 @@ NR = "- Notizen mit Tag needs-review: "
RV = "- Notizen mit Tag review ohne review-done: "
PR = "- Regelwerk-Dateien mit status: pending-review: "
SI = f"- Sitzungsdateien AI/sessions/{HEUTE}-*.md: "
WA = "- Waisen außerhalb der Ausnahmen: "
FEHLERZEILE = "Sitzungsstart-Hook:"
WAISEN = HIER.parent / "linkcheck" / "waisen.py"
AUSNAHMEN = HIER.parent / "linkcheck" / "waisen-ausnahmen.txt"
def schreibe(vault: Path, rel: str, inhalt: str | bytes) -> None:
@@ -74,6 +77,8 @@ def baue_grundlinie(v: Path) -> None:
schreibe(v, f"AI/sessions/{GESTERN}-10.md", "---\ntype: session\n---\n")
schreibe(v, "Notizen/sauber.md",
"---\ntype: note\ntags: [thema]\n---\n\nKein Tag hier, nur das Wort review ohne Raute.\n")
# waisen.py braucht einen Obsidian-Vault; ohne Filter hat die Grundlinie sechs Waisen.
schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": []}')
def grundzeilen(v: Path) -> list[str]:
@@ -86,10 +91,11 @@ def grundzeilen(v: Path) -> list[str]:
RV + "0",
PR + "0",
SI + f"1 ({HEUTE}-10.md)",
WA + "6",
]
def kopiere_hook(v: Path, linkcheck_kopieren: bool = True) -> Path:
def kopiere_hook(v: Path, linkcheck_kopieren: bool = True, waisen_kopieren: bool = True) -> Path:
"""Kopiert den Hook (und optional linkcheck.py) in den Wegwerf-Vault.
Seit der Nachbesserung Runde 2 bestimmt sitzungsstart.py seine Vault-
@@ -102,10 +108,13 @@ def kopiere_hook(v: Path, linkcheck_kopieren: bool = True) -> Path:
ziel.mkdir(parents=True, exist_ok=True)
skript = ziel / HOOK.name
shutil.copy2(HOOK, skript)
lc_ziel = v / ".vault-tools" / "linkcheck"
lc_ziel.mkdir(parents=True, exist_ok=True)
if linkcheck_kopieren:
lc_ziel = v / ".vault-tools" / "linkcheck"
lc_ziel.mkdir(parents=True, exist_ok=True)
shutil.copy2(LINKCHECK, lc_ziel / "linkcheck.py")
if waisen_kopieren:
shutil.copy2(WAISEN, lc_ziel / "waisen.py")
shutil.copy2(AUSNAHMEN, lc_ziel / "waisen-ausnahmen.txt")
return skript
@@ -139,6 +148,7 @@ class Fall:
# die gezielt eine kaputte oder ungewoehnliche stdin-Form pruefen wollen.
stdin: Callable[[Path], bytes] | None = None
linkcheck_kopieren: bool = True
waisen_kopieren: bool = True
# None: Subprozess erbt die Umgebung dieses Treibers unveraendert. Gesetzt:
# ersetzt die Umgebung vollstaendig (Pruefbefund Runde 2).
umgebung: Callable[[Path], dict[str, str]] | None = None
@@ -309,7 +319,9 @@ FAELLE: list[Fall] = [
Fall("leer | Vault ohne jeden Ordner",
lambda v: [shutil.rmtree(p) for p in list(v.iterdir())],
[TN + "fehlt", IN + "Ordner Inbox/ fehlt", NR + "0", RV + "0",
PR + "Ordner AI/Regelwerk/ fehlt", SI + "Ordner AI/sessions/ fehlt"]),
PR + "Ordner AI/Regelwerk/ fehlt", SI + "Ordner AI/sessions/ fehlt"],
verboten=[WA],
praefixe=["Sitzungsstart-Hook: Waisen: RuntimeError: waisen.py Exit 2: Waisen - ABGEBROCHEN: kein Obsidian-Vault"]),
Fall("leer | stdin leer", nichts, None, stdin=lambda v: b""),
Fall("falscher Typ| stdin kein JSON", nichts, None, stdin=lambda v: b"kein json"),
Fall("kodierung | stdin ungueltiges UTF-8", nichts, None, stdin=lambda v: b"\xff\xfe{"),
@@ -332,10 +344,34 @@ FAELLE: list[Fall] = [
umgebung=ohne_pythonioencoding),
Fall("umgebung | linkcheck.py fehlt", nichts,
[DA, TN + "vorhanden", IN + "1 (offen.md)", PR + "0", SI + f"1 ({HEUTE}-10.md)"],
verboten=[NR, RV],
praefixe=["Sitzungsstart-Hook: needs-review/review: RuntimeError: linkcheck.py nicht importierbar"],
verboten=[NR, RV, WA],
praefixe=["Sitzungsstart-Hook: needs-review/review: RuntimeError: linkcheck.py nicht importierbar",
"Sitzungsstart-Hook: Waisen: RuntimeError: waisen.py Exit 2: Waisen - ABGEBROCHEN: linkcheck.py nicht importierbar"],
stdin=lambda v: b"{}",
linkcheck_kopieren=False),
Fall("waisen | verlinkte Notiz senkt die Zahl",
lambda v: schreibe(v, "Notizen/verweis.md", "[[Notizen/sauber]]\n"),
[WA + "5"]),
Fall("waisen | Waise an erster und an letzter Stelle",
lambda v: (schreibe(v, "0-erste/a.md", "x\n"), schreibe(v, "zz-letzte/z.md", "x\n")),
[WA + "8"]),
Fall("waisen | Muster aus der Ausnahmedatei greift",
lambda v: schreibe(v, "Bookstack/archiv/seite.md", "x\n"),
[WA + "6"]),
Fall("waisen | waisen.py fehlt", nichts,
[DA, TN + "vorhanden", IN + "1 (offen.md)", NR + "0", RV + "0", PR + "0", SI + f"1 ({HEUTE}-10.md)"],
verboten=[WA],
praefixe=["Sitzungsstart-Hook: Waisen: RuntimeError: waisen.py fehlt"],
waisen_kopieren=False),
Fall("waisen | kaputte app.json meldet Fehler statt Zahl",
lambda v: schreibe(v, ".obsidian/app.json", "{kaputt"),
[DA, TN + "vorhanden", NR + "0", SI + f"1 ({HEUTE}-10.md)"],
verboten=[WA],
praefixe=["Sitzungsstart-Hook: Waisen: RuntimeError: waisen.py Exit 2: Waisen - ABGEBROCHEN: .obsidian/app.json nicht lesbar"]),
Fall("umgebung | ohne PYTHONIOENCODING zaehlt Waisen mit Umlaut im Namen",
lambda v: schreibe(v, "Notizen/Übersicht 🚀.md", "x\n"),
[WA + "7"],
umgebung=ohne_pythonioencoding),
]
@@ -412,7 +448,7 @@ def frische_kopie(basis: Path, arbeit: Path) -> Path:
def lauf_fall(fall: Fall, basis: Path, arbeit: Path) -> tuple[bool, str]:
v = frische_kopie(basis, arbeit)
fall.aendern(v)
skript = kopiere_hook(v, fall.linkcheck_kopieren)
skript = kopiere_hook(v, fall.linkcheck_kopieren, fall.waisen_kopieren)
if fall.stdin is None:
eingabe = json.dumps({"session_id": "faelle", "cwd": str(v),
"hook_event_name": "SessionStart", "source": "startup"}).encode("utf-8")
@@ -501,7 +537,9 @@ def ip_datei_gesperrt(v: Path) -> tuple[bool, str]:
zeilen = im_prozess(modul, v)
if zeilen is None:
return False, " keine gueltige Ausgabe"
erwartet = grundzeilen(v) + ["Sitzungsstart-Hook: 1 Datei(en) nicht lesbar (erste: Notizen/gesperrt.md)"]
# gesperrt.md ist für waisen.py lesbar und die siebte Waise.
erwartet = [z for z in grundzeilen(v) if not z.startswith(WA)] + [WA + "7"]
erwartet.append("Sitzungsstart-Hook: 1 Datei(en) nicht lesbar (erste: Notizen/gesperrt.md)")
probleme = bewerte(zeilen, erwartet, [], [])
return not probleme, "\n".join(" " + p for p in probleme)
@@ -540,11 +578,27 @@ def ip_ordner_gesperrt(v: Path) -> tuple[bool, str]:
return not probleme, "\n".join(" " + p for p in probleme)
def ip_waisen_zeitgrenze(v: Path) -> tuple[bool, str]:
"""Die Zeitgrenze lässt sich von außen nicht billig überschreiten:
im Prozess auf eine Millisekunde gesetzt, startet waisen.py nicht rechtzeitig."""
skript = kopiere_hook(v)
modul = lade_hook("hook_waisen_zeitgrenze", skript)
modul.ZEITGRENZE_WAISEN_S = 0.001
zeilen = im_prozess(modul, v)
if zeilen is None:
return False, " keine gueltige Ausgabe"
erwartet = [z for z in grundzeilen(v) if not z.startswith(WA)]
erwartet.append("Sitzungsstart-Hook: Waisen: RuntimeError: waisen.py nach 0.001 s abgebrochen, nicht gezählt")
probleme = bewerte(zeilen, erwartet, [WA], [])
return not probleme, "\n".join(" " + p for p in probleme)
IM_PROZESS = [
("absturz | ein Punkt wirft", ip_punkt_wirft),
("absturz | Hauptablauf wirft", ip_hauptablauf_wirft),
("lesen | Datei nicht lesbar", ip_datei_gesperrt),
("lesen | Ordner nicht lesbar", ip_ordner_gesperrt),
("waisen | Zeitgrenze ueberschritten", ip_waisen_zeitgrenze),
]
+33 -1
View File
@@ -9,7 +9,10 @@ GELTUNGSBEREICH
nicht bewertet: Datum laut Maschinenuhr, Tagesnotiz, Inbox-Notizen ohne
type: daily, Notizen mit Tag needs-review, Notizen mit Tag review ohne
review-done, Regelwerk-Dateien mit status: pending-review, heutige
Sitzungsdateien. Die Vault-Wurzel ist ausschließlich der Ordner zwei Ebenen
Sitzungsdateien, dazu die Zahl der Waisen außerhalb der Ausnahmen aus
.vault-tools/linkcheck/waisen.py (eigener Prozess, nach 15 s abgebrochen;
Regeln und Lücken stehen im Kopf von waisen.py). Die Vault-Wurzel ist
ausschließlich der Ordner zwei Ebenen
über dieser Datei. Das Feld cwd der Hook-Eingabe wird nicht mehr gelesen:
cwd folgt einem cd im Bash-Werkzeug und zeigt in einer laufenden Sitzung
nicht zuverlässig auf die Vault-Wurzel (Prüferbefund Runde 2, gemessen
@@ -42,11 +45,15 @@ import datetime
import json
import os
import re
import subprocess
import sys
from pathlib import Path
SKRIPT = Path(__file__).resolve()
LINKCHECK = SKRIPT.parent.parent / "linkcheck"
WAISEN = LINKCHECK / "waisen.py"
# Das Budget des Hooks in .claude/settings.json sind 30 s für alles.
ZEITGRENZE_WAISEN_S = 15.0
MAX_NAMEN = 5
# Nur direkt unter der Vault-Wurzel ausgeschlossen, verglichen in Kleinbuchstaben.
@@ -272,6 +279,30 @@ def punkt_sitzungen(wurzel: Path, heute: str) -> list[str]:
return [f"- Sitzungsdateien AI/sessions/{heute}-*.md: {mit_namen(namen)}"]
def punkt_waisen(wurzel: Path, heute: str) -> list[str]:
if not WAISEN.is_file():
raise RuntimeError(f"waisen.py fehlt unter {WAISEN}")
umgebung = dict(os.environ)
umgebung["PYTHONIOENCODING"] = "utf-8"
try:
lauf = subprocess.run(
[sys.executable, str(WAISEN), "--vault", str(wurzel), "--zahl"],
capture_output=True,
stdin=subprocess.DEVNULL,
timeout=ZEITGRENZE_WAISEN_S,
env=umgebung,
)
except subprocess.TimeoutExpired:
raise RuntimeError(
f"waisen.py nach {ZEITGRENZE_WAISEN_S:g} s abgebrochen, nicht gezählt"
) from None
ausgabe = lauf.stdout.decode("utf-8", errors="replace").strip()
if lauf.returncode in (0, 1) and ausgabe.isdigit():
return [f"- Waisen außerhalb der Ausnahmen: {ausgabe}"]
erste = ausgabe or lauf.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"waisen.py Exit {lauf.returncode}: {erste.split(chr(10))[0].strip()}")
PUNKTE = [
("Datum", punkt_datum),
("Tagesnotiz", punkt_tagesnotiz),
@@ -279,6 +310,7 @@ PUNKTE = [
("needs-review/review", punkt_tags),
("Regelwerk", punkt_regelwerk),
("Sitzungen", punkt_sitzungen),
("Waisen", punkt_waisen),
]