Compare commits

...
5 Commits
Author SHA1 Message Date
Pascal MichelandClaude Opus 5 80a20f1c2f Automatischer Push: AI/Regelwerk/windows-shell.md
- AI/Regelwerk/windows-shell.md

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-15 04:00:01 +02:00
Pascal MichelandClaude Opus 5.5 57e2f64f98 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]>
2026-10-14 11:51:42 +02:00
Pascal MichelandClaude Opus 5.5 d06516097c Codeworkflow: Korrektur nach der Plankritik als Ersetzungsliste (pending-review)
Beleg aus dem Paket FP der Trading Plattform: 129 Ersetzungen in sechs Dateien ohne Bruch. Das
Werkzeug ersetzungen_einsetzen.py liegt jetzt unter .vault-tools/plan-kopie/ (byte-gleich aus der
B4-Uebergabe), Gebrauch in LIESMICH.md, Schritt 9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
2026-10-13 18:24:45 +02:00
Pascal MichelandClaude Opus 5.5 521f346fc7 Quorum-LIESMICH: Zusammenfassung der suite_datei ans Ende, die Stimmen sehen nur die letzten 80 Zeilen
Beleg: Trading Plattform, Quorum 6 am 2026-10-13. Die Zusammenfassung stand oben, die letzten
80 Zeilen zeigten nur Vault-Treiber und Registry-Gegenprobe; alle drei Stimmen meldeten die
pytest-Summe als nicht pruefbar.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
2026-10-13 12:37:38 +02:00
Pascal MichelandClaude Opus 5.5 7ac3f7c3ea Trading-Plattform-Pruefung: SICHERUNG_ERSTER_TAG auf den 2026-10-13 (E9 eingespielt)
Schritt 6 der Administrator-Anleitung, "Sicherung nach D: einspielen": derselbe Tag wie
/var/lib/trading-sicherung/erster-tag auf dem Docker-Host. Ab heute holt die Windows-Aufgabe
die Sicherung beider Instanzen nach D:\Trading-Sicherung. Quorum 5 (Trading Plattform,
docs/quorum/2026-10-13-einspielen.md) frei.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
2026-10-13 05:43:25 +02:00
13 changed files with 1333 additions and 38 deletions

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())
+53 -29
View File
@@ -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.
+172
View File
@@ -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}"
+3 -1
View File
@@ -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)))
+1
View File
@@ -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
+3 -3
View File
@@ -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.
+14 -2
View File
@@ -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)]]
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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.