Auto-Push: Werkzeuge täglich nach claude-werkzeuge spiegeln
taeglich_push.py ruft nach dem Vault-Push werkzeuge_spiegel.py auf: committeter Stand von .vault-tools/ und .claude/ ohne Trading-Werkzeuge und watermarks-remover, README aus werkzeuge-README.md. Fallkatalog 14 von 14, drei Mutanten erkannt. veroeffentlichung.md und Stamm nachgezogen; Gitea seit heute anonym lesbar. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d06516097c
commit
57e2f64f98
7 files changed
+1243
-33
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())
|
||||||
|
```
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
"""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())
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Täglicher Lauf: committet und pusht die Anweisungsschicht, für die Windows-Aufgabenplanung.
|
"""Täglicher Lauf: committet und pusht die Anweisungsschicht und spiegelt die Werkzeuge.
|
||||||
|
|
||||||
Ersteller: Pascal Michel
|
Ersteller: Pascal Michel
|
||||||
Erstellt: 2026-09-17
|
Erstellt: 2026-09-17
|
||||||
|
Geändert: 2026-10-14 (Werkzeug-Spiegel nach claude-werkzeuge)
|
||||||
|
|
||||||
WARUM LOKAL UND NICHT ALS COWORK-ROUTINE
|
WARUM LOKAL UND NICHT ALS COWORK-ROUTINE
|
||||||
Eine Cowork-Routine läuft in Anthropics Cloud und hat weder Zugriff auf
|
Eine Cowork-Routine läuft in Anthropics Cloud und hat weder Zugriff auf
|
||||||
@@ -10,18 +11,23 @@ WARUM LOKAL UND NICHT ALS COWORK-ROUTINE
|
|||||||
nichts committen. Deshalb die Windows-Aufgabenplanung.
|
nichts committen. Deshalb die Windows-Aufgabenplanung.
|
||||||
|
|
||||||
WAS ES TUT
|
WAS ES TUT
|
||||||
Genau dasselbe wie der PostToolUse-Hook: die Arbeitsfunktion aus
|
1. Genau dasselbe wie der frühere PostToolUse-Hook: die Arbeitsfunktion
|
||||||
auto_push.py, unter derselben Sperre. Der Umfang steht in der .gitignore
|
aus auto_push.py, unter derselben Sperre. Der Umfang steht in der
|
||||||
des Vault-Roots, nicht hier.
|
.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
|
WAS ES MELDET
|
||||||
Jeder Lauf schreibt eine Zeile nach lauf.log im selben Ordner — auch ein
|
Jeder Lauf schreibt eine Zeile je Schritt nach lauf.log im selben Ordner,
|
||||||
Lauf ohne Änderung. Eine Protokolldatei, die nur bei Erfolg schreibt,
|
auch ein Lauf ohne Änderung. Eine Protokolldatei, die nur bei Erfolg
|
||||||
lässt sich von einer nicht gestarteten Aufgabe nicht unterscheiden, und
|
schreibt, lässt sich von einer nicht gestarteten Aufgabe nicht
|
||||||
genau das war am 2026-09-07 beim Depotbericht das Problem.
|
unterscheiden, und genau das war am 2026-09-07 beim Depotbericht das
|
||||||
|
Problem.
|
||||||
|
|
||||||
Exit 0: gepusht oder nichts zu tun. Exit 1: Befund, Text steht im
|
Exit 0: gepusht oder nichts zu tun, in beiden Schritten. Exit 1: Befund in
|
||||||
Protokoll. Die Aufgabenplanung zeigt den Code als „Letztes Ergebnis".
|
einem Schritt, Text steht im Protokoll. Die Aufgabenplanung zeigt den Code
|
||||||
|
als „Letztes Ergebnis".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -37,10 +43,10 @@ PROTOKOLL = HIER / "lauf.log"
|
|||||||
MAX_PROTOKOLL_ZEILEN = 400
|
MAX_PROTOKOLL_ZEILEN = 400
|
||||||
|
|
||||||
|
|
||||||
def lade_hook():
|
def lade(name: str):
|
||||||
spec = importlib.util.spec_from_file_location("auto_push", HIER / "auto_push.py")
|
spec = importlib.util.spec_from_file_location(name, HIER / f"{name}.py")
|
||||||
if spec is None or spec.loader is None:
|
if spec is None or spec.loader is None:
|
||||||
raise RuntimeError(f"auto_push.py nicht ladbar: {HIER / 'auto_push.py'}")
|
raise RuntimeError(f"{name}.py nicht ladbar: {HIER / f'{name}.py'}")
|
||||||
modul = importlib.util.module_from_spec(spec)
|
modul = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(modul)
|
spec.loader.exec_module(modul)
|
||||||
return modul
|
return modul
|
||||||
@@ -58,9 +64,40 @@ def schreibe(zeile: str) -> None:
|
|||||||
sys.stdout.write(text)
|
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:
|
def main() -> int:
|
||||||
try:
|
try:
|
||||||
hook = lade_hook()
|
hook = lade("auto_push")
|
||||||
except Exception as fehler: # noqa: BLE001
|
except Exception as fehler: # noqa: BLE001
|
||||||
schreibe(f"BEFUND auto_push.py nicht ladbar: {type(fehler).__name__}: {fehler}")
|
schreibe(f"BEFUND auto_push.py nicht ladbar: {type(fehler).__name__}: {fehler}")
|
||||||
return 1
|
return 1
|
||||||
@@ -74,25 +111,12 @@ def main() -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vorher = hook.git(wurzel, "rev-parse", "HEAD").strip()
|
if vault_push(hook, wurzel) != 0:
|
||||||
hook.arbeite(wurzel)
|
return 1
|
||||||
nachher = hook.git(wurzel, "rev-parse", "HEAD").strip()
|
return werkzeuge_spiegeln(wurzel)
|
||||||
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
|
|
||||||
finally:
|
finally:
|
||||||
hook.gib_sperre_frei(deskriptor, sperre)
|
hook.gib_sperre_frei(deskriptor, sperre)
|
||||||
|
|
||||||
if vorher == nachher:
|
|
||||||
schreibe(f"ok nichts zu tun, HEAD {nachher[:8]}")
|
|
||||||
else:
|
|
||||||
schreibe(f"ok gepusht, HEAD {vorher[:8]} -> {nachher[:8]}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""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}"
|
||||||
@@ -5,7 +5,7 @@ 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."
|
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]
|
tags: [regelwerk, git, veroeffentlichung, werkzeuge]
|
||||||
created: 2026-09-17
|
created: 2026-09-17
|
||||||
updated: 2026-09-23
|
updated: 2026-10-14
|
||||||
---
|
---
|
||||||
|
|
||||||
# Veröffentlichung der Anweisungsschicht
|
# Veröffentlichung der Anweisungsschicht
|
||||||
@@ -18,7 +18,8 @@ Angeordnet am 2026-09-17. Die Anweisungsschicht des Vaults liegt seitdem in eine
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Remote | `https://git.nexhorizons.de/Nexhorizons-Public/claude-regelwerk.git` |
|
| Remote | `https://git.nexhorizons.de/Nexhorizons-Public/claude-regelwerk.git` |
|
||||||
| Zweig | `main` |
|
| Zweig | `main` |
|
||||||
| Sichtbarkeit | Organisation `Nexhorizons-Public` und Repo sind beide public. **Anonym trotzdem gesperrt:** Die Instanz verlangt für jede Ansicht eine Anmeldung (`REQUIRE_SIGNIN_VIEW`), gemessen am 2026-09-17 mit 403 auf der API und einer Weiterleitung auf den Login im Web. Das lässt sich nur in der `app.ini` am Server ändern, nicht über die API. |
|
| Sichtbarkeit | Organisation `Nexhorizons-Public` und Repo sind beide public und **ohne Anmeldung lesbar**, seit Pascal am 2026-10-14 `REQUIRE_SIGNIN_VIEW` in der `app.ini` geändert hat (gemessen: Web und API `200`, anonymer `git clone` geht). Vorher Weiterleitung auf den Login und `403` auf der API. Jedes public Repo der Instanz ist damit anonym lesbar. |
|
||||||
|
| Zweites Repo | `https://git.nexhorizons.de/Nexhorizons-Public/claude-werkzeuge.git`, Spiegel der Werkzeuge, siehe „Werkzeug-Spiegel" unten |
|
||||||
|
|
||||||
## Was hinausgeht — und warum die Liste so herum steht
|
## Was hinausgeht — und warum die Liste so herum steht
|
||||||
|
|
||||||
@@ -68,6 +69,17 @@ Belegt am 2026-09-17: Lauf aus der Aufgabenplanung heraus, Ergebnis `0x0`, Proto
|
|||||||
|
|
||||||
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 ausgelöst wird und ob die Positivliste hält, und nicht, ob eine eingeschlossene Datei etwas enthält, das niemand veröffentlichen wollte.
|
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 ausgelöst wird und ob die Positivliste hält, und nicht, ob eine eingeschlossene Datei etwas enthält, das niemand veröffentlichen wollte.
|
||||||
|
|
||||||
|
## Werkzeug-Spiegel `claude-werkzeuge`
|
||||||
|
|
||||||
|
Angeordnet am 2026-10-14. Nach dem Vault-Push spiegelt derselbe 04:00-Lauf die Werkzeuge in ein zweites öffentliches Repo.
|
||||||
|
|
||||||
|
- **Inhalt:** der committete Stand von `.vault-tools/` und `.claude/`, ohne `.vault-tools/trading-plattform-pruefung/` und `.vault-tools/watermarks-remover/`. Dazu `README.md` aus `.vault-tools/auto-push/werkzeuge-README.md`.
|
||||||
|
- **Neue Werkzeuge gehen automatisch mit.** Die Ausschlüsse sind eine Negativliste in `werkzeuge_spiegel.py` (`AUSSCHLUESSE`), so angeordnet („alle Werkzeuge außer Trading"). Ein neues Werkzeug des Trading-Projekts unter `.vault-tools/` trägst du dort ein, **bevor** es committet wird.
|
||||||
|
- **Klon:** `D:\ClaudeData\claude-werkzeuge`, außerhalb des Vaults. Der Lauf leert ihn bis auf `.git` und packt neu aus. Er weigert sich, wenn `origin` nicht genau auf das Spiegel-Repo zeigt oder der Ordner im Vault liegt.
|
||||||
|
- **Das Repo ist ein Spiegel.** Änderungen direkt in `claude-werkzeuge` überschreibt der nächste Lauf.
|
||||||
|
- **Protokoll:** eine zweite Zeile je Lauf in `lauf.log` (`ok werkzeuge gespiegelt|unveraendert` oder `BEFUND werkzeuge: …`). Scheitert der Vault-Push, entfällt der Spiegel.
|
||||||
|
- **Fallkatalog:** `python .vault-tools/auto-push/faelle_werkzeuge_spiegel.py`, Wegwerf-Vaults mit echten bare-Remotes. Fangnetz, kein Nachweis.
|
||||||
|
|
||||||
## Referenzen
|
## Referenzen
|
||||||
|
|
||||||
- [[CLAUDE|↑ Claude.md (Stamm)]]
|
- [[CLAUDE|↑ Claude.md (Stamm)]]
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ Jede Datei triggert nur den betreffenden Teil — nicht bei jeder Aufgabe alles
|
|||||||
|
|
||||||
## Veröffentlichung
|
## Veröffentlichung
|
||||||
|
|
||||||
Der Vault-Root ist ein Git-Repo und pusht **täglich um 04:00** nach `https://git.nexhorizons.de/Nexhorizons-Public/claude-regelwerk.git` — über die Windows-Aufgabe `Claude-Vault Auto-Push`, nicht über eine Cloud-Routine. Veröffentlicht wird nur die Anweisungsschicht: `CLAUDE.md`, `README.md`, `AI/Regelwerk/`, `.claude/agents/`, `.claude/skills/`, `.claude/workflows/`, `.claude/settings.json` und `.vault-tools/`.
|
Der Vault-Root ist ein Git-Repo und pusht **täglich um 04:00** nach `https://git.nexhorizons.de/Nexhorizons-Public/claude-regelwerk.git` — über die Windows-Aufgabe `Claude-Vault Auto-Push`, nicht über eine Cloud-Routine. Veröffentlicht wird nur die Anweisungsschicht: `CLAUDE.md`, `README.md`, `AI/Regelwerk/`, `.claude/agents/`, `.claude/skills/`, `.claude/workflows/`, `.claude/settings.json` und `.vault-tools/`. Derselbe Lauf spiegelt danach `.vault-tools/` und `.claude/` ohne die Trading-Werkzeuge nach `claude-werkzeuge`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user