Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80a20f1c2f | ||
|
|
57e2f64f98 | ||
|
|
d06516097c | ||
|
|
521f346fc7 | ||
|
|
7ac3f7c3ea |
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
|
||||
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
|
||||
@@ -10,18 +11,23 @@ WARUM LOKAL UND NICHT ALS COWORK-ROUTINE
|
||||
nichts committen. Deshalb die Windows-Aufgabenplanung.
|
||||
|
||||
WAS ES TUT
|
||||
Genau dasselbe wie der PostToolUse-Hook: die Arbeitsfunktion aus
|
||||
auto_push.py, unter derselben Sperre. Der Umfang steht in der .gitignore
|
||||
des Vault-Roots, nicht hier.
|
||||
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 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.
|
||||
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. Exit 1: Befund, Text steht im
|
||||
Protokoll. Die Aufgabenplanung zeigt den Code als „Letztes Ergebnis".
|
||||
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
|
||||
@@ -37,10 +43,10 @@ PROTOKOLL = HIER / "lauf.log"
|
||||
MAX_PROTOKOLL_ZEILEN = 400
|
||||
|
||||
|
||||
def lade_hook():
|
||||
spec = importlib.util.spec_from_file_location("auto_push", HIER / "auto_push.py")
|
||||
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"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)
|
||||
spec.loader.exec_module(modul)
|
||||
return modul
|
||||
@@ -58,9 +64,40 @@ def schreibe(zeile: str) -> None:
|
||||
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_hook()
|
||||
hook = lade("auto_push")
|
||||
except Exception as fehler: # noqa: BLE001
|
||||
schreibe(f"BEFUND auto_push.py nicht ladbar: {type(fehler).__name__}: {fehler}")
|
||||
return 1
|
||||
@@ -74,25 +111,12 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
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])
|
||||
if vault_push(hook, wurzel) != 0:
|
||||
return 1
|
||||
return werkzeuge_spiegeln(wurzel)
|
||||
finally:
|
||||
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__":
|
||||
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: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
|
||||
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-09-17
|
||||
updated: 2026-10-09
|
||||
updated: 2026-10-13
|
||||
---
|
||||
|
||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||
@@ -26,6 +26,8 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
|
||||
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
|
||||
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert; seit 2026-10-09 mit `lo` oben, siehe Nachtrag unten) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, fehlen `ip`, `setpriv` oder `python3` oder kommt `lo` im Namensraum nicht hoch, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt oder ohne `lo` zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
|
||||
|
||||
9. **Korrektur als Ersetzungsliste (seit 2026-10-13):** `ersetzungen_einsetzen.py <planordner> <korrekturdatei> [--pruefen]` setzt eine Liste aus Abschnitten `## Ersetzung N — Titel` mit `Datei:`, `Alt:` und `Neu:` (Blöcke zwischen `~~~~text` und `~~~~`) der Reihe nach ein. Jeder Alt-Text muss zum Zeitpunkt seiner Ersetzung genau einmal in seiner Datei stehen, sonst schreibt das Skript gar nichts. Erst mit `--pruefen`, dann ohne; danach Schritt 4 und eine frisch gebaute Kopie. Die Korrekturdatei liest das Skript relativ zum aktuellen Ordner, nicht zum Planordner. Belegt am 2026-10-13 im Paket FP der Trading Plattform: 129 Ersetzungen in sechs Dateien, Prüflauf und Einsetzen ohne Befund.
|
||||
|
||||
## Was belegt ist
|
||||
|
||||
Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt 3 der Arbeitsweise vom 2026-09-26). Der Treiber fährt die 35 Angriffsfälle aus dem Entwurf auf einem kleinen Projekt unter `/tmp` und prüft nach jedem Fall, dass das Projekt byte-gleich zurückgesetzt ist. Am selben Tag hat der Koordinator neun Fehler in eine Kopie des Läufers eingebaut, und der Treiber hat jeden an seinem Fall gefangen:
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Setzt eine Ersetzungsliste eines Planers in die Taskdateien ein, alles oder nichts.
|
||||
|
||||
Format je Abschnitt (siehe Auftrag an den Planer):
|
||||
## Ersetzung N — Titel
|
||||
Datei: <taskdatei>
|
||||
Alt:
|
||||
~~~~text
|
||||
...
|
||||
~~~~
|
||||
Neu:
|
||||
~~~~text
|
||||
...
|
||||
~~~~
|
||||
Jeder Alt-Text muss zum Zeitpunkt seiner Ersetzung genau einmal in der Datei stehen. Bricht beim ersten
|
||||
Verstoss ab, ohne eine Datei zu schreiben. Mit --pruefen nur pruefen, nicht schreiben.
|
||||
Aufruf: python ersetzungen_einsetzen.py <planordner> <korrekturdatei> [--pruefen]
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ordner = Path(sys.argv[1])
|
||||
korrektur = Path(sys.argv[2])
|
||||
nur_pruefen = "--pruefen" in sys.argv[3:]
|
||||
|
||||
zeilen = korrektur.read_text(encoding="utf-8").split("\n")
|
||||
ersetzungen = []
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
if not zeilen[i].startswith("## Ersetzung "):
|
||||
i += 1
|
||||
continue
|
||||
titel = zeilen[i]
|
||||
i += 1
|
||||
datei = alt = neu = None
|
||||
while i < len(zeilen) and not zeilen[i].startswith("## Ersetzung "):
|
||||
z = zeilen[i]
|
||||
if z.startswith("Datei:"):
|
||||
datei = z.split(":", 1)[1].strip().strip("`")
|
||||
elif z.strip() in ("Alt:", "Neu:"):
|
||||
art = z.strip()[:-1]
|
||||
if i + 1 >= len(zeilen) or not zeilen[i + 1].startswith("~~~~"):
|
||||
sys.exit(f"{titel}: nach {art}: fehlt die Zeile ~~~~text")
|
||||
j = i + 2
|
||||
inhalt = []
|
||||
while j < len(zeilen) and zeilen[j].rstrip() != "~~~~":
|
||||
inhalt.append(zeilen[j])
|
||||
j += 1
|
||||
if j >= len(zeilen):
|
||||
sys.exit(f"{titel}: {art}-Block ohne schliessende ~~~~")
|
||||
text = "\n".join(inhalt)
|
||||
if art == "Alt":
|
||||
alt = text
|
||||
else:
|
||||
neu = text
|
||||
i = j
|
||||
i += 1
|
||||
if datei is None or alt is None or neu is None:
|
||||
sys.exit(f"{titel}: Datei, Alt oder Neu fehlt")
|
||||
ersetzungen.append((titel, datei, alt, neu))
|
||||
|
||||
texte: dict[str, str] = {}
|
||||
for titel, datei, alt, neu in ersetzungen:
|
||||
if datei not in texte:
|
||||
pfad = ordner / datei
|
||||
if not pfad.is_file():
|
||||
sys.exit(f"{titel}: Datei {datei} gibt es nicht")
|
||||
texte[datei] = pfad.read_text(encoding="utf-8")
|
||||
n = texte[datei].count(alt)
|
||||
if n != 1:
|
||||
sys.exit(f"{titel}: Alt-Text steht {n}-mal in {datei} statt einmal; nichts geschrieben")
|
||||
texte[datei] = texte[datei].replace(alt, neu)
|
||||
print(f"ok {titel} ({datei})")
|
||||
|
||||
print(f"{len(ersetzungen)} Ersetzungen in {len(texte)} Datei(en) geprueft")
|
||||
if nur_pruefen:
|
||||
print("nur geprueft, nichts geschrieben")
|
||||
else:
|
||||
for datei, text in texte.items():
|
||||
(ordner / datei).write_text(text, encoding="utf-8", newline="\n")
|
||||
print("geschrieben:", ", ".join(sorted(texte)))
|
||||
@@ -24,6 +24,7 @@ workflow({ scriptPath: "D:/Claude/.claude/workflows/quorum.js" }, { anlass: "kun
|
||||
|
||||
- Der Arbeitsbaum muss außerhalb von `docs/quorum/` sauber sein.
|
||||
- `suite` nur für eine Suite, die in neun Minuten durchläuft und nichts Unversioniertes schreibt; sonst die Suite selbst fahren und `suite_datei` nennen.
|
||||
- Aus `suite_datei` sehen die Stimmen nur die letzten 80 Zeilen (`SUITE_ZEILEN`) und die letzte Zeile `EXIT suite: N`. Die Zusammenfassung aller Messungen steht deshalb am Ende, direkt vor der `EXIT`-Zeile. Stand sie oben, meldeten alle drei Stimmen die Summe der Suite als nicht prüfbar (Trading Plattform, Quorum 6 am 2026-10-13).
|
||||
- `codex: false` setzt sofort Haiku ein, etwa wenn das Codex-Kontingent bekannt aufgebraucht ist.
|
||||
|
||||
## Bericht ablegen
|
||||
|
||||
@@ -113,7 +113,7 @@ KOEPFE = {
|
||||
SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")
|
||||
# Der erste Tag, den die Aufgabe abholt; None holt nichts. Gesetzt beim Einspielen von E9 (S8): Diese
|
||||
# Datei laeuft schon vorher jeden Abend, der Host kennt die Lesebefehle erst danach.
|
||||
SICHERUNG_ERSTER_TAG: date | None = None
|
||||
SICHERUNG_ERSTER_TAG: date | None = date(2026, 10, 13)
|
||||
# So viele Kalendertage vor heute fragt jeder Lauf, dazu heute. Das Fenster liegt in der Aufbewahrung
|
||||
# des Hosts: Er haelt die neuesten zehn Sicherungstage, nur werktags gesichert, also gut zwei Kalenderwochen.
|
||||
# Die Sicherung entsteht 16:45 New Yorker Zeit, in Berlin meist 22:45, vom 25.10. bis 31.10.2026
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
type: resource
|
||||
status: reference
|
||||
status: pending-review
|
||||
title: Codeworkflow
|
||||
summary: Arbeitsweise für Code-Projekte — Standardschleife, Planaufteilung je Task, Subagenten und ihre Modellstufen und Effort, Gegenprüfung, Lauf mit echten Daten, Aufgabenmanagement.
|
||||
tags: [regelwerk, codeworkflow, code-projekte]
|
||||
created: 2026-08-03
|
||||
updated: 2026-10-09
|
||||
updated: 2026-10-13
|
||||
---
|
||||
|
||||
# Codeworkflow
|
||||
@@ -83,7 +83,7 @@ Für jede Prüfung, die eine **offene Menge** einschränkt (verbotene Aktionen,
|
||||
|
||||
Faustregel: Lässt sich die Nachbesserung in einem Auftrag beschreiben, der für sich steht — geänderte Planstelle, Fehlerbild, erwartete Fehlermeldung —, dann ein frischer Subagent. Fortsetzen nur, wenn der Verlauf selbst gebraucht wird, etwa bei einer Fehlersuche über mehrere Runden.
|
||||
|
||||
**Nachbesserung nach einer ausgeführten Welle als Delta, nicht als Neufassung.** Der `planer` schreibt dann neue Taskdateien mit `Aus:`/`wird:`-Blöcken und für ältere Tasks eine Folgenliste. Die Regel für ihn steht in `.claude/agents/planer.md`, weil er das Regelwerk nicht lädt. Dein Teil: Die Plankritik liest nur die neuen Tasks und die Folgenliste (`materialisieren.tasks`). Nach dem Anwenden läuft `plan_bloecke.py nach` auf einer frisch gebauten Kopie. Die Folgenliste setzt ein Executer ein, danach prüfst du die Zeilenenden mit `git ls-files --eol` oder `file`, nicht mit `grep`. Und die `spec.json` des Rot-Nachweises bekommt die neuen Tasks. Gemessen im Hub am 2026-09-24: rund 20 Minuten je Runde schneller als die Neufassung, drei gegen drei Runden, nicht kontrolliert. Am 2026-09-23 waren beim Neuschreiben 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Für die Korrektur nach der Plankritik vor der ersten Ausführung ist das Delta nicht belegt.
|
||||
**Nachbesserung nach einer ausgeführten Welle als Delta, nicht als Neufassung.** Der `planer` schreibt dann neue Taskdateien mit `Aus:`/`wird:`-Blöcken und für ältere Tasks eine Folgenliste. Die Regel für ihn steht in `.claude/agents/planer.md`, weil er das Regelwerk nicht lädt. Dein Teil: Die Plankritik liest nur die neuen Tasks und die Folgenliste (`materialisieren.tasks`). Nach dem Anwenden läuft `plan_bloecke.py nach` auf einer frisch gebauten Kopie. Die Folgenliste setzt ein Executer ein, danach prüfst du die Zeilenenden mit `git ls-files --eol` oder `file`, nicht mit `grep`. Und die `spec.json` des Rot-Nachweises bekommt die neuen Tasks. Gemessen im Hub am 2026-09-24: rund 20 Minuten je Runde schneller als die Neufassung, drei gegen drei Runden, nicht kontrolliert. Am 2026-09-23 waren beim Neuschreiben 10 von 17 Befunden der Nachprüfung durch das Umschreiben selbst entstanden. Auch die Korrektur nach der Plankritik vor der ersten Ausführung geht als Delta: Der `planer` liefert eine Ersetzungsliste im Format von `.vault-tools/plan-kopie/ersetzungen_einsetzen.py` (Gebrauch in `LIESMICH.md` dort, Schritt 9) und schreibt keine Datei neu; du setzt sie nach dem Prüflauf ein und baust die Kopie neu. Beleg vom 2026-10-13 („Trading Plattform“, Paket FP, vier Taskdateien mit je rund 1300 Zeilen): 129 Ersetzungen in sechs Dateien, Prüflauf ohne Befund, jede der 73 roten Zeilen des Plans beim ersten Messen wörtlich; nicht kontrolliert.
|
||||
|
||||
**Die Nachbesserung folgt derselben Reihenfolge wie der Task selbst:** erst die neuen Tests, Fehlschlag im Wortlaut beobachten lassen, dann die Implementierung. Wer beides gleichzeitig schreiben lässt, bekommt einen grünen Lauf ohne Beleg, dass die neuen Tests je etwas gemessen haben.
|
||||
|
||||
|
||||
@@ -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."
|
||||
tags: [regelwerk, git, veroeffentlichung, werkzeuge]
|
||||
created: 2026-09-17
|
||||
updated: 2026-09-23
|
||||
updated: 2026-10-14
|
||||
---
|
||||
|
||||
# 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` |
|
||||
| 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
- [[CLAUDE|↑ Claude.md (Stamm)]]
|
||||
|
||||
@@ -5,7 +5,7 @@ status: pending-review
|
||||
summary: "Verbindliche Regeln für Befehle und Skripte auf Pascals Windows-Maschine: welche Shell, welches Python, Kodierung, Zeilenenden, Quoting-Schichten, WSL und ssh."
|
||||
tags: [regelwerk, windows, werkzeuge]
|
||||
created: 2026-09-13
|
||||
updated: 2026-10-07
|
||||
updated: 2026-10-14
|
||||
---
|
||||
|
||||
# Skripte und Shellbefehle unter Windows
|
||||
@@ -19,6 +19,7 @@ Die Fallgeschichten stehen im Harness-Gedächtnis. Der Name der Erinnerung steht
|
||||
- **Nie implizit `bash` aufrufen**, weder aus Python noch aus PowerShell. Windows sucht einen Programmnamen ohne Pfad zuerst in `System32`. Dort liegt der WSL-Starter, und seit dem 2026-09-03 startet er die Debian-Distribution statt der Git-Bash. Git-Bash sprichst du mit vollem Pfad an: `C:\Program Files\Git\bin\bash.exe`, nicht `…\usr\bin\`, weil der seine Coreutils nicht findet. WSL sprichst du mit `wsl -d <Distro>` an. `shutil.which("bash")` taugt nicht als Prüfung. (`bash-startet-unter-windows-den-wsl-stub`)
|
||||
- **`python`, nicht `python3`.** `python3` ist hier der Microsoft-Store-Stub.
|
||||
- **`PYTHONIOENCODING=utf-8`** vor jedem Python-Aufruf, der Umlaute oder Sonderzeichen ausgibt. Die Konsole fährt cp1252, und ohne die Variable bricht ein Skript mit `UnicodeEncodeError` ab oder zerlegt die Ausgabe.
|
||||
- ⚠ Mit `python -I` wirkt die Variable nicht: `-I` schließt `-E` ein und ignoriert alle `PYTHON*`-Variablen. Dann `python -I -X utf8`. (`python-isoliert-ignoriert-pythonioencoding`)
|
||||
|
||||
## Skripte mit Prosa, Regex oder Backslashes
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ Jede Datei triggert nur den betreffenden Teil — nicht bei jeder Aufgabe alles
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user