Jedes Zeichen der Kategorie Cc ausser LF und jedes der Kategorie Cf in .claude/workflows/**/*.js, gelesen als Bytes. Anlass: plankritik.js mit CRLF scheiterte am Workflow-Werkzeug. Der Hook nach dem Bearbeiten loest auch bei Workflows aus. Regressionslauf 28/28 angeschlagen, 6/6 stumm; Hook-Treiber 58/58; Vorfall in exakter Form gemeldet (U+000D). Co-Authored-By: Claude Opus 5.5 <[email protected]>
109 lines
17 KiB
Markdown
109 lines
17 KiB
Markdown
# Entwurf: Sitzungs-Hooks
|
||
|
||
Angelegt am 2026-09-13 als Entwurfsschritt nach [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], Abschnitt 1. Er gilt für zwei Skripte: `sitzungsstart.py` (SessionStart) und `regelwerk_nach_edit.py` (PostToolUse).
|
||
|
||
## Anlass
|
||
|
||
Das Sitzungsprotokoll in `CLAUDE.md` verlangt zu Beginn jeder Sitzung dieselben Handgriffe: Tagesnotiz suchen, Inbox durchsehen, `needs-review` suchen. Nach jeder Änderung an Stamm, Regelwerk, Skills oder Agenten soll außerdem `regelwerk_check.py` laufen. Beides ist Zählen und Starten, kein Urteil.
|
||
|
||
Für den Sitzungsbeginn ist belegt, dass es schiefgeht: Das Datum kam zweimal aus der falschen Quelle, eine Sitzungsdatei hat eine andere überschrieben, und die Suche nach `needs-review` traf Erwähnungen in Backticks. Für den Regelwerk-Check liegt kein solcher Fall vor. Der Hook soll ihn starten, ohne dass jemand daran denken muss.
|
||
|
||
## Woher die Hook-Schnittstelle kommt
|
||
|
||
Eingabe auf stdin, Ausgabeform, Bedeutung der Exit-Codes und Matcher stammen aus der Hook-Doku (code.claude.com/docs/en/hooks). Der Koordinator hat sie am 2026-09-13 gegen Claude Code 2.1.263 gemessen und im Planauftrag festgehalten. Beim Planen wurde die Doku nicht noch einmal gezogen. Diese Herkunft steht deshalb unten als **Quelle (Koordinator)** und nicht als eigene Quelle.
|
||
|
||
## Entscheidungsliste `sitzungsstart.py`
|
||
|
||
| # | Frage | Antwort | Herkunft |
|
||
|---|---|---|---|
|
||
| 1 | Welche Punkte meldet der Hook? | Datum, Tagesnotiz, Inbox ohne `type: daily`, `needs-review`, `review` ohne `review-done`, `pending-review` im Regelwerk, heutige Sitzungsdateien | **Projektvorgabe** — `CLAUDE.md`, Abschnitt Sitzungsprotokoll, und Planauftrag |
|
||
| 2 | Welches Datum? | Maschinenuhr, im Text ausdrücklich so beschriftet | **Projektvorgabe** — das Sitzungsdatum aus dem Kontext hat Vorrang (Harness-Gedächtnis `datum-aus-dem-sitzungskontext`) |
|
||
| 3 | Welche Ausgabeform? | Exit 0, genau ein JSON-Objekt mit `hookSpecificOutput.hookEventName` und `additionalContext` | **Quelle (Koordinator)** — Hook-Doku, SessionStart |
|
||
| 4 | Woher kommt die Vault-Wurzel? | Ausschließlich der Ordner zwei Ebenen über dem Skript. Das Feld `cwd` der Hook-Eingabe wird nicht mehr gelesen | **Projektvorgabe (Koordinator, Nachbesserung Runde 2)** — Prüferbefund 1: `cwd` folgt einem `cd` im Bash-Werkzeug und zeigt in einer laufenden Sitzung nicht zuverlässig auf die Vault-Wurzel (Claude Code 2.1.263). Der Hook ist ohnehin nur für diesen einen Vault eingetragen, siehe „Nachbesserung Runde 2" unten |
|
||
| 5 | Was ist eine offene Inbox-Notiz? | Jede `.md` unter `Inbox/`, auch in Unterordnern ohne Punkt, deren Frontmatter nicht `type: daily` trägt. Ohne Frontmatter zählt sie als offen | **Projektvorgabe** — `CLAUDE.md` Sitzungsprotokoll Punkt 2; Unterordner als Annahme im Plan benannt |
|
||
| 6 | Was ist ein Inline-Tag? | `#name` am Zeilenanfang oder nach Leerraum, danach kein Buchstabe, keine Ziffer, kein `_`, `-` oder `/`. Groß- und Kleinschreibung egal | **Projektvorgabe** — die Obsidian-Doku zur Tag-Syntax wurde nicht gezogen; Grenze und Schreibweise sind unsere Festlegung |
|
||
| 7 | Zählt ein Tag in Code? | Nein, weder im Fenced-Codeblock noch im Inline-Code. Ausgeblendet über `strip_code()` aus `.vault-tools/linkcheck/linkcheck.py` | **Projektvorgabe** — [[AI/Regelwerk/frontmatter\|frontmatter.md]], Abschnitt Tags: ein nur erwähnter Tag steht in Backticks. Die Codeblock-Regel selbst ist in `.vault-tools/linkcheck/ENTWURF-codeblock-erkennung.md` belegt |
|
||
| 8 | Was ist ein Frontmatter-Tag? | Ein Eintrag unter `tags:` als Inline-Liste, Block-Liste oder Skalar, ohne Anführungszeichen, Klammern und führende Raute verglichen | **Projektvorgabe** — frontmatter.md: Tags im Frontmatter ohne `#`, als Liste |
|
||
| 9 | Kaputtes YAML? | Tolerant: Ein Tag in einer nicht geschlossenen Liste zählt. Ein Frontmatter ohne Schlusszeile ist keins | **Projektvorgabe** — lieber eine Markierung zu viel melden als eine übersehen |
|
||
| 10 | Welche Ordner fallen aus der Tag-Suche? | `Import/` und `_index/` direkt unter der Wurzel, Punktordner und `node_modules` in jeder Tiefe | **Projektvorgabe** — Planauftrag; dieselben Ausschlüsse wie im Vernetzungs-Audit vom 2026-09-13 |
|
||
| 11 | Wann zählt `review`? | Wenn die Notiz `review` trägt (Frontmatter oder Inline) und nirgends `review-done`. Gleiche Ausschlüsse wie Frage 10 | **Projektvorgabe** — `CLAUDE.md` Sitzungsprotokoll Punkt 3; die gleichen Ausschlüsse sind als Annahme im Plan benannt |
|
||
| 12 | Wie viele Namen? | Höchstens fünf je Punkt, alphabetisch, danach `+N weitere` | **Projektvorgabe** — Planauftrag |
|
||
| 13 | Was passiert bei einem Fehler? | Jeder Punkt läuft für sich in `try`. Ein Fehler wird als Zeile `Sitzungsstart-Hook: …` gemeldet, die übrigen Punkte laufen weiter. Ein Fehler außerhalb der Punkte behält die gesammelten Zeilen | **Projektvorgabe** — pruefwerkzeuge.md Abschnitt 4 |
|
||
| 14 | Fehlt `linkcheck.py`? | Der Tag-Punkt meldet sich als Fehler und zählt nicht | **Projektvorgabe** — pruefwerkzeuge.md Abschnitt 6: eine fehlende Voraussetzung ist ein Befund |
|
||
| 15 | Laufzeit? | Unter 5 Sekunden über den ganzen Vault angepeilt, gemessen mit `faelle_sitzungsstart.py --echt` | **Projektvorgabe** — Planauftrag |
|
||
|
||
## Entscheidungsliste `regelwerk_nach_edit.py`
|
||
|
||
| # | Frage | Antwort | Herkunft |
|
||
|---|---|---|---|
|
||
| 1 | Wann läuft der Check? | Wenn `tool_input.file_path` innerhalb der Wurzel auf `CLAUDE.md`, `AI/Regelwerk/**`, `.claude/agents/**`, `.claude/skills/**` oder, seit 2026-09-26, `.claude/workflows/**` zeigt | **Projektvorgabe** — `CLAUDE.md`, Abschnitt Selbstpflege: nach jeder Änderung an Stamm, Regelwerk, Skills, Agenten oder gespeicherten Workflows |
|
||
| 2 | Wie wird der Pfad verglichen? | Backslash zu Schrägstrich, relativ zur Wurzel aufgelöst, `.` und `..` aufgelöst, alles klein geschrieben, Vergleich mit Trenner hinter der Wurzel | **Projektvorgabe** — Windows-Dateisystem ohne Rücksicht auf Groß- und Kleinschreibung; Symlinks bleiben unaufgelöst (Lücke) |
|
||
| 3 | Woher die Wurzel? | Wie bei `sitzungsstart.py`, Frage 4: ausschließlich der Ordner zwei Ebenen über dem Skript, `cwd` zählt nicht | **Projektvorgabe (Koordinator, Nachbesserung Runde 2)** — auf diesen Hook übertragen |
|
||
| 4 | Welche Eingaben bleiben still? | Leeres stdin, kein JSON-Objekt, `tool_input` oder `file_path` fehlt oder hat den falschen Typ, Pfad außerhalb des Geltungsbereichs | **Projektvorgabe** — Planauftrag |
|
||
| 5 | Wann gilt der Check als ohne Befund? | Exit 0 **und** die Schlusszeile `Keine der N gelaufenen Prüfungen hat angeschlagen.` **und** keine Zeile `ACHTUNG` | **Quelle** — `regelwerk_check.py` Zeilen 366–376 |
|
||
| 6 | Was ist ein Befund? | Alles andere: Exit ungleich 0, fehlende Schlusszeile, `ACHTUNG`, fehlendes Skript, Timeout, Startfehler, eigener Absturz | **Projektvorgabe** — pruefwerkzeuge.md Abschnitt 6 |
|
||
| 7 | Welche Ausgabe? | Exit 0, ein JSON-Objekt mit `hookSpecificOutput` (PostToolUse, `additionalContext`) und einzeiliger `systemMessage`; ohne Befund keine Ausgabe | **Quelle (Koordinator, korrigiert nach Prüfrunde 2)** — Hilfetext von `claude.exe 2.1.263`: bei PostToolUse zeigt Exit 2 stderr sofort dem Modell. Ob JSON bei einem Exit ungleich 0 gelesen wird, sagt der Hilfetext nicht; der Prüfer fand im Binary Hinweise, dass es auch dann übernommen wird. Exit 0 plus JSON ist die in Doku und laufenden Plugins belegte Form, deshalb bleibt es dabei |
|
||
| 8 | Wie lange darf der Check laufen? | 20 Sekunden, dann Abbruch und Meldung | **Projektvorgabe** — unter dem `timeout` von 30 Sekunden, den der settings.json-Eintrag setzen soll |
|
||
| 9 | Wie viel Text geht in den Kontext? | Höchstens 40 Zeilen und 4000 Zeichen, danach ein Kürzungshinweis | **Projektvorgabe** — eine Längengrenze der Doku ist nicht belegt (Lücke) |
|
||
| 10 | Prüft der Hook `tool_name`? | Nein, das macht der Matcher `Edit\|Write\|MultiEdit` | **Projektvorgabe** |
|
||
| 11 | Mit welchem Interpreter? | `sys.executable`, stdin geschlossen, `PYTHONIOENCODING=utf-8` | **Projektvorgabe** — [[AI/Regelwerk/windows-shell\|windows-shell.md]] |
|
||
|
||
Keine Zeile steht auf „Annahme". Wo eine Antwort auf einer Annahme beruhte, ist sie zur Projektvorgabe erhoben und im Plan unter „Getroffene Annahmen" benannt.
|
||
|
||
## Angriffslisten
|
||
|
||
Die einzelnen Fälle stehen an genau einer Stelle, in den Treibern `faelle_sitzungsstart.py` und `faelle_nach_edit.py`. Hier steht nur, welche Klassen aus pruefwerkzeuge.md Abschnitt 3 abgedeckt sind, damit eine Lücke auffällt.
|
||
|
||
| Klasse | `sitzungsstart.py` | `regelwerk_nach_edit.py` |
|
||
|---|---|---|
|
||
| fehlt | Tagesnotiz, `cwd` | `tool_input`, `file_path`, `cwd`, `regelwerk_check.py` |
|
||
| null | `cwd` | `tool_input`, `file_path`, `cwd` |
|
||
| falscher Typ | stdin als Liste, `cwd` als Zahl oder Datei, `tags` als Skalar | stdin als Liste oder Zahl, `tool_input` als Liste, `file_path` als Zahl oder Liste |
|
||
| leer | leere Datei, leere Notiz, leeres stdin, leeres `cwd`, Vault ohne Ordner | leeres stdin, leerer `file_path` |
|
||
| doppelt | zwei Sitzungsdateien von heute | — |
|
||
| zusätzlich | sieben Inbox-Notizen (Namensgrenze), Inbox-Unterordner, seit Runde 2 auch ein Punktordner innerhalb der Inbox (eigener Fall neben dem Punktordner-Ausschluss unter der Vault-Wurzel) | Pfad außerhalb, Unterordner-`CLAUDE.md`, `settings.json`, Write und MultiEdit |
|
||
| Wertebereich | Tagesnotiz von gestern | Nullzeichen, Regelwerk-Ordner selbst, sehr lange Ausgabe |
|
||
| Verweis ins Leere | `cwd` existiert nicht, `cwd` zeigt auf einen anderen Ordner (Ergebnis bleibt der Skript-Vault, Runde 2) | `cwd` existiert nicht, `cwd` zeigt auf einen anderen Ordner (Runde 2), neue Datei, `..` hinaus und hinein, Präfix-Fallen |
|
||
| Reihenfolge | Tag an erster und letzter Stelle, `review` an erster und letzter Stelle | — (eine Eingabe hat keine Listenposition) |
|
||
| Grenze | `#needs-review` gegen `#needs-reviewer`/`#needs-review/x`/`abc#needs-review`, Inline-Großschreibung zählt; seit Runde 2 auch `#review` gegen `#reviewer`/`#review/x`/`abc#review`, `#review-done` gegen `#review-done-x`, und ein großgeschriebener Tag im Frontmatter (`tags: [Needs-Review]`) | — |
|
||
| Umgebung | Inbox, Regelwerk, sessions, `linkcheck.py` fehlen; CRLF, BOM, ungültiges UTF-8, kaputtes YAML; seit Runde 2 auch ohne `PYTHONIOENCODING` mit Nicht-ASCII-Inhalt (Notizinhalt mit Umlauten und Emoji) | Check fehlt, hängt, stürzt ab, Exit 2, Exit 0 mit ACHTUNG, Exit 0 ohne Schlusszeile; Backslash, Laufwerksschreibweise, ungültiges UTF-8; seit Runde 2 auch ohne `PYTHONIOENCODING` mit ungültigem UTF-8 und mit echtem Nicht-ASCII-Pfad |
|
||
| Code | Tag im Fenced-, Tilde- und Inline-Code | — |
|
||
| Absturz im Skript | ein Punkt wirft, Hauptablauf wirft, Datei nicht lesbar, seit Runde 2 auch ein Ordner nicht lesbar (`onerror` von `os.walk`) | Hook selbst wirft |
|
||
|
||
Beide Treiber prüfen zuerst eine Grundlinie. Beim Regelwerk-Hook besteht sie aus zwei Läufen: Ein grüner Check bleibt still und ist nachweislich gelaufen, ein Befund wird gemeldet. Ohne den zweiten Lauf bestünde ein Hook, der immer schweigt, alle stillen Fälle.
|
||
|
||
## Nachbesserung Runde 2
|
||
|
||
**Anlass.** Die erste Prüfrunde gegen beide Hooks fand zwei wichtige und sechs kleine Befunde; der CRLF-Befund zu `ENTWURF-codeblock-erkennung.md` wurde bewusst nicht behoben. „Runde 2" meint in Kommentaren und Nachträgen die Nachbesserung nach dieser ersten Prüfrunde. Die Fixe stehen hier zusammengefasst, Details in den jeweiligen Kopfkommentaren und in den Angriffslisten oben.
|
||
|
||
**Befund 1 (wichtig): Vault-Wurzel aus `cwd`.** Beide Hooks lasen die Vault-Wurzel zuerst aus `cwd` der Hook-Eingabe. `cwd` folgt aber einem `cd` im Bash-Werkzeug (z. B. in einen Projektordner mit eigenem Git-Repo) und zeigt in einer laufenden Sitzung nicht mehr auf den Vault — `sitzungsstart.py` hätte dann bei `resume`/`clear`/`compact` einen falschen Stand gemeldet, `regelwerk_nach_edit.py` wäre bei Änderungen am Regelwerk still geblieben, ohne dass das sichtbar gewesen wäre. **Korrektur (Koordinatorentscheidung):** Beide Hooks bestimmen ihre Wurzel jetzt ausschließlich aus dem eigenen Skriptpfad (`Path(__file__).resolve().parents[2]`), das Feld `cwd` wird nicht mehr gelesen — auch nicht über die zwischenzeitlich erwogene Umgebungsvariable `CLAUDE_PROJECT_DIR`. Die Hooks sind ohnehin nur für den einen Vault eingetragen, in dem sie liegen. Neuer Fall in beiden Treibern: `cwd` zeigt auf einen anderen, echten Ordner — das Ergebnis muss trotzdem der Vault sein, in dem der (kopierte) Hook selbst liegt. Weil die Wurzel jetzt am Ort des Skripts hängt, kopieren beide Regressionstreiber den Hook seither für **jeden** Fall zuerst in den Wegwerf-Vault (`kopiere_hook()` bzw. der entsprechende Block in `lauf_fall()`), statt das Original mit einem `cwd`-Feld auf den Wegwerf-Vault zu verweisen. Dieselbe Kopie war auch für `ip_hook_wirft()` in `faelle_nach_edit.py` nötig: ohne sie fand `main()` keinen Geltungsbereich für den Testpfad, der monkeygepatchte Absturz wurde nie ausgelöst, und der Fall wäre unbemerkt grün geblieben, weil `stdout` leer war statt eines JSON-Objekts.
|
||
|
||
**Befund 2 (wichtig): Umgebung/Ausgabekodierung fehlte in der Angriffsliste.** Claude Code setzt `PYTHONIOENCODING` beim Hook-Aufruf nicht; nur die globale Planvorgabe setzte sie bisher vor jedem Treiberlauf selbst. Ohne die Variable ist `sys.stdout.encoding` auf dieser Maschine `cp1252`. Solange `ensure_ascii=True` gilt (beide Hooks), bleibt die Ausgabe reines ASCII und das Problem ist unsichtbar — genau deshalb bekam die Klasse eigene Fälle: **beide** Treiber fahren jetzt mindestens einen Fall mit einer Umgebung ohne `PYTHONIOENCODING` und mit echtem Nicht-ASCII-Inhalt (Notiztext mit Umlauten und Emoji bzw. ein Dateipfad mit Umlauten und Emoji) und prüfen, dass `stdout` als UTF-8 dekodierbares, gültiges JSON bleibt. Mutationsbeleg (auf Scratchpad-Kopien, nicht im Vault): `ensure_ascii=False` gesetzt, derselbe Fall wird in beiden Treibern rot, aber aus unterschiedlichen Gründen (so von der zweiten Prüfrunde nachgemessen): `sitzungsstart.py` gibt nur Dateinamen aus, das Ü im Namen landet als cp1252-Byte in stdout; `regelwerk_nach_edit.py` wirft beim Emoji im Pfad einen `UnicodeEncodeError`, den `main()` fängt und durch eine vollständige Ersatzmeldung ersetzt. Dass der Umlaut-Fall auch ohne Emoji anschlägt, sichert erst die strenge Dekodierung aus der Nachbesserung nach Prüfrunde 2.
|
||
|
||
**Befunde 3 und 4 (klein): fehlende Grenzfälle.** Ergänzt: die Grenzen von `#review`/`#review-done` (`#reviewer`, `#review/x`, `abc#review`, `#review-done-x`), ein großgeschriebener Tag im Frontmatter (`tags: [Needs-Review]`, nicht nur inline), ein Punktordner innerhalb der Inbox (getrennt vom Punktordner-Ausschluss unter der Vault-Wurzel), und der `onerror`-Pfad von `os.walk` in `durchlaufe()` (im Prozess, weil von außen nicht ohne echte Rechteänderung auslösbar).
|
||
|
||
**Befund 5 und 6 (klein, Linkcheck): „gleiches Zaunzeichen" ohne Fall, Tab vor dem Schließzaun.** In `linkcheck.py`/`faelle.py` behoben, siehe `.vault-tools/linkcheck/ENTWURF-codeblock-erkennung.md`, Abschnitt „Nachbesserung Runde 2".
|
||
|
||
**Befund 7 (klein): Exit-2-Aussage korrigiert.** Frage 7 der Entscheidungsliste zu `regelwerk_nach_edit.py` behauptete, PostToolUse ignoriere Exit 2. Nach dem Hilfetext von `claude.exe 2.1.263` zeigt Exit 2 bei PostToolUse stattdessen stderr sofort dem Modell. Die Umsetzung war davon nicht betroffen (Exit ist immer 0), nur die Begründung in Frage 7 und im Kopfkommentar von `regelwerk_nach_edit.py` war falsch und ist jetzt korrigiert.
|
||
|
||
## Nachbesserung nach Prüfrunde 2
|
||
|
||
Die zweite Prüfrunde fand keinen wichtigen Befund an den Hooks mehr, nur eine Aufräumliste. Der Koordinator hat sie ohne weitere Executer-Runde selbst abgearbeitet:
|
||
|
||
- **Strenge Dekodierung in beiden Treibern.** `fahre()` dekodiert stdout jetzt ohne `errors="replace"`. Ungültiges UTF-8 ergibt eine Meldung, die kein JSON ist, und der Fall wird rot.
|
||
- **Zwei Fälle ergänzt** in `faelle_sitzungsstart.py`: `abc#review-done` zählt nicht als erledigt, eine Punktdatei außerhalb von Punktordnern (`Notizen/.versteckt.md`) zählt nicht.
|
||
- **Linkcheck:** Die Schließregel ist wieder symmetrisch zur Öffnerregel (`\s{0,3}`). Die strengere Fassung ließ einen in einer Liste mit Tab eingerückten Block bis zum Dateiende offen. Details: `.vault-tools/linkcheck/ENTWURF-codeblock-erkennung.md`.
|
||
- **Begründungen korrigiert:** Mutationsbeleg zu Befund 2 und Frage 7 (Exit-Codes), siehe oben.
|
||
|
||
## Absturzfestigkeit
|
||
|
||
Beide Skripte enden in jedem Fall mit Exit 0, weil Claude Code stdout sonst nicht als JSON liest. Jede Ebene der Eingabe wird auf ihren Typ geprüft, bevor darauf zugegriffen wird. Ein unerwarteter Fehler in der Arbeit selbst wird gemeldet. Die letzte Schranke im `__main__`-Block greift nur noch, wenn das Schreiben der Ausgabe selbst scheitert, etwa an einem geschlossenen stdout. Dann gibt es nichts mehr zu melden.
|
||
|
||
## Abbruchbedingung
|
||
|
||
Schluss ist, wenn beide Treiber mit grüner Grundlinie und ohne Fehlschlag durchlaufen, der Lauf mit `--echt` über den echten Vault Ergebnisse liefert, die der Koordinator mit eigener Suche bestätigt hat, und eine Prüfrunde nur noch Umformulierungen findet.
|
||
|
||
## 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.
|