Projektwissen der AG Prozesse aus AG_QGIS/Projektwissen abgeleitet

Übernommen ohne Historie aus AG_QGIS/Projektwissen (main @ decd2a0),
Skill sn-bpmn vom Branch BPMN-Skill @ 6efcac4 (ADR 0001).

- Skripte hallo.py, gitea.py, setup_workspace.py auf die Organisation
  AG_Prozesse umgestellt; das Workspace-Repo zählt über [workspace] repo
  als Ticket-Repo, auch ohne weitere [[repos]].
- pruefe_umlaute.py prüft jetzt die .py-Dateien des Workspace selbst.
- Branchmodell main plus Arbeitsbranches (ADR 0002).
- AGENTS.md, README, Leitfäden, Skills, Vorlagen, Fallstricke und Glossar
  auf die Prozessmodellierung zugeschnitten; QGIS- und Plugin-Spezifisches
  entfernt.
- Tests: python -m pytest -q .agents/skills -> 32 bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 12:34:37 +02:00
co-authored by Claude Opus 5.5
commit 012509b6d1
36 changed files with 4414 additions and 0 deletions
@@ -0,0 +1,56 @@
---
name: sn-aufgabe-bearbeiten
description: Standardablauf für eine Aufgabe im Projektwissen der AG Prozesse (Konverter md_to_bpmn.py, Skills, Skripte, Doku, Prozessmodelle) von Gitea-Issue über Branch, Umsetzung und Tests bis zum vorbereiteten Pull Request mit Übergabenotiz. Verwenden bei "bearbeite Issue", "setze um", "Bugfix", "neue Funktion".
---
# Aufgabe bearbeiten
Für das reine Modellieren eines Prozesses gilt der Skill `sn-bpmn`. Dieser Ablauf gilt, wenn etwas im Repository
geändert und per Pull Request zurückgeführt wird.
## 0. Stand holen (nicht überspringen)
```bash
python scripts/hallo.py
```
Holt den Serverstand und aktualisiert `main` per Fast-Forward, wo das gefahrlos geht.
Ohne diesen Schritt wird womöglich etwas behoben, das längst behoben ist.
## 1. Auftrag verstehen (nicht überspringen)
- Issue **mit Kommentaren** lesen – Skill `sn-ticket`, `python scripts/gitea.py ticket Projektwissen <nr>`.
Label `Diskussion` = noch kein Konsens: Annahmen im PR kennzeichnen.
- Ziel, Abnahmekriterien und Grenzen in drei Sätzen zurückspiegeln, bevor etwas geändert wird.
Fehlt ein Abnahmekriterium, ist das die erste Rückfrage – nicht der erste Commit.
## 2. Kontext laden – gezielt
- `docs/status/STATUS.md` (wer arbeitet woran), bei Überraschungen `docs/wissen/fallstricke.md`.
- Am Konverter: `grep -n "def \|class " .agents/skills/sn-bpmn/md_to_bpmn.py` für die Struktur, dann nur die
betroffenen Abschnitte lesen. Die Datei hat über 1000 Zeilen.
- Fachliche Regeln: `.agents/skills/sn-bpmn/Modellierregeln.md`, `Elementkatalog.md`, `Anleitung.md`.
## 3. Branch
Immer von `main`. Vollständig: `docs/leitfaden/05_git_arbeitsweise.md`.
```bash
git switch main && git pull --ff-only
git switch -c feature/<issue-nr>-<kurzname> # oder bugfix/… ; klein, ASCII, mit Nummer
```
## 4. Umsetzen
- Kleine Schritte, jeder für sich prüfbar. Nach jedem Schritt `python -m pytest -q .agents/skills`.
- Regeln aus `AGENTS.md`: nur Standardbibliothek, kein Umbau nebenbei, Stil der Datei beibehalten, Deutsch mit
echten Umlauten.
- **Neues Verhalten des Konverters bekommt einen Test** in `.agents/skills/sn-bpmn/test_md_to_bpmn.py` – pytest,
ohne Netzwerk, ein Test je Verhalten. Bei einem Bugfix zuerst den Test schreiben, der den Fehler zeigt.
- **Jeder neue oder geänderte Test bekommt einen deutschen Docstring**, erste Zeile `"""Prüft, dass …"""`.
- Ändert sich die Markdown-Syntax oder ein Element: `Anleitung.md` bzw. `Elementkatalog.md` mitziehen.
## 5. Nachweis
- `python -m pytest -q .agents/skills` → alles grün.
- `python scripts/pruefe_umlaute.py` → keine Funde.
- Ändert sich die erzeugte BPMN-Datei: ein Beispiel erzeugen und in PICTURE importieren. Kannst du das nicht
selbst, im PR als offenen Punkt eintragen. **Nicht als erledigt ausgeben.**
## 6. Abschluss
- Commit-Nachrichten deutsch, Präsens, erste Zeile ≤ 72 Zeichen, Issue-Nummer am Ende.
- Pull-Request-Text nach `docs/vorlagen/pull_request.md` vorbereiten; Übergabenotiz mit Skill `sn-uebergabe`.
- **Hier ist Schluss.** Push, Pull Request, Merge und alles auf Gitea macht der Mensch.
+229
View File
@@ -0,0 +1,229 @@
# Markdown → BPMN 2.0 für PICTURE
`md_to_bpmn.py` wandelt eine einfache textuelle Prozessbeschreibung (`.md`)
in eine standardkonforme BPMN-2.0-XML-Datei um, die sich über den
BPMN-Import in PICTURE laden lässt. BPMN 2.0 ist ein offener, seit 2011
stabiler OMG-Standard – das Skript muss dafür kein PICTURE-internes
Schema "lernen" und erzeugt keine PICTURE-spezifischen UUIDs.
## Empfohlener Arbeitsablauf bei unstrukturiertem Ausgangstext
1. Rohtext (Fließtext, Stichpunkte, Sitzungsnotizen o. ä.) bereitstellen.
2. Daraus wird eine strukturierte Arbeitsschritt-`.md` in der unten
beschriebenen Syntax abgeleitet und zur Prüfung vorgelegt.
3. Fachlich prüfen und in der `.md` direkt anpassen (Reihenfolge,
Verzweigungen, Verantwortliche, Formulierungen) – dabei gegen
`Modellierregeln.md` abgleichen.
4. Aus der freigegebenen `.md` die BPMN-2.0-Datei erzeugen und in PICTURE
importieren.
Schritt 2 erfordert Interpretation von Fließtext und ist kein rein
mechanischer Schritt – die Qualität hängt von der Klarheit der Quelle und
der fachlichen Prüfung in Schritt 3 ab. Die fachlichen BPMN-Modellierregeln
stehen in `Modellierregeln.md`.
## Aufruf
```bash
python3 md_to_bpmn.py <prozess.md> <ausgabe.bpmn>
```
Beispiel:
```bash
python3 md_to_bpmn.py beispiel_prozess.md beispiel_prozess.bpmn
```
Die erzeugte `.bpmn`-Datei kann anschließend über den BPMN-Import-Dialog
in PICTURE geladen werden.
### Diagramm vorab prüfen, ohne PICTURE
Um zu prüfen, ob `md_to_bpmn.py` selbst korrekt gerechnet hat (Struktur,
Anschlusspunkte, Layout) – unabhängig davon, was ein Import-Werkzeug daraus
macht – eignet sich ein originalgetreuer BPMN-Viewer:
- **`demo.bpmn.io`** – im Browser, kein Install nötig: `.bpmn`-Datei per
Drag & Drop reinziehen.
- **Camunda Modeler** – kostenlose Desktop-App, gleiche Rendering-Engine
wie bpmn.io, auch offline.
Beide übernehmen importierte Anschlusspunkte originalgetreu, statt sie neu
zu berechnen.
## Markdown-Syntax
### Kopfzeile (optional, vor dem ersten Schritt)
```
Prozess: <Name des Prozesses>
```
### Schritte
Jeder Schritt ist eine Zeile der Form:
```
<ID> <Typ>: <Beschriftung>
```
- **ID**: ein kurzes, eindeutiges Kürzel ohne Leerzeichen (z. B. `S1`, `3.3`, `A`)
- **Typ** (Groß-/Kleinschreibung egal):
| Schreibweise im .md | Erzeugtes BPMN-Element |
|--------------------------------------|-------------------------------------|
| `Start` | Start-Ereignis |
| `Ende` / `End` | End-Ereignis |
| `Zwischenereignis` / `Intermediate` | Zwischen-Ereignis (Catch) |
| `Task` / `Aufgabe` | Aufgabe (Task) |
| `Manuelle Aufgabe` | Manuelle Aufgabe |
| `User Aufgabe` | User-Aufgabe |
| `Service Aufgabe` | Service-Aufgabe |
| `Script Aufgabe` | Script-Aufgabe |
| `Geschäftsregel Aufgabe` | Geschäftsregel-Aufgabe |
| `Sendeaufgabe` | Aufgabe (sendend) |
| `Empfangsaufgabe` | Aufgabe (empfangend) |
| `Teilprozess` / `Subprocess` | Geschlossener Teilprozess |
| `Aufruf` | Aufruf-Element (Call Activity) |
| `Gateway XOR` | Exklusives Gateway (XOR) |
| `Gateway OR` / `Gateway ODER` | Inklusives Gateway (OR) |
| `Gateway AND` / `Gateway UND` | Paralleles Gateway (AND) |
| `Gateway Ereignisbasiert` | Ereignisbasiertes Gateway |
| `Hinweis` / `Note` | Textuelle Anmerkung (unverbunden) |
| `Datenobjekt` | Datenobjekt (unverbunden) |
| `Datenspeicher` | Datenspeicher (unverbunden) |
| `Gruppe` | Gruppe (unverbunden) |
Vollständige Liste inkl. Bedeutung: `Elementkatalog.md`.
### Typisierte Ereignisse
Start-, Zwischen- und Endereignisse können ein zweites Wort für die Art des
Ereignisses bekommen: `<Start|Zwischenereignis|Ende> <Kind>: <Beschriftung>`.
Mögliche `<Kind>`-Wörter: `Nachricht`, `Zeit`, `Eskalation`, `Bedingung`,
`Link`, `Fehler`, `Signal`, `Terminierung`. Details und Einschränkungen (z. B.
`Terminierung` nur als Endereignis, `Link` nur als Zwischenereignis) stehen in
`Elementkatalog.md` und `Modellierregeln.md`.
```
S1 Start Nachricht: Antrag geht ein
S9 Zwischenereignis Zeit: 14 Tage Frist abwarten
S17 Ende Terminierung: Verfahren beendet
```
Ein Zwischenereignis ist standardmäßig empfangend (Catch); mit einem
zusätzlichen dritten Wort `sendend` wird daraus ein auslösendes (Throw-)
Ereignis, z. B. `S2 Zwischenereignis Nachricht sendend: Status melden`.
### Geschlossene Pools und Nachrichtenflüsse
Externe Beteiligte (Bürger, andere Behörden, …), mit denen nur Nachrichten
ausgetauscht werden, vor dem ersten Schritt deklarieren:
```
Extern: Bürger
Extern: Landratsamt
```
Unter einem Schritt erzeugt `~> <Name>` einen ausgehenden, `<~ <Name>` einen
eingehenden Nachrichtenfluss zu einem so deklarierten Beteiligten:
```
S8 Sendeaufgabe: Bescheid versenden
~> Bürger
-> S9
```
Nachrichtenflüsse sind nur zwischen Pools erlaubt (siehe `Modellierregeln.md`)
– das Ziel muss immer ein per `Extern:` deklarierter Name sein, nie eine
Schritt-ID im selben Pool.
### Verzweigungen / Sprünge
Unter einem Schritt können beliebig viele Pfeil-Zeilen stehen:
```
S3 Gateway XOR: Antrag vollständig?
Ja -> S4
Nein -> S3a
```
- `<Bezeichnung> -> <Ziel-ID>` – beschriftete Kante (z. B. für Ja/Nein-Zweige)
- `-> <Ziel-ID>` – unbeschriftete Kante (z. B. für parallele AND-Zweige oder
einfache Weiterleitung)
**Wichtig:** Wenn ein Schritt **keine** Pfeil-Zeile hat, wird automatisch
eine Kante zum **nächsten Schritt in der Datei** erzeugt. Ihr müsst also nur
dort explizite Pfeile setzen, wo der Ablauf von der reinen Lesereihenfolge
abweicht (Verzweigung, Sprung zurück, Zusammenführung).
Rücksprünge (Schleifen, z. B. „Nachforderung" zurück zur Prüfung) werden
automatisch erkannt und am Rand des Diagramms herumgeführt.
### Verantwortliche / Swimlanes
Schreibt ihr am Anfang der Beschriftung eines Schritts einen kurzen Namen
gefolgt von einem Doppelpunkt, wird dieser als **Verantwortlicher** erkannt
und daraus automatisch eine Swimlane gemacht:
```
V1 Task: Flurneuordnung: Gewannenkonzept festlegen
O3 Task: ÖbVI: Waldgrenzen verhandeln
O9 Task: LRA: Aufmessung im Ortstermin
```
Aus den drei unterschiedlichen Verantwortlichen entstehen drei Lanes in
einem Pool; Schritte ohne Verantwortlichen-Angabe landen gesammelt in einer
Lane „Nicht zugeordnet". Ohne jede Verantwortlichen-Angabe entsteht wie
bisher ein Diagramm ohne Lanes.
Das ist eine Heuristik: Ein Doppelpunkt weit vorn in einer kurzen
Beschriftung (max. 30 Zeichen, max. 4 Wörter, kein „?") wird als
Verantwortlicher gedeutet. Enthält eine Beschriftung an dieser Stelle
zufällig einen Doppelpunkt, der keine Zuständigkeit meint, entsprechend
umformulieren.
### Vollständige Beispiele
- Ohne Lanes: `beispiel_prozess.md` / `beispiel_prozess.bpmn`
- Mit Lanes: `gewannenvermessung_lanes.md` / `gewannenvermessung_lanes.bpmn`
## Einschränkungen der aktuellen Version
- Es wird pro `.md`-Datei **ein** Diagramm (ein Prozess) erzeugt, keine
mehrstufigen Prozessnetze mit verknüpften Teilprozess-Diagrammen.
- `Teilprozess` (geschlossener Teilprozess) und `Aufruf` (Aufruf-Element)
werden als Bausteine angelegt, aber **ohne** Inhalt bzw. **ohne**
Verknüpfung zu einem Ziel-Prozess – Details bzw. Verknüpfung lassen sich
in PICTURE nach dem Import nachtragen. Der **aufgeklappte** Teilprozess
(Inhalt inline im selben Diagramm) wird noch nicht unterstützt.
- Wiederholungs-Marker (Schleife, Multi-Instanz sequenziell/parallel) werden
noch nicht unterstützt; bei Bedarf als `Hinweis` an der betroffenen
Aktivität dokumentieren.
- Assoziationen (gepunktete Linien) von Datenobjekten/-speichern zu
Aktivitäten werden nicht automatisch gezeichnet; Datenobjekt/-speicher
landen unverbunden im Diagramm und werden nach dem Import in PICTURE bei
Bedarf manuell mit den betroffenen Aktivitäten verbunden.
- Das automatische Layout ist ein einfacher Schichten-Algorithmus
(ähnlich den meisten BPMN-Auto-Layouts) – bei komplexen Prozessen lohnt
sich nach dem Import ein manuelles Nachjustieren einzelner Boxen und
Beschriftungen. Das gilt besonders für geschlossene Pools (externe
Beteiligte): Sie werden schlicht unterhalb des Hauptpools gestapelt.
- Jede Kante bekommt in der erzeugten `.bpmn`-Datei einen eigenen
Anschlusspunkt (Modellierregel 4, in `Modellierregeln.md`) statt eines
gemeinsamen Mittelpunkts. Bei Task/Ereignis/Datenobjekt usw. werden dazu
mehrere Punkte über die tatsächliche Kontur verteilt (Rechteckkante bzw.
Kreisbogen). **Ein Gateway bekommt dagegen nur einen der vier festen
PICTURE-Eckpunkte (0°/90°/180°/270°) je Kante**, nie einen Punkt
dazwischen auf der Rautenkante – mehr als vier Kanten an einem Gateway
(eingehend plus ausgehend) können sich damit nicht mehr eindeutig
darstellen lassen, siehe Modellierregel 4. Wirkt das Ergebnis in einem
Anzeige-Werkzeug trotzdem so, als würden Verzweigungen an einem Punkt
zusammenlaufen, zuerst in einem originalgetreuen BPMN-Viewer (z. B.
`demo.bpmn.io`, siehe unten) prüfen: Ein Viewer, der importierte
Anschlusspunkte respektiert statt sie neu zu berechnen, zeigt die
verteilten Punkte korrekt an.
- Der BPMN-Import in PICTURE übernimmt die reine Ablaufstruktur
zuverlässig; PICTURE-/FIM-spezifische Zusatzfelder (Fallzahl,
Bearbeitungszeit, RACI usw.) sind kein Teil des BPMN-Standards und
müssten nach dem Import ergänzt werden.
+99
View File
@@ -0,0 +1,99 @@
# PICTURE-Elementkatalog (BPMN 2.0)
Quelle: Informationsportal der PICTURE-Prozessplattform, Abschnitt „BPMN 2.0"
(Stand der Übernahme: 2026-09-21). PICTURE deckt die OMG-Konformitätsklassen
„Deskriptives BPMN" und „Analytisches BPMN" ab – Symbole/Icons sind hier
bewusst weggelassen, da für den Skill nur Name, Bedeutung und DSL-Zuordnung
relevant sind. Bei Bedarf original im PICTURE-Portal nachschlagen.
Spalte „`md_to_bpmn.py`" zeigt das DSL-Schlüsselwort aus `Anleitung.md`.
**„–“ bedeutet: von `md_to_bpmn.py` aktuell nicht erzeugbar** – im Entwurf als
Freitext/Hinweis dokumentieren und nach dem Import manuell in PICTURE ergänzen.
## Aktivitäten
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Aufgabe | Arbeitsschritt, der nicht weiter verfeinert wird. | `Task` / `Aufgabe` |
| Manuelle Aufgabe | Ausführung durch einen Menschen ohne IT-Unterstützung. | `Manuelle Aufgabe` |
| User-Aufgabe | Ausführung durch einen Menschen mit Software-Unterstützung. | `User Aufgabe` |
| Service-Aufgabe | Automatische Ausführung durch einen Dienst/Service. | `Service Aufgabe` |
| Script-Aufgabe | Automatische Ausführung durch eine Process Engine per Script. | `Script Aufgabe` |
| Geschäftsregel-Aufgabe | Automatische Ausführung nach Entscheidungs-/Berechnungsregeln. | `Geschäftsregel Aufgabe` |
| Aufgabe (sendend) | Versand einer Nachricht an einen externen Beteiligten. | `Sendeaufgabe` |
| Aufgabe (empfangend) | Empfang einer Nachricht von einem externen Beteiligten. | `Empfangsaufgabe` |
| Geschlossener Teilprozess | Gekapselte Aktivitätsmenge, Details in separatem Diagramm ausgelagert. | `Teilprozess` |
| Aufgeklappter Teilprozess | Gekapselte, aber inline dargestellte Aktivitätsmenge (keine Auslagerung). | – (noch nicht unterstützt, siehe Einschränkungen in `Anleitung.md`) |
| Aufruf-Element (Call Activity) | Verknüpfung mit einem separaten, eigenständigen Prozess. | `Aufruf` |
## Wiederholungen
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Schleife | Aktivität wiederholt sich, solange eine Bedingung zutrifft. | – (noch nicht unterstützt) |
| Multi-Instanz (sequenziell) | Aktivität läuft für eine bekannte Anzahl Objekte nacheinander. | – (noch nicht unterstützt) |
| Multi-Instanz (parallel) | Wie sequenziell, aber alle Ausführungen parallel. | – (noch nicht unterstützt) |
## Ereignisse
Start-, Zwischen- und Endereignisse können optional typisiert werden. In der
DSL: `<Start|Zwischenereignis|Ende> <Kind> [sendend|empfangend]: <Label>`.
| Ereignis-Kind | Bedeutung | `md_to_bpmn.py`-Wort |
|---|---|---|
| (untypisiert) | Allgemeines Ereignis, meist Start/Ende. | – (kein Zusatzwort) |
| Nachricht | Empfang/Versand einer Nachricht (Dokument, Information). | `Nachricht` |
| Zeit | Periodisches, zeitpunkt- oder zeitspannenbezogenes Ereignis. | `Zeit` |
| Eskalation | Meldung an den nächsthöheren Verantwortlichen. | `Eskalation` |
| Bedingung | Reaktion auf veränderte (Geschäfts-)Bedingungen. | `Bedingung` |
| Link | Fortsetzung eines aufgetrennten Sequenzflusses; nur als Zwischenereignis, paarweise sendend/empfangend. Von Analysefunktionen nicht unterstützt. | `Link` (nur Zwischenereignis) |
| Fehler | Auslösen/Behandeln definierter Fehler. | `Fehler` |
| Signal | Signal an (potenziell) mehrere Prozesse gleichzeitig. | `Signal` |
| Terminierung | Sofortige Beendigung der Prozessinstanz. | `Terminierung` (nur Endereignis) |
Start-Ereignisse sind immer eintretend/empfangend, Endereignisse immer
auslösend/sendend (Skript weist eine widersprüchliche Angabe ab).
Zwischenereignisse sind standardmäßig empfangend (Catch); mit `sendend`
wird daraus ein Throw-Ereignis.
## Kontrollfluss-Operatoren (Gateways)
| Name | Bedeutung | `md_to_bpmn.py` |
|---|---|---|
| Parallele Verzweigung (AND-Split) | Alle nachfolgenden Pfade laufen gleichzeitig. | `Gateway AND` |
| Exklusive datenbasierte Verzweigung (XOR) | Genau ein Pfad wird abhängig von den Daten gewählt. | `Gateway XOR` |
| Inklusive datenbasierte Verzweigung (OR) | Ein oder mehrere Pfade werden gewählt. | `Gateway OR` |
| Exklusive ereignisbasierte Verzweigung | Es folgt der Pfad, dessen Ereignis/empfangende Aufgabe zuerst eintritt; alle anderen Pfade verfallen für diese Instanz. Darf **nicht** spiegelbildlich als Zusammenführung genutzt werden. | `Gateway Ereignisbasiert` |
| Entweder-Oder-Zusammenführung (XOR-Merge) | Ankommende Pfade laufen unabhängig voneinander einfach weiter; Nachfolger wird ggf. mehrfach durchlaufen. | `Gateway XOR` als Zusammenführung |
| Verschmelzung (AND-Merge) | Wartet auf **alle** eingehenden Pfade, bevor es weitergeht. | `Gateway AND` als Zusammenführung |
| Oder-Verschmelzung (OR-Merge) | Wartet auf alle **aktiven** eingehenden Pfade. | `Gateway OR` als Zusammenführung |
`md_to_bpmn.py` erzeugt für Split und Merge denselben Gateway-Typ (XOR/OR/AND
sind in BPMN dieselben Elemente, nur mit unterschiedlicher Kanten-Anzahl) –
siehe `Modellierregeln.md` für die Regel, welcher Merge-Typ zu welchem
Split-Typ passt, und für PICTUREs Grenze von vier Anschlusspunkten je
Gateway (Modellierregel 4).
## Sequenz- und Nachrichtenflüsse
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Sequenzfluss | Reihenfolge innerhalb eines Pools. | `->` |
| Nachrichtenfluss | Nur zwischen Pools, nie innerhalb desselben Pools. | `~>` (ausgehend) / `<~` (eingehend) zu einem per `Extern:` deklarierten Beteiligten |
## Artefakte
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Datenobjekt / Dokument | Information, die im Prozess erstellt/genutzt/weitergegeben wird. Sparsam einsetzen, nur bei besonders wichtigem Sachverhalt. | `Datenobjekt` |
| Datenspeicher | Dauerhafte, von der Prozessinstanz unabhängige Ablage (Datenbank, Aktenschrank). | `Datenspeicher` |
| Textuelle Anmerkung | Kommentar zu einem beliebigen Modellelement. | `Hinweis` |
| Gruppe | Rein visuelle Klammer ohne Einfluss auf Ablauf/Verantwortlichkeiten. | `Gruppe` |
## Pools & Schwimmbahnen
| Name | Beschreibung | `md_to_bpmn.py` |
|---|---|---|
| Pool | Grenze eines Prozesses/Teilprozesses. | entsteht automatisch, sobald Lanes oder `Extern:`-Beteiligte vorkommen |
| Schwimmbahn | Unterteilung eines Pools nach Organisationseinheit/Rolle/System. | automatisch aus `Verantwortlicher: Aufgabe`-Präfix |
| Geschlossener Pool | Externe/r Beteiligte/r bzw. externer Prozess ohne dargestellten Inhalt, tauscht nur Nachrichten aus. | `Extern: <Name>` + `~>`/`<~` |
+73
View File
@@ -0,0 +1,73 @@
# BPMN-Modellierregeln
Fachliche Regeln, an denen sich ein Entwurf (`entwuerfe/<name>.md`) und die
daraus erzeugte BPMN-Datei messen lassen müssen – zusätzlich zur reinen
Markdown-Syntax aus `Anleitung.md`. Beim Erzeugen und beim Prüfen eines
Entwurfs (Schritt 2 in `SKILL.md`) gegen diese Liste abgleichen und
Verstöße im Annahmen-/Hinweis-Abschnitt benennen. Welche Elemente es
überhaupt gibt und wofür sie stehen, steht in `Elementkatalog.md`.
1. **Verzweigungen laufen passend zusammen.** Verzweigte Pfade müssen in
einem Verzweigungselement (Gateway) zusammenlaufen, das zur Art der
Verzweigung passt, bevor der Pfad in den nächsten Arbeitsschritt nach
der Verzweigung mündet. Pfade nicht einfach ohne zusammenführendes
Gateway in denselben Folgeschritt münden lassen. Welcher Gateway-Typ
zusammenführt, hängt vom Split ab – und die drei Typen unterscheiden
sich fachlich, nicht nur formal:
- **XOR-Split** (genau ein Pfad wird genommen) → **XOR-Merge**
(Entweder-Oder-Zusammenführung): die ankommenden Pfade laufen
unabhängig voneinander einfach weiter, es wird auf nichts gewartet.
- **AND-Split** (alle Pfade parallel) → **AND-Merge** (Verschmelzung):
wartet, bis **alle** eingehenden Pfade angekommen sind, bevor es
weitergeht. Ein AND-Split ohne passenden AND-Merge am Ende der
parallelen Pfade führt zu mehrfach durchlaufenen Folgeschritten.
- **OR-Split** (ein oder mehrere Pfade) → **OR-Merge**
(Oder-Verschmelzung): wartet auf alle **aktiven** eingehenden Pfade,
nicht zwingend auf alle theoretisch möglichen.
- **Ereignisbasierte Verzweigung** ist ein Sonderfall: Sie darf nicht
spiegelbildlich als Zusammenführung genutzt werden, also nie mehr
als eine eingehende Kante haben (`md_to_bpmn.py` lehnt das ab).
2. **Nachrichtenfluss nur zwischen Pools.** Ein Nachrichtenfluss verbindet
ausschließlich Elemente unterschiedlicher Pools (z. B. den eigenen
Prozess mit einem externen Beteiligten). Innerhalb desselben Pools
werden Informationsübergaben über den normalen Sequenzfluss
modelliert, nie über Nachrichtenfluss.
3. **Datenobjekte, Datenspeicher und Gruppen sind keine Ablaufelemente.**
Sie hängen nicht am Sequenzfluss (kein `->` zu/von ihnen) und werden
sparsam eingesetzt – nur wenn der Sachverhalt (z. B. ein zentrales
Dokument oder eine geteilte Ablage) für das Verständnis des Prozesses
wirklich wichtig ist, nicht für jede beiläufig erwähnte Unterlage.
4. **Jede Kante bekommt einen eigenen Anschlusspunkt.** Jeder Eingang und
jeder Ausgang an einem Element muss einen eigenen Verbindungspunkt am
Element nutzen, nicht denselben Punkt wie eine andere Kante. Laufen
mehrere Kanten sichtbar in einem einzigen Punkt zusammen, sieht das
Diagramm so aus, als wären mehrere fachlich getrennte Verzweigungen
eine einzige Linie – das verschleiert genau die Verzweigung, die
dargestellt werden soll. `md_to_bpmn.py` setzt das automatisch um, aber
je nach Elementtyp unterschiedlich:
- **Bei Task/Ereignis/Datenobjekt usw.** werden mehrere Punkte entlang
der tatsächlichen Kontur verteilt (Rechteckkante bzw. Kreisbogen).
- **Bei einem Gateway hat PICTURE nur vier feste Anschlusspunkte** – je
einen bei 0°/90°/180°/270° (Ost/Süd/West/Nord), die Kante verläuft
dort immer gerade. `md_to_bpmn.py` weist deshalb jeder Kante an einem
Gateway einen dieser vier Eckpunkte zu, nicht einen Punkt auf der
Rautenkante dazwischen. **Daraus folgt eine harte Obergrenze: ein
Gateway kann höchstens vier Kanten insgesamt (eingehend plus
ausgehend zusammengerechnet) eindeutig darstellen.** Ein XOR-Split
mit einer eingehenden Kante hat also nur für maximal drei ausgehende
Zweige Platz; bei vier oder mehr Zweigen muss mindestens eine Kante
sich einen Punkt mit einer anderen teilen – das ist eine Grenze von
PICTURE selbst, kein Darstellungsfehler von `md_to_bpmn.py`. Bei
einer Verzweigung mit vielen Zweigen und einer eingehenden Kante
lohnt sich daher, die Entscheidung in mehrere, weniger stark
verzweigte Gateways aufzuteilen, wenn jeder Zweig einzeln sichtbar
bleiben soll.
Wirkt ein Diagramm trotzdem so, als würden Verzweigungen an einem
Punkt zusammenlaufen, zuerst in einem originalgetreuen BPMN-Viewer
(z. B. `demo.bpmn.io`) prüfen, ob es an der erzeugten Datei liegt oder
tatsächlich an der Vier-Punkte-Grenze eines Gateways mit mehr als vier
Kanten.
+47
View File
@@ -0,0 +1,47 @@
---
name: sn-bpmn
description: Wandelt eine Prozessbeschreibung (Rohtext, Stichpunkte oder bereits strukturierte Arbeitsschritt-.md) in eine BPMN-2.0-Datei für den Import in PICTURE um. Aufrufen, wenn ein Verwaltungsprozess als Ablaufdiagramm für PICTURE modelliert werden soll.
---
# BPMN-Prozess-Konverter
Begleitet den Weg von einer (oft unstrukturierten) Prozessbeschreibung zu
einer importierbaren BPMN-2.0-Datei für PICTURE, mit `md_to_bpmn.py` in
diesem Verzeichnis.
Lies `Anleitung.md` in diesem Verzeichnis für die vollständige
Markdown-Syntax (Schritt-Typen, Verzweigungen, Rücksprung-Erkennung,
Verantwortliche/Swimlanes), `Elementkatalog.md` für die vollständige
Liste der PICTURE-BPMN-Elemente und `Modellierregeln.md` für die
fachlichen BPMN-Modellierregeln, bevor du eine Arbeitsschritt-.md
schreibst oder bearbeitest.
## Ablauf
1. **Quelle ermitteln.** Frage nach der Rohtext-, Word- oder PDF-Quelle,
falls keine angegeben oder angehängt wurde.
2. **Entwurf erzeugen.** Lies die Quelle und leite daraus eine
strukturierte Arbeitsschritt-`.md` nach der Syntax aus `Anleitung.md`
ab. Triff nachvollziehbare fachliche Annahmen (Schrittgrenzen,
Verzweigungen, Verantwortliche wo erkennbar) und schreibe das Ergebnis
nach `entwuerfe/<name>.md` (Verzeichnis bei Bedarf anlegen). Gleiche den
Entwurf gegen `Modellierregeln.md` ab und behebe Verstöße direkt im
Entwurf. Fasse kurz zusammen, welche Annahmen getroffen wurden, und
weise auf Stellen hin, die fachlich geprüft werden sollten.
3. **Auf Freigabe warten.** Nicht eigenständig weiterbearbeiten. Der
Nutzer passt `entwuerfe/<name>.md` selbst an (Reihenfolge,
Verzweigungen, Verantwortliche, Formulierungen) und meldet sich, wenn
sie passt.
4. **BPMN erzeugen.** Erst nach ausdrücklicher Freigabe (z. B. "ist
angepasst", "passt so", "wandle um"):
```
python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md <name>.bpmn
```
Ergebnis kurz melden (Anzahl Schritte/Kanten/Lanes, Pfad der Datei).
Wenn der Nutzer direkt eine bereits fertige Arbeitsschritt-`.md` angibt
und ausdrücklich nur die Umwandlung möchte, Schritt 2-3 überspringen
und direkt umwandeln.
File diff suppressed because it is too large Load Diff
+379
View File
@@ -0,0 +1,379 @@
# -*- coding: utf-8 -*-
"""Tests für md_to_bpmn.py - reines Python/pytest, ohne Netzwerk."""
import xml.etree.ElementTree as ET
import pytest
import md_to_bpmn as m
def test_bestehende_syntax_bleibt_kompatibel():
"""Prüft, dass Start/Task/Gateway-XOR/Ende mit Verzweigung wie bisher geparst werden."""
text = """
S1 Start: Beginn
-> S2
S2 Task: Antrag pruefen
S3 Gateway XOR: Vollständig?
Ja -> S4
Nein -> S2
S4 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
assert externals == []
assert steps['S1'].type == 'StartEvent'
assert steps['S3'].type == 'XorGateway'
assert ('Ja', 'S4') in steps['S3'].edges
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}exclusiveGateway") is not None
def test_lane_erkennung_bleibt_unveraendert():
"""Prüft, dass 'Verantwortlicher: Rest' weiterhin als Swimlane erkannt wird."""
text = """
S1 Start: Beginn
-> V1
V1 Task: Flurneuordnung: Gewannenkonzept festlegen
O3 Task: ÖbVI: Waldgrenzen verhandeln
S_end Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
lanes = {s.lane for s in steps.values() if s.lane}
assert lanes == {'Flurneuordnung', 'ÖbVI'}
@pytest.mark.parametrize('typewort, erwarteter_typ', [
('Manuelle Aufgabe', 'ManualTask'),
('User Aufgabe', 'UserTask'),
('Service Aufgabe', 'ServiceTask'),
('Script Aufgabe', 'ScriptTask'),
('Geschäftsregel Aufgabe', 'BusinessRuleTask'),
('Sendeaufgabe', 'SendTask'),
('Empfangsaufgabe', 'ReceiveTask'),
('Teilprozess', 'SubProcess'),
('Aufruf', 'CallActivity'),
('Gateway Ereignisbasiert', 'EventBasedGateway'),
('Datenobjekt', 'DataObject'),
('Datenspeicher', 'DataStore'),
('Gruppe', 'Group'),
])
def test_neue_elementtypen_werden_aufgeloest(typewort, erwarteter_typ):
"""Prüft, dass alle neuen Aufgaben-/Gateway-/Artefakttypen korrekt zugeordnet werden."""
typ, kind, direction = m.resolve_type(typewort, 'Testzeile')
assert typ == erwarteter_typ
def test_typisiertes_startereignis_erzeugt_eventdefinition():
"""Prüft, dass ein typisiertes Startereignis das passende BPMN-eventDefinition-Kind erhaelt."""
text = """
S1 Start Nachricht: Antrag eingegangen
-> S2
S2 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}messageEventDefinition") is not None
def test_zwischenereignis_sendend_wird_throw_event():
"""Prüft, dass 'sendend' bei einem Zwischenereignis ein intermediateThrowEvent erzeugt."""
text = """
S1 Start: Beginn
-> S2
S2 Zwischenereignis Nachricht sendend: Status melden
-> S3
S3 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
assert m.bpmn_tag_for(steps['S2']) == 'intermediateThrowEvent'
def test_terminierung_nur_als_endereignis_erlaubt():
"""Prüft, dass 'Terminierung' bei einem Startereignis einen Fehler ausloest."""
text = """
S1 Start Terminierung: X
-> S2
S2 Ende: Y
"""
with pytest.raises(ValueError, match='Terminierung'):
m.parse_markdown(text)
def test_link_nur_als_zwischenereignis_erlaubt():
"""Prüft, dass 'Link' bei einem Endereignis einen Fehler ausloest."""
text = """
S1 Start: X
-> S2
S2 Ende Link: Y
"""
with pytest.raises(ValueError, match='Link'):
m.parse_markdown(text)
def test_ereignisbasiertes_gateway_verbietet_zusammenfuehrung():
"""Prüft, dass ein ereignisbasiertes Gateway mit mehreren eingehenden Kanten abgelehnt wird."""
text = """
S1 Start: X
-> S2
-> S3
S2 Task: A
-> S4
S3 Task: B
-> S4
S4 Gateway Ereignisbasiert: C
-> S5
S5 Ende: Y
"""
with pytest.raises(ValueError, match='Zusammenführung'):
m.parse_markdown(text)
def test_datenobjekt_bekommt_keinen_sequenzfluss():
"""Prüft, dass ein Sequenzfluss zu einem Datenobjekt abgelehnt wird."""
text = """
S1 Start: X
-> D1
D1 Datenobjekt: Akte
S2 Ende: Y
"""
with pytest.raises(ValueError, match='keine Ablaufelemente'):
m.parse_markdown(text)
def test_datenobjekt_wird_nicht_automatisch_verkettet():
"""Prüft, dass die automatische Verkettung über unverbundene Artefakte hinwegspringt."""
text = """
S1 Start: X
D1 Datenobjekt: Akte
S2 Ende: Y
"""
meta, steps, order, externals = m.parse_markdown(text)
assert steps['S1'].edges == [(None, 'S2')]
assert steps['D1'].edges == []
def test_datenobjekt_und_gruppe_erzeugen_erwartete_elemente():
"""Prüft, dass Datenobjekt und Gruppe als eigenstaendige, unverbundene Artefakte gerendert werden."""
text = """
S1 Start: X
-> S2
S2 Ende: Y
D1 Datenobjekt: Akte
G1 Gruppe: Fachliche Pruefung
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert defs.find(f".//{{{m.NS['bpmn']}}}dataObjectReference") is not None
assert defs.find(f".//{{{m.NS['bpmn']}}}group") is not None
assert defs.find(f".//{{{m.NS['bpmn']}}}categoryValue") is not None
def test_externer_beteiligter_erzeugt_messageflow():
"""Prüft, dass ein deklarierter externer Beteiligter mit Nachrichtenfluss korrekt verknuepft wird."""
text = """
Extern: Bürger
S1 Start: X
-> S2
S2 SendTask: Bescheid senden
~> Bürger
-> S3
S3 Ende: Y
"""
meta, steps, order, externals = m.parse_markdown(text)
assert externals == ['Bürger']
defs = m.build_bpmn(meta, steps, order, externals)
msg_flow = defs.find(f".//{{{m.NS['bpmn']}}}messageFlow")
assert msg_flow is not None
participants = defs.findall(f".//{{{m.NS['bpmn']}}}participant")
assert any(p.get('name') == 'Bürger' and p.get('processRef') is None for p in participants)
def test_unbekannter_externer_beteiligter_wird_abgelehnt():
"""Prüft, dass ein Nachrichtenfluss zu einem nicht deklarierten externen Beteiligten scheitert."""
text = """
S1 Start: X
~> Unbekannt
S2 Ende: Y
"""
with pytest.raises(ValueError, match='unbekannter externer Beteiligter'):
m.parse_markdown(text)
def test_verteilte_anschlusspunkte_liegen_auf_der_rautenkontur():
"""Prüft, dass verteilte Anschlusspunkte an einem Gateway auf der Diamant-Kontur liegen, nicht auf der Bounding-Box."""
x, y, w, h = 590, 2290, 50, 50
cx, cy = x + w / 2, y + h / 2
for idx in range(4):
px, py = m.spread_point(x, y, w, h, 'S', idx, 4, 'diamond')
# Manhattan-Abstand zum Mittelpunkt, skaliert auf die Diamant-Halbachsen, muss 1 ergeben.
assert abs(abs(px - cx) / (w / 2) + abs(py - cy) / (h / 2) - 1) < 1e-9
def test_verteilte_anschlusspunkte_liegen_auf_dem_ereigniskreis():
"""Prüft, dass verteilte Anschlusspunkte an einem Ereignis auf dem Kreisradius liegen, nicht auf der Bounding-Box."""
x, y, w, h = 100, 100, 36, 36
cx, cy = x + w / 2, y + h / 2
r = w / 2
for idx in range(3):
px, py = m.spread_point(x, y, w, h, 'S', idx, 3, 'circle')
dist = ((px - cx) ** 2 + (py - cy) ** 2) ** 0.5
assert abs(dist - r) < 1e-9
def test_gateway_kanten_bekommen_eigene_kardinalpunkte():
"""Prüft, dass bis zu vier Kanten an einem Gateway je einen eigenen Eckpunkt (N/O/S/W) bekommen, wie PICTURE es rendert."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Auswahl
A -> S3
B -> S4
C -> S5
S3 Ende: A
S4 Ende: B
S5 Ende: C
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
ns = m.NS
gw = next(el for el in defs.iter(f"{{{ns['bpmn']}}}exclusiveGateway") if el.get('name') == 'Auswahl')
shape = defs.find(f".//{{{ns['bpmndi']}}}BPMNShape[@bpmnElement='{gw.get('id')}']")
x, y, w, h = (float(shape[0].get(a)) for a in ('x', 'y', 'width', 'height'))
vertices = {(x + w / 2, y), (x + w / 2, y + h), (x, y + h / 2), (x + w, y + h / 2)}
out_ids = {o.text for o in gw.findall(f"{{{ns['bpmn']}}}outgoing")}
in_ids = {i.text for i in gw.findall(f"{{{ns['bpmn']}}}incoming")}
used = set()
for fid in out_ids | in_ids:
edge = defs.find(f".//{{{ns['bpmndi']}}}BPMNEdge[@bpmnElement='{fid}']")
wps = edge.findall(f"{{{ns['di']}}}waypoint")
wp = wps[0] if fid in out_ids else wps[-1]
point = (float(wp.get('x')), float(wp.get('y')))
assert point in vertices
used.add(point)
assert len(used) == 4
def test_gateway_mit_mehr_als_vier_kanten_bleibt_gueltiges_xml():
"""Prüft, dass ein Gateway mit mehr als vier anliegenden Kanten (über PICTUREs Grenze hinaus) trotzdem gültiges XML ohne Absturz erzeugt."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Fuenf-Wege
A -> S3
B -> S4
C -> S5
D -> S6
E -> S7
S3 Ende: A
S4 Ende: B
S5 Ende: C
S6 Ende: D
S7 Ende: E
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
ET.tostring(defs) # loest bei kaputter Struktur eine Exception aus
def test_mehrere_kanten_an_derselben_seite_bekommen_eigenen_anschlusspunkt():
"""Prüft, dass mehrere ausgehende Kanten an einem Gateway nicht im selben Punkt starten."""
text = """
S1 Start: X
-> S2
S2 Gateway XOR: Auswahl
A -> S3
B -> S4
C -> S5
S3 Ende: A
S4 Ende: B
S5 Ende: C
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
starts = []
for edge in defs.iter(f"{{{m.NS['bpmndi']}}}BPMNEdge"):
wp = edge.find(f"{{{m.NS['di']}}}waypoint")
starts.append((wp.get('x'), wp.get('y')))
# 3 der 4 Kanten gehen vom selben Gateway aus - deren Startpunkte muessen sich unterscheiden.
assert len(set(starts)) == len(starts)
def _liegt_in_rautenflaeche(px, py, x, y, w, h, sicherheitsabstand=0.5):
"""Manhattan-Abstand zum Mittelpunkt, skaliert auf die Diamant-Halbachsen: < 1 heisst innerhalb."""
cx, cy = x + w / 2, y + h / 2
wert = abs(px - cx) / (w / 2) + abs(py - cy) / (h / 2)
return wert < 1 - (sicherheitsabstand / max(w, h))
def _kante_kreuzt_gateway(defs, ns):
"""Liefert (Kanten-Id, Gateway-Id) für die erste Kante, deren Streckenzug durchs Innere
eines Gateways (statt nur an dessen Kontur entlang) laeuft - oder None, wenn keine kreuzt."""
shapes = {}
for shape in defs.iter(f"{{{ns['bpmndi']}}}BPMNShape"):
b = shape.find(f"{{{ns['dc']}}}Bounds")
if b is not None:
shapes[shape.get('bpmnElement')] = tuple(float(b.get(a)) for a in ('x', 'y', 'width', 'height'))
gateway_ids = set()
for tag in ('exclusiveGateway', 'inclusiveGateway', 'parallelGateway', 'eventBasedGateway'):
gateway_ids.update(el.get('id') for el in defs.iter(f"{{{ns['bpmn']}}}{tag}"))
for edge in defs.iter(f"{{{ns['bpmndi']}}}BPMNEdge"):
pts = [(float(wp.get('x')), float(wp.get('y'))) for wp in edge.findall(f"{{{ns['di']}}}waypoint")]
for gid in gateway_ids:
if gid not in shapes:
continue
x, y, w, h = shapes[gid]
for (x1, y1), (x2, y2) in zip(pts, pts[1:]):
for s in range(41):
t = s / 40
if _liegt_in_rautenflaeche(x1 + (x2 - x1) * t, y1 + (y2 - y1) * t, x, y, w, h):
return edge.get('bpmnElement'), gid
return None
def test_kante_fuehrt_nicht_durchs_innere_eines_gateways():
"""Prüft, dass eine Kante von/zu einem Gateway, dessen Ziel/Quelle nicht auf der Achse des
zugewiesenen Anschlusspunkts liegt, aussen herum und nicht durchs Innere der Raute fuehrt."""
text = """
S1 Start: X
-> S5
S5 Gateway XOR: Sofort?
Ja -> S6
Nein -> S6m
S6 Task: Leitungsplaene einholen
-> S6m
S6m Gateway XOR: Zusammenfuehrung
-> S7
S7 Ende: Fertig
"""
meta, steps, order, externals = m.parse_markdown(text)
defs = m.build_bpmn(meta, steps, order, externals)
assert _kante_kreuzt_gateway(defs, m.NS) is None
def test_ausgabe_ist_wohlgeformtes_xml(tmp_path):
"""Prüft, dass die erzeugte .bpmn-Datei sich als valides XML parsen laesst."""
src = tmp_path / 'prozess.md'
src.write_text("""
Extern: Bürger
S1 Start Nachricht: Antrag geht ein
-> S2
S2 Gateway XOR: Vollständig?
Ja -> S3
Nein -> S2
S3 SendTask: Bescheid senden
~> Bürger
-> S4
S4 Ende Terminierung: Fertig
""", encoding='utf-8')
out = tmp_path / 'prozess.bpmn'
meta, steps, order, externals = m.parse_markdown(src.read_text(encoding='utf-8'))
defs = m.build_bpmn(meta, steps, order, externals)
out.write_bytes(m.prettify(defs))
parsed = ET.parse(out)
assert parsed.getroot().tag == f"{{{m.NS['bpmn']}}}definitions"
+74
View File
@@ -0,0 +1,74 @@
---
name: sn-ticket
description: Gitea-Tickets lesen, durchsuchen und beantworten - offene Issues auflisten, ein Issue mit allen Kommentaren holen, Issue- und Kommentartexte als Entwurf vorbereiten, den ein Mensch absendet. Verwenden bei "welche Tickets gibt es", "lies Issue", "was steht in dem Ticket", "leg ein Issue an", "kommentiere das Ticket".
---
# Tickets auf Gitea
Alle Repositories der Organisation `AG_Prozesse` liegen auf `entwicklung.flurneuordnung-sachsen.de`. Die API ist
**lesend ohne Anmeldung** erreichbar; dafür gibt es `scripts/gitea.py` (nur Standardbibliothek, kein Token).
> **Der Agent verändert auf dem Server nichts.** Kein Anlegen, kein Kommentieren, kein Schließen, kein Zuweisen.
> Der Agent bereitet den Text vor, der Mensch fügt ihn ein und sendet ab. Grund: Jede Aktion trägt den Namen
> einer Person und deren Verantwortung. Siehe `docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6.
## Lesen
```bash
python scripts/gitea.py tickets # alle offenen Issues der Organisation
python scripts/gitea.py tickets Projektwissen # nur ein Repo (Kurzname genügt)
python scripts/gitea.py tickets --label Bug # nach Label filtern
python scripts/gitea.py ticket Projektwissen 12 # ein Issue mit allen Kommentaren
python scripts/gitea.py suche "Swimlane" # Volltext über alle Repos
python scripts/gitea.py pulls # offene Pull Requests
```
Bekannt sind das Workspace-Repo (`[workspace] repo` in `workspace.toml`, derzeit `Projektwissen`) und alle
`[[repos]]`-Einträge; eindeutige Kurznamen (`wissen`) werden aufgelöst.
**Immer `ticket` statt `tickets` verwenden, bevor mit der Arbeit begonnen wird.** Die Kommentare enthalten
regelmäßig die eigentliche Anforderung — die Titelzeile führt in die Irre.
## Ein Ticket verstehen, bevor Code angefasst wird
1. Issue **mit Kommentaren** lesen (`ticket`), nicht nur den Titel.
2. Label `Diskussion` bedeutet: es gibt noch keinen Konsens. Dann nicht umsetzen, sondern die offene Frage
benennen. Wird trotzdem umgesetzt, sind die Annahmen im Pull Request zu kennzeichnen.
3. **Ziel, Abnahmekriterien und Grenzen in drei Sätzen zurückspiegeln**, bevor die erste Datei geöffnet wird.
Steht im Ticket kein Abnahmekriterium, ist das die erste Rückfrage — nicht der erste Commit.
4. Fehlt fachlicher Kontext: `docs/wissen/glossar.md`, bei BPMN-Fragen
`.agents/skills/sn-bpmn/Modellierregeln.md`.
Danach weiter mit dem Skill `sn-aufgabe-bearbeiten`.
## Entwurf vorbereiten
```bash
python scripts/gitea.py entwurf issue Projektwissen # neues Issue
python scripts/gitea.py entwurf kommentar Projektwissen 12 # Kommentar zu Issue 12
```
Das legt eine Datei unter `entwuerfe/` an (nicht versioniert) und nennt die Gitea-URL zum Absenden. Vorgehen:
1. Entwurf anlegen lassen.
2. Text ausfüllen — bei Issues nach `docs/vorlagen/issue.md`: Ziel, Betroffen, fachlicher Hintergrund,
Abnahmekriterien, Nicht Teil dieses Issues.
3. Dem Menschen den Pfad der Datei und die Ziel-URL nennen.
4. **Dort aufhören.** Nicht versuchen, den Text per `curl`, API-Token oder auf anderem Weg abzusenden.
Ein guter Issue-Entwurf ist so geschrieben, dass ein anderer Agent ihn ohne Rückfragen umsetzen kann: ein
beobachtbares Ziel, benannte Dateien, klare Abgrenzung.
## Wenn Gitea nicht erreichbar ist
Das Skript meldet das im Klartext. Mögliche Ursachen: kein Netz, Proxy nicht gesetzt (`HTTPS_PROXY`), Server
nicht verfügbar. Dann **nicht raten**, was im Ticket steht — mit der Arbeit warten oder den Menschen um den
Ticketinhalt bitten.
## Grenzen
- Keine Zugangsdaten, keine personenbezogenen Daten, keine echten Verfahrensdaten und keine internen
Originalunterlagen in Ticket-Texten.
- Keine Ticket-Inhalte in Zusammenfassungen erfinden oder ergänzen. Was nicht im Ticket steht, ist eine offene
Frage, keine Annahme.
- Labels, Zuweisungen, Meilensteine und Status ändert der Mensch im Browser.
+32
View File
@@ -0,0 +1,32 @@
---
name: sn-uebergabe
description: Aufgabe sauber abschließen oder pausieren – Übergabenotiz nach Vorlage schreiben, STATUS.md und Wissensbasis (fallstricke, glossar, entscheidungen, sn-bpmn-Doku) nachziehen, damit die nächste Person oder der nächste Agent nicht neu erkunden muss. Verwenden am Ende jeder Aufgabe, vor dem PR, bei Sitzungsende oder "Übergabe".
---
# Übergabe schreiben und Wissen sichern
## 1. Übergabenotiz (immer)
Vorlage `docs/vorlagen/uebergabe.md` ausfüllen; sie gehört in die PR-Beschreibung (bei Pausen: als Kommentar im Issue).
Kurz und konkret: Stand, geänderte Dateien mit Grund, Nachweis (Testzeile, PICTURE-Import), Annahmen, nächste Schritte,
und die zwei bis drei Dateien, mit denen es weitergeht.
## 2. Wissensbasis prüfen – nur was **nicht aus dem Code ablesbar** ist
| Frage | Wenn ja → |
|---|---|
| Hat etwas überraschend Zeit gekostet (PICTURE-Import, Layoutfehler, Werkzeugfalle)? | `docs/wissen/fallstricke.md`, passende Rubrik, oben, mit Datum und Quelle |
| Hat sich die Markdown-Syntax, ein Element oder eine Modellierregel geändert? | `.agents/skills/sn-bpmn/Anleitung.md`, `Elementkatalog.md` bzw. `Modellierregeln.md` |
| Fachbegriff musste erklärt werden? | `docs/wissen/glossar.md` |
| Hat das Team etwas entschieden? | neue Datei `docs/entscheidungen/NNNN-<thema>.md` nach Vorlage im dortigen README, Tabelle im README ergänzen |
| Nichts davon? | In der Übergabenotiz „Wissensbasis: nichts Neues“ |
## 3. `docs/status/STATUS.md`
- Zeile in „Wer arbeitet woran“ aktualisieren; bei Merge Eintrag unter „Erledigt“; erledigte Baustellen streichen.
- Datum im Kopf aktualisieren.
## 4. Werkzeug-Gedächtnis
Notizen aus der Claude-Auto-Memory, die andere brauchen, jetzt in die Dateien oben
übertragen. Lokale Pfade und persönliche Vorlieben bleiben lokal (`CLAUDE.local.md`, gitignored).
## 5. Vor dem Beenden
- Zwischenstand committet (auch WIP-Commit auf dem Feature-Branch ist besser als ungesicherte Änderungen).
- Offene Fragen im Issue notiert, nicht nur im Chat.
+23
View File
@@ -0,0 +1,23 @@
---
description: Sitzungsbeginn - Stand aller Repositories holen und Hauptbranch aktualisieren
allowed-tools: Bash(python scripts/hallo.py:*), Bash(python3 scripts/hallo.py:*), Read
---
Führe `python scripts/hallo.py` aus und werte das Ergebnis aus.
Berichte danach kurz und ohne die Rohausgabe zu wiederholen:
1. **Welche Repositories aktualisiert wurden** und um wie viele Commits.
2. **Was Aufmerksamkeit braucht** — ungesicherte Änderungen, eigene nicht gepushte Commits,
Repositories auf einem fremden Branch, fehlende Klone, nicht erreichbare Repositories.
Nenne dabei jeweils den konkreten nächsten Schritt.
3. **Wenn alles sauber ist:** ein Satz, mehr nicht.
Wenn Repositories fehlen, weise auf `python scripts/setup_workspace.py` hin.
Verändere selbst nichts: kein `merge`, kein `stash`, kein `reset`, kein Branchwechsel, kein `push`.
Das Skript hat bereits alles getan, was gefahrlos möglich ist. Alles Übrige entscheidet der Mensch —
siehe `docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6.
Frage zum Schluss, woran gearbeitet werden soll. Biete an, mit `python scripts/gitea.py tickets`
die offenen Tickets zu zeigen.
+44
View File
@@ -0,0 +1,44 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"deny": [
"Bash(git push:*)",
"Bash(git merge:*)",
"Bash(git rebase:*)",
"Bash(git tag:*)",
"Bash(git reset --hard:*)",
"Bash(git clean:*)",
"Bash(git stash:*)",
"Bash(git remote set-url:*)",
"Bash(git config --global:*)",
"Bash(gh:*)",
"Bash(tea:*)",
"Read(./**/.env)",
"Read(./**/*.token)",
"Read(./**/credentials*)"
],
"ask": [
"Bash(git commit:*)",
"Bash(git switch:*)",
"Bash(git checkout:*)"
],
"allow": [
"Bash(git status:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(git show:*)",
"Bash(git branch:*)",
"Bash(git fetch:*)",
"Bash(git pull --ff-only:*)",
"Bash(git add:*)",
"Bash(python scripts/hallo.py:*)",
"Bash(python scripts/gitea.py:*)",
"Bash(python scripts/setup_workspace.py:*)",
"Bash(python scripts/pruefe_umlaute.py:*)",
"Bash(python -m pytest:*)"
]
}
}
+13
View File
@@ -0,0 +1,13 @@
# Zeilenenden vereinheitlichen. Das Team arbeitet unter Windows; ohne diese Datei
# landen je nach Rechner CRLF im Repository und erzeugen Diffs ohne Inhalt.
* text=auto eol=lf
# Binärdateien nicht anfassen
*.png binary
*.jpg binary
*.ico binary
*.xlsx binary
*.zip binary
*.pdf binary
*.docx binary
*.vsdx binary
+26
View File
@@ -0,0 +1,26 @@
# Weitere Repositories der Gruppe (siehe workspace.toml) sind eigenständige Git-Klone
# und werden hier nicht versioniert. Beim Eintragen eines neuen Repos hier ergänzen.
# Von scripts/setup_workspace.py aus .agents/skills/ gespiegelt (Claude Code liest nur .claude/skills/).
# Ohne Schrägstrich, damit auch ein Symlink bzw. eine Junction erfasst wird.
.claude/skills
# Entwürfe: Gitea-Tickets und -Kommentare (scripts/gitea.py entwurf ...) sowie
# Arbeitsschritt-Entwürfe des Skills sn-bpmn. Sie werden nicht versioniert.
entwuerfe/
# Lokales Referenzmaterial (Quelldokumente, Word/PDF-Prozessbeschreibungen).
# Enthält ggf. interne Unterlagen und wird deshalb nie versioniert.
referenz/
# Lokale, nicht geteilte Dateien
CLAUDE.local.md
AGENTS.local.md
.claude/settings.local.json
.codex/
*.local.toml
# Python / Tests
__pycache__/
*.py[cod]
.pytest_cache/
+91
View File
@@ -0,0 +1,91 @@
# Projektwissen AG_Prozesse – Anweisungen für KI-Agenten und Mitwirkende
Kanonische, werkzeugneutrale Anweisungsdatei. Claude Code liest sie über `CLAUDE.md`.
Sie bleibt bewusst kurz: Details liegen in `docs/` und werden nur bei Bedarf gelesen.
## Worum es geht
- Arbeitsgruppe der sächsischen Flurbereinigungsbehörden („LNO Sachsen") zur **Modellierung von
Verwaltungsprozessen in BPMN 2.0**. Zielwerkzeug für den Import ist **PICTURE**.
- Kernwerkzeug ist der Skill `sn-bpmn`: Prozessbeschreibung (Rohtext, Stichpunkte, Word/PDF) → strukturierte
Arbeitsschritt-`.md` → BPMN-2.0-Datei (`.agents/skills/sn-bpmn/md_to_bpmn.py`).
- Dieser Ordner ist der Klon des Repositories **`AG_Prozesse/Projektwissen`**. Weitere Repositories der Gruppe
würden als eigenständige Git-Klone in Unterordnern liegen und in `workspace.toml` stehen – derzeit gibt es keine.
- Die Arbeitsweise (Skripte, Skills, Git-Regeln) stammt aus `AG_QGIS/Projektwissen`, siehe
`docs/entscheidungen/0001-uebernahme-aus-ag-qgis.md`.
## Zuerst lesen – nur was die Aufgabe braucht
1. `docs/status/STATUS.md` – aktueller Stand, offene Baustellen, wer woran arbeitet.
2. `docs/wissen/fallstricke.md` – bekannte Stolperfallen (BPMN-Layout, PICTURE-Import, Werkzeuge).
3. Für Modellierungsaufgaben: `.agents/skills/sn-bpmn/SKILL.md` und die dort genannten Dateien.
4. Fachbegriffe: `docs/wissen/glossar.md`. Übersicht aller Dokumente: `docs/README.md`.
## Harte Regeln
### Git und Server – der Mensch drückt ab
- **Nie pushen, mergen, taggen oder etwas auf Gitea verändern.** Agenten bereiten vor, eine Person sendet ab.
`.claude/settings.json` sperrt `git push`, `git merge`, `git rebase`, `git tag` technisch.
- Hauptbranch ist `main`. Nie direkt auf `main` committen: eigener Branch `feature/<nr>-<kurzname>` bzw.
`bugfix/<nr>-<kurzname>`, dann Pull Request. Vollständig: `docs/leitfaden/05_git_arbeitsweise.md`.
- Sitzungsbeginn: `python scripts/hallo.py` (bzw. `/hallo`), bevor irgendetwas geändert wird.
### Tests
- Vor jedem Commit: `python -m pytest -q .agents/skills` – alle Tests grün.
- **Neues Verhalten von `md_to_bpmn.py` ohne Test wird nicht gemergt.** Bei einer Fehlerbehebung zuerst der Test,
der den Fehler zeigt.
- **Jeder neue oder geänderte Test bekommt einen deutschen Docstring**, erste Zeile in der Form `"""Prüft, dass …"""`.
- Tests werden nicht gelöscht, nicht mit `skip` stillgelegt und nicht in ihrer Erwartung angepasst, um grün zu werden –
außer das Verhalten wurde bewusst geändert und der Pull Request begründet es.
### Inhalte
- **Keine personenbezogenen Daten, keine Zugangsdaten, keine internen Originalunterlagen** im Repository, in der Doku
oder in Prompts. Quelldokumente (Word/PDF) bleiben lokal unter `referenz/` (nicht versioniert); ins Repository kommen
nur die daraus abgeleiteten, bereinigten Arbeitsschritt-`.md` und BPMN-Dateien.
- Rollen und Verantwortliche in Prozessmodellen als Funktion benennen („Sachbearbeitung Flurbereinigung"),
nie als Person.
- Keine Python-Abhängigkeiten außer der Standardbibliothek und `pytest` ohne Issue-Absprache und Eintrag in
`fallstricke.md`.
- Kein Umbau „nebenbei". Strukturänderungen nur über eigenes Issue.
### Allgemein
- Sprache: Doku, Kommentare, Docstrings, Commit-Nachrichten, Issues und PRs auf Deutsch, mit echten Umlauten
(Prüfung: `python scripts/pruefe_umlaute.py`). Bezeichner nach dem Stil der jeweiligen Datei.
## Arbeitsablauf in Kurzform (Details: Skill `sn-aufgabe-bearbeiten`)
1. `python scripts/hallo.py` – Stand holen.
2. Gitea-Issue lesen (Skill `sn-ticket`, `python scripts/gitea.py ticket Projektwissen <nr>`) oder als Entwurf
vorbereiten. Ziel und Abnahmekriterien klären, bevor etwas geändert wird.
3. Branch `feature/<issue-nr>-<kurzname>` bzw. `bugfix/<issue-nr>-<kurzname>` von `main`.
4. In kleinen Schritten umsetzen; nur betroffene Dateien und Abschnitte lesen.
5. `python -m pytest -q .agents/skills` und `python scripts/pruefe_umlaute.py`. Erzeugte BPMN-Dateien zusätzlich
in PICTURE importieren und das Ergebnis im PR nennen.
6. Pull-Request-Text nach `docs/vorlagen/pull_request.md` vorbereiten, inklusive Übergabenotiz (Skill `sn-uebergabe`).
**Push und Pull Request macht der Mensch.**
7. Neue Erkenntnisse in `docs/wissen/` oder `docs/entscheidungen/` festhalten, nicht nur im lokalen Gedächtnis des Werkzeugs.
## Tokensparend arbeiten
- Gezielt `grep`, dann nur die nötigen Abschnitte lesen. `md_to_bpmn.py` (über 1000 Zeilen) nie komplett laden.
- Keine Dateien oder Testausgaben in den Chat kopieren; Pfade mit Zeilennummern nennen.
- Am Ende jeder Aufgabe eine Übergabenotiz, damit die nächste Sitzung nicht neu erkunden muss.
- Breite Suchen an einen Unteragenten delegieren, wenn das Werkzeug das kann; Hauptkontext schlank halten.
## Befehle
- Sitzungsbeginn: `python scripts/hallo.py` (Claude Code: `/hallo`)
- Tickets: `python scripts/gitea.py tickets` | `python scripts/gitea.py ticket Projektwissen 12` | `… suche "<text>"`
- Tests: `python -m pytest -q .agents/skills`
- Umlaute prüfen: `python scripts/pruefe_umlaute.py`
- Workspace einrichten oder aktualisieren: `python scripts/setup_workspace.py` (mit `--pull` für fast-forward)
- BPMN erzeugen: `python .agents/skills/sn-bpmn/md_to_bpmn.py entwuerfe/<name>.md <name>.bpmn`
## Skills
Liegen in `.agents/skills/` (Agent-Skills-Standard) und werden nach `.claude/skills/` gespiegelt:
`sn-bpmn`, `sn-aufgabe-bearbeiten`, `sn-ticket`, `sn-uebergabe`.
+17
View File
@@ -0,0 +1,17 @@
@AGENTS.md
## Hinweise für Claude Code
- **Sitzungsbeginn:** `/hallo` ausführen, bevor irgendetwas geändert wird. Der Befehl liegt in
`.claude/commands/hallo.md` und ruft `scripts/hallo.py` auf.
- **Berechtigungen:** `.claude/settings.json` sperrt `git push`, `git merge`, `git rebase`, `git tag` und
`gh`/`tea`. Das ist Absicht, keine Fehlkonfiguration: auf dem Server verändert nur der Mensch etwas
(`docs/leitfaden/05_git_arbeitsweise.md` Abschnitt 6). Läuft ein Kommando in eine Ablehnung, ist der richtige
nächste Schritt, den fertigen Befehl zu nennen — nicht, ihn zu umgehen.
- **Skills** liegen kanonisch in `.agents/skills/`; `python scripts/setup_workspace.py --nur-skills` spiegelt sie
nach `.claude/skills/`. Nur `.agents/skills/` bearbeiten.
- **Die Auto-Memory ist lokal** und wird nicht geteilt. Alles, was andere Entwickler wissen müssen, gehört nach
`docs/wissen/`, `docs/entscheidungen/` oder `docs/status/STATUS.md` (Skill `sn-uebergabe`).
- **Beim Arbeiten in einem weiteren Repo-Ordner** (siehe `workspace.toml`) gilt zusätzlich dessen `AGENTS.md`.
- **Für breite Suchen** den Explore-Unteragenten nutzen; Ergebnisse nur als Zusammenfassung in den
Hauptkontext übernehmen.
+120
View File
@@ -0,0 +1,120 @@
# Projektwissen — AG Prozesse Sachsen
**Hier fängst du an.** Dieses Repository ist das gemeinsame Gedächtnis der Arbeitsgruppe, die Verwaltungsprozesse
der sächsischen Flurbereinigungsbehörden („LNO Sachsen") in **BPMN 2.0** modelliert und in **PICTURE** importiert.
Es enthält die Regeln, die Wissensbasis, die Anweisungen für KI-Agenten, die Skripte und den Skill `sn-bpmn`,
der aus einer Prozessbeschreibung eine importierbare BPMN-Datei macht.
```mermaid
flowchart LR
Q["Prozessbeschreibung<br/><i>Rohtext, Word, PDF</i>"] --> E["Arbeitsschritt-.md<br/><i>Entwurf, Skill sn-bpmn</i>"]
E -->|"fachlich prüfen<br/>und anpassen"| E
E -->|"md_to_bpmn.py"| B["BPMN 2.0<br/><i>.bpmn</i>"]
B --> P(["Import in PICTURE"])
style E fill:#e8f6ec,stroke:#2f9e44
```
> **Ordnername ≠ Reponame.** Das Repository heißt `Projektwissen`, der lokale Ordner heißt `AG_Prozesse` — damit
> weitere Repositories der Gruppe später daneben geklont werden können. Der Klon-Befehl unten setzt das richtig.
## In 15 Minuten startklar
**Vorher da sein muss:** Git, Python ≥ 3.11 mit `pytest`, ein Gitea-Konto mit Schreibrecht in der Organisation
`AG_Prozesse`. Details und Windows-Besonderheiten:
[`docs/leitfaden/04_werkzeuge_einrichten.md`](docs/leitfaden/04_werkzeuge_einrichten.md).
```bash
# 1. Projektwissen holen — Zielordner AG_Prozesse nicht vergessen
git clone https://entwicklung.flurneuordnung-sachsen.de/AG_Prozesse/Projektwissen.git AG_Prozesse
cd AG_Prozesse
# 2. Skills für Claude Code bereitstellen (und ggf. weitere Repositories holen)
python scripts/setup_workspace.py
# 3. Prüfen, dass alles läuft (Sekunden)
python -m pytest -q .agents/skills
```
```bash
# 4. Claude Code installieren (Windows/PowerShell; macOS/Linux siehe Leitfaden 04)
irm https://claude.ai/install.ps1 | iex
# 5. KI-Werkzeug starten — im Ordner AG_Prozesse
claude
```
Zum Warmwerden diese Frage stellen:
> *„Fasse `docs/status/STATUS.md` zusammen und nenne die drei wichtigsten Regeln aus `AGENTS.md`."*
Kommt eine brauchbare Antwort, ist die Einrichtung fertig. Kommt keine, wurde das Werkzeug im falschen Ordner
gestartet.
## Einen Prozess modellieren
Im Ordner `AG_Prozesse` Claude Code starten und zum Beispiel sagen:
> *„Modelliere den Prozess aus `referenz/antragspruefung.docx` als BPMN."*
Der Skill `sn-bpmn` erzeugt daraus einen Entwurf `entwuerfe/<name>.md`, nennt die getroffenen Annahmen und wartet.
Du prüfst und änderst den Entwurf fachlich, dann: *„passt so, wandle um"*. Heraus kommt `<name>.bpmn` für den
Import in PICTURE. Syntax des Entwurfs: [`.agents/skills/sn-bpmn/Anleitung.md`](.agents/skills/sn-bpmn/Anleitung.md).
`referenz/` und `entwuerfe/` werden nicht versioniert — Quelldokumente bleiben auf deinem Rechner.
## Am Werkzeug mitarbeiten
| # | Schritt | Womit |
|---|---|---|
| 1 | Stand holen — **immer zuerst** | `/hallo` bzw. `python scripts/hallo.py` |
| 2 | Offene Tickets ansehen | `python scripts/gitea.py tickets` |
| 3 | Ein Ticket im Detail lesen | `python scripts/gitea.py ticket Projektwissen 12` |
| 4 | Aufgabe an den Agenten geben | Skill `sn-aufgabe-bearbeiten`, Muster in [`03_arbeitsweise.md`](docs/leitfaden/03_arbeitsweise.md) |
| 5 | Nachweis erbringen | `python -m pytest -q .agents/skills` |
| 6 | Pull Request | Vorlage [`docs/vorlagen/pull_request.md`](docs/vorlagen/pull_request.md) |
## Die Regeln, die immer gelten
Die vollständige, für Agenten verbindliche Fassung steht in [`AGENTS.md`](AGENTS.md). Das Wichtigste in Kurzform:
1. **Der Mensch drückt ab.** Agenten bereiten alles vor — Push, Merge, Tag und alles auf dem Gitea-Server macht
eine Person mit ihrem eigenen Zugang. → [`05_git_arbeitsweise.md`](docs/leitfaden/05_git_arbeitsweise.md)
2. **Nie direkt auf `main` committen.** Immer Arbeitsbranch `feature/…` oder `bugfix/…` und Pull Request.
3. **Neues Verhalten des Konverters ohne Test wird nicht gemergt.**
4. **Keine personenbezogenen Daten und keine internen Originalunterlagen** im Repository oder in Prompts.
Rollen im Modell als Funktion, nie als Person.
5. **Kein Umbau nebenbei.** Strukturänderungen brauchen ein eigenes Issue.
6. **Deutsch mit echten Umlauten** in Doku, Kommentaren, Commits, Issues und Pull Requests.
7. **Wissen gehört in `docs/`**, nicht in das Gedächtnis des KI-Werkzeugs — das ist lokal und teilt sich nicht.
## Wegweiser
| Pfad | Inhalt |
|---|---|
| [`AGENTS.md`](AGENTS.md) / `CLAUDE.md` | Anweisungen für Agenten (kanonisch ist `AGENTS.md`) |
| [`workspace.toml`](workspace.toml) | Gitea-Organisation und Verzeichnis weiterer Repositories |
| `scripts/` | `hallo.py`, `setup_workspace.py`, `gitea.py`, `pruefe_umlaute.py` |
| `.agents/skills/` | Skills, gespiegelt nach `.claude/skills/`; `sn-bpmn` enthält den Konverter |
| `.claude/commands/` | Slash-Befehle für Claude Code (`/hallo`) |
| `docs/status/` | **Aktueller Stand** — immer zuerst lesen |
| `docs/leitfaden/` | Arbeitsweise, Werkzeuge, Git |
| `docs/wissen/` | Fallstricke, Glossar — das gemeinsame Gedächtnis |
| `docs/entscheidungen/` | Warum etwas so ist (ADR, append-only) |
| `docs/vorlagen/` | Issue, Pull Request, Übergabe, Repo-`AGENTS.md` |
## Wenn etwas nicht geht
| Symptom | Ursache und Abhilfe |
|---|---|
| `git clone` meldet „Repository not found" | Zugang zur Organisation `AG_Prozesse` fehlt — Maintainer fragen |
| `No module named pytest` | `python -m pip install pytest` im selben Python |
| Der Agent kennt die Regeln oder den Skill nicht | Falscher Startordner, oder `python scripts/setup_workspace.py --nur-skills` fehlt |
| PICTURE lehnt die BPMN-Datei ab oder zeichnet sie falsch | [`docs/wissen/fallstricke.md`](docs/wissen/fallstricke.md), Abschnitt BPMN |
| Konflikte beim Pullen | `/hallo` vor der Arbeit vergessen — siehe [`05_git_arbeitsweise.md`](docs/leitfaden/05_git_arbeitsweise.md) |
## Mitmachen
Auch dieses Repository wird per Pull Request geändert — Regeln, Wissensbasis und Skripte sind genauso
versioniert wie Code. Wer eine Regel für falsch hält, macht ein Issue auf. Wer etwas gelernt hat, das andere
Zeit kostet, trägt es in [`docs/wissen/fallstricke.md`](docs/wissen/fallstricke.md) ein.
+18
View File
@@ -0,0 +1,18 @@
# Dokumentation – Wegweiser
| Ordner | Charakter | Wann lesen |
|---|---|---|
| `status/STATUS.md` | lebend, kurz | **immer zuerst** – aktueller Stand, Baustellen |
| `wissen/fallstricke.md` | lebend | bei Überraschungen; vor Arbeiten am Konverter und am PICTURE-Import |
| `wissen/glossar.md` | lebend | Fachbegriffe (Flurbereinigung, BPMN) |
| `entscheidungen/` | append-only (ADR) | warum etwas so ist; neue Entscheidung = neue Datei |
| `leitfaden/03_arbeitsweise.md` | stabil | Einstieg, Arbeitszyklus, Checklisten |
| `leitfaden/04_werkzeuge_einrichten.md` | stabil | Einrichtung: Claude Code als CLI und in VS Code; von null zur ersten Aufgabe |
| `leitfaden/05_git_arbeitsweise.md` | **verbindlich** | Branches, Pull Requests, was der Agent darf |
| `vorlagen/` | Vorlagen | Issue, PR, Übergabe, Repo-`AGENTS.md` |
Die Fachdoku des Konverters (Markdown-Syntax, Elementkatalog, Modellierregeln) liegt beim Skill in
`.agents/skills/sn-bpmn/`.
Regel: **Lebende** Dokumente werden mit jeder Änderung gepflegt, **Momentaufnahmen** nicht nachträglich geändert
(neue Analyse = neue Datei mit Datum).
@@ -0,0 +1,34 @@
# 0001 – Arbeitsweise aus AG_QGIS/Projektwissen übernehmen, ohne Historie
Status: vorgeschlagen · Datum: 2026-09-23 · Beteiligte: AG Prozesse
## Kontext
Die BPMN-bezogene Zusammenarbeit bekommt eine eigene Gitea-Organisation `AG_Prozesse`. In `AG_QGIS/Projektwissen`
sind Regeln, Skripte, Skills und eine Wissensbasis für die KI-gestützte Zusammenarbeit entstanden, und der Skill
`sn-bpmn` wurde dort auf dem Branch `BPMN-Skill` entwickelt. Das meiste davon ist nicht QGIS-spezifisch.
## Entscheidung
`AG_Prozesse/Projektwissen` wird **neu angelegt** und übernimmt ausgewählte Dateien aus
`AG_QGIS/Projektwissen` (Stand `main` @ `decd2a0`, `sn-bpmn` von `BPMN-Skill` @ `6efcac4`) – **ohne Git-Historie**.
QGIS- und Plugin-Spezifisches (Testrunner, Testkatalog, QGIS-Profil, ALKIS/WFS-Wissen, Modulkarte, Release)
wird nicht übernommen.
Folgende Grundsätze aus AG_QGIS gelten hier unverändert (dort ADR 0001, 0002, 0004, 0006, 0008):
- `AGENTS.md` ist die einzige kanonische Anweisungsdatei; `CLAUDE.md` ist nur ein Zeiger darauf.
- Das Repository heißt `Projektwissen`, der lokale Ordner `AG_Prozesse/`. Weitere Repositories der Gruppe werden
als eigenständige Klone daneben gelegt und in `workspace.toml` eingetragen.
- Geteiltes Wissen liegt versioniert in `docs/`, nicht im lokalen Gedächtnis eines KI-Werkzeugs.
- Die Anleitung beschreibt Claude Code; die Dateistruktur bleibt werkzeugneutral (`.agents/skills/` ist kanonisch).
- Agenten verändern nichts auf dem Server; Push, Merge und Tag macht der Mensch.
## Folgen
Die Historie bleibt übersichtlich und enthält keine QGIS-Inhalte, auch nicht solche, die dort früher einmal
committet und wieder entfernt wurden. Die Herkunft ist über diese Datei und die genannten Commits nachvollziehbar.
Verbesserungen an den gemeinsamen Skripten (`hallo.py`, `gitea.py`, `setup_workspace.py`) müssen künftig in beiden
Repositories nachgezogen werden, solange es kein gemeinsames Werkzeug-Repository gibt. Der Branch `BPMN-Skill` in
AG_QGIS ist abgelöst.
## Verworfene Alternativen
Klonen und Löschen mit voller Historie (fremde Historie, alte Inhalte bleiben sichtbar); gemeinsames
Werkzeug-Repository für beide Gruppen (derzeit mehr Aufwand als Nutzen, später möglich).
@@ -0,0 +1,21 @@
# 0002 – Branchmodell: `main` plus Arbeitsbranches
Status: vorgeschlagen · Datum: 2026-09-23 · Beteiligte: AG Prozesse
## Kontext
In AG_QGIS gibt es drei Release-Kanäle (`unstable`, `testing`, `stable`), weil QGIS-Plugins über Plugin-Feeds an die
Arbeitsplätze ausgeliefert werden. Hier gibt es keine Auslieferung: Ergebnisse sind Dokumente, Prozessmodelle und
der Konverter, die direkt aus dem Repository genutzt werden.
## Entscheidung
Es gibt einen Hauptbranch **`main`**. Gearbeitet wird auf `feature/<issue-nr>-<kurzname>` bzw.
`bugfix/<issue-nr>-<kurzname>`, zurückgeführt per Pull Request mit Review. Auf `main` wird nie direkt committet;
auf Gitea wird `main` gegen direkte Pushes geschützt.
## Folgen
Keine Kanäle, keine Rückführung zwischen Kanälen, keine Release-Tags. Wird später eine versionierte Auslieferung
nötig (etwa des Konverters), werden Tags auf `main` gesetzt und diese Entscheidung ergänzt.
## Verworfene Alternativen
Drei Kanäle wie in AG_QGIS (Aufwand ohne Auslieferungsweg); direkt auf `main` arbeiten (kein Review, keine
Nachvollziehbarkeit).
+29
View File
@@ -0,0 +1,29 @@
# Entscheidungen (ADR)
Eine Datei je Entscheidung, fortlaufend nummeriert, nie nachträglich geändert. Wird eine Entscheidung ersetzt,
bekommt die alte den Status „ersetzt durch NNNN“ und die neue verweist zurück.
Vorlage:
```markdown
# NNNN – Titel
Status: vorgeschlagen | angenommen | ersetzt durch NNNN · Datum: JJJJ-MM-TT · Beteiligte: …
## Kontext
Warum stand die Entscheidung an? Welche Randbedingungen?
## Entscheidung
Was wurde entschieden (ein Satz), danach die wichtigsten Details.
## Folgen
Was wird dadurch einfacher, was schwerer, was muss getan werden?
## Verworfene Alternativen
Kurz, mit Grund.
```
| Nr. | Titel | Status |
|---|---|---|
| 0001 | Arbeitsweise aus AG_QGIS/Projektwissen übernehmen, ohne Historie | vorgeschlagen |
| 0002 | Branchmodell: `main` plus Arbeitsbranches | vorgeschlagen |
+134
View File
@@ -0,0 +1,134 @@
# Arbeitsweise: So arbeiten wir mit KI-Agenten an Prozessmodellen
Für Kolleginnen und Kollegen, die neu einsteigen. Fachwissen wird vorausgesetzt, Software-Erfahrung nicht.
Wir arbeiten mit Claude Code; die Dateien bleiben werkzeugneutral (ADR 0001). Die Kurzfassung für den Agenten
selbst ist `AGENTS.md`, dieser Leitfaden erklärt das Warum und das Wie. Git im Detail: `05_git_arbeitsweise.md`.
Einrichtung Schritt für Schritt: `04_werkzeuge_einrichten.md`.
## 1. Zwei Arten von Arbeit
| Arbeit | Wer | Womit | Ergebnis |
|---|---|---|---|
| **Einen Prozess modellieren** | Fachleute | Skill `sn-bpmn` | BPMN-Datei für PICTURE |
| **Werkzeug und Wissen verbessern** | alle, die einen PR stellen | Skill `sn-aufgabe-bearbeiten` | Pull Request auf `main` |
Wer nur modelliert, braucht Git kaum: Claude Code im Ordner `AG_Prozesse` starten, Quelle nennen, Entwurf prüfen,
umwandeln. Git und Pull Requests kommen erst ins Spiel, wenn sich am Konverter, an den Regeln oder an der Doku
etwas ändern soll — oder wenn ein abgenommenes Modell im Repository abgelegt wird (STATUS, Baustelle 2).
## 2. Einen Prozess modellieren
```mermaid
flowchart TD
A["Quelle nach referenz/ legen<br/><i>Word, PDF, Rohtext</i>"] --> B["Agent: sn-bpmn<br/>Entwurf entwuerfe/&lt;name&gt;.md"]
B --> C["Annahmen lesen, Entwurf fachlich prüfen und anpassen"]
C -->|"noch nicht richtig"| C
C -->|"passt so, wandle um"| D["&lt;name&gt;.bpmn"]
D --> E["Import in PICTURE"]
E -->|"Darstellungsfehler"| F["Issue anlegen<br/><i>Skill sn-ticket</i>"]
```
- **Der Agent wartet nach dem Entwurf.** Er wandelt erst um, wenn du ausdrücklich zustimmst. Das ist Absicht: die
fachliche Richtigkeit verantwortest du, nicht der Agent.
- **Annahmen ernst nehmen.** Der Agent nennt, wo er Schrittgrenzen, Verzweigungen oder Verantwortliche geraten hat.
Genau dort liegen die Fehler.
- **Rollen statt Personen.** Verantwortliche immer als Funktion („Sachbearbeitung"), nie mit Namen.
- **Darstellungsfehler in PICTURE** (Kanten durch Elemente, überlappende Lanes) sind Fehler des Konverters, nicht
deines Entwurfs. Issue anlegen, mit dem Entwurf als Beispiel (bereinigt).
## 3. Wie man einem Agenten einen Auftrag gibt
Der Agent liest `AGENTS.md` automatisch. Was er **nicht** weiß: was genau gewollt ist, wo es endet, und was fachlich
richtig ist. Ein guter Auftrag hat vier Teile:
1. **Ziel und Issue:** „Bearbeite Issue #12: …“ oder das Ziel in zwei Sätzen.
2. **Kontext-Zeiger statt Erklärungen:** „Betroffen ist die Lane-Beschriftung in `md_to_bpmn.py`, Regel in
`Modellierregeln.md` Abschnitt Lanes.“
3. **Grenzen:** „Keine Änderung an der Markdown-Syntax. Keine neuen Abhängigkeiten. Kein Umbau.“
4. **Abnahme:** „Fertig ist es, wenn ein Test für einen Rollennamen über 30 Zeichen existiert, alle Tests grün sind
und die Übergabenotiz geschrieben ist.“
Beispiel eines schlechten Auftrags: „Mach die Diagramme schöner.“ Das erzeugt lange Erkundung, unklare Änderungen
und einen PR, den niemand reviewen kann.
**Während der Arbeit:** Rückfragen beantworten, fachlich prüfen, was der Agent behauptet, und ihn bei
Abschweifungen zurückholen („Nur Issue #12“). Wenn er anfängt, Tests zu ändern statt Code, nach der Begründung
fragen.
## 4. Die Regeln und warum es sie gibt
| Regel (aus `AGENTS.md`) | Warum |
|---|---|
| Der Mensch pusht, mergt und legt Tickets an | Jede Aktion auf dem Server trägt den Namen einer Person |
| Nie direkt auf `main` | `main` ist der abgenommene Stand, den alle nutzen |
| Neues Konverter-Verhalten nur mit Test | Tests sind der einzige schnelle Nachweis für Agentenarbeit; Layoutfehler kommen sonst unbemerkt zurück |
| Keine personenbezogenen Daten, keine Originalunterlagen | Alles, was der Agent liest, geht an den Anbieter; das Repository ist für die ganze Gruppe sichtbar |
| Kein Umbau nebenbei | Umbauten ohne Plan sind nicht reviewbar |
| Deutsch mit echten Umlauten | Modelle landen in PICTURE und werden von Fachleuten gelesen |
| Übergabenotiz | Die nächste Person oder der nächste Agent soll nicht von vorn beginnen |
## 5. Wissen festhalten
| Situation | Was tun |
|---|---|
| Etwas hat überraschend Zeit gekostet | Eintrag in `docs/wissen/fallstricke.md` (Rubrik, Datum, Quelle) |
| Markdown-Syntax, Element oder Modellierregel geändert | `.agents/skills/sn-bpmn/Anleitung.md`, `Elementkatalog.md`, `Modellierregeln.md` |
| Team hat etwas entschieden | neue Datei `docs/entscheidungen/NNNN-<thema>.md` nach Vorlage |
| Aufgabe abgegeben oder pausiert | Übergabenotiz im PR (Vorlage) und Zeile in `STATUS.md` |
| Fachbegriff musste erklärt werden | `docs/wissen/glossar.md` |
Der Skill `sn-uebergabe` führt den Agenten am Ende jeder Aufgabe durch genau diese Liste. Das eigene Gedächtnis des
Werkzeugs (Claude Auto-Memory) ist ein Notizzettel; teilbares Wissen gehört in die Dateien oben.
## 6. Was liegt wo, und was liest der Agent
| Datei | Wird gelesen |
|---|---|
| `AGENTS.md` | ja, über `CLAUDE.md` (`@AGENTS.md`) – kanonisch |
| `CLAUDE.md` | ja, enthält nur den Verweis und wenige Claude-spezifische Zeilen |
| `.agents/skills/` | kanonischer Ort; gelesen wird der Spiegel `.claude/skills/` |
| `.claude/commands/` | Slash-Befehle, z. B. `/hallo` |
| `.claude/settings.json` | Berechtigungen – sperrt `git push`, `merge`, `tag` |
| `docs/` | auf Abruf, nicht automatisch |
| `referenz/`, `entwuerfe/` | nur, wenn du darauf verweist; nicht versioniert |
| `CLAUDE.local.md` | ja, lokal, nicht versioniert |
## 7. Häufige Fragen
**Der Agent kennt den Skill `sn-bpmn` nicht.** Er wurde im falschen Ordner gestartet, oder der Skill-Spiegel fehlt:
`python scripts/setup_workspace.py --nur-skills`.
**Der Agent wandelt sofort um, ohne auf mich zu warten.** Das widerspricht dem Skill. Stoppen und auf Schritt 3
in `.agents/skills/sn-bpmn/SKILL.md` verweisen.
**Der Agent „repariert“ rote Tests durch Anpassen der Erwartung.** Nach Begründung fragen; nur akzeptieren, wenn das
Verhalten bewusst geändert wurde und das Issue es abdeckt.
**Der Agent hat eine Datei komplett umgeschrieben.** Diff prüfen, auf das Issue zurückführen, Rest verwerfen.
**Zwei Personen arbeiten am selben Thema.** In `STATUS.md` steht, wer woran arbeitet; vorher dort eintragen und
kleine PRs machen.
## 8. Checklisten
**Vor dem PR**
- [ ] Issue verlinkt, Branch von `main`
- [ ] `python -m pytest -q .agents/skills` grün, `python scripts/pruefe_umlaute.py` ohne Funde
- [ ] Bei geänderter BPMN-Ausgabe: PICTURE-Import dokumentiert
- [ ] Keine personenbezogenen Daten, keine Originalunterlagen
- [ ] Übergabenotiz im PR; Wissensbasis nachgezogen, falls nötig
- [ ] Diff gelesen: nur, was das Issue verlangt
**Review**
- [ ] Fachlich richtig? (BPMN-Regeln, Begriffe)
- [ ] Tests vorhanden und sinnvoll (nicht nur angepasst)?
- [ ] Umfang überschaubar (< 400 Zeilen)?
- [ ] Nach dem Merge: `STATUS.md`
**Sitzungsende (auch bei Abbruch)**
- [ ] Zwischenstand auf dem Arbeitsbranch committet
- [ ] Offene Fragen im Issue notiert
- [ ] `STATUS.md`-Zeile aktualisiert
+186
View File
@@ -0,0 +1,186 @@
# Werkzeuge einrichten: von null bis zur ersten Aufgabe
Stand 23.09.2026. Ergänzt [`03_arbeitsweise.md`](03_arbeitsweise.md) um die konkrete Einrichtung. Wer Abschnitt 2
durchgearbeitet hat, kann beide Varianten aus Abschnitt 3 und 4 nutzen und jederzeit zwischen ihnen wechseln —
sie greifen auf dieselben Dateien zu.
## 1. Zwei Varianten
Wir arbeiten mit **Claude Code**. Es gibt zwei Formen, dieselbe Software zu bedienen:
| Variante | Form | Wofür geeignet |
|---|---|---|
| **A · Claude Code CLI** | Terminal | lange, eigenständige Aufgaben; Skripte |
| **B · Claude Code in VS Code** | Erweiterung mit Chatpanel | Einstieg; Entwürfe und Diffs im Editor sehen und einzeln annehmen |
Beide lesen `CLAUDE.md` (und darüber `AGENTS.md`), beide finden die Skills unter `.claude/skills/`, beide
arbeiten über Git mit Gitea. **Empfehlung für Einsteiger: Variante B**, weil man jede Änderung — auch am
Arbeitsschritt-Entwurf — als Diff sieht, bevor sie übernommen wird.
**Nicht genutzt und nicht unterstützt:** Cloud-Agenten (Claude Code Web, Codex Cloud, Copilot Coding Agent) —
sie setzen GitHub voraus, wir arbeiten mit Gitea. Reine Chat-Oberflächen ohne Ordnerzugriff (claude.ai,
chatgpt.com) sind für Fachfragen brauchbar, aber nicht für Änderungen am Repository. Wer nur Chat hat,
formuliert Issues nach [`docs/vorlagen/issue.md`](../vorlagen/issue.md) und lässt sie von jemandem mit Agent
umsetzen.
## 2. Gemeinsame Grundlage (einmal pro Rechner)
### 2.1 Konten und Zugänge
- **Gitea-Konto** auf `entwicklung.flurneuordnung-sachsen.de` mit Schreibrecht in der Organisation `AG_Prozesse`
(beim Maintainer anfragen). Für HTTPS-Push ein **Zugriffstoken** anlegen:
Gitea → Einstellungen → Anwendungen → Token, Berechtigung `repository: read/write`.
- **Claude-Konto**: Abo (Pro, Max, Team, Enterprise) oder Console-Konto. Dienstliche E-Mail verwenden;
die Abrechnung klärt die Behörde.
- **Behördennetz:** ausgehendes HTTPS zu `claude.ai` und `api.anthropic.com` muss möglich sein. Bei Proxy die
Variablen `HTTPS_PROXY` und `HTTP_PROXY` setzen; bei Problemen mit der Zertifikatsprüfung die IT ansprechen.
- **PICTURE-Zugang** zum Prüfen des BPMN-Imports (nicht für alle nötig).
### 2.2 Git
- **Windows:** [Git for Windows](https://git-scm.com/downloads/win) mit den Standardoptionen. Enthält Git Bash
und den Credential Manager, der sich das Gitea-Token nach dem ersten Push merkt. Claude Code setzt Git for
Windows voraus.
- **macOS/Linux:** Git über Xcode-Tools bzw. die Paketverwaltung.
- Einmalig einstellen — **die E-Mail-Adresse muss die des Gitea-Kontos sein.** Gitea ordnet Commits über die
Adresse dem Benutzerkonto zu; passt sie nicht, erscheinen die Commits im Browser ohne Zuordnung, und niemand
sieht, wer etwas geändert hat. Nachträglich lässt sich das nur durch Umschreiben der Historie korrigieren.
```bash
git config --global user.name "Vorname Nachname"
git config --global user.email "<dienstliche Adresse aus dem Gitea-Konto>"
```
Wer den Rechner auch privat nutzt, setzt die dienstliche Adresse besser **je Repository** statt global —
`--local` statt `--global`.
- Prüfen, bevor der erste Commit entsteht:
```bash
git -C AG_Prozesse config user.email # muss die Gitea-Adresse sein
```
### 2.3 Python
Benötigt: **Python ≥ 3.11** und das Paket `pytest` für die Tests des Konverters.
- Python von python.org installieren, beim Setup „Add to PATH" anhaken, dann `python -m pip install pytest`.
- Prüfen: `python --version` und `python -m pytest --version` müssen beide ohne Fehler durchlaufen.
Die Skripte (`hallo.py`, `gitea.py`, `setup_workspace.py`, `pruefe_umlaute.py`) und der Konverter
`md_to_bpmn.py` brauchen **nur die Standardbibliothek**.
### 2.4 Projektwissen holen
```bash
git clone https://entwicklung.flurneuordnung-sachsen.de/AG_Prozesse/Projektwissen.git AG_Prozesse
cd AG_Prozesse
python scripts/setup_workspace.py # spiegelt die Skills, holt ggf. weitere Repos
python -m pytest -q .agents/skills # erwartet: alle Tests grün
```
Der Zielordner `AG_Prozesse` ist bewusst anders benannt als das Repository: darin können später weitere
Repositories der Gruppe nebeneinander liegen.
## 3. Variante A: Claude Code CLI
1. **Installieren.**
Windows (PowerShell): `irm https://claude.ai/install.ps1 | iex`
macOS/Linux: `curl -fsSL https://claude.ai/install.sh | bash`
Node.js wird nicht benötigt; der Installer aktualisiert sich selbst.
2. **Anmelden:** `claude` starten, im Browser mit dem Claude-Konto anmelden.
3. **Starten:** im Ordner `AG_Prozesse` `claude` eingeben. Beim ersten Mal den Import `@AGENTS.md` bestätigen.
4. **Prüfen:** `/context` listet `CLAUDE.md` und `AGENTS.md` unter „Memory files". `/hallo` und die Skills
(`sn-bpmn`, `sn-aufgabe-bearbeiten`, `sn-ticket`, `sn-uebergabe`) erscheinen im Menü.
5. **Berechtigungen:** Der Standardmodus fragt vor Änderungen. Für Routinearbeit ist „Accept edits" praktikabel.
`--dangerously-skip-permissions` niemals außerhalb einer Sandbox.
Das Repo verbietet über `.claude/settings.json` ohnehin `git push`, `git merge` und `git tag` —
siehe [`05_git_arbeitsweise.md`](05_git_arbeitsweise.md) Abschnitt 6.
6. **Erste Aufgabe:** Abschnitt 6 dieser Seite.
Die Auto-Memory ist lokal und wird nicht geteilt; Teilbares gehört nach `docs/` — Skill `sn-uebergabe`.
## 4. Variante B: Claude Code in VS Code
1. **VS Code installieren**, dann **den Ordner `AG_Prozesse` öffnen** (Datei → Ordner öffnen).
2. **Erweiterung installieren:** `Strg+Shift+X` → „Claude Code" (Herausgeber Anthropic) → Installieren.
Die Erweiterung bringt eine eigene CLI-Kopie mit; die Installation aus Variante A ist nur nötig, wenn man
`claude` zusätzlich im integrierten Terminal nutzen will.
3. **Anmelden:** Symbol in der Aktivitätsleiste → Sign in → Browser-Anmeldung.
4. **Python-Interpreter wählen:** `Strg+Shift+P` → „Python: Select Interpreter" → das Python aus Abschnitt 2.3.
5. **Arbeiten:** Panel öffnen. Markierter Text wird automatisch mitgesendet (`Alt+K` fügt einen Verweis ein).
Diffs erscheinen im Editor und werden einzeln angenommen oder verworfen. Den Berechtigungsmodus wählt man
unten im Eingabefeld; Pläne lässt man vor der Umsetzung im Plan-Modus prüfen.
### 4.1 Empfohlene Erweiterungen
| Erweiterung | Kennung | Wofür |
|---|---|---|
| Claude Code | `anthropic.claude-code` | der Agent selbst |
| Python | `ms-python.python` | Syntax, Interpreterwahl, Debugger |
| Markdown Preview Mermaid Support | `bierner.markdown-mermaid` | zeigt Mermaid-Diagramme in der Vorschau |
Ein BPMN-Betrachter als Erweiterung ist nicht nötig: `md_to_bpmn.py` kann eine Vorschau ohne PICTURE erzeugen,
siehe `.agents/skills/sn-bpmn/Anleitung.md`.
### 4.2 Persönliche Einstellungen
**Diese Einstellungen gehören in die Benutzereinstellungen, nicht ins Repository.** Das Repo enthält bewusst
keinen `.vscode/`-Ordner. Wer sie haben will, kopiert sie sich in die eigene `settings.json`
(`Strg+Shift+P` → „Preferences: Open User Settings (JSON)").
```jsonc
{
"files.exclude": { "**/__pycache__": true, "**/.pytest_cache": true },
// Einheitliche Dateiform — das Repo ist auf UTF-8 und LF eingestellt
"files.encoding": "utf8",
"files.eol": "\n",
"[python]": { "editor.tabSize": 4, "editor.insertSpaces": true },
"[markdown]": { "editor.wordWrap": "on" }
}
```
## 5. Einschränkungen und Datenschutz
- **Gitea statt GitHub.** Alle Cloud-Agenten setzen GitHub voraus und entfallen damit. Issues und Pull Requests
entstehen im Gitea-Browser; lesend geht die API auch ohne Anmeldung — dafür gibt es `scripts/gitea.py` und
den Skill `sn-ticket`.
- **Inhalte gehen an den Anbieter.** Alles, was der Agent liest, wird an den Anbieter übertragen — auch eine
Prozessbeschreibung aus `referenz/`. Deshalb: keine Zugangsdaten, keine personenbezogenen Daten, keine als
vertraulich eingestuften Unterlagen in Prompts oder Dateien. Im Zweifel die Quelle vorher bereinigen.
- **Der Mensch drückt ab.** Agenten dürfen nicht pushen, mergen, taggen oder irgendetwas auf dem Gitea-Server
verändern. Das ist in `.claude/settings.json` technisch hinterlegt, nicht nur als Bitte formuliert.
- **Windows-Symlinks.** `setup_workspace.py` fällt auf eine Junction oder Kopie zurück, wenn Symlinks nicht
erlaubt sind. Nach Änderungen an Skills `python scripts/setup_workspace.py --nur-skills` erneut ausführen.
## 6. Von null zur ersten Aufgabe (etwa eine Stunde)
1. Abschnitt 2 durcharbeiten; `python -m pytest -q .agents/skills` ist grün.
2. Variante A oder B einrichten. Prüfung: der Agent nennt auf Nachfrage die Regeln aus `AGENTS.md`.
3. `/hallo` ausführen — holt den Stand.
4. Zum Kennenlernen einen kleinen, unkritischen Prozess modellieren lassen (Skill `sn-bpmn`), den Entwurf
prüfen, umwandeln und in PICTURE importieren.
5. Für Änderungen am Repository: `python scripts/gitea.py tickets`, ein Ticket auswählen, in
`docs/status/STATUS.md` unter „Wer arbeitet woran" eintragen.
6. Auftrag nach dem Muster in [`03_arbeitsweise.md`](03_arbeitsweise.md) Abschnitt 3 geben, mit dem Skill
`sn-aufgabe-bearbeiten`.
7. Ergebnis prüfen: Tests grün, Diff gelesen.
8. Branch pushen (selbst!), Pull Request im Gitea-Browser nach
[`docs/vorlagen/pull_request.md`](../vorlagen/pull_request.md), Übergabenotiz aus dem Skill `sn-uebergabe`.
9. Review durch eine Kollegin oder einen Kollegen; nach dem Merge `STATUS.md` nachziehen.
## 7. Prüfliste „Einrichtung fertig"
- [ ] `git --version` und `python --version` (≥ 3.11) laufen, `python -m pytest --version` ohne Fehler
- [ ] `git config user.email` im Ordner `AG_Prozesse` liefert die Adresse des Gitea-Kontos
- [ ] `python scripts/setup_workspace.py` ohne Fehler, `.claude/skills/` vorhanden
- [ ] `python -m pytest -q .agents/skills` grün
- [ ] `python scripts/hallo.py` läuft und zeigt den Stand
- [ ] `python scripts/gitea.py tickets` läuft
- [ ] Claude Code im Ordner `AG_Prozesse` gestartet; `/context` zeigt `CLAUDE.md` und `AGENTS.md`
- [ ] Gitea-Token gespeichert (Test: `git fetch` ohne Passwortabfrage)
## Quellen
- Claude Code: [VS-Code-Erweiterung](https://code.claude.com/docs/en/vs-code) ·
[Gedächtnis und CLAUDE.md](https://code.claude.com/docs/en/memory) ·
[Skills](https://code.claude.com/docs/en/skills) ·
[Einstellungen und Berechtigungen](https://code.claude.com/docs/en/settings)
- [Agent-Skills-Standard](https://agentskills.io/home)
+150
View File
@@ -0,0 +1,150 @@
# Git-Arbeitsweise — verbindlich für alle Repositories
Stand 23.09.2026. Diese Regeln gelten für **jedes** Repository der Organisation `AG_Prozesse`, für Menschen und für
KI-Agenten gleichermaßen. Die Kurzfassung für den Agenten steht in `AGENTS.md`; hier steht das Vollständige mit
Begründung. Wer eine Abweichung braucht, macht ein Issue auf — nicht einfach einen Push.
> **Die goldene Regel**
>
> Auf `main` wird nicht gearbeitet. `main` nimmt ausschließlich fertige Arbeit per Pull Request entgegen.
> Gearbeitet wird in kurzlebigen Branches, die nach dem Merge gelöscht werden.
>
> **Und: Der Mensch drückt ab.** Agenten bereiten alles vor. Push, Merge und Tag macht eine Person mit ihrem
> eigenen Zugang.
## 1. Das Modell
Es gibt genau einen dauerhaften Branch: **`main`**. Er ist immer der abgenommene Stand. Alles andere sind
Arbeitsbranches, die von `main` abzweigen und per Pull Request zurückkommen (ADR 0002). Release-Kanäle wie in
AG_QGIS gibt es hier nicht, weil nichts ausgeliefert wird.
## 2. Der normale Weg
```bash
# 1. main aktualisieren (macht auch /hallo bzw. python scripts/hallo.py)
git switch main
git pull --ff-only
# 2. Eigenen Branch abzweigen
git switch -c feature/12-lane-beschriftung
# 3. In kleinen Schritten arbeiten und committen
git add .agents/skills/sn-bpmn/md_to_bpmn.py .agents/skills/sn-bpmn/test_md_to_bpmn.py
git commit -m "Lane-Beschriftung bricht lange Rollennamen um (#12)"
# 4. Prüfen
python -m pytest -q .agents/skills
python scripts/pruefe_umlaute.py
# 5. Push — durch den Menschen, mit dessen Zugangsdaten
git push -u origin feature/12-lane-beschriftung
```
Danach im Gitea-Browser einen **Pull Request nach `main`** öffnen, nach der Vorlage
`docs/vorlagen/pull_request.md`, mit Übergabenotiz (Skill `sn-uebergabe`).
Ein Pull Request sollte an einem Tag entstehen und in einer Viertelstunde reviewbar sein. Mehr als etwa
400 geänderte Zeilen heißt: das Issue war zu groß.
## 3. Branch-Namen
| Präfix | Beispiel | Wofür |
|---|---|---|
| `feature/` | `feature/12-lane-beschriftung` | neue Funktion, neues Prozessmodell |
| `bugfix/` | `bugfix/15-gateway-kanten` | Fehlerbehebung |
| `doku/` | `doku/glossar-bpmn` | nur Dokumentation, ohne Issue zulässig |
**Regeln für den Namen:** Kleinbuchstaben, ASCII, Bindestriche statt Leerzeichen. Keine Umlaute, kein `ß`, keine
Punkte. Issue-Nummer direkt nach dem Präfix, damit Gitea den Branch dem Ticket zuordnet.
## 4. Commit-Nachrichten
Deutsch, Präsens, erste Zeile höchstens 72 Zeichen, Issue-Nummer am Ende in Klammern.
```
Lane-Beschriftung bricht lange Rollennamen um (#12)
PICTURE schneidet Lane-Titel über 30 Zeichen ab. Der Konverter bricht
jetzt an Wortgrenzen um; kurze Namen bleiben einzeilig.
```
Was die Nachricht beantworten muss: **was** ändert sich und **warum**. Das *Wie* steht im Diff.
Nicht brauchbar: „Fix", „Update", „Änderungen", „wip".
## 5. Sitzungsbeginn
Vor dem ersten Handgriff — auch wenn es „nur schnell etwas" ist:
```bash
python scripts/hallo.py # oder in Claude Code: /hallo
```
Das holt den Stand vom Server (`git fetch`), aktualisiert `main` per Fast-Forward, wo das gefahrlos möglich ist,
und meldet alles Übrige, ohne es anzufassen. Es merged nicht, es stasht nicht, und es fasst keine ungesicherten
Änderungen an.
## 6. Was der Agent darf und was nicht
| Der Agent darf | Der Agent darf nicht |
|---|---|
| `git fetch`, `git pull --ff-only`, `git status`, `git log`, `git diff` | `git push` in jeder Form |
| lokale Branches anlegen und wechseln | `git merge`, `git rebase` |
| `git add`, `git commit` auf einem Arbeitsbranch | `git tag` |
| Tests laufen lassen, BPMN-Dateien erzeugen | Pull Requests mergen |
| Issue- und PR-Texte als Entwurf vorbereiten | `git reset --hard`, `git push --force`, `git clean` |
| Tickets über `scripts/gitea.py` lesen | irgendetwas auf dem Gitea-Server verändern |
Das ist nicht nur eine Bitte. `.claude/settings.json` im Repo verbietet die entsprechenden Kommandos technisch;
der Agent bekommt eine Ablehnung statt einer Ausführung. Wer die Regel für einen Einzelfall aussetzen will, tut
das bewusst und lokal, nicht im Repo.
**Warum so streng:** Ein Push trägt den Namen einer Person und ihre Verantwortung. Ein Agent kann fachlich nicht
beurteilen, ob ein Prozessmodell abgenommen ist — und ein versehentlicher Push auf `main` trifft sofort alle
anderen Behörden.
## 7. Branch-Schutz auf Gitea einrichten (Maintainer)
Je Repository unter **Einstellungen → Branches** (englisch: *Settings → Branches*) eine Branch-Schutzregel für
`main` anlegen:
| Einstellung | Wert | Wirkung |
|---|---|---|
| Branch-Name | `main` | betrifft genau diesen Branch |
| Push deaktivieren | an | kein direkter Push, nur Merge über Pull Request |
| Merge-Freigabe erforderlich | mindestens 1 | Review durch eine andere Person ist Pflicht |
| Veraltete Freigaben verwerfen | an | ein neuer Push macht das Review ungültig |
**Ausnahme beim Anlegen:** Der allererste Stand eines neuen Repositorys wird direkt nach `main` gepusht, weil es
vorher nichts gibt, wogegen ein Pull Request laufen könnte. Die Schutzregel danach setzen.
## 8. Aufräumen
Nach dem Merge wird der Arbeitsbranch gelöscht — in Gitea per Schaltfläche direkt am gemergten Pull Request,
lokal so:
```bash
git switch main
git pull --ff-only
git branch -d feature/12-lane-beschriftung
git fetch --prune
```
## 9. Häufige Fehler
**„Ich habe direkt auf `main` committet."** Solange nichts gepusht ist, ist das reparabel:
`git switch -c feature/<nr>-<name>` legt einen Branch auf dem aktuellen Stand an, danach
`git switch main && git reset --hard origin/main`. Der `reset` ist hier zulässig, weil er nur die lokale
Kopie auf den Serverstand zurücksetzt — trotzdem vorher `git log` prüfen.
**„Mein Branch ist hinter `main`."** Vor dem Pull Request aktualisieren:
`git switch main && git pull --ff-only`, dann zurück auf den Arbeitsbranch und `git merge main`.
Kein `rebase` auf bereits gepushten Branches — das schreibt Historie um, die andere schon haben.
**„`git pull` will einen Merge-Commit machen."** Dann ist der lokale Branch vorausgelaufen. `--ff-only` bricht
in dem Fall bewusst ab. Erst klären, was die eigenen Commits sind, statt blind zu mergen.
## Siehe auch
- `AGENTS.md` — Kurzfassung der harten Regeln
- `docs/leitfaden/03_arbeitsweise.md` — der Arbeitszyklus vom Issue bis zum Merge
- `docs/entscheidungen/0002-branchmodell-main.md` — warum das Modell so aussieht
+34
View File
@@ -0,0 +1,34 @@
# Status des Projekts
Kurz und aktuell. Wird bei jedem Merge und jeder Übergabe gepflegt (Skill `sn-uebergabe`). Älteres wandert nach unten
in „Erledigt" oder wird gelöscht. **Stand: 23.09.2026.**
## Aktuelle Phase
**Aufbau.** Das Repository wurde aus `AG_QGIS/Projektwissen` abgeleitet (ADR 0001): Arbeitsweise, Skripte und
Skills sind übernommen, alles QGIS- und Plugin-Spezifische ist entfernt. Kern ist der Skill `sn-bpmn`
(Prozessbeschreibung → BPMN 2.0 für PICTURE), übernommen vom Branch `BPMN-Skill` in AG_QGIS.
## Wer arbeitet woran
| Person / Behörde | Schwerpunkt | Stand |
|---|---|---|
| Daniel Helbig (LK Meißen) | Aufbau des Repositorys, Skill `sn-bpmn` | aktiv |
## Offene Baustellen (priorisiert)
1. **Ersten Stand auf Gitea bringen** (`main`) und Branch-Schutz für `main` setzen (nur per Pull Request).
2. **Ablageort für abgenommene Prozessmodelle festlegen.** Entwürfe liegen unversioniert in `entwuerfe/`.
Vorschlag: `prozesse/<name>/<name>.md` plus `<name>.bpmn`, versioniert, Änderungen per Pull Request. → ADR.
3. **Glossar um BPMN-/PICTURE-Begriffe ergänzen** und die Flurbereinigungsbegriffe auf das prüfen, was für
Prozesse gebraucht wird.
4. **Leitfäden vom Team gegenlesen lassen.** `docs/leitfaden/03–05` sind für die Prozessarbeit neu gefasst;
die Beispiele (Issue #12 usw.) sind erfunden und durch echte zu ersetzen, sobald es sie gibt.
5. **Umgang mit `sn-bpmn` in AG_QGIS klären.** Der Branch `BPMN-Skill` in `AG_QGIS/Projektwissen` ist durch dieses
Repository abgelöst und kann dort nach Absprache gelöscht werden.
## Erledigt
- 23.09.2026 **Repository aus AG_QGIS/Projektwissen abgeleitet** (Stand `decd2a0` bzw. `BPMN-Skill` @ `6efcac4`),
ohne Historie. Skripte auf die Organisation `AG_Prozesse` umgestellt, Hauptbranch `main`, Test über `pytest`
(32 Tests grün), `pruefe_umlaute.py` prüft jetzt den Workspace selbst.
+14
View File
@@ -0,0 +1,14 @@
# <Repo-Name>
Teil des Projektwissens der AG Prozesse. Übergeordnete Regeln, Fallstricke und Skills liegen im Workspace:
`../AGENTS.md` und `../docs/` (wenn dieses Repo als Unterordner des Workspace geklont ist).
Einzelklon? Projektwissen holen: https://entwicklung.flurneuordnung-sachsen.de/AG_Prozesse/Projektwissen
## Rolle
<Ein bis drei Sätze: Was liegt in diesem Repository, wer nutzt es.>
## Aufbau
<Ordner mit Einzeiler; große Dateien (> 600 Zeilen) mit Zeilenzahl nennen.>
## Zusätzliche Regeln
<Nur, was über den Workspace hinausgeht.>
+3
View File
@@ -0,0 +1,3 @@
@AGENTS.md
Beim Arbeiten im Workspace (`../AGENTS.md` vorhanden) gelten dessen Regeln und Skills zusätzlich.
+18
View File
@@ -0,0 +1,18 @@
## Ziel
<Was soll am Ende möglich sein? Ein Satz aus Nutzersicht.>
## Betroffen
Bereich: Prozessmodell | Konverter `md_to_bpmn.py` | Skill | Skripte | Doku
Dateien (falls bekannt):
## Fachlicher Hintergrund
<Begriffe, Regeln, Beispiel (keine echten Verfahrensdaten, keine Namen). Verweise auf Glossar.>
## Abnahmekriterien
- [ ] <beobachtbares Verhalten 1>
- [ ] <beobachtbares Verhalten 2>
- [ ] Bei Änderungen am Konverter: Test vorhanden, `python -m pytest -q .agents/skills` grün
- [ ] PICTURE-Import: <nötig / nicht nötig>
## Nicht Teil dieses Issues
<Abgrenzung, damit der Agent nicht abschweift.>
+16
View File
@@ -0,0 +1,16 @@
## Was
<Zwei Sätze. Issue: #…>
## Wie geprüft
- `python -m pytest -q .agents/skills`: <Ergebniszeile>
- `python scripts/pruefe_umlaute.py`: <Ergebniszeile>
- PICTURE-Import: <PICTURE-Version, Datei, Ergebnis> | nicht nötig, weil …
## Übergabenotiz
<nach docs/vorlagen/uebergabe.md>
## Checkliste
- [ ] Nur Änderungen zum Issue
- [ ] Keine personenbezogenen Daten, keine internen Originalunterlagen
- [ ] Doku des Skills nachgezogen (Anleitung, Elementkatalog, Modellierregeln) oder nicht betroffen
- [ ] Wissensbasis nachgezogen (oder „nichts Neues“)
+10
View File
@@ -0,0 +1,10 @@
## Übergabenotiz
**Aufgabe:** <Issue-Link und Ziel in einem Satz>
**Stand:** fertig | in Arbeit | blockiert – <wo genau>
**Geändert:** <Dateien mit Kurzgrund, keine Diffs>
**Nachweis:** `pytest`: <Ergebniszeile>; PICTURE-Import: <was, womit, Ergebnis | nicht nötig, weil …>
**Entscheidungen/Annahmen:** <fachliche Annahmen, die jemand bestätigen sollte>
**Offen / nächste Schritte:** <konkret, in Reihenfolge>
**Wissensbasis:** <Einträge in fallstricke/glossar/STATUS | „nichts Neues“>
**Kontext für die nächste Sitzung:** <die zwei bis drei Dateien/Abschnitte, mit denen es weitergeht>
+50
View File
@@ -0,0 +1,50 @@
# Fallstricke und Erkenntnisse
Gemeinsames Gedächtnis: Dinge, die man **nicht aus dem Code ablesen kann** und die schon einmal Zeit gekostet haben.
Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik. Veraltetes löschen, nicht auskommentieren.
## BPMN und PICTURE (Skill sn-bpmn, md_to_bpmn.py)
- **Ein Gateway hat in PICTURE nur vier feste Anschlusspunkte (N/O/S/W auf der Rautenkontur), keinen
variablen Rand wie ein Rechteck.** Eine Kante, die diesen Punkt verlässt bzw. dort ankommt, darf sich
danach **nicht** direkt zur Ziel-/Quellkoordinate drehen, wenn diese auf der anderen Achse liegt – sonst
läuft der rechtwinklige Streckenzug quer durchs Innere der Raute zurück, statt außen herumzugehen
(`orthogonal_points()` in `md_to_bpmn.py`, behoben BPMN-Skill 21.09.2026). Ursache: die feste
Anschlusspunkt-Zuteilung (`assign_gateway_sides()`, Präferenzreihenfolge S/O/W/N bzw. N/W/O/S) kennt die
tatsächliche Lage des anderen Endes nicht. Die Kante muss beim Verlassen/Ankommen an einer Raute zuerst
ein Stück auf der sicheren Stub-Achse bleiben (die einzige garantiert freie Koordinate außerhalb der
Kontur), bevor sie zur anderen Achse abbiegt – sonst reicht ein einfacher Bounding-Box-Test nicht, um den
Fehler zu erkennen (ein Punkt auf der Rautenkontur liegt immer innerhalb der Bounding-Box). Prüfen mit
echter Diamant-Geometrie (Manhattan-Abstand zum Mittelpunkt, skaliert auf die Halbachsen, < 1 heißt innen),
nicht mit der Bounding-Box – siehe `test_kante_fuehrt_nicht_durchs_innere_eines_gateways` in
`test_md_to_bpmn.py`.
## Text und Sprache
- **Deutscher Text ohne Umlaute/ß ist ein eigenständiges Muster, kein Einzelfall.** KI-generierter Text weicht
gern auf ASCII aus (`gueltig` statt `gültig`, `ueber` statt `über`) – in AG_QGIS über alle Plugins verteilt
gefunden (Plugin_SN_Basis #42), im BPMN-Skill in Elementnamen und Docstrings. Neuen deutschen Text
(Beschriftungen im Modell, Kommentare, Docstrings) immer mit echten Umlauten/ß schreiben. Vor dem Commit
`python scripts/pruefe_umlaute.py` laufen lassen – findet das Muster in `.py`-Dateien (String-Inhalte und
Kommentare mit Leerzeichen). Die Wortstammliste ist bewusst konservativ; Ersatz fürs Lesen ist sie nicht.
Arbeitsschritt-`.md` prüft das Skript nicht. (AG_QGIS 09/2026)
## Umgebung und Werkzeuge
- **Gitea ordnet Commits über die E-Mail-Adresse zu.** Stimmt `user.email` nicht mit einer im Gitea-Konto
hinterlegten Adresse überein, erscheinen die Commits ohne Zuordnung zum Benutzer. Es gilt die dienstliche
Adresse, die im Gitea-Konto hinterlegt ist. Wer den Rechner auch privat nutzt, setzt sie je Repository mit
`git config --local` statt global. Nachträglich nur durch Umschreiben der Historie korrigierbar. (AG_QGIS 04.09.2026)
- **`pytest` ist die einzige Abhängigkeit außerhalb der Standardbibliothek.** Die Tests des Konverters brauchen
es; `md_to_bpmn.py` selbst läuft ohne. (23.09.2026)
- **`.vscode/` gehört nicht ins Repository.** Einstellungen mit Pfaden eines einzelnen Arbeitsplatzes gehören in
die Benutzereinstellungen, siehe `docs/leitfaden/04_werkzeuge_einrichten.md`. (AG_QGIS 04.09.2026)
## Zusammenarbeit
- **Tool-Gedächtnis ist lokal.** Die Claude-Auto-Memory sieht niemand sonst. Geteiltes Wissen nur in diesem Ordner.
- **Issues sind teils Diskussionen**, nicht Aufträge (Label `Diskussion`). Vor der Umsetzung Konsens im Issue
abwarten oder im PR als Annahme kennzeichnen.
- **Quelldokumente enthalten oft Interna.** Word/PDF-Prozessbeschreibungen nur lokal unter `referenz/` ablegen
(nicht versioniert). Vor dem Committen eines Arbeitsschritt-Entwurfs prüfen, dass keine Namen, Aktenzeichen
oder Verfahrensdaten darin stehen. (23.09.2026)
+41
View File
@@ -0,0 +1,41 @@
# Glossar
Fachbegriffe und Abkürzungen, die in Prozessmodellen, Issues und Gesprächen vorkommen. Ergänzen, wenn ein Begriff
im Issue erklärt werden musste.
## Flurbereinigung
| Begriff | Bedeutung |
|---|---|
| **LNO** | Ländliche Neuordnung; Sammelbegriff für Flurbereinigungsverfahren in Sachsen |
| **FlurbG** | Flurbereinigungsgesetz. § 38: Vorarbeiten und gemeinschaftliche Anlagen; § 41: Wege- und Gewässerplan mit landschaftspflegerischem Begleitplan |
| **Plan 41** | Wege- und Gewässerplan nach § 41 FlurbG |
| **Verfahren** | Ein Flurbereinigungsverfahren; identifiziert über die **VKZ** |
| **VKZ / Verfahrensnummer** | Verfahrenskennzahl (sechsstellig) |
| **Verfahrensgebiet** | Flächenumring des Verfahrens |
| **Neuordnungsgebiet / Neuvermessungsgebiet** | Gebiet der Bodenordnung bzw. Gebiet, in dem Grenzen neu vermessen werden |
| **Beteiligung** | Welche Flurstücke und Eigentümer an einem Verfahren beteiligt sind |
| **ALKIS** | Amtliches Liegenschaftskatasterinformationssystem |
| **Gemarkung / Flurstück** | Katasterbezirk bzw. Katasterparzelle |
| **NoBIS** | Fachverfahren der Flurbereinigung (Neuordnungs-Bodenordnungs-Informationssystem) |
| **BI-Server** | Berichtssystem, aus dem Auswertungen zu Verfahren und Beteiligten kommen |
## Prozessmodellierung
| Begriff | Bedeutung |
|---|---|
| **BPMN 2.0** | Business Process Model and Notation, offener OMG-Standard für Prozessdiagramme; Austauschformat `.bpmn` (XML) |
| **PICTURE** | Prozessmanagement-Werkzeug der Verwaltung; Ziel des BPMN-Imports |
| **Arbeitsschritt-`.md`** | Strukturierte Markdown-Beschreibung eines Prozesses, Eingabe für `md_to_bpmn.py`; Syntax in `.agents/skills/sn-bpmn/Anleitung.md` |
| **Task / Aufgabe** | Ein Arbeitsschritt im Modell |
| **Gateway** | Verzweigung oder Zusammenführung (Raute); XOR = genau ein Weg, AND = alle Wege |
| **Lane / Swimlane** | Bahn für eine verantwortliche Rolle; entsteht aus den Verantwortlichen der Arbeitsschritte |
| **Start- / Endereignis** | Anfang bzw. Ende des Prozesses |
| **Elementkatalog** | Liste der von PICTURE unterstützten BPMN-Elemente, `.agents/skills/sn-bpmn/Elementkatalog.md` |
## Organisation
| Begriff | Bedeutung |
|---|---|
| **AG Prozesse** | Arbeitsgruppe Prozesse der sächsischen Flurbereinigungsbehörden; Gitea-Organisation `AG_Prozesse` |
| **AG QGIS** | Schwestergruppe für die QGIS-Plugins; Gitea-Organisation `AG_QGIS`, Herkunft der Arbeitsweise (ADR 0001) |
+626
View File
@@ -0,0 +1,626 @@
#!/usr/bin/env python3
"""Lesender Zugriff auf das Gitea-Ticketsystem der Organisation aus workspace.toml.
Fragt die öffentlich lesbare Gitea-API ab (kein Token, keine Anmeldung, keine
Installation – nur Standardbibliothek) und gibt Issues, Pull Requests und
Suchtreffer token-sparend für KI-Agenten und Entwickler aus. Läuft auf Linux,
macOS und Windows mit jedem Python >= 3.11.
WICHTIGE PROJEKTREGEL: Agenten dürfen auf dem Gitea-Server nichts verändern.
Dieses Skript stellt ausschließlich GET-Anfragen. Schreibende Aktionen (neues
Issue, neuer Kommentar) werden nur als Entwurfsdatei unter entwuerfe/ vorbereitet.
Ein Mensch prüft den Text und sendet ihn selbst über die Weboberfläche ab.
Aufrufe (aus dem Workspace-Root):
python scripts/gitea.py tickets # offene Issues ALLER Repos
python scripts/gitea.py tickets Projektwissen # nur ein Repo (Kurzname genügt)
python scripts/gitea.py tickets --geschlossen # geschlossene statt offene
python scripts/gitea.py tickets --label Bug # nach Label filtern
python scripts/gitea.py ticket Projektwissen 12 # ein Issue mit allen Kommentaren
python scripts/gitea.py suche "Swimlane" # Volltext über Titel, Text und Kommentare,
# offene und geschlossene Tickets
python scripts/gitea.py pulls # offene Pull Requests
python scripts/gitea.py entwurf issue Projektwissen # Entwurfsdatei anlegen
python scripts/gitea.py entwurf kommentar Projektwissen 12 # Entwurf für einen Kommentar
Exit-Code: 0 = in Ordnung, 1 = Gitea nicht erreichbar / nicht gefunden,
2 = falsche Angabe (Repo unbekannt oder mehrdeutig).
"""
from __future__ import annotations
import argparse
import json
import shutil
import tomllib
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
ENTWURFSORDNER = ROOT / "entwuerfe"
VORLAGE_ISSUE = ROOT / "docs" / "vorlagen" / "issue.md"
BENUTZERKENNUNG = "AG_Prozesse-Projektwissen/1.0"
ZEITLIMIT = 20 # Sekunden je Anfrage
SEITENGROESSE = 50 # Gitea liefert sonst nur wenige Einträge je Seite
MAX_SEITEN = 20 # Sicherheitsgrenze gegen Endlosschleifen
LEER = "-"
class GiteaFehler(Exception):
"""Netz-, HTTP- oder Formatfehler beim Zugriff auf die API (Exit-Code 1)."""
class NichtGefunden(GiteaFehler):
"""HTTP 404: Repo oder Issue existiert nicht bzw. hat kein Ticketsystem."""
class EingabeFehler(Exception):
"""Unbekannter oder mehrdeutiger Repo-Name (Exit-Code 2)."""
# ---------------------------------------------------------------- Manifest ---
def lade_manifest() -> dict:
with MANIFEST.open("rb") as f:
return tomllib.load(f)
def zugangsdaten(manifest: dict) -> tuple[str, str]:
"""Basis-URL und Organisation aus [workspace] – nie hart kodieren."""
ws = manifest.get("workspace", {})
basis = str(ws.get("gitea", "")).rstrip("/")
org = str(ws.get("org", ""))
if not basis or not org:
raise GiteaFehler("In workspace.toml fehlen [workspace] gitea oder org.")
return basis, org
def repo_namen(manifest: dict) -> list[str]:
"""Das Workspace-Repo ([workspace] repo) zuerst, danach die Klone aus [[repos]]."""
eigenes = manifest.get("workspace", {}).get("repo")
namen = [eigenes] if eigenes else []
return namen + [r["name"] for r in manifest.get("repos", []) if r.get("name")]
def loese_repo(kurzname: str, namen: list[str]) -> str:
"""Löst einen Kurznamen wie 'wissen' zu 'Projektwissen' auf.
Reihenfolge: exakter Name gewinnt, dann Groß-/Kleinschreibung egal, dann
eindeutige Übereinstimmung am Namensende, dann eindeutiger Teilstring.
"""
if kurzname in namen:
return kurzname
klein = kurzname.lower()
for pruefung in (
lambda n: n.lower() == klein,
lambda n: n.lower().endswith(klein),
lambda n: klein in n.lower(),
):
treffer = [n for n in namen if pruefung(n)]
if len(treffer) == 1:
return treffer[0]
if len(treffer) > 1:
raise EingabeFehler(
f"Repo-Name '{kurzname}' ist mehrdeutig. Gemeint sein könnte: "
+ ", ".join(treffer)
)
raise EingabeFehler(
f"Repo '{kurzname}' steht nicht in workspace.toml. Bekannt: " + ", ".join(namen)
)
# ------------------------------------------------------------------- API ------
def baue_url(basis: str, pfad: str, parameter: dict[str, str] | None = None) -> str:
url = f"{basis}/api/v1{pfad}"
if parameter:
url += "?" + urllib.parse.urlencode(parameter)
return url
def hole_json(url: str) -> object:
"""Eine einzelne GET-Anfrage. Schreibende Methoden gibt es hier bewusst nicht."""
anfrage = urllib.request.Request(
url, method="GET",
headers={"User-Agent": BENUTZERKENNUNG, "Accept": "application/json"},
)
try:
with urllib.request.urlopen(anfrage, timeout=ZEITLIMIT) as antwort:
roh = antwort.read()
except urllib.error.HTTPError as fehler:
if fehler.code == 404:
raise NichtGefunden("Repo/Issue nicht gefunden: " + url) from None
raise GiteaFehler(
f"Gitea antwortet mit HTTP {fehler.code} ({fehler.reason}) auf {url}"
) from None
except TimeoutError:
raise GiteaFehler(
f"Gitea nicht erreichbar: Zeitlimit von {ZEITLIMIT} s überschritten. "
"Netzverbindung oder Proxy prüfen."
) from None
except urllib.error.URLError as fehler:
raise GiteaFehler(
f"Gitea nicht erreichbar: {fehler.reason}. Netzverbindung oder Proxy prüfen."
) from None
except OSError as fehler:
raise GiteaFehler(
f"Gitea nicht erreichbar: {fehler}. Netzverbindung oder Proxy prüfen."
) from None
try:
return json.loads(roh.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise GiteaFehler(f"Unerwartete Antwort (kein JSON) von {url}") from None
def hole_liste(basis: str, pfad: str, parameter: dict[str, str]) -> list[dict]:
"""Blättert über die Seiten, bis eine leere oder unvollständige Seite kommt."""
ergebnisse: list[dict] = []
for seite in range(1, MAX_SEITEN + 1):
abfrage = dict(parameter, limit=str(SEITENGROESSE), page=str(seite))
daten = hole_json(baue_url(basis, pfad, abfrage))
if not isinstance(daten, list):
# Gitea liefert bei deaktiviertem Ticketsystem ein Objekt statt einer Liste.
raise NichtGefunden("Repo/Issue nicht gefunden: " + baue_url(basis, pfad, abfrage))
ergebnisse.extend(daten)
if len(daten) < SEITENGROESSE:
break
else:
print(f"Hinweis: Sicherheitsgrenze von {MAX_SEITEN} Seiten erreicht – Liste evtl. unvollständig.")
return ergebnisse
def hole_issues(basis: str, org: str, repo: str, state: str, art: str = "issues") -> list[dict]:
"""Issues EINES Repos. art='issues' blendet Pull Requests aus, 'pulls' zeigt nur sie."""
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues"
try:
return hole_liste(basis, pfad, {"state": state, "type": art})
except NichtGefunden:
raise NichtGefunden(
f"Repo/Issue nicht gefunden: {org}/{repo} – Repo existiert nicht oder "
"hat auf Gitea kein Ticketsystem."
) from None
def hole_issues_org(basis: str, org: str, state: str, art: str = "issues",
suchtext: str | None = None) -> list[dict]:
"""Issues ALLER Repos der Organisation über den Sammelendpunkt.
Spart pro Aufruf zehn Einzelabfragen; Repos ohne Ticketsystem fallen dabei
automatisch weg. Mit suchtext sucht Gitea serverseitig (Titel, Text und
Kommentare).
"""
parameter = {"owner": org, "state": state, "type": art}
if suchtext:
parameter["q"] = suchtext
return hole_liste(basis, "/repos/issues/search", parameter)
def sammle_issues(basis: str, org: str, repo: str | None, namen: list[str], state: str,
art: str = "issues", suchtext: str | None = None) -> list[tuple[str, list[dict]]]:
"""Holt die Einträge und gruppiert sie in der Reihenfolge aus workspace.toml."""
if suchtext:
# Auch für ein einzelnes Repo über den Sammelendpunkt: nur er durchsucht
# zusätzlich die Kommentare.
eintraege = hole_issues_org(basis, org, state, art, suchtext)
if repo:
eintraege = [e for e in eintraege
if (e.get("repository") or {}).get("name") == repo]
elif repo:
eintraege = hole_issues(basis, org, repo, state, art)
else:
eintraege = hole_issues_org(basis, org, state, art)
return gruppiere_nach_repo(eintraege, namen)
def gruppiere_nach_repo(eintraege: list[dict], namen: list[str]) -> list[tuple[str, list[dict]]]:
nach_repo: dict[str, list[dict]] = {}
for eintrag in eintraege:
name = (eintrag.get("repository") or {}).get("name") or "(unbekanntes Repo)"
nach_repo.setdefault(name, []).append(eintrag)
reihenfolge = [n for n in namen if n in nach_repo]
reihenfolge += sorted(n for n in nach_repo if n not in namen)
return [(n, nach_repo[n]) for n in reihenfolge]
def enthaelt(eintrag: dict, suchtext: str) -> bool:
"""Lokaler Volltextvergleich über Titel und Text (ohne Kommentare)."""
klein = suchtext.lower()
return klein in (eintrag.get("title") or "").lower() or klein in (eintrag.get("body") or "").lower()
def hole_kommentare(basis: str, org: str, repo: str, nummer: int) -> list[dict]:
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues/{nummer}/comments"
return hole_liste(basis, pfad, {})
def hole_issue(basis: str, org: str, repo: str, nummer: int) -> dict:
pfad = f"/repos/{urllib.parse.quote(org)}/{urllib.parse.quote(repo)}/issues/{nummer}"
try:
daten = hole_json(baue_url(basis, pfad))
except NichtGefunden:
raise NichtGefunden(f"Repo/Issue nicht gefunden: {repo} #{nummer}") from None
if not isinstance(daten, dict) or "number" not in daten:
raise NichtGefunden(f"Repo/Issue nicht gefunden: {repo} #{nummer}")
return daten
# ------------------------------------------------------------- Formatierung ---
def terminalbreite(vorgabe: int | None = None) -> int:
breite = vorgabe or shutil.get_terminal_size((100, 24)).columns
return max(60, min(breite, 200))
def kuerze(text: str, breite: int) -> str:
text = " ".join((text or "").split())
if len(text) <= breite:
return text
return text[: max(1, breite - 1)] + "…"
def zeitpunkt(wert: str | None) -> datetime | None:
if not wert:
return None
try:
return datetime.fromisoformat(wert.replace("Z", "+00:00"))
except ValueError:
return None
def datum(wert: str | None) -> str:
zeit = zeitpunkt(wert)
return zeit.strftime("%Y-%m-%d") if zeit else LEER
def datum_zeit(wert: str | None) -> str:
zeit = zeitpunkt(wert)
return zeit.strftime("%Y-%m-%d %H:%M") if zeit else LEER
def alter_tage(wert: str | None) -> int:
zeit = zeitpunkt(wert)
if not zeit:
return 0
jetzt = datetime.now(timezone.utc)
return max(0, (jetzt - zeit.astimezone(timezone.utc)).days)
def labelnamen(eintrag: dict) -> list[str]:
return [l.get("name", "") for l in (eintrag.get("labels") or [])]
def zuweisung(eintrag: dict) -> list[str]:
namen = [a.get("login", "") for a in (eintrag.get("assignees") or [])]
if not namen and eintrag.get("assignee"):
namen = [eintrag["assignee"].get("login", "")]
return [n for n in namen if n]
def mehrzahl(anzahl: int, einzahl: str, mehrzahlform: str) -> str:
return f"{anzahl} {einzahl if anzahl == 1 else mehrzahlform}"
def anzahl_tickets(anzahl: int, geschlossen: bool, pull: bool = False) -> str:
"""Zählbare Wortform für die Summenzeile, z.B. '1 offenes Ticket'."""
if pull:
wort = ("offener Pull Request", "offene Pull Requests")
if geschlossen:
wort = ("geschlossener Pull Request", "geschlossene Pull Requests")
elif geschlossen:
wort = ("geschlossenes Ticket", "geschlossene Tickets")
else:
wort = ("offenes Ticket", "offene Tickets")
return mehrzahl(anzahl, *wort)
def zustandskuerzel(eintrag: dict) -> str:
return "[offen]" if eintrag.get("state") == "open" else "[geschl.]"
def autor(eintrag: dict) -> str:
nutzer = eintrag.get("user") or {}
login = nutzer.get("login", "unbekannt")
voll = (nutzer.get("full_name") or "").strip()
return f"{voll} ({login})" if voll and voll != login else login
def tabellenkopf(breiten: dict[str, int]) -> str:
return (" " + "Nr".rjust(breiten["nr"])
+ " " + "Titel".ljust(breiten["titel"])
+ " " + "Labels".ljust(breiten["labels"])
+ " " + "Zugewiesen".ljust(breiten["zug"])
+ " " + "Alter".rjust(breiten["alter"]))
def spaltenbreiten(breite: int, eintraege: list[dict]) -> dict[str, int]:
nr = max(4, max((len(str(e.get("number", 0))) + 1 for e in eintraege), default=4))
labels, zug, alter = 16, 11, 5
titel = breite - (nr + labels + zug + alter + 10)
return {"nr": nr, "titel": max(20, titel), "labels": labels, "zug": zug, "alter": alter}
def tabellenzeile(eintrag: dict, breiten: dict[str, int], zusatz: str = "") -> str:
zeile = (" " + ("#" + str(eintrag.get("number", 0))).rjust(breiten["nr"])
+ " " + kuerze(eintrag.get("title", ""), breiten["titel"]).ljust(breiten["titel"])
+ " " + kuerze(", ".join(labelnamen(eintrag)) or LEER, breiten["labels"]).ljust(breiten["labels"])
+ " " + kuerze(", ".join(zuweisung(eintrag)) or LEER, breiten["zug"]).ljust(breiten["zug"])
+ " " + f"{alter_tage(eintrag.get('created_at'))}T".rjust(breiten["alter"]))
return zeile + ("\n" + " " * (breiten["nr"] + 4) + zusatz if zusatz else "")
def drucke_gruppen(gruppen: list[tuple[str, list[dict]]], breite: int, leermeldung: str,
zusatz=None) -> tuple[int, int]:
"""Gibt die nach Repo gruppierte Tabelle aus; Spaltenkopf nur einmal."""
gruppen = [(repo, eintraege) for repo, eintraege in gruppen if eintraege]
if not gruppen:
print("\n" + leermeldung)
return 0, 0
breiten = spaltenbreiten(breite, [e for _, eintraege in gruppen for e in eintraege])
print(tabellenkopf(breiten))
gesamt = 0
for repo, eintraege in gruppen:
gesamt += len(eintraege)
print(f"\n{repo} ({len(eintraege)})")
for eintrag in sorted(eintraege, key=lambda e: -e.get("number", 0)):
print(tabellenzeile(eintrag, breiten, zusatz(eintrag, breiten) if zusatz else ""))
return gesamt, len(gruppen)
def passt_zum_label(eintrag: dict, label: str | None) -> bool:
if not label:
return True
return any(label.lower() == name.lower() for name in labelnamen(eintrag))
# ---------------------------------------------------------------- Befehle -----
def befehl_tickets(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
state = "closed" if args.geschlossen else "open"
bezeichnung = "geschlossene" if args.geschlossen else "offene"
gefunden = sammle_issues(basis, org, repo, namen, state)
breite = terminalbreite(args.breite)
gruppen = [(name, [i for i in issues if passt_zum_label(i, args.label)])
for name, issues in gefunden]
filter_text = f", Label '{args.label}'" if args.label else ""
print(f"{org} – {bezeichnung} Tickets{filter_text} (Stand {datetime.now():%Y-%m-%d})")
gesamt, repos_mit = drucke_gruppen(gruppen, breite, "Keine passenden Tickets gefunden.")
print(f"\nSumme: {anzahl_tickets(gesamt, args.geschlossen)} in {repos_mit} von "
f"{mehrzahl(1 if repo else len(namen), 'Repo', 'Repos')} | Alter in Tagen"
f" | Details: python scripts/gitea.py ticket <Repo> <Nr>")
return 0
def befehl_ticket(basis: str, org: str, repo: str, nummer: int) -> int:
eintrag = hole_issue(basis, org, repo, nummer)
kommentare = hole_kommentare(basis, org, repo, nummer)
art = "Pull Request" if eintrag.get("pull_request") else "Issue"
status = "offen" if eintrag.get("state") == "open" else "geschlossen"
print(f"# {repo} #{eintrag['number']} – {eintrag.get('title', '')}\n")
print(f"- Art: {art}")
print(f"- Status: {status}" + (f" (geschlossen am {datum(eintrag.get('closed_at'))})"
if eintrag.get("closed_at") else ""))
print(f"- Autor: {autor(eintrag)}")
print(f"- Angelegt: {datum_zeit(eintrag.get('created_at'))}"
f" (vor {alter_tage(eintrag.get('created_at'))} Tagen)"
f", zuletzt geändert: {datum_zeit(eintrag.get('updated_at'))}")
print(f"- Labels: {', '.join(labelnamen(eintrag)) or 'keine'}")
print(f"- Zugewiesen: {', '.join(zuweisung(eintrag)) or 'niemandem'}")
if eintrag.get("milestone"):
print(f"- Meilenstein: {eintrag['milestone'].get('title', '')}")
print(f"- URL: {eintrag.get('html_url', '')}\n")
print("## Beschreibung\n")
print((eintrag.get("body") or "").strip() or "_(kein Text)_")
print(f"\n## Kommentare ({len(kommentare)})")
if not kommentare:
print("\n_(keine)_")
for nr, kommentar in enumerate(kommentare, start=1):
print(f"\n### {nr}. {autor(kommentar)} – {datum_zeit(kommentar.get('created_at'))}\n")
print((kommentar.get("body") or "").strip() or "_(kein Text)_")
return 0
def fundstelle(eintrag: dict, begriff: str, breite: int) -> str:
"""Kurzer Textausschnitt um die erste Fundstelle im Text."""
text = " ".join((eintrag.get("body") or "").split())
stelle = text.lower().find(begriff.lower())
if stelle < 0:
if begriff.lower() in (eintrag.get("title") or "").lower():
return "(Treffer im Titel)"
return "(Treffer in einem Kommentar)"
start = max(0, stelle - breite // 3)
ausschnitt = text[start : start + breite]
return ("…" if start else "") + ausschnitt + ("…" if start + breite < len(text) else "")
def befehl_suche(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
# Gesucht wird standardmäßig in offenen UND geschlossenen Tickets: die Antwort
# auf eine Fachfrage steht oft in einem längst geschlossenen Ticket.
state = "open" if args.offen else ("closed" if args.geschlossen else "all")
gefunden = sammle_issues(basis, org, repo, namen, state, suchtext=args.begriff)
if not any(issues for _, issues in gefunden):
# Zweiter Versuch ohne Server-Suche: Gitea findet je nach Index nicht jeden Teilstring.
gefunden = [(name, [i for i in issues if enthaelt(i, args.begriff)])
for name, issues in sammle_issues(basis, org, repo, namen, state)]
breite = terminalbreite(args.breite)
gruppen = [(name, [i for i in issues if passt_zum_label(i, args.label)])
for name, issues in gefunden]
def ausschnitt(eintrag: dict, breiten: dict[str, int]) -> str:
zustand = "" if state != "all" else zustandskuerzel(eintrag) + " "
return zustand + fundstelle(eintrag, args.begriff, breite - breiten["nr"] - 10)
zustaende = {"open": "nur offene", "closed": "nur geschlossene", "all": "offene und geschlossene"}
print(f"Suche nach '{args.begriff}' in {org} ({zustaende[state]} Tickets,"
f" Titel, Text und Kommentare)")
gesamt, _ = drucke_gruppen(gruppen, breite, "Keine Treffer.", ausschnitt)
print(f"\nSumme: {gesamt} Treffer | Details: python scripts/gitea.py ticket <Repo> <Nr>")
return 0
def befehl_pulls(basis: str, org: str, repo: str | None, namen: list[str],
args: argparse.Namespace) -> int:
state = "closed" if args.geschlossen else "open"
bezeichnung = "geschlossene" if args.geschlossen else "offene"
gefunden = sammle_issues(basis, org, repo, namen, state, art="pulls")
breite = terminalbreite(args.breite)
gruppen = [(name, [p for p in pulls if passt_zum_label(p, args.label)])
for name, pulls in gefunden]
print(f"{org} – {bezeichnung} Pull Requests (Stand {datetime.now():%Y-%m-%d})")
gesamt, _ = drucke_gruppen(gruppen, breite, "Keine passenden Pull Requests gefunden.",
lambda eintrag, breiten: f"von {autor(eintrag)}")
print(f"\nSumme: {anzahl_tickets(gesamt, args.geschlossen, pull=True)} in "
f"{mehrzahl(1 if repo else len(namen), 'Repo', 'Repos')}")
return 0
def befehl_entwurf(basis: str, org: str, repo: str, art: str, nummer: int | None) -> int:
"""Legt eine Entwurfsdatei an. Abgeschickt wird sie ausschließlich von Hand."""
zeitstempel = datetime.now().strftime("%Y%m%d-%H%M%S")
web = f"{basis}/{org}/{repo}"
if art == "issue":
ziel = ENTWURFSORDNER / f"{zeitstempel}-issue-{repo}.md"
kopf = f"# Entwurf: neues Issue in {org}/{repo}"
vorlage = (VORLAGE_ISSUE.read_text(encoding="utf-8").strip()
if VORLAGE_ISSUE.exists()
else "## Ziel\n<Was soll am Ende möglich sein?>\n\n## Abnahmekriterien\n- [ ] <…>")
quelle = ("docs/vorlagen/issue.md" if VORLAGE_ISSUE.exists()
else "Notvorlage (docs/vorlagen/issue.md fehlt)")
koerper = f"Titel: <Kurzer, sprechender Titel>\n\n{vorlage}"
absenden = (f"1. Seite öffnen: {web}/issues/new\n"
"2. Titelzeile und Text unterhalb von `--- ENTWURF ---` von Hand einfügen"
" (die Zeile `Titel:` selbst nicht mitkopieren).\n"
"3. Labels, Zuweisung und Meilenstein setzen und das Issue anlegen.")
hinweis = f"Vorlage: {quelle}"
else:
eintrag = hole_issue(basis, org, repo, int(nummer))
ziel = ENTWURFSORDNER / f"{zeitstempel}-kommentar-{repo}-{nummer}.md"
kopf = f"# Entwurf: Kommentar zu {org}/{repo} #{nummer}"
koerper = "<Kommentartext hier eintragen>"
absenden = (f"1. Seite öffnen: {web}/issues/{nummer}\n"
"2. Text unterhalb von `--- ENTWURF ---` in das Kommentarfeld einfügen.\n"
"3. Kommentar abschicken.")
hinweis = (f"Ticket: #{nummer} – {eintrag.get('title', '')}"
f" (Status: {'offen' if eintrag.get('state') == 'open' else 'geschlossen'})")
inhalt = "\n".join([
kopf,
"",
f"- Ziel-Repo: {org}/{repo} ({web})",
f"- {hinweis}",
f"- Angelegt: {datetime.now():%Y-%m-%d %H:%M} von scripts/gitea.py",
"",
"> **Dieser Entwurf wird NICHT automatisch abgeschickt.** KI-Agenten verändern auf",
"> Gitea nichts. Ein Mensch prüft den Text und stellt ihn selbst über die",
"> Weboberfläche ein.",
"",
"--- ENTWURF ---",
"",
koerper,
"",
"--- ENDE ENTWURF ---",
"",
"## Absenden (von Hand)",
"",
absenden,
"",
])
ENTWURFSORDNER.mkdir(parents=True, exist_ok=True)
ziel.write_text(inhalt, encoding="utf-8")
print(f"Entwurf angelegt: {ziel.relative_to(ROOT)}")
print("Bitte prüfen, ergänzen und selbst auf Gitea einstellen – das Skript sendet nichts.")
return 0
# ------------------------------------------------------------------ main ------
def main() -> int:
breite_eltern = argparse.ArgumentParser(add_help=False)
breite_eltern.add_argument("--breite", type=int, help="Terminalbreite erzwingen (Standard: automatisch)")
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
unter = ap.add_subparsers(dest="befehl", required=True)
p = unter.add_parser("tickets", parents=[breite_eltern], help="Issues auflisten (Standard: alle Repos, offen)")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname (z.B. Projektwissen); ohne Angabe: alle")
p.add_argument("--geschlossen", action="store_true", help="geschlossene statt offener Tickets")
p.add_argument("--label", help="nur Tickets mit diesem Label (z.B. Bug)")
p = unter.add_parser("ticket", help="ein Issue mit allen Kommentaren als Markdown")
p.add_argument("repo", help="Repo oder Kurzname (z.B. Projektwissen)")
p.add_argument("nummer", type=int, help="Issue-Nummer")
p = unter.add_parser("suche", parents=[breite_eltern], help="Volltextsuche über Titel und Text")
p.add_argument("begriff", help="Suchbegriff (Groß-/Kleinschreibung egal)")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname; ohne Angabe: alle")
p.add_argument("--offen", action="store_true", help="nur offene Tickets (Standard: offene und geschlossene)")
p.add_argument("--geschlossen", action="store_true", help="nur geschlossene Tickets")
p.add_argument("--label", help="zusätzlich nach Label filtern")
p = unter.add_parser("pulls", parents=[breite_eltern], help="Pull Requests auflisten")
p.add_argument("repo", nargs="?", help="Repo oder Kurzname; ohne Angabe: alle")
p.add_argument("--geschlossen", action="store_true", help="geschlossene statt offener PRs")
p.add_argument("--label", help="nur PRs mit diesem Label")
p = unter.add_parser("entwurf", help="Entwurfsdatei für einen Menschen vorbereiten (sendet nichts)")
p.add_argument("art", choices=["issue", "kommentar"], help="Art des Entwurfs")
p.add_argument("repo", help="Repo oder Kurzname (z.B. Projektwissen)")
p.add_argument("nummer", nargs="?", type=int, help="Issue-Nummer (nur bei 'kommentar')")
args = ap.parse_args()
try:
manifest = lade_manifest()
basis, org = zugangsdaten(manifest)
namen = repo_namen(manifest)
if not namen:
raise GiteaFehler("workspace.toml nennt weder [workspace] repo noch [[repos]]-Einträge.")
if args.befehl == "entwurf" and args.art == "kommentar" and args.nummer is None:
print("Für 'entwurf kommentar' wird die Issue-Nummer gebraucht, "
"z.B. python scripts/gitea.py entwurf kommentar Projektwissen 12")
return 2
repo = loese_repo(args.repo, namen) if getattr(args, "repo", None) else None
if args.befehl == "tickets":
return befehl_tickets(basis, org, repo, namen, args)
if args.befehl == "ticket":
return befehl_ticket(basis, org, repo, args.nummer)
if args.befehl == "suche":
return befehl_suche(basis, org, repo, namen, args)
if args.befehl == "pulls":
return befehl_pulls(basis, org, repo, namen, args)
if args.befehl == "entwurf":
return befehl_entwurf(basis, org, repo, args.art, args.nummer)
except EingabeFehler as fehler:
print(fehler)
return 2
except NichtGefunden as fehler:
print(str(fehler))
return 1
except GiteaFehler as fehler:
print(str(fehler))
return 1
except KeyboardInterrupt:
print("Abgebrochen.")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+415
View File
@@ -0,0 +1,415 @@
#!/usr/bin/env python3
"""Sitzungsbeginn: zeigt den Stand aller Klone und aktualisiert die Hauptbranches.
Für jedes Repository aus workspace.toml – und für den Workspace-Ordner selbst, sofern
er ein Git-Repository ist – holt das Skript den Stand vom Server (git fetch --prune),
ermittelt Branch, ungesicherte Änderungen und den Abstand zu origin und macht genau
dann einen fast-forward-Pull, wenn das gefahrlos ist:
* das Arbeitsverzeichnis ist sauber (git status --porcelain leer),
* der aktuelle Branch ist der Hauptbranch aus workspace.toml,
* es gibt einen Upstream,
* wir sind hinter origin und haben keine eigenen Commits davor.
In allen anderen Fällen wird nur berichtet und nichts verändert. Am Ende stehen eine
Tabelle (eine Zeile je Repository) und, falls nötig, konkrete Handlungsempfehlungen.
Absolut verboten und deshalb nirgends im Code enthalten:
git merge, git rebase, git stash, git reset, git checkout/switch auf einen anderen
Branch, git push, git tag sowie jedes Verwerfen von Änderungen.
Ungesicherte oder unversionierte Dateien werden nie angefasst. Die einzige schreibende
Git-Operation des Skripts ist `git pull --ff-only` unter den vier Bedingungen oben.
Aufrufe (aus dem Workspace-Root):
python scripts/hallo.py # fetch + fast-forward-Pull + Bericht
python scripts/hallo.py --nur-pruefen # fetch + Bericht, kein Pull
python scripts/hallo.py --ohne-netz # nur lokaler Stand, kein fetch
python scripts/hallo.py --repo Projektwissen
Der Hauptbranch des Workspace-Repos selbst kann in workspace.toml unter
[workspace] branch = "..." gesetzt werden; ohne Eintrag gilt sein aktueller Branch.
Exit-Code: 0 = nichts Kritisches, 1 = ein Repo-Ordner fehlt oder ein Git-Befehl ist
unerwartet gescheitert, 2 = Aufruffehler (unbekanntes Repo, workspace.toml unlesbar).
"""
from __future__ import annotations
import argparse
import os
import subprocess
import textwrap
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml"
def repos_root() -> Path:
"""Ordner, unter dem die weiteren Repositories aus [[repos]] liegen.
Standard: ROOT selbst (Repos als direkte Unterordner, wie in workspace.toml
dokumentiert). Wer die Klone woanders liegen hat, kann das lokal und nicht
versioniert in workspace.local.toml überschreiben:
[workspace]
repos_root = "D:/Klone/AG_Prozesse"
"""
if not LOKALE_ÜBERSCHREIBUNG.is_file():
return ROOT
with LOKALE_ÜBERSCHREIBUNG.open("rb") as f:
lokal = tomllib.load(f)
pfad = lokal.get("workspace", {}).get("repos_root")
return Path(pfad).expanduser() if pfad else ROOT
BREITE = 100 # Zielbreite der Tabelle (lesbar im 100-Zeichen-Terminal)
FETCH_TIMEOUT = 60 # Sekunden je git fetch, damit ein hängender Server nicht blockiert
# Git ohne interaktive Rückfragen: sonst wartet ein fetch ewig auf eine Passworteingabe.
GIT_UMGEBUNG = dict(os.environ, GIT_TERMINAL_PROMPT="0", GCM_INTERACTIVE="never")
def git(*args: str, cwd: Path, timeout: int | None = None) -> tuple[int, str]:
"""Führt git aus und liefert (Rückgabecode, Ausgabe). Wirft nie eine Ausnahme."""
try:
p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True,
errors="replace", env=GIT_UMGEBUNG, timeout=timeout)
except FileNotFoundError:
return 127, "git wurde nicht gefunden (steht git im PATH?)"
except subprocess.TimeoutExpired:
return 124, f"Zeitüberschreitung nach {timeout} s"
return p.returncode, (p.stdout + p.stderr).strip()
def kurz(text: str, breite: int = 70) -> str:
"""Letzte, gekürzte Zeile einer Git-Ausgabe – für einzeilige Meldungen."""
zeilen = [z for z in text.splitlines() if z.strip()]
letzte = zeilen[-1].strip() if zeilen else ""
return letzte if len(letzte) <= breite else letzte[: breite - 1] + "…"
def kuerze(text: str, breite: int) -> str:
return text if len(text) <= breite else text[: max(1, breite - 1)] + "~"
def umbrechen(zeile: str) -> str:
"""Bricht Fließtext auf BREITE um; Befehlszeilen (vier Leerzeichen Einzug) bleiben ganz."""
if zeile.startswith(" ") or len(zeile) <= BREITE:
return zeile
einzug = " " * (len(zeile) - len(zeile.lstrip()))
return textwrap.fill(zeile.strip(), width=BREITE, initial_indent=einzug,
subsequent_indent=einzug)
def commits(anzahl: int) -> str:
return f"{anzahl} Commit" if anzahl == 1 else f"{anzahl} Commits"
def aenderungen(anzahl: int) -> str:
return f"{anzahl} ungesicherte Änderung" if anzahl == 1 else f"{anzahl} ungesicherte Änderungen"
@dataclass
class Befund:
"""Zustand eines Klons – wird erst gefüllt, dann als Tabellenzeile ausgegeben."""
name: str
kanal: str
pfad: Path
ist_wurzel: bool = False
vorhanden: bool = True
lesbar: bool = True
branch: str = "-"
geaendert: int = 0
hinter: int = 0
vor: int = 0
upstream: bool = False
remote: bool = True
offline: bool = False
aktualisiert: bool = False
zustand: str = "-"
aktion: str = "-"
kritisch: bool = False
hinweise: list[str] = field(default_factory=list)
def befehl(self, rest: str) -> str:
"""Kopierbarer Git-Befehl, aufzurufen aus dem Workspace-Root."""
return f"git {rest}" if self.ist_wurzel else f"git -C {self.name} {rest}"
@property
def abstand(self) -> str:
return "-" if not (self.vorhanden and self.upstream) else f"{self.hinter}/{self.vor}"
def zustand_ermitteln(b: Befund) -> str:
"""Ein Klartextwort für die Spalte Zustand (ohne Emoji, auch für KI-Agenten lesbar)."""
if b.geaendert:
return f"geaendert ({b.geaendert})"
if b.branch == "HEAD":
return "detached HEAD"
if b.branch != b.kanal:
return "fremder branch"
if not b.upstream:
return "kein upstream"
if b.offline:
return "offline"
return "sauber"
def einlesen(b: Befund, ohne_netz: bool) -> None:
"""Nur lesende Git-Befehle: fetch, rev-parse, status, rev-list."""
if not b.pfad.is_dir():
b.vorhanden, b.zustand, b.aktion, b.kritisch = False, "fehlt", "Ordner fehlt", True
return
if not (b.pfad / ".git").exists():
b.vorhanden, b.zustand, b.aktion, b.kritisch = False, "kein git-repo", "Ordner ohne .git", True
return
_, remotes = git("remote", cwd=b.pfad)
b.remote = bool(remotes.strip())
if not b.remote:
b.hinweise.append("kein Remote konfiguriert – kein Abgleich mit origin möglich.")
elif not ohne_netz:
rc, ausgabe = git("fetch", "--prune", "--quiet", cwd=b.pfad, timeout=FETCH_TIMEOUT)
if rc != 0:
b.offline = True
b.hinweise.append(f"origin nicht erreichbar: {kurz(ausgabe)}")
rc, branch = git("rev-parse", "--abbrev-ref", "HEAD", cwd=b.pfad)
if rc != 0:
b.lesbar, b.zustand, b.aktion, b.kritisch = False, "git-fehler", f"FEHLER: {kurz(branch, 30)}", True
b.hinweise.append(f"git rev-parse fehlgeschlagen: {kurz(branch)}")
return
b.branch = branch
if not b.kanal: # Workspace-Repo ohne Vorgabe in workspace.toml
b.kanal = branch
rc, status = git("status", "--porcelain", cwd=b.pfad)
if rc != 0:
b.lesbar, b.zustand, b.aktion, b.kritisch = False, "git-fehler", f"FEHLER: {kurz(status, 30)}", True
b.hinweise.append(f"git status fehlgeschlagen: {kurz(status)}")
return
b.geaendert = len([z for z in status.splitlines() if z.strip()])
rc, _ = git("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}", cwd=b.pfad)
b.upstream = rc == 0
if b.upstream:
rc, zaehl = git("rev-list", "--left-right", "--count", "HEAD...@{upstream}", cwd=b.pfad)
teile = zaehl.split()
if rc == 0 and len(teile) == 2 and all(t.isdigit() for t in teile):
b.vor, b.hinter = int(teile[0]), int(teile[1])
else:
b.kritisch = True
b.hinweise.append(f"Abstand zu origin nicht bestimmbar: {kurz(zaehl)}")
b.zustand = zustand_ermitteln(b)
def abgleichen(b: Befund, nur_pruefen: bool, ohne_netz: bool) -> None:
"""Entscheidet über den fast-forward-Pull. Bei jedem Zweifel: nichts tun, nur melden."""
if not (b.vorhanden and b.lesbar):
return
if b.geaendert:
b.aktion = "uebersprungen: Aenderungen vorhanden"
return
if b.branch == "HEAD":
b.aktion = "uebersprungen: detached HEAD"
return
if b.branch != b.kanal:
b.aktion = "uebersprungen: fremder branch"
return
if not b.upstream:
b.aktion = "uebersprungen: kein upstream"
return
if b.offline: # ohne erfolgreichen fetch ist der Abstand nur geraten
b.aktion = "uebersprungen: offline"
return
if b.vor and b.hinter:
b.aktion = "uebersprungen: divergiert"
return
if b.vor:
eigene = "1 eigener Commit" if b.vor == 1 else f"{b.vor} eigene Commits"
b.aktion = f"uebersprungen: {eigene}"
return
if not b.hinter:
b.aktion = "bereits aktuell"
return
if nur_pruefen or ohne_netz:
b.aktion = f"hinter origin ({commits(b.hinter)})"
return
rc, ausgabe = git("pull", "--ff-only", "--quiet", cwd=b.pfad)
if rc == 0:
b.aktion = f"aktualisiert ({commits(b.hinter)})"
b.aktualisiert, b.hinter = True, 0
b.zustand = zustand_ermitteln(b)
else:
b.aktion, b.kritisch = "FEHLER: pull fehlgeschlagen", True
b.hinweise.append(f"git pull --ff-only fehlgeschlagen: {kurz(ausgabe)}")
def tabelle_ausgeben(befunde: list[Befund]) -> None:
"""Eine Zeile je Repository, insgesamt hoechstens BREITE Zeichen breit."""
spalten = [
("Repo", 34, [b.name for b in befunde]),
("Branch", 14, [b.branch for b in befunde]),
("Zustand", 16, [b.zustand for b in befunde]),
("hinter/vor", 10, [b.abstand for b in befunde]),
]
breiten = [min(hoechst, max([len(kopf)] + [len(w) for w in werte]))
for kopf, hoechst, werte in spalten]
platz = max(20, BREITE - (sum(breiten) + len(breiten)))
b_aktion = min(platz, max([len("Aktion")] + [len(b.aktion) for b in befunde]))
def zeile(felder: list[str]) -> str:
gefuellt = [kuerze(w, br).ljust(br) for w, br in zip(felder[:-1], breiten)]
return " ".join(gefuellt) + " " + kuerze(felder[-1], b_aktion)
kopfzeile = zeile(["Repo", "Branch", "Zustand", "hinter/vor", "Aktion"])
print(kopfzeile)
print("-" * (sum(breiten) + len(breiten) + b_aktion))
for b in befunde:
print(zeile([b.name, b.branch, b.zustand, b.abstand, b.aktion]).rstrip())
def empfehlungen(befunde: list[Befund], nur_pruefen: bool, ohne_netz: bool) -> list[str]:
"""Konkrete, kopierbare Handlungsempfehlungen – nur für das, was wirklich ansteht."""
zeilen: list[str] = []
fehlend = [b.name for b in befunde if b.zustand == "fehlt"]
if fehlend:
zeilen.append(f"Repository-Ordner fehlen: {', '.join(fehlend)}. Klonen mit:")
zeilen.append(" python scripts/setup_workspace.py")
ohne_git = [b.name for b in befunde if b.zustand == "kein git-repo"]
if ohne_git:
zeilen.append(f"Ordner ohne .git (kein Klon): {', '.join(ohne_git)}. Von Hand prüfen und "
"wegräumen, danach: python scripts/setup_workspace.py")
for b in befunde:
if not b.vorhanden:
continue
block: list[str] = []
if b.offline:
block.append("origin nicht erreichbar – Netz/VPN prüfen. Die Angaben zu hinter/vor "
"stammen vom letzten erfolgreichen fetch.")
if not b.remote:
block.append("kein Remote konfiguriert – gehört der Ordner wirklich zum Workspace?")
if b.geaendert:
block.append(f"{aenderungen(b.geaendert)} – dieses Skript fasst sie nicht an. "
"Selbst sichten, committen oder verwerfen:")
block.append(f" {b.befehl('status')}")
if b.branch == "HEAD":
block.append("HEAD ist losgelöst (kein Branch). Vor der Arbeit auf einen Branch wechseln – "
"das macht das Skript bewusst nicht selbst.")
elif b.branch != b.kanal:
block.append(f"steht auf Branch '{b.branch}' statt auf dem Hauptbranch '{b.kanal}'. "
f"Das ist richtig so, solange daran gearbeitet wird: Der Hauptbranch wird nie "
f"direkt bearbeitet, die Rückführung läuft über einen Pull Request auf Gitea. "
f"Ist der Branch fertig, den PR stellen; '{b.kanal}' wird von diesem Skript "
f"nur aktualisiert, wenn er ausgecheckt ist.")
if b.upstream and b.vor and b.hinter:
block.append(f"divergiert: {commits(b.vor)} lokal, {commits(b.hinter)} auf origin. "
"Weder Merge noch Rebase noch Reset macht dieses Skript – bitte selbst entscheiden.")
elif b.upstream and b.vor:
block.append(f"{commits(b.vor)} lokal, nicht gepusht. Pushen ist Sache des Menschen "
"und passiert nur nach Absprache:")
block.append(f" {b.befehl(f'push origin {b.branch}')}")
if not b.upstream and b.remote and b.branch != "HEAD":
block.append(f"Branch '{b.branch}' hat keinen Upstream. Erst nach Absprache und nur vom "
"Menschen:")
block.append(f" {b.befehl(f'push -u origin {b.branch}')}")
if b.hinter and not b.geaendert and b.branch == b.kanal and not b.vor:
grund = ("fehlendem Netzkontakt" if b.offline else "--nur-pruefen" if nur_pruefen
else "--ohne-netz" if ohne_netz else "einem Hindernis")
block.append(f"{commits(b.hinter)} hinter origin, wegen {grund} nicht geholt. Nachholen:")
block.append(f" python scripts/hallo.py --repo {b.name}")
elif b.hinter and (b.geaendert or b.branch != b.kanal):
block.append(f"Hauptbranch liegt {commits(b.hinter)} hinter origin – erst nach dem "
f"Aufräumen oben aktualisierbar.")
for meldung in b.hinweise:
if meldung.startswith(("origin nicht erreichbar", "kein Remote")):
continue # oben bereits in Klartext ausgegeben
block.append(meldung)
if block:
zeilen.append(f"{b.name}:")
zeilen.extend(f" {z}" if not z.startswith(" ") else z for z in block)
return zeilen
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--nur-pruefen", action="store_true", help="kein Pull, nur Bericht (fetch findet statt)")
ap.add_argument("--ohne-netz", action="store_true", help="kein fetch und kein Pull, nur lokaler Zustand")
ap.add_argument("--repo", action="append", metavar="NAME", help="nur dieses Repo (mehrfach angebbar)")
args = ap.parse_args()
try:
with MANIFEST.open("rb") as f:
manifest = tomllib.load(f)
except (OSError, tomllib.TOMLDecodeError) as fehler:
print(f"workspace.toml nicht lesbar: {fehler}")
return 2
repos = manifest.get("repos", [])
# Das Workspace-Repo heißt wie in [workspace] repo angegeben, sonst wie sein Ordner.
wurzel_name = manifest.get("workspace", {}).get("repo") or ROOT.name
bekannt = [wurzel_name] + [r["name"] for r in repos]
if args.repo:
unbekannt = [n for n in args.repo if n not in bekannt]
if unbekannt:
print(f"Unbekannte Repositories: {', '.join(unbekannt)}")
print(f"Bekannt sind: {', '.join(bekannt)}")
return 2
def gewuenscht(name: str) -> bool:
return args.repo is None or name in args.repo
befunde: list[Befund] = []
wurzel_ist_repo = (ROOT / ".git").exists()
if wurzel_ist_repo and gewuenscht(wurzel_name):
befunde.append(Befund(name=wurzel_name, kanal=manifest.get("workspace", {}).get("branch", ""),
pfad=ROOT, ist_wurzel=True))
rroot = repos_root()
befunde += [Befund(name=r["name"], kanal=r["branch"], pfad=rroot / r["name"])
for r in repos if gewuenscht(r["name"])]
modus = ("ohne Netz – kein fetch, kein Pull" if args.ohne_netz
else "nur prüfen – fetch, kein Pull" if args.nur_pruefen
else "fetch und fast-forward-Pull")
print(f"Hallo. Workspace: {ROOT}")
if rroot != ROOT:
print(f"Repositories liegen laut workspace.local.toml unter: {rroot}")
print(f"Modus: {modus} | Repositories: {len(befunde)}")
if not wurzel_ist_repo and gewuenscht(wurzel_name):
print(f"Hinweis: '{wurzel_name}' selbst ist noch kein Git-Repository – geprüft werden nur die Klone.")
if not args.ohne_netz:
print("Hole den Stand von origin (git fetch --prune) ...")
print()
for b in befunde:
einlesen(b, args.ohne_netz)
abgleichen(b, args.nur_pruefen, args.ohne_netz)
tabelle_ausgeben(befunde)
aktualisiert = sum(1 for b in befunde if b.aktualisiert)
aktuell = sum(1 for b in befunde if b.aktion == "bereits aktuell")
offen = len(befunde) - aktualisiert - aktuell
wort = "Repository" if len(befunde) == 1 else "Repositories"
print(f"\n{len(befunde)} {wort}: {aktualisiert} aktualisiert, {aktuell} bereits aktuell, "
f"{offen} mit Befund.")
if args.ohne_netz:
print("Ohne Netz gelaufen: hinter/vor stammt vom letzten fetch und kann veraltet sein.")
zu_tun = empfehlungen(befunde, args.nur_pruefen, args.ohne_netz)
if zu_tun:
print("\nZu tun")
print("-" * 6)
for zeile in zu_tun:
print(umbrechen(zeile))
return 1 if any(b.kritisch for b in befunde) else 0
if __name__ == "__main__":
raise SystemExit(main())
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""Findet deutschen Text in .py-Dateien, der offenbar ohne Umlaute/ß geschrieben wurde.
Hintergrund: AG_QGIS, Plugin_SN_Basis #42. KI-generierter Text weicht gelegentlich auf eine
ASCII-Ersatzschreibweise aus ("gueltig" statt "gültig", "ueber" statt "über") statt
echte Umlaute zu verwenden. Das betrifft vor allem Meldungstexte, Beschriftungen
(etwa BPMN-Elementnamen) und Docstrings/Kommentare.
Technische Bezeichner (Variablen-, Funktions- und Schlüsselnamen) sind davon
ausdrücklich nicht betroffen: Sie sind bewusst ASCII und enthalten kein Leerzeichen, während geschriebener deutscher Text so gut
wie immer eines enthält. Deshalb prüft dieses Skript nur String-Inhalte mit
mindestens einem Leerzeichen sowie Kommentartext - keine Bezeichner.
Kein Ersatz fürs Korrekturlesen: Die Liste verdächtiger Wortstämme ist bewusst kurz
gehalten (nur eindeutige Fälle), um keine Fehlalarme zu erzeugen. Neue, hier nicht
erfasste Wortstämme ergänzen.
Aufrufe (aus dem Workspace-Root):
python scripts/pruefe_umlaute.py # alle .py-Dateien im Workspace
python scripts/pruefe_umlaute.py .agents/skills # nur unterhalb dieser Ordner
Exit-Code: 0 = keine Funde, 1 = mindestens ein Fund.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
# Ordner, die nie geprüft werden (Git-Interna, gespiegelte Skills, lokale Entwürfe).
AUSGESCHLOSSEN = {".git", ".claude", "__pycache__", "entwuerfe"}
# Dateien, die bewusst ASCII-Ersatzschreibweisen enthalten: hallo.py schreibt seine
# Tabellenwerte absichtlich ohne Umlaute, dieses Skript nennt die Muster selbst.
DATEIEN_OHNE_PRUEFUNG = {"scripts/hallo.py", "scripts/pruefe_umlaute.py"}
# Nur eindeutige Fälle: Wortstämme, die als ASCII-Ersatzschreibweise so gut wie nie
# etwas anderes als ein fehlender Umlaut/ß sein können. Absichtlich kein "ss" für
# "ß" (zu viele legitime Doppel-S-Wörter) und kein "ae/oe/ue" pauschal (zu viele
# Fehlalarme in technischen Bezeichnern und Fremdwörtern).
VERDAECHTIGE_STAEMME = [
r"gueltig\w*", r"ungueltig\w*", r"verfuegbar\w*", r"ausgewaehlt\w*",
r"gewaehlt\w*", r"waehlen\b", r"auswaehlen\b", r"loeschen\b", r"loescht\w*",
r"groesse\w*", r"zuruecksetzen\b", r"erlaeuter\w*", r"bestaetig\w*",
r"natuerlich\w*", r"moeglich\w*", r"zusaetzlich\w*", r"abhaengig\w*",
r"unzulaessig\w*", r"ueberpruef\w*", r"vollstaendig\w*", r"geaendert\w*",
r"aenderung\w*", r"einfuegen\b", r"ausfuehr\w*", r"benoetig\w*",
r"gewuenscht\w*", r"ueber\w*", r"fuer\b", r"hoehe\w*", r"waere\b",
]
_STAMM_PATTERN = re.compile(
r"\b(?:" + "|".join(VERDAECHTIGE_STAEMME) + r")\b", re.IGNORECASE
)
# Erlaubt genau diese exakten Fundstellen (Datei, Zeile) - für Ausnahmefälle, die
# geprüft und bewusst akzeptiert wurden. Aktuell leer: siehe Docstring, warum
# technische Bezeichner die Heuristik ohnehin nicht erreichen.
ALLOWLIST: set[tuple[str, int]] = set()
_STRING_PATTERN = re.compile(
r'"""(?P<triple_d>(?:.|\n)*?)"""'
r"|'''(?P<triple_s>(?:.|\n)*?)'''"
r'|"(?P<double>(?:[^"\\\n]|\\.)*)"'
r"|'(?P<single>(?:[^'\\\n]|\\.)*)'"
)
_KOMMENTAR_PATTERN = re.compile(r"#(?P<text>.*)$", re.MULTILINE)
def _kandidaten(quelltext: str):
"""Liefert (start_offset, text) für String-Inhalte und Kommentare mit Leerzeichen."""
for m in _STRING_PATTERN.finditer(quelltext):
text = next(g for g in m.groups() if g is not None)
if " " in text:
yield m.start(), text
for m in _KOMMENTAR_PATTERN.finditer(quelltext):
text = m.group("text")
if " " in text:
yield m.start("text"), text
def pruefe_datei(pfad: Path) -> list[tuple[int, str, str]]:
"""Liefert Funde als (Zeilennummer, Wortstamm, Kontext) für eine Datei.
Ein Kandidat (String-Inhalt oder Kommentar) kann mehrzeilig sein (Docstrings);
deshalb wird für jeden Treffer einzeln die Zeile anhand seiner eigenen Position
berechnet, nicht anhand des Kandidaten-Anfangs.
"""
quelltext = pfad.read_text(encoding="utf-8", errors="replace")
funde = []
for offset, text in _kandidaten(quelltext):
for treffer in _STAMM_PATTERN.finditer(text):
zeile = quelltext.count("\n", 0, offset + treffer.start()) + 1
zeilen_text = text.splitlines()[treffer.string.count("\n", 0, treffer.start())] \
if "\n" in text else text
kontext = zeilen_text.strip()
if len(kontext) > 90:
kontext = kontext[:87] + "..."
funde.append((zeile, treffer.group(0), kontext))
return funde
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("ordner", nargs="*", help="Ordner relativ zum Workspace-Root (Standard: alles)")
args = ap.parse_args()
wurzeln = [ROOT / o for o in args.ordner] if args.ordner else [ROOT]
fehlend = [w for w in wurzeln if not w.is_dir()]
if fehlend:
print("Ordner nicht gefunden: " + ", ".join(str(w) for w in fehlend))
return 2
gesamt_funde = 0
dateien = 0
for wurzel in wurzeln:
for py_datei in sorted(wurzel.rglob("*.py")):
if any(teil in AUSGESCHLOSSEN for teil in py_datei.relative_to(ROOT).parts):
continue
relativ = py_datei.relative_to(ROOT).as_posix()
if relativ in DATEIEN_OHNE_PRUEFUNG:
continue
dateien += 1
for zeile, stamm, kontext in pruefe_datei(py_datei):
if (relativ, zeile) in ALLOWLIST:
continue
print(f"{relativ}:{zeile}: '{stamm}' in: {kontext}")
gesamt_funde += 1
status = "GRÜN" if gesamt_funde == 0 else "ROT"
print(f"\n{status}: {gesamt_funde} Fund/e in {dateien} Datei(en).")
return 1 if gesamt_funde else 0
if __name__ == "__main__":
raise SystemExit(main())
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Richtet den AG_Prozesse-Workspace ein bzw. aktualisiert ihn.
1. Klont fehlende Repositories aus workspace.toml in Unterordner (Default-Branch).
2. Holt für vorhandene Klone den Stand vom Server (git fetch) und zeigt Branch,
Vorsprung/Rückstand und ungesicherte Änderungen an. Ein Pull passiert nur
mit --pull und nur bei sauberem Arbeitsverzeichnis (fast-forward).
3. Spiegelt die Skills aus .agents/skills/ nach .claude/skills/ (Claude Code liest
nur dort; der Agent-Skills-Standard und andere Werkzeuge lesen .agents/skills/).
Aufrufe:
python scripts/setup_workspace.py # klonen + Status + Skills
python scripts/setup_workspace.py --pull # zusätzlich fast-forward pull
python scripts/setup_workspace.py --nur-skills
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml"
def repos_root() -> Path:
"""Ordner für die weiteren Repository-Klone, siehe workspace.local.toml (nicht versioniert)."""
if not LOKALE_ÜBERSCHREIBUNG.is_file():
return ROOT
with LOKALE_ÜBERSCHREIBUNG.open("rb") as f:
lokal = tomllib.load(f)
pfad = lokal.get("workspace", {}).get("repos_root")
return Path(pfad).expanduser() if pfad else ROOT
def git(*args: str, cwd: Path) -> tuple[int, str]:
p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, errors="replace")
return p.returncode, (p.stdout + p.stderr).strip()
def sync_skills() -> None:
quelle = ROOT / ".agents" / "skills"
ziel = ROOT / ".claude" / "skills"
if not quelle.is_dir():
print("Keine Skills in .agents/skills/ gefunden.")
return
ziel.parent.mkdir(exist_ok=True)
# Alten Spiegel entfernen (Symlink, Junction oder Kopie)
if ziel.is_symlink() or (ziel.exists() and not ziel.is_dir()):
ziel.unlink()
elif ziel.is_dir():
try:
os.rmdir(ziel) # leere Junction/Ordner
except OSError:
shutil.rmtree(ziel)
art = "symlink"
try:
os.symlink(quelle, ziel, target_is_directory=True)
except (OSError, NotImplementedError):
art = "kopie"
if os.name == "nt":
try:
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
art = "junction"
except Exception:
pass
if art == "kopie":
shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("__pycache__"))
anzahl = len([p for p in quelle.iterdir() if (p / "SKILL.md").is_file()])
print(f"Skills: {anzahl} aus .agents/skills/ nach .claude/skills/ gespiegelt ({art}).")
if art == "kopie":
print(" Hinweis: Kopie statt Link – nach Änderungen an .agents/skills/ dieses Skript erneut ausführen.")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pull", action="store_true", help="saubere Klone per fast-forward aktualisieren")
ap.add_argument("--nur-skills", action="store_true", help="nur Skills spiegeln, kein Git")
args = ap.parse_args()
if not args.nur_skills:
with (ROOT / "workspace.toml").open("rb") as f:
manifest = tomllib.load(f)
ws = manifest["workspace"]
basis_url = f"{ws['gitea']}/{ws['org']}"
rroot = repos_root()
print(f"Workspace: {ROOT}\nGitea: {basis_url}\n")
if rroot != ROOT:
print(f"Repositories liegen laut workspace.local.toml unter: {rroot}\n")
rroot.mkdir(parents=True, exist_ok=True)
for repo in manifest.get("repos", []):
name, branch = repo["name"], repo["branch"]
pfad = rroot / name
if not (pfad / ".git").is_dir():
print(f"[{name}] klone Branch {branch} ...")
rc, out = git("clone", "--branch", branch, f"{basis_url}/{name}.git", name, cwd=rroot)
print(" " + ("ok" if rc == 0 else f"FEHLER: {out}"))
continue
git("fetch", "--quiet", "--prune", cwd=pfad)
_, aktuell = git("rev-parse", "--abbrev-ref", "HEAD", cwd=pfad)
_, dirty = git("status", "--porcelain", cwd=pfad)
n_dirty = len([z for z in dirty.splitlines() if z.strip()])
_, ab = git("rev-list", "--left-right", "--count", f"HEAD...origin/{aktuell}", cwd=pfad)
vor, _, zurueck = ab.partition("\t") if "\t" in ab else ("?", "", "?")
meldung = f"[{name}] Branch {aktuell} (+{vor}/-{zurueck.strip()} zu origin), {n_dirty} ungesicherte Änderungen"
if aktuell != branch:
meldung += f" <- Hinweis: Manifest erwartet {branch}"
if args.pull and n_dirty == 0 and aktuell == branch:
rc, out = git("pull", "--ff-only", "--quiet", cwd=pfad)
meldung += " -> pull " + ("ok" if rc == 0 else f"FEHLER: {out.splitlines()[-1] if out else rc}")
print(meldung)
print()
sync_skills()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+20
View File
@@ -0,0 +1,20 @@
# Manifest des AG_Prozesse-Workspace.
# Wird von scripts/hallo.py, scripts/setup_workspace.py und scripts/gitea.py gelesen
# (Python >= 3.11, tomllib).
[workspace]
name = "AG_Prozesse"
gitea = "https://entwicklung.flurneuordnung-sachsen.de"
org = "AG_Prozesse"
# Das Workspace-Repository selbst (dieser Ordner). Es wird nicht geklont, zählt aber
# für scripts/gitea.py als Repo mit Tickets und für scripts/hallo.py als Arbeitsbranch.
repo = "Projektwissen"
branch = "main"
python_min = "3.11"
# Weitere Repositories der Gruppe werden hier als eigenständige Klone eingetragen und
# von scripts/setup_workspace.py in Unterordner geklont. Derzeit gibt es keine.
#
# [[repos]]
# name = "Beispiel" # Repo-Name auf Gitea = Ordnername
# branch = "main"