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:
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())
|
||||
```
|
||||
Reference in new issue
Block a user