Abgleich mit dem Vault-Stand 80a20f1

6 Datei(en) geändert.
Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
This commit is contained in:
Pascal Michel committed 2026-10-15 04:00:03 +02:00
1 parent fa1c26b407
commit dd5e612452
6 files changed
+1237 -54

No files matched your search

@@ -0,0 +1,687 @@
---
type: plan-task
title: "Task 01 — Werkzeuge täglich nach claude-werkzeuge spiegeln"
status: draft
summary: "Neues Modul werkzeuge_spiegel.py mit Fallkatalog und README-Quelle; taeglich_push.py ruft es nach dem Vault-Push auf, damit der 04:00-Lauf auch das öffentliche Repo claude-werkzeuge nachzieht."
tags: [plan-task, vault-tools]
created: 2026-10-14
updated: 2026-10-14
modell: haiku
modell_begruendung: "Vier Dateien stehen wörtlich im Plan; der Executer schreibt ab, fährt den Fallkatalog und drei vorgegebene Mutanten. Typografische Zeichen in den Blöcken übernimmt Haiku 5.5 byte-genau."
---
Hub: [[AI/Regelwerk/veroeffentlichung|veroeffentlichung.md]]
# Task 01 — Werkzeuge täglich nach claude-werkzeuge spiegeln
## Ziel
Der tägliche Lauf `taeglich_push.py` pusht nach dem Vault auch den committeten Stand von `.vault-tools/` und `.claude/` (ohne `trading-plattform-pruefung` und `watermarks-remover`) in das öffentliche Repo `Nexhorizons-Public/claude-werkzeuge`. Der Klon dafür liegt außerhalb des Vaults unter `D:\ClaudeData\claude-werkzeuge`.
## Zieldateien
- `.vault-tools/auto-push/werkzeuge_spiegel.py` (neu)
- `.vault-tools/auto-push/faelle_werkzeuge_spiegel.py` (neu)
- `.vault-tools/auto-push/werkzeuge-README.md` (neu)
- `.vault-tools/auto-push/taeglich_push.py` (ganz ersetzen)
Alle Pfade relativ zu `D:\Claude`. Keine andere Datei anfassen. Kein `git commit`, kein `git push`, `taeglich_push.py` **nicht** ausführen (das pusht echt).
## Vorgehen
1. Die vier Dateien byte-genau aus den Blöcken unten anlegen, Zeilenenden LF, UTF-8 ohne BOM.
2. Fallkatalog fahren, erwartet: letzte Zeile `14 von 14 Faellen bestanden.`, Exit 0.
3. Drei Mutanten, je einzeln auf einer **Kopie** von `werkzeuge_spiegel.py`: Kopie nach `werkzeuge_spiegel.py.orig` legen, Mutation setzen, Fallkatalog fahren, erwartete FEHLER-Zeile sehen, Original byte-gleich zurückkopieren, `.orig` löschen.
- M1: in `gehoert_dazu` den Teil `and not pfad.startswith(AUSSCHLUESSE)` streichen → erwartet `FEHLER` bei `grundlinie`.
- M2: in `spiegele` die Zeile `raeume(eintrag)` durch `pass` ersetzen → erwartet `FEHLER` bei `grundlinie` (veraltet.txt bleibt drüben).
- M3: in `bereite_klon` die Zeile `if vorhanden != adresse:` durch `if False:` ersetzen → erwartet `FEHLER` bei `fremder klon -> befund, nichts geleert`.
4. Danach den Fallkatalog noch einmal fahren: wieder 14 von 14.
5. `python -m py_compile` auf alle drei .py-Dateien.
## Prüfen
```bash
cd /d/Claude && python .vault-tools/auto-push/faelle_werkzeuge_spiegel.py; echo "Exit $?"
python .vault-tools/plan-diff/plan_diff.py .vault-tools/auto-push/docs/plans/2026-10-14-werkzeuge-spiegel.md --projekt /d/Claude
```
## Fertig heißt
- Fallkatalog: `14 von 14 Faellen bestanden.`, Exit 0, im Bericht wörtlich zitiert.
- Jede der drei Mutationen hat die genannte FEHLER-Zeile erzeugt; Zeile je Mutant wörtlich im Bericht.
- `plan_diff.py` endet mit Exit 0.
- `git status --short` zeigt nur die vier Zieldateien und diesen Plan.
## Code
`.vault-tools/auto-push/werkzeuge_spiegel.py` neu anlegen:
```python
"""Spiegelt die Werkzeuge des Vaults in das öffentliche Repo claude-werkzeuge.
Ersteller: Pascal Michel
Erstellt: 2026-10-14
WAS GESPIEGELT WIRD
Der committete Stand (HEAD) des Vaults unter .vault-tools/ und .claude/,
ohne die Ordner in AUSSCHLUESSE, dazu werkzeuge-README.md als README.md.
Ungesicherte Änderungen im Arbeitsbaum gehen nicht mit. Was die
.gitignore des Vaults draußen hält (settings.local.json, __pycache__,
lauf.log), ist deshalb auch hier draußen.
AUSSCHLUESSE ist eine Negativliste: Ein neuer Werkzeugordner geht beim
nächsten Lauf mit hinaus. So angeordnet (Pascal, 2026-10-14: künftig alle
Werkzeuge außer denen des Trading-Projekts). Ein neues Werkzeug des
Trading-Projekts gehört deshalb in AUSSCHLUESSE, bevor es committet wird.
WOHIN
In einen Klon außerhalb des Vaults (KLON). Der Lauf holt den Remote-Stand,
leert den Klon bis auf .git, packt den Stand neu aus und committet und
pusht nur, wenn sich etwas geändert hat. Die Remote-Adresse entsteht aus
der origin-Adresse des Vaults, letzter Pfadteil ersetzt; Benutzername und
Host stehen so nirgends im Quelltext. Das Token kommt wie beim Vault-Push
über askpass.sh aus GITEA_TOKEN.
SCHUTZ
Geleert wird nur ein Klon, dessen origin genau auf die erwartete Adresse
zeigt. Ein Klon im Vault, ein fremder Klon und ein nicht leerer Ordner
ohne .git sind Befunde, nichts wird gelöscht.
BENANNTE LÜCKEN
* Kein Geheimnis-Scan. Was im Vault unter .vault-tools/ oder .claude/
committet ist, geht hinaus, wie beim Vault-Push.
* Nur reguläre Dateien. Symlinks im Vault werden übersprungen.
* Eine Änderung direkt im Repo claude-werkzeuge überschreibt der nächste
Lauf.
* Ein gescheiterter Push lässt den Commit im Klon stehen; der nächste Lauf
setzt den Klon auf den Remote-Stand zurück und committet neu.
"""
from __future__ import annotations
import io
import os
import shutil
import stat
import subprocess
import tarfile
from pathlib import Path
KLON = Path(r"D:\ClaudeData\claude-werkzeuge")
REPO_NAME = "claude-werkzeuge.git"
README_QUELLE = ".vault-tools/auto-push/werkzeuge-README.md"
ASKPASS_REL = Path(".vault-tools") / "auto-push" / "askpass.sh"
QUELLEN = (".vault-tools/", ".claude/")
# Verankert ab der Vault-Wurzel, nie als bloße Ordnernamen.
AUSSCHLUESSE = (
".vault-tools/trading-plattform-pruefung/",
".vault-tools/watermarks-remover/",
)
TIMEOUT_GIT_S = 30
TIMEOUT_NETZ_S = 90
class SpiegelFehler(Exception):
"""Ein Schritt ist gescheitert; die Meldung ist der Befund."""
def git(wo: Path, wurzel: Path, *argumente: str, timeout: int = TIMEOUT_GIT_S) -> bytes:
umgebung = dict(
os.environ,
GIT_TERMINAL_PROMPT="0",
GIT_ASKPASS=str(wurzel / ASKPASS_REL),
)
try:
lauf = subprocess.run(
["git", "-C", str(wo), *argumente],
env=umgebung,
stdin=subprocess.DEVNULL,
capture_output=True,
timeout=timeout,
)
except subprocess.TimeoutExpired as abgelaufen:
raise SpiegelFehler(f"git {argumente[0]} nach {timeout} s abgebrochen") from abgelaufen
except OSError as fehler:
raise SpiegelFehler(f"git nicht startbar: {type(fehler).__name__}: {fehler}") from fehler
if lauf.returncode != 0:
text = (lauf.stdout + b"\n" + lauf.stderr).decode("utf-8", errors="replace")
raise SpiegelFehler(f"git {argumente[0]}: Exit {lauf.returncode}: {' '.join(text.split())[:300]}")
return lauf.stdout
def gehoert_dazu(pfad: str) -> bool:
return pfad.startswith(QUELLEN) and not pfad.startswith(AUSSCHLUESSE)
def ziel_adresse(wurzel: Path) -> str:
adresse = git(wurzel, wurzel, "remote", "get-url", "origin").decode("utf-8").strip()
if "/" not in adresse:
raise SpiegelFehler(f"origin des Vaults ohne Pfad: {adresse}")
return adresse.rsplit("/", 1)[0] + "/" + REPO_NAME
def raeume(pfad: Path) -> None:
"""Löscht Datei oder Ordner, auch schreibgeschützte Dateien."""
def erzwinge(funktion, ziel, _fehler):
os.chmod(ziel, stat.S_IWRITE)
funktion(ziel)
if pfad.is_dir() and not pfad.is_symlink():
shutil.rmtree(pfad, onexc=erzwinge)
else:
pfad.unlink()
def bereite_klon(wurzel: Path, klon: Path, adresse: str) -> None:
"""Klont beim ersten Lauf, setzt danach auf den Remote-Stand zurück."""
basis = wurzel.resolve()
ziel = klon.resolve()
if ziel == basis or basis in ziel.parents:
raise SpiegelFehler(f"Klon {klon} liegt im Vault; abgebrochen, nichts geleert")
if not (klon / ".git").is_dir():
if klon.exists() and any(klon.iterdir()):
raise SpiegelFehler(f"{klon} ist kein Git-Klon und nicht leer; abgebrochen, nichts geleert")
klon.parent.mkdir(parents=True, exist_ok=True)
git(klon.parent, wurzel, "clone", "-q", "-c", "core.autocrlf=false", adresse, str(klon),
timeout=TIMEOUT_NETZ_S)
return
vorhanden = git(klon, wurzel, "remote", "get-url", "origin").decode("utf-8").strip()
if vorhanden != adresse:
raise SpiegelFehler(
f"origin des Klons zeigt auf {vorhanden}, erwartet {adresse}; abgebrochen, nichts geleert"
)
git(klon, wurzel, "fetch", "-q", "origin", timeout=TIMEOUT_NETZ_S)
git(klon, wurzel, "reset", "-q", "--hard", "origin/main")
def spiegele(wurzel: Path, klon: Path = KLON) -> tuple[str, str]:
"""Gibt ("gespiegelt" oder "unveraendert", Text) zurück. Wirft SpiegelFehler."""
adresse = ziel_adresse(wurzel)
stand = git(wurzel, wurzel, "rev-parse", "--short", "HEAD").decode("utf-8").strip()
# Ganzer HEAD statt Pfadangaben: git archive bricht mit "pathspec did not match"
# ab, sobald eine Quelle leer ist. Gefiltert wird danach mit gehoert_dazu.
archiv = git(wurzel, wurzel, "archive", "--format=tar", "HEAD")
with tarfile.open(fileobj=io.BytesIO(archiv)) as tar:
mitglieder = [m for m in tar.getmembers() if m.isfile() and gehoert_dazu(m.name)]
if README_QUELLE not in {m.name for m in mitglieder}:
raise SpiegelFehler(f"README-Quelle {README_QUELLE} fehlt im Vault-Stand {stand}")
bereite_klon(wurzel, klon, adresse)
for eintrag in klon.iterdir():
if eintrag.name != ".git":
raeume(eintrag)
for mitglied in mitglieder:
tar.extract(mitglied, klon, filter="data")
readme = tar.extractfile(README_QUELLE)
(klon / "README.md").write_bytes(readme.read())
git(klon, wurzel, "add", "-A")
roh = git(klon, wurzel, "diff", "--cached", "--name-only", "-z").decode("utf-8")
geaendert = [z for z in roh.split("\0") if z]
if not geaendert:
return "unveraendert", f"Vault-Stand {stand}"
git(klon, wurzel, "commit", "-q", "-m",
f"Abgleich mit dem Vault-Stand {stand}\n\n{len(geaendert)} Datei(en) geändert.\n"
"Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.")
git(klon, wurzel, "push", "-q", "origin", "HEAD:main", timeout=TIMEOUT_NETZ_S)
return "gespiegelt", f"{len(geaendert)} Datei(en), Vault-Stand {stand}"
```
`.vault-tools/auto-push/faelle_werkzeuge_spiegel.py` neu anlegen:
```python
"""Fährt die Fälle gegen werkzeuge_spiegel.py, auf Wegwerf-Vaults mit echten bare-Remotes.
Ersteller: Pascal Michel
Erstellt: 2026-10-14
Jeder Wegwerf-Vault hat als origin ein bare-Repo claude-regelwerk.git, daneben
liegt claude-werkzeuge.git mit einem veralteten Stand. Der Spiegel leitet seine
Adresse aus der des Vaults ab, wie im Betrieb. Geprüft wird, was nachher
drüben liegt, nicht was das Skript meldet.
Aufruf:
python .vault-tools/auto-push/faelle_werkzeuge_spiegel.py
"""
from __future__ import annotations
import importlib.util
import os
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
HIER = Path(__file__).resolve().parent
SPIEGEL = HIER / "werkzeuge_spiegel.py"
README = HIER / "werkzeuge-README.md"
CRLF_INHALT = b"zeile eins\r\nzeile zwei\r\n"
ERWARTET = {
"README.md",
".vault-tools/auto-push/werkzeuge_spiegel.py",
".vault-tools/auto-push/werkzeuge-README.md",
".vault-tools/linkcheck/a.py",
".vault-tools/linkcheck/crlf.txt",
".claude/agents/x.md",
}
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 lade_spiegel():
spec = importlib.util.spec_from_file_location("werkzeuge_spiegel", SPIEGEL)
if spec is None or spec.loader is None:
raise RuntimeError(f"nicht ladbar: {SPIEGEL}")
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def schreibe(pfad: Path, inhalt: bytes) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(inhalt)
def richte_repo_ein(wo: Path, remote: Path) -> None:
git(wo, "init", "-q")
git(wo, "symbolic-ref", "HEAD", "refs/heads/main")
git(wo, "config", "user.name", "Fall")
git(wo, "config", "user.email", "[email protected]")
git(wo, "config", "commit.gpgsign", "false")
git(wo, "config", "core.autocrlf", "false")
git(wo, "remote", "add", "origin", remote.as_uri())
def committe(wo: Path, text: str) -> None:
git(wo, "add", "-A")
git(wo, "commit", "-q", "-m", text)
def baue(basis: Path) -> tuple[Path, Path]:
"""Legt Vault, beide bare-Remotes und den veralteten Spiegelstand an."""
vault = basis / "vault"
remotes = basis / "remotes"
regelwerk = remotes / "claude-regelwerk.git"
werkzeuge = remotes / "claude-werkzeuge.git"
remotes.mkdir(parents=True)
subprocess.run(["git", "init", "-q", "--bare", str(regelwerk)], check=True)
subprocess.run(["git", "init", "-q", "--bare", "--initial-branch=main", str(werkzeuge)], check=True)
schreibe(vault / ".vault-tools" / "auto-push" / "werkzeuge_spiegel.py", SPIEGEL.read_bytes())
schreibe(vault / ".vault-tools" / "auto-push" / "werkzeuge-README.md", README.read_bytes())
schreibe(vault / ".vault-tools" / "linkcheck" / "a.py", b"print('a')\n")
schreibe(vault / ".vault-tools" / "linkcheck" / "crlf.txt", CRLF_INHALT)
schreibe(vault / ".vault-tools" / "trading-plattform-pruefung" / "t.py", b"trading\n")
schreibe(vault / ".vault-tools" / "watermarks-remover" / "w.sh", b"aus\n")
schreibe(vault / ".claude" / "agents" / "x.md", b"# x\n")
schreibe(vault / "Inbox" / "geheim.md", b"Kundendaten\n")
richte_repo_ein(vault, regelwerk)
committe(vault, "Grundstand")
# Ungesicherte Datei im Arbeitsbaum: darf nie hinueber.
schreibe(vault / ".vault-tools" / "linkcheck" / "lokal.tmp", b"nicht committet\n")
alt = basis / "alt"
schreibe(alt / "README.md", b"alt\n")
schreibe(alt / "veraltet.txt", b"weg damit\n")
richte_repo_ein(alt, werkzeuge)
committe(alt, "Alter Stand")
git(alt, "push", "-q", "origin", "main")
return vault, werkzeuge
def 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 inhalt(remote: Path, pfad: str) -> bytes:
return git(remote, "cat-file", "blob", f"main:{pfad}").stdout
def 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")
def main() -> int:
spiegel = lade_spiegel()
basis = Path(tempfile.mkdtemp(prefix="werkzeuge-spiegel-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:42} {text}")
def wirft(wurzel: Path, klon: Path) -> str | None:
try:
spiegel.spiegele(wurzel, klon)
except spiegel.SpiegelFehler as fehler:
return str(fehler)
return None
try:
# --- Grundlinie -------------------------------------------------
vault, werkzeuge = baue(basis / "a")
klon = basis / "a" / "klon"
status, text = spiegel.spiegele(vault, klon)
drueben = dateien(werkzeuge)
pruefe(
"grundlinie",
status == "gespiegelt" and drueben == ERWARTET,
f"{status}: {text}; zu viel {sorted(drueben - ERWARTET)}, fehlt {sorted(ERWARTET - drueben)}",
)
pruefe(
"readme aus der quelle",
inhalt(werkzeuge, "README.md") == README.read_bytes(),
f"{len(inhalt(werkzeuge, 'README.md'))} Bytes drueben",
)
pruefe(
"bytes bleiben (crlf)",
inhalt(werkzeuge, ".vault-tools/linkcheck/crlf.txt") == CRLF_INHALT,
repr(inhalt(werkzeuge, ".vault-tools/linkcheck/crlf.txt")),
)
# --- Zweiter Lauf ohne Aenderung --------------------------------
vorher = commits(werkzeuge)
status, text = spiegel.spiegele(vault, klon)
pruefe(
"zweiter lauf -> unveraendert",
status == "unveraendert" and commits(werkzeuge) == vorher,
f"{status}: {text}; Commits {vorher}->{commits(werkzeuge)}",
)
# --- Committet geht mit, ungesichert nicht ----------------------
a_py = vault / ".vault-tools" / "linkcheck" / "a.py"
schreibe(a_py, b"print('neu')\n")
committe(vault, "a geaendert")
schreibe(a_py, b"print('ungesichert')\n")
status, text = spiegel.spiegele(vault, klon)
pruefe(
"committete aenderung geht mit",
status == "gespiegelt" and inhalt(werkzeuge, ".vault-tools/linkcheck/a.py") == b"print('neu')\n",
f"{status}: {text}; drueben {inhalt(werkzeuge, '.vault-tools/linkcheck/a.py')!r}",
)
# --- Loeschung geht mit -----------------------------------------
git(vault, "rm", "-q", ".claude/agents/x.md")
committe(vault, "x geloescht")
spiegel.spiegele(vault, klon)
pruefe(
"loeschung geht mit",
".claude/agents/x.md" not in dateien(werkzeuge),
f"drueben {sorted(dateien(werkzeuge))}",
)
# --- Schutz: nichts leeren, was nicht unser Klon ist ------------
fremd = basis / "fremd"
fremd.mkdir()
git(fremd, "init", "-q")
# Erreichbares Remote in anderer Schreibweise: ohne Schutz liefe der
# Abgleich durch und leerte den Ordner.
git(fremd, "remote", "add", "origin", str(werkzeuge))
schreibe(fremd / "bleibt.txt", b"x\n")
meldung = wirft(vault, fremd)
pruefe(
"fremder klon -> befund, nichts geleert",
meldung is not None and (fremd / "bleibt.txt").is_file(),
str(meldung)[:100],
)
innen = vault / "klon-im-vault"
schreibe(innen / "bleibt.txt", b"x\n")
meldung = wirft(vault, innen)
pruefe(
"klon im vault -> befund, nichts geleert",
meldung is not None and (innen / "bleibt.txt").is_file(),
str(meldung)[:100],
)
ohne_git = basis / "ohne-git"
schreibe(ohne_git / "bleibt.txt", b"x\n")
meldung = wirft(vault, ohne_git)
pruefe(
"ordner ohne git -> befund, nichts geleert",
meldung is not None and (ohne_git / "bleibt.txt").is_file(),
str(meldung)[:100],
)
# --- Fehlende Voraussetzungen sind Befunde ----------------------
vault2, werkzeuge2 = baue(basis / "b")
raeume(werkzeuge2)
meldung = wirft(vault2, basis / "b" / "klon")
pruefe("remote fehlt beim klonen -> befund", meldung is not None, str(meldung)[:100])
vault3, werkzeuge3 = baue(basis / "c")
git(vault3, "rm", "-q", ".vault-tools/auto-push/werkzeuge-README.md")
committe(vault3, "readme weg")
meldung = wirft(vault3, basis / "c" / "klon")
pruefe(
"readme-quelle fehlt -> befund, nichts gepusht",
meldung is not None and "README" in meldung and commits(werkzeuge3) == 1,
f"Commits {commits(werkzeuge3)}; {str(meldung)[:80]}",
)
pruefe(
"readme-quelle fehlt -> kein klon angelegt",
not (basis / "c" / "klon").exists(),
f"klon vorhanden: {(basis / 'c' / 'klon').exists()}",
)
vault4, werkzeuge4 = baue(basis / "d")
klon4 = basis / "d" / "klon"
spiegel.spiegele(vault4, klon4)
raeume(werkzeuge4)
schreibe(vault4 / ".vault-tools" / "linkcheck" / "a.py", b"print('d')\n")
committe(vault4, "a geaendert")
meldung = wirft(vault4, klon4)
pruefe("remote weg bei bestehendem klon -> befund", meldung is not None, str(meldung)[:100])
vault5, _ = baue(basis / "e")
git(vault5, "remote", "remove", "origin")
meldung = wirft(vault5, basis / "e" / "klon")
pruefe("vault ohne origin -> befund", meldung is not None, str(meldung)[:100])
print()
print(f"{bestanden} von {gesamt} Faellen bestanden.")
return 0 if bestanden == gesamt else 1
finally:
raeume(basis)
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
raise SystemExit(main())
```
`.vault-tools/auto-push/werkzeuge-README.md` neu anlegen:
```markdown
# Claude-Werkzeuge
Werkzeuge, Skills, Agenten und Workflows, die rund um Claude Code für einen Obsidian-Vault entstanden sind. Die Regeln dazu stehen im Schwester-Repo [claude-regelwerk](https://git.nexhorizons.de/Nexhorizons-Public/claude-regelwerk).
- Dieses Repo ist ein Spiegel. Es wird täglich aus dem Vault neu geschrieben; Änderungen direkt hier gehen beim nächsten Lauf verloren.
- Die Ordnerstruktur entspricht der im Vault (`.vault-tools/`, `.claude/`). Pfade in Skripten und Dokumenten passen so ohne Anpassung.
- Viele Skripte setzen Vault-Konventionen voraus (Ordner, Frontmatter, `AI/Regelwerk/`). Vor dem Einsatz den Kopfkommentar und, wo vorhanden, `LIESMICH.md` oder `ENTWURF.md` im Werkzeugordner lesen.
- Nicht enthalten: die Prüfwerkzeuge des Trading-Projekts.
## Aufbau
- `.vault-tools/` — Skripte, je Werkzeug ein Ordner.
- `.claude/agents/` — Rollen mit Modell und Effort.
- `.claude/skills/` — Skills (Playbooks), je Skill ein Ordner mit `SKILL.md`.
- `.claude/workflows/` — gespeicherte Workflows für das Workflow-Werkzeug von Claude Code.
- `.claude/settings.json` — Hook-Verdrahtung.
## Zugangsdaten
Keine im Repo. Skripte, die eine API brauchen, lesen sie aus Umgebungsvariablen.
```
`.vault-tools/auto-push/taeglich_push.py` ganz ersetzen:
```python
"""Täglicher Lauf: committet und pusht die Anweisungsschicht und spiegelt die Werkzeuge.
Ersteller: Pascal Michel
Erstellt: 2026-09-17
Geändert: 2026-10-14 (Werkzeug-Spiegel nach claude-werkzeuge)
WARUM LOKAL UND NICHT ALS COWORK-ROUTINE
Eine Cowork-Routine läuft in Anthropics Cloud und hat weder Zugriff auf
D:\\Claude noch auf die Umgebungsvariable GITEA_TOKEN dieser Maschine (so
wörtlich in der Routinen-Doku, gelesen am 2026-09-17). Sie könnte hier
nichts committen. Deshalb die Windows-Aufgabenplanung.
WAS ES TUT
1. Genau dasselbe wie der frühere PostToolUse-Hook: die Arbeitsfunktion
aus auto_push.py, unter derselben Sperre. Der Umfang steht in der
.gitignore des Vault-Roots, nicht hier.
2. Danach, noch unter der Sperre, spiegelt werkzeuge_spiegel.py den
committeten Stand von .vault-tools/ und .claude/ in das öffentliche
Repo claude-werkzeuge. Scheitert Schritt 1, entfällt Schritt 2.
WAS ES MELDET
Jeder Lauf schreibt eine Zeile je Schritt nach lauf.log im selben Ordner,
auch ein Lauf ohne Änderung. Eine Protokolldatei, die nur bei Erfolg
schreibt, lässt sich von einer nicht gestarteten Aufgabe nicht
unterscheiden, und genau das war am 2026-09-07 beim Depotbericht das
Problem.
Exit 0: gepusht oder nichts zu tun, in beiden Schritten. Exit 1: Befund in
einem Schritt, Text steht im Protokoll. Die Aufgabenplanung zeigt den Code
als „Letztes Ergebnis".
"""
from __future__ import annotations
import importlib.util
import sys
import traceback
from datetime import datetime
from pathlib import Path
HIER = Path(__file__).resolve().parent
PROTOKOLL = HIER / "lauf.log"
MAX_PROTOKOLL_ZEILEN = 400
def lade(name: str):
spec = importlib.util.spec_from_file_location(name, HIER / f"{name}.py")
if spec is None or spec.loader is None:
raise RuntimeError(f"{name}.py nicht ladbar: {HIER / f'{name}.py'}")
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def schreibe(zeile: str) -> None:
stempel = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
text = f"{stempel} {zeile}\n"
try:
alt = PROTOKOLL.read_text(encoding="utf-8").splitlines(keepends=True)
except OSError:
alt = []
alt.append(text)
PROTOKOLL.write_text("".join(alt[-MAX_PROTOKOLL_ZEILEN:]), encoding="utf-8")
sys.stdout.write(text)
def vault_push(hook, wurzel: Path) -> int:
try:
vorher = hook.git(wurzel, "rev-parse", "HEAD").strip()
hook.arbeite(wurzel)
nachher = hook.git(wurzel, "rev-parse", "HEAD").strip()
except hook.GitFehler as fehler:
schreibe(f"BEFUND {fehler.kopf} | {' '.join(fehler.rumpf.split())[:400]}")
return 1
except Exception as fehler: # noqa: BLE001
schreibe(f"BEFUND unerwarteter Abbruch: {type(fehler).__name__}: {fehler}")
schreibe(" " + " ".join(traceback.format_exc().split())[:400])
return 1
if vorher == nachher:
schreibe(f"ok nichts zu tun, HEAD {nachher[:8]}")
else:
schreibe(f"ok gepusht, HEAD {vorher[:8]} -> {nachher[:8]}")
return 0
def werkzeuge_spiegeln(wurzel: Path) -> int:
try:
spiegel = lade("werkzeuge_spiegel")
status, text = spiegel.spiegele(wurzel)
except Exception as fehler: # noqa: BLE001 - SpiegelFehler und jeder Absturz sind Befunde
schreibe(f"BEFUND werkzeuge: {type(fehler).__name__}: {' '.join(str(fehler).split())[:400]}")
return 1
schreibe(f"ok werkzeuge {status}, {text}")
return 0
def main() -> int:
try:
hook = lade("auto_push")
except Exception as fehler: # noqa: BLE001
schreibe(f"BEFUND auto_push.py nicht ladbar: {type(fehler).__name__}: {fehler}")
return 1
wurzel = HIER.parents[1]
sperre = wurzel / hook.SPERRE_REL
deskriptor = hook.hole_sperre(sperre)
if deskriptor is None:
# Kein Befund: ein anderer Lauf arbeitet und nimmt alles mit.
schreibe("uebersprungen Sperre belegt, ein anderer Lauf arbeitet")
return 0
try:
if vault_push(hook, wurzel) != 0:
return 1
return werkzeuge_spiegeln(wurzel)
finally:
hook.gib_sperre_frei(deskriptor, sperre)
if __name__ == "__main__":
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
raise SystemExit(main())
```