Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen

Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore
arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur,
was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen,
das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor.

Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale
Bezeichnung ersetzt; die Regeln selbst sind unverändert.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-17 22:38:15 +02:00
commit c064c729ed
82 files changed
+13428

No files matched your search

@@ -0,0 +1,309 @@
"""Einstiegspunkt. Hier und nur hier wird geschrieben."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from . import todo_datei, zustand as zustand_modul
from .abgleich import (
Basiseintrag,
WorkItem,
berechne,
item_erledigt,
)
from .api import ApiFehler, PlaneApi
from .bericht import schreibe_bericht, sichere_ausgabe
from .konfiguration import KonfigurationsFehler, lade_konfiguration
def baue_api(konfiguration) -> PlaneApi:
return PlaneApi(konfiguration)
def sammle_projekte(quelle: Path) -> list[Path]:
"""Alle Projektordner mit einer todo.md, alphabetisch."""
if not quelle.exists():
return []
return sorted(
ordner
for ordner in quelle.iterdir()
if ordner.is_dir() and (ordner / "todo.md").exists()
)
def lade_work_items(api, projekt_id: str, identifier: str) -> tuple[list[WorkItem], dict]:
"""Wandelt Plane-Rohdaten in WorkItem um. Braucht die States für die Gruppe."""
gruppe_je_state = {s["id"]: s.get("group", "") for s in api.states(projekt_id)}
items = []
for roh in api.work_items(projekt_id):
items.append(
WorkItem(
id=roh["id"],
marker=f"{identifier}-{roh['sequence_id']}",
titel=roh.get("name", ""),
erledigt=item_erledigt(gruppe_je_state.get(roh.get("state"), "")),
)
)
return items, gruppe_je_state
def _argumente(argumente: list[str]) -> argparse.Namespace:
zerleger = argparse.ArgumentParser(prog="plane.cli", add_help=True)
zerleger.add_argument("--anwenden", action="store_true")
zerleger.add_argument("--nur-projekt", dest="nur_projekt", default=None)
zerleger.add_argument("--projekte", action="store_true")
zerleger.add_argument("--projekt-anlegen", dest="projekt_anlegen", default=None)
zerleger.add_argument("--kennung", default=None)
zerleger.add_argument("--konflikt", action="append", default=[])
return zerleger.parse_args(argumente)
def hauptprogramm(argumente, umgebung, api_bauer=baue_api, strom=None) -> int:
strom = strom if strom is not None else sys.stdout
optionen = _argumente(argumente)
try:
konfiguration = lade_konfiguration(umgebung)
except KonfigurationsFehler as fehler:
sichere_ausgabe(strom, str(fehler))
return 2
api = api_bauer(konfiguration)
try:
if optionen.projekte:
return _liste_projekte(api, strom)
if optionen.projekt_anlegen:
return _lege_projekt_an(api, optionen, strom)
return _abgleich_lauf(api, konfiguration, optionen, strom)
except ApiFehler as fehler:
sichere_ausgabe(strom, str(fehler))
return 3
def _entscheidungen(rohe: list[str]) -> dict[str, str]:
ergebnis: dict[str, str] = {}
for eintrag in rohe:
marker, trenner, seite = eintrag.partition("=")
if not trenner or seite.strip().lower() not in ("vault", "plane"):
raise ValueError(
f"--konflikt erwartet MARKER=vault oder MARKER=plane, "
f"bekommen habe ich „{eintrag}“."
)
ergebnis[marker.strip()] = seite.strip().lower()
return ergebnis
def _abgleich_lauf(api, konfiguration, optionen, strom) -> int:
try:
entscheidungen = _entscheidungen(optionen.konflikt)
except ValueError as fehler:
sichere_ausgabe(strom, str(fehler))
return 2
projekte_nach_id = {p["id"]: p for p in api.projekte()}
alle_laeufe = []
for ordner in sammle_projekte(konfiguration.quelle):
if optionen.nur_projekt and optionen.nur_projekt.lower() not in ordner.name.lower():
continue
datei = todo_datei.lies(ordner / "todo.md")
projekt_id = todo_datei.plane_projekt(datei.frontmatter)
if not projekt_id:
sichere_ausgabe(strom, "")
sichere_ausgabe(strom, f"=== {ordner.name} ===")
sichere_ausgabe(
strom,
" UEBERSPRUNGEN - im Frontmatter der todo.md fehlt plane_project. "
"Mit --projekte die Plane-Projekte auflisten und die UUID eintragen.",
)
continue
projekt = projekte_nach_id.get(projekt_id)
if projekt is None:
sichere_ausgabe(strom, "")
sichere_ausgabe(strom, f"=== {ordner.name} ===")
sichere_ausgabe(
strom,
f" UEBERSPRUNGEN - das Plane-Projekt {projekt_id} gibt es nicht "
f"(oder der Schluessel sieht es nicht).",
)
continue
items, _ = lade_work_items(api, projekt_id, projekt["identifier"])
zustand = zustand_modul.lies(ordner / zustand_modul.DATEINAME)
if zustand.warnung:
sichere_ausgabe(strom, f" HINWEIS: {zustand.warnung}")
aktionen = berechne(datei.aufgaben, items, zustand.eintraege)
alle_laeufe.append((ordner, datei, projekt, aktionen, zustand))
schreibe_bericht(strom, ordner.name, aktionen, anwenden=optionen.anwenden)
if not optionen.anwenden:
return 0
bekannte = {a.marker for _, _, _, aktionen, _ in alle_laeufe for a in aktionen if a.marker}
unbekannt = sorted(set(entscheidungen) - bekannte)
if unbekannt:
sichere_ausgabe(
strom,
"Abbruch, nichts geschrieben: --konflikt nennt Marker, die in diesem Lauf "
"nicht vorkommen: " + ", ".join(unbekannt),
)
return 2
for ordner, datei, projekt, aktionen, zustand in alle_laeufe:
_wende_an(api, projekt, ordner, datei, aktionen, zustand, entscheidungen, strom)
return 0
def _wende_an(api, projekt, ordner, datei, aktionen, zustand, entscheidungen, strom) -> None:
"""Schritt 1: nach Plane schreiben, Zustand je Aufruf fortschreiben.
Schritt 2: todo.md einmal komplett. Schritt 3: Laufzeit setzen."""
from .abgleich import (
ANHAENGEN,
ANLEGEN,
ITEM_ENTFERNT,
KONFLIKT,
NACH_PLANE,
NACH_VAULT,
status_fuer,
)
states = api.states(projekt["id"])
zustand.plane_projekt = projekt["id"]
aenderungen: dict[int, tuple[str, bool, str | None]] = {}
neue_zeilen: list[str] = []
for aktion in aktionen:
seite = entscheidungen.get(aktion.marker or "")
if aktion.art == ANLEGEN:
roh = api.work_item_anlegen(
projekt["id"],
aktion.vault.text,
status_fuer(states, aktion.vault.erledigt),
)
marker = f"{projekt['identifier']}-{roh['sequence_id']}"
zustand.setze(
Basiseintrag(marker, roh["id"], aktion.vault.text, aktion.vault.erledigt)
)
aenderungen[aktion.vault.zeilennummer] = (
aktion.vault.text,
aktion.vault.erledigt,
marker,
)
elif aktion.art == NACH_PLANE or (aktion.art == KONFLIKT and seite == "vault"):
api.work_item_aendern(
projekt["id"],
aktion.plane.id,
{
"name": aktion.vault.text,
"state": status_fuer(states, aktion.vault.erledigt),
},
)
zustand.setze(
Basiseintrag(
aktion.marker, aktion.plane.id, aktion.vault.text, aktion.vault.erledigt
)
)
elif aktion.art == NACH_VAULT or (aktion.art == KONFLIKT and seite == "plane"):
aenderungen[aktion.vault.zeilennummer] = (
aktion.plane.titel,
aktion.plane.erledigt,
aktion.marker,
)
zustand.setze(
Basiseintrag(
aktion.marker, aktion.plane.id, aktion.plane.titel, aktion.plane.erledigt
)
)
elif aktion.art == ANHAENGEN:
neue_zeilen.append(
todo_datei.baue_zeile(
"", aktion.plane.titel, aktion.plane.erledigt, aktion.marker
)
)
zustand.setze(
Basiseintrag(
aktion.marker, aktion.plane.id, aktion.plane.titel, aktion.plane.erledigt
)
)
elif aktion.art == ITEM_ENTFERNT:
aenderungen[aktion.vault.zeilennummer] = (
aktion.vault.text,
aktion.vault.erledigt,
None,
)
zustand.entferne_marker(aktion.marker)
if aenderungen or neue_zeilen:
inhalt = todo_datei.schreibe(datei, aenderungen, neue_zeilen)
(ordner / "todo.md").write_text(inhalt, encoding="utf-8")
zustand.setze_lauf_zeit()
def _liste_projekte(api, strom) -> int:
sichere_ausgabe(strom, "Plane-Projekte im Workspace:")
for projekt in sorted(api.projekte(), key=lambda p: p.get("name", "")):
sichere_ausgabe(
strom,
f" {projekt.get('identifier', '?'):<12} {projekt.get('name', '')} "
f"{projekt['id']}",
)
sichere_ausgabe(
strom,
"Die passende UUID als plane_project ins Frontmatter der todo.md eintragen.",
)
return 0
def _lege_projekt_an(api, optionen, strom) -> int:
if not optionen.kennung:
sichere_ausgabe(
strom, "--projekt-anlegen braucht zusaetzlich --kennung (z. B. --kennung PLA)."
)
return 2
name = optionen.projekt_anlegen
vorhanden = [p for p in api.projekte() if p.get("name", "").strip() == name.strip()]
if vorhanden:
sichere_ausgabe(
strom,
f"Abbruch: In Plane gibt es bereits ein Projekt „{name}“ "
f"({vorhanden[0]['id']}). Nichts angelegt.",
)
return 2
if not optionen.anwenden:
sichere_ausgabe(
strom,
f"Probelauf - anlegen wuerde: Projekt „{name}“ "
f"mit Kennung {optionen.kennung.upper()}. Mit --anwenden ausfuehren.",
)
return 0
neu = api.projekt_anlegen(name, optionen.kennung)
sichere_ausgabe(
strom,
f"Angelegt: „{name}“, Kennung {neu.get('identifier')}, "
f"UUID {neu.get('id')}.",
)
sichere_ausgabe(
strom,
"Diese UUID als plane_project ins Frontmatter der passenden todo.md eintragen.",
)
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(hauptprogramm(sys.argv[1:], None))