Automatischer Push der Anweisungsschicht
Der Vault-Root pusht ab jetzt nach jeder Änderung an CLAUDE.md, AI/Regelwerk/, .claude/agents/, .claude/skills/, .claude/settings.json oder .vault-tools/ selbsttätig nach Gitea. - .vault-tools/auto-push/auto_push.py: PostToolUse-Hook, committet und pusht. Fehlendes Token, fehlendes Remote und gescheiterter Push sind Befunde und gehen in den Kontext, nicht ins Schweigen. - .vault-tools/auto-push/askpass.py: reicht GITEA_TOKEN an git weiter, damit es weder in .git/config noch in einer Kommandozeile steht. - .vault-tools/auto-push/faelle_auto_push.py: 28 Fälle gegen ein echtes Wegwerf-Repo mit echtem Remote, alle bestanden. Der Lauf hat zwei Fehler gefunden, die jetzt behoben sind: die Pfadauflösung scheiterte an Windows-Kurznamen, und die Laufzeitsperre landete im Repo. - AI/Regelwerk/veroeffentlichung.md und ein Abschnitt im Stamm halten die Regel fest. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
2c9a4475fc
commit
a81299e673
8 files changed
+812
-2
No files matched your search
+14
-2
@@ -12,7 +12,9 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python",
|
||||
"args": ["D:/Claude/.vault-tools/session-hooks/sitzungsstart.py"],
|
||||
"args": [
|
||||
"D:/Claude/.vault-tools/session-hooks/sitzungsstart.py"
|
||||
],
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
@@ -25,8 +27,18 @@
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python",
|
||||
"args": ["D:/Claude/.vault-tools/session-hooks/regelwerk_nach_edit.py"],
|
||||
"args": [
|
||||
"D:/Claude/.vault-tools/session-hooks/regelwerk_nach_edit.py"
|
||||
],
|
||||
"timeout": 30
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python",
|
||||
"args": [
|
||||
"D:/Claude/.vault-tools/auto-push/auto_push.py"
|
||||
],
|
||||
"timeout": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
!/.vault-tools/
|
||||
|
||||
# Niemals, egal wo
|
||||
# Laufzeitsperre des Auto-Push-Hooks. Sie entsteht bei jedem Lauf und
|
||||
# hat im Repo nichts zu suchen (Fallkatalog vom 2026-09-17).
|
||||
.vault-tools/auto-push/.sperre
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""GIT_ASKPASS-Helfer: gibt das Gitea-Token aus der Umgebung an git weiter.
|
||||
|
||||
Ohne diesen Helfer landet das Token entweder in .git/config (Remote-URL mit
|
||||
Zugangsdaten) oder in einer Kommandozeile (http.extraheader) und ist dort fuer
|
||||
jeden Prozess der Maschine lesbar. git ruft dieses Skript mit der Eingabe-
|
||||
aufforderung als Argument auf und liest die Antwort von stdout.
|
||||
|
||||
Eine fehlende Variable ist ein Befund: das Skript endet mit Exit 1 und einer
|
||||
Meldung auf stderr, statt eine leere Zeile auszugeben. Sonst haengt git an
|
||||
einer stillen Fehlanmeldung.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
BENUTZER_VARIABLE = "GITEA_BENUTZER"
|
||||
TOKEN_VARIABLE = "GITEA_TOKEN"
|
||||
|
||||
|
||||
def main(argumente: list[str]) -> int:
|
||||
frage = " ".join(argumente).lower()
|
||||
if "username" in frage or "benutzer" in frage:
|
||||
name = "GITEA_BENUTZER"
|
||||
wert = os.environ.get(BENUTZER_VARIABLE, "").strip()
|
||||
else:
|
||||
name = "GITEA_TOKEN"
|
||||
wert = os.environ.get(TOKEN_VARIABLE, "").strip()
|
||||
if not wert:
|
||||
sys.stderr.write(f"{name} ist nicht gesetzt oder leer.\n")
|
||||
return 1
|
||||
sys.stdout.write(wert + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# Von git als GIT_ASKPASS aufgerufen. Reicht die Frage an askpass.py weiter.
|
||||
exec python "D:/Claude/.vault-tools/auto-push/askpass.py" "$@"
|
||||
@@ -0,0 +1,349 @@
|
||||
"""PostToolUse-Hook: committet und pusht jede Änderung an der veröffentlichten Anweisungsschicht.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-17
|
||||
|
||||
GELTUNGSBEREICH
|
||||
Ausgelöst wird nur, wenn tool_input.file_path innerhalb der Vault-Wurzel auf
|
||||
CLAUDE.md, README.md, AI/Regelwerk/**, .claude/agents/**, .claude/skills/**,
|
||||
.claude/settings.json oder .vault-tools/** zeigt. Die Wurzel ist
|
||||
ausschließlich der Ordner zwei Ebenen über dieser Datei; weder
|
||||
CLAUDE_PROJECT_DIR noch das Feld cwd der Hook-Eingabe werden gelesen, weil
|
||||
cwd einem cd im Bash-Werkzeug folgt (dieselbe Begründung wie in
|
||||
regelwerk_nach_edit.py).
|
||||
|
||||
Was tatsächlich gepusht wird, entscheidet aber nicht diese Liste, sondern die
|
||||
.gitignore der Vault-Wurzel. Sie arbeitet als Positivliste: ausgeschlossen
|
||||
ist alles, eingeschlossen nur, was dort ausdrücklich wieder aufgenommen ist.
|
||||
Die Liste hier bestimmt nur, ob der Hook überhaupt anläuft — sie ist eine
|
||||
Abkürzung, kein zweiter Filter. Wer den Umfang ändern will, ändert die
|
||||
.gitignore.
|
||||
|
||||
Still bleibt der Hook in drei Lagen: die Eingabe taugt nicht (leer, kein
|
||||
JSON-Objekt, kein file_path als Zeichenkette); der Pfad liegt außerhalb des
|
||||
Geltungsbereichs; oder es gibt nichts zu committen. Jede andere Lage —
|
||||
fehlendes Token, fehlendes Remote, gescheiterter Commit, gescheiterter Push,
|
||||
Zeitüberschreitung — ist ein Befund und wird gemeldet.
|
||||
|
||||
VERHALTEN BEI GESCHEITERTEM PUSH
|
||||
Der Commit bleibt lokal stehen und der Befund geht in den Kontext. Der
|
||||
nächste Lauf schiebt ihn mit, weil `git push` alles bis HEAD überträgt. Ein
|
||||
gescheiterter Push verliert also keine Arbeit, er verzögert sie.
|
||||
|
||||
NEBENLÄUFIGKEIT
|
||||
Zwei schnell aufeinanderfolgende Edits starten zwei Hook-Läufe. Ohne Sperre
|
||||
kollidieren sie an der index.lock von git. Deshalb eine eigene Sperrdatei mit
|
||||
O_EXCL; wer sie nicht bekommt, wartet bis SPERRE_WARTE_S und läuft danach
|
||||
selbst. Findet er nichts mehr zu committen, bleibt er still — der andere Lauf
|
||||
hat seine Änderung dann bereits mitgenommen.
|
||||
|
||||
BENANNTE LÜCKEN
|
||||
* Ob tool_name Edit, Write oder MultiEdit ist, prüft der Matcher in
|
||||
settings.json, nicht dieses Skript.
|
||||
* Der Hook prüft den Inhalt nicht. Er hat keinen Geheimnis-Scan und keine
|
||||
Freigabe. Was in einer eingeschlossenen Datei steht, geht heraus, sobald
|
||||
sie geschrieben wird — auch halbfertig. Das ist die ausdrückliche Vorgabe
|
||||
(Pascal, 2026-09-17), und es ist der Grund, warum die .gitignore eine
|
||||
Positivliste ist und keine Ausschlussliste.
|
||||
* Pfade werden ueber realpath aufgeloest. Ein Symlink von ausserhalb auf
|
||||
eine eingeschlossene Datei loest den Hook deshalb aus, ein Symlink von
|
||||
innen nach aussen nicht.
|
||||
* Ein Push wird nicht wiederholt. Nachgeholt wird er beim nächsten Lauf.
|
||||
|
||||
Exit ist immer 0. Gemeldet wird über additionalContext plus systemMessage, wie
|
||||
beim Nachbar-Hook regelwerk_nach_edit.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SKRIPT = Path(__file__).resolve()
|
||||
|
||||
# Unter dem timeout von 60 s, den der settings.json-Eintrag setzen soll.
|
||||
TIMEOUT_GIT_S = 15
|
||||
TIMEOUT_PUSH_S = 40
|
||||
SPERRE_WARTE_S = 25
|
||||
SPERRE_TAKT_S = 0.4
|
||||
MAX_ZEILEN = 30
|
||||
MAX_ZEICHEN = 3000
|
||||
MAX_DATEIEN_IN_MELDUNG = 12
|
||||
|
||||
# Kleinbuchstaben und Schrägstrich vorwärts, verglichen gegen normiere().
|
||||
GELTUNG_DATEIEN = ("claude.md", "readme.md", ".gitignore", ".gitattributes")
|
||||
GELTUNG_ORDNER = (
|
||||
"ai/regelwerk/",
|
||||
".claude/agents/",
|
||||
".claude/skills/",
|
||||
".vault-tools/",
|
||||
)
|
||||
GELTUNG_EINZELN = (".claude/settings.json",)
|
||||
|
||||
ASKPASS_REL = Path(".vault-tools") / "auto-push" / "askpass.sh"
|
||||
SPERRE_REL = Path(".vault-tools") / "auto-push" / ".sperre"
|
||||
|
||||
|
||||
def lies_stdin() -> bytes:
|
||||
if sys.stdin is None or sys.stdin.isatty():
|
||||
return b""
|
||||
return sys.stdin.buffer.read()
|
||||
|
||||
|
||||
def lies_eingabe(roh: bytes) -> object:
|
||||
# errors=replace: ein kaputtes Byte im Pfad soll laut enden, nicht still.
|
||||
text = roh.decode("utf-8-sig", errors="replace")
|
||||
if not text.strip():
|
||||
return None
|
||||
try:
|
||||
return json.loads(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def bestimme_wurzel() -> Path:
|
||||
return SKRIPT.parents[2]
|
||||
|
||||
|
||||
def normiere(pfad: str) -> str:
|
||||
# realpath, nicht abspath: Windows liefert denselben Ort mal als
|
||||
# "C:/Users/Pascal Michel/..." und mal als 8.3-Kurznamen
|
||||
# "C:/Users/PASCAL~1/...". Ein Vergleich mit abspath haelt die beiden fuer
|
||||
# verschiedene Pfade, und der Hook bleibt dann still statt zu pushen --
|
||||
# im Fallkatalog vom 2026-09-17 sind daran sieben Faelle gescheitert.
|
||||
return os.path.normpath(os.path.realpath(pfad)).replace("\\", "/").lower().rstrip("/")
|
||||
|
||||
|
||||
def im_geltungsbereich(wurzel: Path, datei: str) -> bool:
|
||||
kandidat = datei.replace("\\", "/")
|
||||
try:
|
||||
if not os.path.isabs(kandidat):
|
||||
kandidat = os.path.join(str(wurzel), kandidat)
|
||||
pfad = normiere(kandidat)
|
||||
basis = normiere(str(wurzel))
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
# Mit Trenner vergleichen, sonst träfe "<wurzel>-CLAUDE.md" neben dem Vault.
|
||||
if not pfad.startswith(basis + "/"):
|
||||
return False
|
||||
rel = pfad[len(basis) + 1:]
|
||||
return (
|
||||
rel in GELTUNG_DATEIEN
|
||||
or rel in GELTUNG_EINZELN
|
||||
or rel.startswith(GELTUNG_ORDNER)
|
||||
)
|
||||
|
||||
|
||||
def dekodiere(roh: bytes | str | None) -> str:
|
||||
if roh is None:
|
||||
return ""
|
||||
if isinstance(roh, str):
|
||||
return roh
|
||||
return roh.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
class GitFehler(Exception):
|
||||
"""Ein git-Aufruf ist gescheitert; die Meldung ist der Befund."""
|
||||
|
||||
def __init__(self, kopf: str, rumpf: str) -> None:
|
||||
super().__init__(kopf)
|
||||
self.kopf = kopf
|
||||
self.rumpf = rumpf
|
||||
|
||||
|
||||
def git(wurzel: Path, *argumente: str, timeout: int = TIMEOUT_GIT_S) -> str:
|
||||
umgebung = dict(
|
||||
os.environ,
|
||||
PYTHONIOENCODING="utf-8",
|
||||
GIT_TERMINAL_PROMPT="0",
|
||||
GIT_ASKPASS=str(wurzel / ASKPASS_REL),
|
||||
GIT_CONFIG_NOSYSTEM="0",
|
||||
)
|
||||
befehl = ["git", "-C", str(wurzel), *argumente]
|
||||
try:
|
||||
lauf = subprocess.run(
|
||||
befehl,
|
||||
env=umgebung,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except subprocess.TimeoutExpired as abgelaufen:
|
||||
teil = (dekodiere(abgelaufen.stdout) + dekodiere(abgelaufen.stderr)).strip()
|
||||
raise GitFehler(
|
||||
f"git {argumente[0]} nach {timeout} s abgebrochen",
|
||||
f"`git {' '.join(argumente)}` lief länger als {timeout} s.\n"
|
||||
f"Bis dahin ausgegeben:\n{teil or '(nichts)'}",
|
||||
) from abgelaufen
|
||||
except OSError as fehler:
|
||||
raise GitFehler(
|
||||
f"git nicht startbar: {type(fehler).__name__}",
|
||||
f"`git {' '.join(argumente)}` ließ sich nicht starten: "
|
||||
f"{type(fehler).__name__}: {fehler}",
|
||||
) from fehler
|
||||
|
||||
if lauf.returncode != 0:
|
||||
ausgabe = (dekodiere(lauf.stdout) + "\n" + dekodiere(lauf.stderr)).strip()
|
||||
raise GitFehler(
|
||||
f"git {argumente[0]}: Exit {lauf.returncode}",
|
||||
f"`git {' '.join(argumente)}` endete mit Exit {lauf.returncode}.\n"
|
||||
f"{ausgabe or '(keine Ausgabe)'}",
|
||||
)
|
||||
return dekodiere(lauf.stdout)
|
||||
|
||||
|
||||
def hole_sperre(sperre: Path) -> int | None:
|
||||
"""Gibt den Dateideskriptor zurück, oder None, wenn die Sperre belegt bleibt."""
|
||||
frist = time.monotonic() + SPERRE_WARTE_S
|
||||
while True:
|
||||
try:
|
||||
return os.open(str(sperre), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
except OSError as fehler:
|
||||
if fehler.errno not in (errno.EEXIST, errno.EACCES):
|
||||
raise
|
||||
# Eine Sperre, die älter ist als die Wartezeit, stammt aus einem
|
||||
# abgestürzten Lauf. Sie wird entfernt, nicht ewig respektiert.
|
||||
try:
|
||||
if time.time() - sperre.stat().st_mtime > SPERRE_WARTE_S * 2:
|
||||
sperre.unlink(missing_ok=True)
|
||||
continue
|
||||
except OSError:
|
||||
pass
|
||||
if time.monotonic() >= frist:
|
||||
return None
|
||||
time.sleep(SPERRE_TAKT_S)
|
||||
|
||||
|
||||
def gib_sperre_frei(deskriptor: int, sperre: Path) -> None:
|
||||
try:
|
||||
os.close(deskriptor)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
sperre.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def baue_meldung(dateien: list[str]) -> str:
|
||||
rest = len(dateien) - MAX_DATEIEN_IN_MELDUNG
|
||||
sichtbar = dateien[:MAX_DATEIEN_IN_MELDUNG]
|
||||
kopf = (
|
||||
f"Automatischer Push: {len(dateien)} Datei(en) geändert"
|
||||
if len(dateien) != 1
|
||||
else f"Automatischer Push: {dateien[0]}"
|
||||
)
|
||||
rumpf = "\n".join(f"- {d}" for d in sichtbar)
|
||||
if rest > 0:
|
||||
rumpf += f"\n- … und {rest} weitere"
|
||||
return (
|
||||
f"{kopf}\n\n{rumpf}\n\n"
|
||||
"Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.\n"
|
||||
"\n"
|
||||
"Co-Authored-By: Claude Opus 5 <[email protected]>\n"
|
||||
)
|
||||
|
||||
|
||||
def arbeite(wurzel: Path) -> tuple[str, str] | None:
|
||||
"""None, wenn nichts zu tun war; sonst (systemMessage, Rumpf)."""
|
||||
if not (wurzel / ".git").exists():
|
||||
raise GitFehler(
|
||||
"Auto-Push: kein Git-Repo",
|
||||
f"{wurzel} ist kein Git-Repository. Der Hook ist eingetragen, kann "
|
||||
"aber nichts pushen. Das ist ein Befund, keine Entwarnung.",
|
||||
)
|
||||
if not (wurzel / ASKPASS_REL).is_file():
|
||||
raise GitFehler(
|
||||
"Auto-Push: askpass-Helfer fehlt",
|
||||
f"{wurzel / ASKPASS_REL} fehlt. Ohne ihn fragt git nach einem "
|
||||
"Passwort und der Push hängt oder scheitert.",
|
||||
)
|
||||
if not os.environ.get("GITEA_TOKEN", "").strip():
|
||||
raise GitFehler(
|
||||
"Auto-Push: GITEA_TOKEN nicht gesetzt",
|
||||
"Die Umgebungsvariable GITEA_TOKEN ist leer oder fehlt. Der Commit "
|
||||
"wird trotzdem gemacht, der Push scheitert. Variable setzen und "
|
||||
"eine beliebige Regelwerk-Datei erneut speichern.",
|
||||
)
|
||||
|
||||
git(wurzel, "add", "-A")
|
||||
gestaged = [z for z in git(wurzel, "diff", "--cached", "--name-only").splitlines() if z.strip()]
|
||||
if not gestaged:
|
||||
return None
|
||||
|
||||
git(wurzel, "commit", "-m", baue_meldung(gestaged))
|
||||
git(wurzel, "push", "origin", "HEAD", timeout=TIMEOUT_PUSH_S)
|
||||
return None
|
||||
|
||||
|
||||
def kuerze(text: str) -> str:
|
||||
zeilen = text.splitlines()
|
||||
rest = len(zeilen) - MAX_ZEILEN
|
||||
if rest > 0:
|
||||
zeilen = zeilen[:MAX_ZEILEN] + [f"[... {rest} weitere Zeilen gekürzt]"]
|
||||
ergebnis = "\n".join(zeilen)
|
||||
if len(ergebnis) > MAX_ZEICHEN:
|
||||
ergebnis = ergebnis[:MAX_ZEICHEN] + f"\n[... gekürzt auf {MAX_ZEICHEN} Zeichen]"
|
||||
return ergebnis
|
||||
|
||||
|
||||
def melde(system: str, kontext: str) -> None:
|
||||
ausgabe = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": kuerze(kontext),
|
||||
},
|
||||
"systemMessage": " ".join(system.split()),
|
||||
}
|
||||
sys.stdout.write(json.dumps(ausgabe, ensure_ascii=True))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main(roh: bytes | None = None) -> int:
|
||||
try:
|
||||
daten = lies_eingabe(lies_stdin() if roh is None else roh)
|
||||
if not isinstance(daten, dict):
|
||||
return 0
|
||||
eingabe = daten.get("tool_input")
|
||||
if not isinstance(eingabe, dict):
|
||||
return 0
|
||||
datei = eingabe.get("file_path")
|
||||
if not isinstance(datei, str) or not datei.strip():
|
||||
return 0
|
||||
|
||||
wurzel = bestimme_wurzel()
|
||||
if not im_geltungsbereich(wurzel, datei):
|
||||
return 0
|
||||
|
||||
sperre = wurzel / SPERRE_REL
|
||||
deskriptor = hole_sperre(sperre)
|
||||
if deskriptor is None:
|
||||
# Ein anderer Lauf arbeitet noch. Er nimmt diese Änderung mit,
|
||||
# weil er `git add -A` über den ganzen Baum fährt.
|
||||
return 0
|
||||
try:
|
||||
ergebnis = arbeite(wurzel)
|
||||
finally:
|
||||
gib_sperre_frei(deskriptor, sperre)
|
||||
if ergebnis is not None:
|
||||
melde(*ergebnis)
|
||||
return 0
|
||||
except GitFehler as fehler:
|
||||
melde(fehler.kopf, fehler.rumpf)
|
||||
return 0
|
||||
except Exception as fehler: # noqa: BLE001 - ein Absturz ist selbst ein Befund
|
||||
melde(
|
||||
f"Auto-Push abgebrochen: {type(fehler).__name__}",
|
||||
f"Der Auto-Push-Hook ist unerwartet abgebrochen: "
|
||||
f"{type(fehler).__name__}: {fehler}",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,338 @@
|
||||
"""Fährt die Angriffsliste gegen auto_push.py, je Fall auf einem Wegwerf-Vault.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-17
|
||||
|
||||
Jeder Fall bekommt einen eigenen Wegwerf-Vault: ein echtes Git-Repo mit einem
|
||||
zweiten Repo als bare-Remote daneben. Damit ist der Push echt — es wird
|
||||
wirklich übertragen, nur eben in ein Verzeichnis statt über das Netz. Ein
|
||||
Auto-Push, der nur gegen eine Attrappe geprüft wurde, belegt nichts über den
|
||||
Fall, der zählt: dass die Änderung nachher drüben liegt.
|
||||
|
||||
Der Hook wird für jeden Fall in den Wegwerf-Vault kopiert, weil er seine
|
||||
Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad bestimmt.
|
||||
|
||||
Grundlinie zuerst: eine geänderte Datei im Geltungsbereich muss nach dem Lauf
|
||||
im Remote liegen. Läuft die Grundlinie nicht, misst der Rest nichts.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/auto-push/faelle_auto_push.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
HOOK = HIER / "auto_push.py"
|
||||
ASKPASS_PY = HIER / "askpass.py"
|
||||
|
||||
# Die .gitignore des echten Vaults, gekürzt auf das, was die Fälle brauchen.
|
||||
GITIGNORE = """/*
|
||||
!/.gitignore
|
||||
!/CLAUDE.md
|
||||
!/AI/
|
||||
/AI/*
|
||||
!/AI/Regelwerk/
|
||||
!/.vault-tools/
|
||||
.vault-tools/auto-push/.sperre
|
||||
__pycache__/
|
||||
*.pyc
|
||||
"""
|
||||
|
||||
|
||||
def raeume(pfad: Path) -> None:
|
||||
"""rmtree, das auch schreibgeschuetzte git-Objektdateien wegbekommt."""
|
||||
|
||||
def erzwinge(funktion, ziel, _fehler):
|
||||
try:
|
||||
os.chmod(ziel, stat.S_IWRITE)
|
||||
funktion(ziel)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
shutil.rmtree(pfad, onexc=erzwinge)
|
||||
|
||||
|
||||
def git(wo: Path, *argumente: str) -> subprocess.CompletedProcess[bytes]:
|
||||
return subprocess.run(
|
||||
["git", "-C", str(wo), *argumente],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def baue_vault(basis: Path) -> tuple[Path, Path]:
|
||||
"""Legt Arbeits-Vault und bare-Remote an und macht den ersten Commit."""
|
||||
vault = basis / "vault"
|
||||
remote = basis / "remote.git"
|
||||
(vault / "AI" / "Regelwerk").mkdir(parents=True)
|
||||
(vault / ".vault-tools" / "auto-push").mkdir(parents=True)
|
||||
(vault / "Inbox").mkdir()
|
||||
|
||||
(vault / ".gitignore").write_text(GITIGNORE, encoding="utf-8")
|
||||
(vault / "CLAUDE.md").write_text("# Stamm\n", encoding="utf-8")
|
||||
(vault / "AI" / "Regelwerk" / "probe.md").write_text("# Probe\n", encoding="utf-8")
|
||||
# Eine Datei, die niemals herausgehen darf.
|
||||
(vault / "Inbox" / "geheim.md").write_text("Kundendaten\n", encoding="utf-8")
|
||||
|
||||
shutil.copy2(HOOK, vault / ".vault-tools" / "auto-push" / "auto_push.py")
|
||||
shutil.copy2(ASKPASS_PY, vault / ".vault-tools" / "auto-push" / "askpass.py")
|
||||
shim = vault / ".vault-tools" / "auto-push" / "askpass.sh"
|
||||
shim.write_bytes(b"#!/bin/sh\necho unbenutzt\n")
|
||||
|
||||
subprocess.run(["git", "init", "-q", "--bare", str(remote)], check=True)
|
||||
git(vault, "init", "-q")
|
||||
git(vault, "symbolic-ref", "HEAD", "refs/heads/main")
|
||||
git(vault, "config", "user.name", "Fall")
|
||||
git(vault, "config", "user.email", "[email protected]")
|
||||
git(vault, "config", "commit.gpgsign", "false")
|
||||
git(vault, "remote", "add", "origin", remote.as_uri())
|
||||
git(vault, "add", "-A")
|
||||
git(vault, "commit", "-q", "-m", "Grundstand")
|
||||
git(vault, "push", "-q", "-u", "origin", "main")
|
||||
return vault, remote
|
||||
|
||||
|
||||
def lauf_hook(vault: Path, eingabe: dict, umgebung_extra: dict | None = None) -> tuple[int, str, str]:
|
||||
umgebung = dict(os.environ, PYTHONIOENCODING="utf-8", GITEA_TOKEN="egal")
|
||||
umgebung.update(umgebung_extra or {})
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(vault / ".vault-tools" / "auto-push" / "auto_push.py")],
|
||||
input=json.dumps(eingabe).encode("utf-8"),
|
||||
capture_output=True,
|
||||
env=umgebung,
|
||||
timeout=90,
|
||||
check=False,
|
||||
)
|
||||
return (
|
||||
lauf.returncode,
|
||||
lauf.stdout.decode("utf-8", errors="replace"),
|
||||
lauf.stderr.decode("utf-8", errors="replace"),
|
||||
)
|
||||
|
||||
|
||||
def remote_dateien(remote: Path) -> set[str]:
|
||||
lauf = git(remote, "ls-tree", "-r", "--name-only", "main")
|
||||
if lauf.returncode != 0:
|
||||
return set()
|
||||
return {z for z in lauf.stdout.decode("utf-8", errors="replace").splitlines() if z}
|
||||
|
||||
|
||||
def remote_commits(remote: Path) -> int:
|
||||
lauf = git(remote, "rev-list", "--count", "main")
|
||||
if lauf.returncode != 0:
|
||||
return -1
|
||||
return int(lauf.stdout.decode().strip() or "0")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fall:
|
||||
name: str
|
||||
beschreibung: str
|
||||
|
||||
|
||||
def main() -> int:
|
||||
basis = Path(tempfile.mkdtemp(prefix="auto-push-faelle-"))
|
||||
gesamt = 0
|
||||
bestanden = 0
|
||||
|
||||
def pruefe(name: str, ok: bool, text: str) -> None:
|
||||
nonlocal gesamt, bestanden
|
||||
gesamt += 1
|
||||
bestanden += int(ok)
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {name:34} {text}")
|
||||
|
||||
try:
|
||||
# --- Grundlinie -------------------------------------------------
|
||||
vault, remote = baue_vault(basis / "grundlinie")
|
||||
vorher = remote_commits(remote)
|
||||
(vault / "AI" / "Regelwerk" / "probe.md").write_text("# Probe geaendert\n", encoding="utf-8")
|
||||
code, aus, fehler = lauf_hook(vault, {"tool_input": {"file_path": str(vault / "AI" / "Regelwerk" / "probe.md")}})
|
||||
nachher = remote_commits(remote)
|
||||
pruefe(
|
||||
"grundlinie",
|
||||
code == 0 and nachher == vorher + 1 and not aus.strip(),
|
||||
f"Exit {code}, Commits {vorher}->{nachher}, Ausgabe {len(aus)} Z. {fehler[:80]}",
|
||||
)
|
||||
|
||||
# --- Ausgeschlossene Datei geht nie heraus ----------------------
|
||||
drueben = remote_dateien(remote)
|
||||
pruefe(
|
||||
"positivliste haelt",
|
||||
not any(d.startswith("Inbox/") for d in drueben),
|
||||
f"Remote fuehrt {sorted(drueben)}",
|
||||
)
|
||||
pruefe(
|
||||
"sperrdatei bleibt drinnen",
|
||||
not any(d.endswith(".sperre") for d in drueben),
|
||||
f"Remote fuehrt {sorted(drueben)}",
|
||||
)
|
||||
|
||||
# --- Lauf ohne Aenderung bleibt still ---------------------------
|
||||
vorher = remote_commits(remote)
|
||||
code, aus, _ = lauf_hook(vault, {"tool_input": {"file_path": str(vault / "CLAUDE.md")}})
|
||||
pruefe(
|
||||
"nichts geaendert -> still",
|
||||
code == 0 and not aus.strip() and remote_commits(remote) == vorher,
|
||||
f"Exit {code}, Ausgabe {len(aus)} Z., Commits {remote_commits(remote)}",
|
||||
)
|
||||
|
||||
# --- Eingaben, die nichts ausloesen duerfen ---------------------
|
||||
stille_faelle = [
|
||||
("eingabe leer", b""),
|
||||
("eingabe kein json", b"{kaputt"),
|
||||
("eingabe kein objekt", b"[1,2,3]"),
|
||||
("eingabe null", b"null"),
|
||||
("kein tool_input", b'{"tool_name":"Edit"}'),
|
||||
("tool_input keine map", b'{"tool_input":"x"}'),
|
||||
("kein file_path", b'{"tool_input":{"content":"x"}}'),
|
||||
("file_path keine zeichenkette", b'{"tool_input":{"file_path":42}}'),
|
||||
("file_path leer", b'{"tool_input":{"file_path":" "}}'),
|
||||
]
|
||||
for name, roh in stille_faelle:
|
||||
(vault / "CLAUDE.md").write_text(f"# Stamm {name}\n", encoding="utf-8")
|
||||
vorher = remote_commits(remote)
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(vault / ".vault-tools" / "auto-push" / "auto_push.py")],
|
||||
input=roh,
|
||||
capture_output=True,
|
||||
env=dict(os.environ, PYTHONIOENCODING="utf-8", GITEA_TOKEN="egal"),
|
||||
timeout=60,
|
||||
check=False,
|
||||
)
|
||||
aus = lauf.stdout.decode("utf-8", errors="replace")
|
||||
pruefe(
|
||||
name,
|
||||
lauf.returncode == 0 and not aus.strip() and remote_commits(remote) == vorher,
|
||||
f"Exit {lauf.returncode}, Ausgabe {len(aus)} Z.",
|
||||
)
|
||||
git(vault, "checkout", "--", "CLAUDE.md")
|
||||
|
||||
# --- Pfade ausserhalb des Geltungsbereichs ----------------------
|
||||
aussen = [
|
||||
("pfad ausserhalb vault", str(basis / "woanders.md")),
|
||||
("nachbarordner mit praefix", str(vault) + "-anders/CLAUDE.md"),
|
||||
("inbox-notiz loest nicht aus", str(vault / "Inbox" / "geheim.md")),
|
||||
]
|
||||
for name, pfad in aussen:
|
||||
(vault / "CLAUDE.md").write_text(f"# Stamm {name}\n", encoding="utf-8")
|
||||
vorher = remote_commits(remote)
|
||||
code, aus, _ = lauf_hook(vault, {"tool_input": {"file_path": pfad}})
|
||||
pruefe(
|
||||
name,
|
||||
code == 0 and not aus.strip() and remote_commits(remote) == vorher,
|
||||
f"Exit {code}, Commits unveraendert: {remote_commits(remote) == vorher}",
|
||||
)
|
||||
git(vault, "checkout", "--", "CLAUDE.md")
|
||||
|
||||
# --- Pfadformen, die ausloesen muessen --------------------------
|
||||
formen = [
|
||||
("relativer pfad", "AI/Regelwerk/probe.md"),
|
||||
("backslash-pfad", str(vault / "AI" / "Regelwerk" / "probe.md").replace("/", "\\")),
|
||||
("pfad mit punktsegment", str(vault / "AI" / ".." / "AI" / "Regelwerk" / "probe.md")),
|
||||
("grossschreibung", str(vault / "AI" / "Regelwerk" / "probe.md").upper()),
|
||||
("vault-tools zaehlt mit", str(vault / ".vault-tools" / "auto-push" / "notiz.md")),
|
||||
]
|
||||
for nummer, (name, pfad) in enumerate(formen):
|
||||
ziel = vault / "AI" / "Regelwerk" / "probe.md"
|
||||
ziel.write_text(f"# Probe Form {nummer}\n", encoding="utf-8")
|
||||
vorher = remote_commits(remote)
|
||||
code, aus, fehler = lauf_hook(vault, {"tool_input": {"file_path": pfad}})
|
||||
pruefe(
|
||||
name,
|
||||
code == 0 and remote_commits(remote) == vorher + 1,
|
||||
f"Exit {code}, Commits {vorher}->{remote_commits(remote)} {aus[:60]}",
|
||||
)
|
||||
|
||||
# --- Reihenfolge: erste und letzte Datei einer Sammeländerung ---
|
||||
for position in ("erste", "letzte"):
|
||||
vault2, remote2 = baue_vault(basis / f"reihenfolge-{position}")
|
||||
for i in range(5):
|
||||
(vault2 / "AI" / "Regelwerk" / f"a{i}.md").write_text(f"# {i}\n", encoding="utf-8")
|
||||
ausloeser = "a0.md" if position == "erste" else "a4.md"
|
||||
code, aus, _ = lauf_hook(vault2, {"tool_input": {"file_path": str(vault2 / "AI" / "Regelwerk" / ausloeser)}})
|
||||
dateien = remote_dateien(remote2)
|
||||
fehlt = [f"AI/Regelwerk/a{i}.md" for i in range(5) if f"AI/Regelwerk/a{i}.md" not in dateien]
|
||||
pruefe(
|
||||
f"reihenfolge {position}",
|
||||
code == 0 and not fehlt,
|
||||
f"Exit {code}, fehlend im Remote: {fehlt or 'nichts'}",
|
||||
)
|
||||
|
||||
# --- Fehlende Voraussetzungen sind Befunde ----------------------
|
||||
vault3, remote3 = baue_vault(basis / "ohne-token")
|
||||
(vault3 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8")
|
||||
code, aus, _ = lauf_hook(vault3, {"tool_input": {"file_path": str(vault3 / "CLAUDE.md")}}, {"GITEA_TOKEN": ""})
|
||||
pruefe(
|
||||
"kein token -> befund",
|
||||
code == 0 and "GITEA_TOKEN" in aus and remote_commits(remote3) == 1,
|
||||
f"Exit {code}, meldet: {'ja' if 'GITEA_TOKEN' in aus else 'NEIN'}",
|
||||
)
|
||||
|
||||
vault4, remote4 = baue_vault(basis / "ohne-askpass")
|
||||
(vault4 / ".vault-tools" / "auto-push" / "askpass.sh").unlink()
|
||||
(vault4 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8")
|
||||
code, aus, _ = lauf_hook(vault4, {"tool_input": {"file_path": str(vault4 / "CLAUDE.md")}})
|
||||
pruefe(
|
||||
"kein askpass -> befund",
|
||||
code == 0 and "askpass" in aus.lower(),
|
||||
f"Exit {code}, meldet: {'ja' if 'askpass' in aus.lower() else 'NEIN'}",
|
||||
)
|
||||
|
||||
vault5, _ = baue_vault(basis / "ohne-git")
|
||||
raeume(vault5 / ".git")
|
||||
(vault5 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8")
|
||||
code, aus, _ = lauf_hook(vault5, {"tool_input": {"file_path": str(vault5 / "CLAUDE.md")}})
|
||||
pruefe(
|
||||
"kein repo -> befund",
|
||||
code == 0 and "Git-Repo" in aus,
|
||||
f"Exit {code}, meldet: {'ja' if 'Git-Repo' in aus else 'NEIN'}",
|
||||
)
|
||||
|
||||
vault6, remote6 = baue_vault(basis / "remote-weg")
|
||||
raeume(remote6)
|
||||
(vault6 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8")
|
||||
code, aus, _ = lauf_hook(vault6, {"tool_input": {"file_path": str(vault6 / "CLAUDE.md")}})
|
||||
lokal = git(vault6, "rev-list", "--count", "HEAD").stdout.decode().strip()
|
||||
pruefe(
|
||||
"push scheitert -> befund, commit bleibt",
|
||||
code == 0 and "push" in aus.lower() and lokal == "2",
|
||||
f"Exit {code}, lokale Commits {lokal}, meldet: {'ja' if 'push' in aus.lower() else 'NEIN'}",
|
||||
)
|
||||
|
||||
# --- Belegte Sperre bleibt still --------------------------------
|
||||
vault7, remote7 = baue_vault(basis / "sperre")
|
||||
sperre = vault7 / ".vault-tools" / "auto-push" / ".sperre"
|
||||
sperre.write_bytes(b"")
|
||||
(vault7 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8")
|
||||
vorher = remote_commits(remote7)
|
||||
code, aus, _ = lauf_hook(vault7, {"tool_input": {"file_path": str(vault7 / "CLAUDE.md")}})
|
||||
pruefe(
|
||||
"belegte sperre -> still",
|
||||
code == 0 and not aus.strip() and remote_commits(remote7) == vorher,
|
||||
f"Exit {code}, Ausgabe {len(aus)} Z.",
|
||||
)
|
||||
|
||||
print()
|
||||
print(f"{bestanden} von {gesamt} Faellen bestanden.")
|
||||
return 0 if bestanden == gesamt else 1
|
||||
finally:
|
||||
shutil.rmtree(basis, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
type: resource
|
||||
status: reference
|
||||
title: "Veröffentlichung der Anweisungsschicht"
|
||||
summary: "Welcher Teil des Vaults in das Gitea-Repo claude-regelwerk geht, wie die Positivliste funktioniert und was der automatische Push tut. Laden, bevor der veröffentlichte Umfang geändert wird oder wenn ein Auto-Push-Befund im Kontext steht."
|
||||
tags: [regelwerk, git, veroeffentlichung, werkzeuge]
|
||||
created: 2026-09-17
|
||||
updated: 2026-09-17
|
||||
---
|
||||
|
||||
# Veröffentlichung der Anweisungsschicht
|
||||
|
||||
Angeordnet am 2026-09-17. Die Anweisungsschicht des Vaults liegt seitdem in einem eigenen Git-Repo und wird nach jeder Änderung automatisch gepusht. Laden, wenn der Umfang geändert werden soll oder wenn eine Auto-Push-Meldung im Kontext steht.
|
||||
|
||||
**Der Vault-Root ist jetzt ein Git-Repo.** Das war er vorher nicht, und es widerspricht nicht [[AI/Regelwerk/projekte|projekte.md]]: Die Projekte unter `Code Projekte/` behalten ihre eigenen Repos, die von diesem hier nicht berührt werden.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Remote | `https://git.nexhorizons.de/Nexhorizons/claude-regelwerk.git` |
|
||||
| Zweig | `main` |
|
||||
| Sichtbarkeit | Repo auf public, aber die Organisation `Nexhorizons` ist privat und die Instanz verlangt für jede Ansicht eine Anmeldung. **Faktisch ist es damit nur für angemeldete Nutzer der Instanz sichtbar.** |
|
||||
|
||||
## Was hinausgeht — und warum die Liste so herum steht
|
||||
|
||||
Die `.gitignore` im Vault-Root ist eine **Positivliste**: Sie schließt mit `/*` erst alles aus und nimmt dann einzeln wieder auf. Eine Ausschlussliste wäre hier falsch herum — sie müsste jede künftige Notiz, jeden neuen Kundenordner und jede Depotdatei vorhersehen, und die erste vergessene Zeile ist eine Veröffentlichung.
|
||||
|
||||
Eingeschlossen sind: `CLAUDE.md`, `README.md`, `AI/Regelwerk/`, `.claude/agents/`, `.claude/skills/`, `.claude/settings.json`, `.vault-tools/`.
|
||||
|
||||
Draußen bleibt damit alles andere, insbesondere `Inbox/`, `Code Projekte/`, `Security - Compliance Projekte/`, `Finanzen/`, `IT Workspace/`, `AI/sessions/`, `AI/secondbrain/`, `Bookstack/`, `Import/` und `.claude/settings.local.json`.
|
||||
|
||||
**Wer den Umfang ändern will, ändert die `.gitignore`** — nicht die Pfadliste im Hook. Die Liste dort entscheidet nur, ob der Hook überhaupt anläuft.
|
||||
|
||||
## Was vor der Aufnahme einer Datei geprüft wird
|
||||
|
||||
Vor jeder Erweiterung der Positivliste, von Hand:
|
||||
|
||||
1. **Kundennamen, Domains, IP-Adressen, Snippet- und Ticket-IDs.** In `seo.md` stand ein Kundenname im Klartext; er ist am 2026-09-17 durch eine neutrale Bezeichnung ersetzt worden. Die Fallgeschichte gehört ins Harness-Gedächtnis, nicht ins Regelwerk.
|
||||
2. **Zugangsdaten.** Skripte lesen Zugänge aus Umgebungsvariablen, nie aus einer Datei im Repo. Gilt auch für Standardwerte und Beispielaufrufe in Kommentaren.
|
||||
3. **`__pycache__` und Laufzeitdateien.** `.pyc` trägt kompilierte Zeichenketten, unter anderem URLs, und ist bereits einmal aufgefallen.
|
||||
|
||||
## Der automatische Push
|
||||
|
||||
`.vault-tools/auto-push/auto_push.py`, eingetragen als PostToolUse-Hook auf `Edit|Write|MultiEdit`. Nach jeder Änderung an einer eingeschlossenen Datei: `git add -A`, Commit, Push. Das Token kommt über `.vault-tools/auto-push/askpass.py` aus der Umgebungsvariable `GITEA_TOKEN`; in `.git/config` steht nur der Benutzername.
|
||||
|
||||
**Es gibt keine Freigabe und keinen Geheimnis-Scan.** Was in einer eingeschlossenen Datei steht, geht heraus, sobald sie geschrieben wird — auch ein halbfertiger Zwischenstand. Das ist so angeordnet und der Grund, warum die Positivliste die einzige Schranke ist.
|
||||
|
||||
**Eine fehlende Voraussetzung ist ein Befund, kein Grund zu schweigen.** Fehlendes Token, fehlendes Remote, gescheiterter Push: Der Hook meldet das in den Kontext. Ein gescheiterter Push verliert nichts — der Commit bleibt lokal, der nächste Lauf schiebt ihn mit.
|
||||
|
||||
**Wenn eine Auto-Push-Meldung kommt**, ist sie zu lesen und nicht wegzuklicken. Der häufigste Fall wird ein Push-Fehler bei fehlendem Netz sein; dann genügt es, später eine beliebige eingeschlossene Datei erneut zu speichern.
|
||||
|
||||
Der Fallkatalog liegt daneben: `python .vault-tools/auto-push/faelle_auto_push.py` fährt 28 Fälle gegen ein echtes Wegwerf-Repo mit echtem Remote. **Er ist ein Fangnetz, kein Nachweis** — er sieht, ob der Hook auslöst und ob die Positivliste hält, und nicht, ob eine eingeschlossene Datei etwas enthält, das niemand veröffentlichen wollte.
|
||||
|
||||
## Referenzen
|
||||
|
||||
- [[CLAUDE|↑ Claude.md (Stamm)]]
|
||||
- [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]]: Angriffsliste und „eine fehlende Voraussetzung ist ein Befund"
|
||||
- [[AI/Regelwerk/windows-shell|windows-shell.md]]: Geheimnisse nur als Länge ausgeben
|
||||
- [[AI/Regelwerk/projekte|projekte.md]]: die eigenen Repos der Code-Projekte
|
||||
@@ -80,6 +80,7 @@ Jede Datei triggert nur den betreffenden Teil — nicht bei jeder Aufgabe alles
|
||||
- Projekt in Code Projekte / Security-Compliance anlegen oder bearbeiten → [[AI/Regelwerk/projekte|projekte.md]]
|
||||
- Mechanische Prüfung, Prüfskript, Hook oder Gate-Erweiterung bauen → [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]] — **vor der ersten Zeile Code laden.**
|
||||
- Skript oder Shellbefehl unter Windows (PowerShell, Python, Git-Bash, WSL, ssh) → [[AI/Regelwerk/windows-shell|windows-shell.md]]
|
||||
- Veröffentlichten Umfang ändern, `.gitignore` im Vault-Root anfassen oder eine Auto-Push-Meldung auflösen → [[AI/Regelwerk/veroeffentlichung|veroeffentlichung.md]]
|
||||
- Dokumentenset ab etwa 20 Dateien über Subagenten, oder voneinander abhängige Dokumente → [[AI/Regelwerk/dokumentations-grossprojekte|dokumentations-grossprojekte.md]]
|
||||
- Suchen, Links prüfen, `.vault-tools/`, Obsidian-CLI und -Skills → [[AI/Regelwerk/navigation|navigation.md]]
|
||||
- Frontmatter schreiben → [[AI/Regelwerk/frontmatter|frontmatter.md]]
|
||||
@@ -91,6 +92,14 @@ Jede Datei triggert nur den betreffenden Teil — nicht bei jeder Aufgabe alles
|
||||
- Auftrag an `planer` oder `pruefer` (Opus 5) schreiben → [[AI/Regelwerk/opus-5|opus-5.md]]; an `executer` oder Recherche (Sonnet 5) → [[AI/Regelwerk/sonnet-5|sonnet-5.md]]; Subagenten-Auftrag, Agent- oder Skill-Datei, System-Prompt allgemein → [[AI/Regelwerk/prompting|prompting.md]]. Verbindlich für Fable 5.1 als Auftraggeber, andere Modelle entscheiden selbst.
|
||||
- Code gegen die Claude Messages API (eigener Agent, Harness, Werkzeug) → [[AI/Regelwerk/api-mechanik|api-mechanik.md]] — nicht für die Claude-Code-Sitzung selbst.
|
||||
|
||||
## Veröffentlichung
|
||||
|
||||
Der Vault-Root ist ein Git-Repo und pusht nach **jeder** Änderung automatisch nach `https://git.nexhorizons.de/Nexhorizons/claude-regelwerk.git`. Veröffentlicht wird nur die Anweisungsschicht: `CLAUDE.md`, `README.md`, `AI/Regelwerk/`, `.claude/agents/`, `.claude/skills/`, `.claude/settings.json` und `.vault-tools/`.
|
||||
|
||||
Die `.gitignore` im Root ist eine **Positivliste** — sie schließt erst alles aus und nimmt dann einzeln wieder auf. Notizen, Projekte, Finanzen, Sitzungen und `settings.local.json` bleiben damit draußen, auch wenn morgen ein neuer Ordner dazukommt.
|
||||
|
||||
**Es gibt keine Freigabe vor dem Push.** Was in einer eingeschlossenen Datei steht, ist nach dem Speichern draußen — auch ein Zwischenstand. Wer eine Datei neu in die Positivliste aufnimmt, prüft sie vorher auf Kundennamen, Domains und Zugangsdaten. Alles Weitere, auch die Fehlerfälle des Hooks: [[AI/Regelwerk/veroeffentlichung|veroeffentlichung.md]].
|
||||
|
||||
## Selbstpflege des Regelwerks
|
||||
|
||||
Du darfst Dateien unter `AI/Regelwerk/` eigenständig anpassen, wenn du wiederkehrende Fehler, Lücken oder Verbesserungen erkennst — z. B. aus der Lessons-Learned-Schleife im Codeworkflow. Vorgehen:
|
||||
|
||||
Reference in new issue
Block a user