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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>` + `~>`/`<~` |
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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/
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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 |
|
||||
@@ -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/<name>.md"]
|
||||
B --> C["Annahmen lesen, Entwurf fachlich prüfen und anpassen"]
|
||||
C -->|"noch nicht richtig"| C
|
||||
C -->|"passt so, wandle um"| D["<name>.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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.>
|
||||
@@ -0,0 +1,3 @@
|
||||
@AGENTS.md
|
||||
|
||||
Beim Arbeiten im Workspace (`../AGENTS.md` vorhanden) gelten dessen Regeln und Skills zusätzlich.
|
||||
@@ -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.>
|
||||
@@ -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“)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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) |
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user