feat: slice 1 - the neckbeard framework chain runs end to end

Tracer bullet of the migration design (Gate 4, slice 1): pinned v0.1.1
baseline under docs/sources/upstream/ with provenance note, the
Karpathy block moved verbatim to docs/sources/regelwerk/ (standing
rule mapped onto the sources read-only mechanism), AGENTS.md assembled
from the byte-true upstream sections plus the project section 6
(group rules condensed from the old CLAUDE.md), CLAUDE.md reduced to
the upstream pointer, WORKFLOW.md and all four templates copied,
schema.yaml extended (issue milestone/priority/status columns,
component type, wiki area vision - all flagged in the header),
validate.py and gen_status.py forked with marked extensions,
pruefe_upstream_drift.py added, STATUS.md generated, CI gains the
offline validate job, README directory link defused.

Verified: validate 0 errors 0 warnings (the three pre-existing
directory-link errors are gone), gen_status --check current,
drift check 0 findings, baseline byte-identical to the reference
checkout (10/10 files), four negative tests fire (WIP limit 3x
in-progress, waiting without wartegrund, component slug mismatch,
single-byte drift in WORKFLOW.md). gen_status needs Python >= 3.10
locally (write_text newline) - noted for the design AAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thore Cimbal
2026-08-11 12:00:00 +00:00
co-authored by Claude Fable 5
parent 5e46372ea8
commit e36ed337a7
27 changed files with 2311 additions and 235 deletions
+16
View File
@@ -33,3 +33,19 @@ stillstandspruefung:
# Befunde sind kein Betriebsausfall, aber sie sollen sichtbar bleiben. Die rote # Befunde sind kein Betriebsausfall, aber sie sollen sichtbar bleiben. Die rote
# Pipeline ist bei uns die Alarmanlage (gitops/CLAUDE.md, TURN-Rotation). # Pipeline ist bei uns die Alarmanlage (gitops/CLAUDE.md, TURN-Rotation).
allow_failure: false allow_failure: false
# Offline-Gate bei jedem Push: Artefakte gegen schema.yaml, STATUS.md
# aktuell, Framework-Dateien unveraendert (Design 2026-08-11, Slice 1).
# Braucht nur den Baum - bewusst ohne Token und ohne Netz.
validate:
stage: pruefen
image: python:3.12-alpine
rules:
- if: $CI_PIPELINE_SOURCE == "push"
before_script:
- pip install --quiet pyyaml
script:
- python3 scripts/validate.py
- python3 scripts/gen_status.py --check
- python3 scripts/pruefe_upstream_drift.py
allow_failure: false
+179
View File
@@ -0,0 +1,179 @@
# AGENTS.md — Canonical Agent Instructions
Canonical instruction set for any coding agent working in this repository
(Claude Code, GPT-OSS harnesses, others). `CLAUDE.md` points here.
This file is loaded into every session — keep it short. Process details
live in `WORKFLOW.md`; read that when a task begins, not preemptively.
Tradeoff: these rules bias toward caution over speed. For trivial tasks,
use judgment — but say so.
## 1. Operating Rules
### Think before coding
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
### Simplicity first
- Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked. No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
- Test: "Would a senior engineer call this overcomplicated?" If yes, simplify.
- Before writing new code, stop at the first rung that holds:
needed at all? → codebase already has it? → stdlib? → platform-native?
→ installed dependency? → one line? → only then: the minimum that works.
(Ladder after ponytail, MIT.)
- Never cut, at any rung: trust-boundary validation, data-loss handling,
security, accessibility.
- Lazy about the solution, never about reading the code first.
### Surgical changes
- Touch only what you must. Match existing style, even if you'd differ.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- If you notice unrelated dead code, mention it — don't delete it.
- Remove imports/variables/functions that YOUR changes made unused;
leave pre-existing dead code alone unless asked.
- Every changed line must trace directly to the request.
### Goal-driven execution
- Transform tasks into verifiable goals:
"fix the bug" → "write a test that reproduces it, then make it pass".
- For multi-step work, state a brief plan: step → verify, step → verify.
- A task is well-defined only if it names all four:
**files, action, verify, done.** Missing one? The task is too vague — say so.
### Verification before completion
- Never claim something works without evidence: a test run, command
output, a rendered result. "Should work" is not a status.
- Report every task/slice with exactly one status:
`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`.
- Uncertainty is reported, never swallowed. Flag your shakiest calls.
## 2. Project Initialization (Gate 0)
At session start, read `PROJECT.md`. If it does not exist, initialization
is your first task: before anything else, ask the Gate 0 questions defined
in `WORKFLOW.md` — response language, size-S gate exception (yes/no),
one-line project purpose, audience — write the answers to `PROJECT.md`,
and have `validate.py` accept it. Never guess these answers; ask.
## 3. Workflow
For anything beyond a trivial change, read `WORKFLOW.md` and follow its
gates. At task start, propose a size class (S/M/L); the human confirms
(possibly batched later). **Never advance past a gate without explicit
human approval** — sole exception: size-S tasks, and only if `PROJECT.md`
explicitly grants that exception.
## 4. Repository Map
| Path | Purpose |
|---|---|
| `WORKFLOW.md` | Gate 0 (init) + Gates 15, size classes, debugging path, session handoff, refinement ritual |
| `PROJECT.md` | Per-project answers from Gate 0: language, size-S exception, purpose, audience |
| `STATUS.md` | Generated overview: open issues, active designs, recent ADRs — do not edit by hand |
| `schema.yaml` | Frontmatter schema — single source of truth for artifact structure |
| `docs/adr/` | Architecture Decision Records — binding; never edited, only superseded |
| `docs/design/` | One design doc per undertaking; completed ones move to `done/` |
| `docs/aar/` | Standalone After Action Reviews (incidents, major deviations only) |
| `docs/issues/` | In-repo issues, one file each; status lives in frontmatter |
| `docs/wiki/` | Wiki areas as folders, created on demand — rules in `docs/wiki/index.md` |
| `docs/sources/` | Immutable original sources; wiki pages cite them — read-only for agents |
| `scripts/` | Deterministic tooling: `validate.py`, `gen_status.py` |
Before proposing options (Gate 2), read the relevant ADRs and AARs first —
past decisions and learnings are input, not trivia.
## 5. Artifact Rules
- All artifacts are standard Markdown with YAML frontmatter conforming to
`schema.yaml`. Standard links only (`[text](path.md)`), no wikilinks.
Diagrams as Mermaid. This keeps every artifact portable across LLMs,
GitLab, and Obsidian.
- Never invent frontmatter fields or status values. `validate.py` is
authoritative; if it rejects your artifact, fix the artifact, not the
validator.
- Deterministic jobs (status generation, validation, link checks) are done
by scripts, not by you. If a deterministic job lacks a script, propose
one instead of doing it by inference.
<!-- projektabschnitt -->
## 6. Gruppenregeln (Projekt axion1337.chat)
Dieses Repo steuert die Gruppe `axion1337.chat`. Die Abschnitte 15
oben sind neckbeard v0.1.1 und bleiben byte-treu (Baseline:
`docs/sources/upstream/neckbeard-v0.1.1/`, Prüfung:
`scripts/pruefe_upstream_drift.py`); §1 ist destilliert aus den
wortgleich archivierten
[Karpathy-Guidelines](docs/sources/regelwerk/karpathy-guidelines.md).
**Änderungen an dieser Datei nur mit sorb abgestimmt.** Dieser
Abschnitt gilt für jede Session in allen Repos der Gruppe;
Komponenten-Repos tragen nur Projektspezifika plus einen Pointer
hierher ([ADR-0013](docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md)).
Ohne Lab-Zugang: dieses Repo ist als Push-Mirror unter
`https://rohana.axion1337.de/sorb/management` lesbar — pushen dorthin ist tabu.
### Quelle der Wahrheit & Mirror-Topologie
- `git.lab/axion1337.chat/*` ist kanonisch; Gitea/rohana wird per
Push-Mirror beliefert und bleibt Flux-Quelle, Registry und
Release-Download ([ADR-0001](decisions/0001-gitlab-kanonisch-push-mirror.md),
[ADR-0004](decisions/0004-site-to-site-vpn-hetzner-lab.md)). Die
Flux-Quelle **nicht auf git.lab „geradeziehen"** — die Produktion darf
nicht an der Lab-Verfügbarkeit hängen.
- **Nie direkt zu Gitea pushen** (der Mirror überschreibt per Force).
Landet doch ein Commit dort: Kanonisierungs-Verfahren in
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md).
- Einzige bewusste Ausnahme: der TURN-Rotations-CronJob schreibt nach
Gitea; der tägliche CI-Job `canonize_rotation` holt es zurück. Seine
rote Pipeline **ist** der Alarm — es gibt bewusst keinen zweiten Meldeweg.
### Issues & Board
- `docs/issues/` ist kanonisch für den Management-Scope
([ADR-0012](docs/adr/0012-issues-im-repo-gitlab-als-spiegel.md));
GitLab ist bespiegelte Ansicht. Jedes Issue trägt genau einen
Meilenstein (M1M5) und genau eine Priorität — das Schema erzwingt
beides. Zeitkritisches trägt ein `due`-Datum, nicht „bald".
- **WIP-Limit 2** (Validator-Regel). Die Zusage-Status `next` und
`in-progress` vergibt **nur sorb**; Sessions bilden ab (`waiting` mit
`wartegrund`, Erledigtes `done` mit Begründung im Issue-Commit),
sagen aber nichts zu.
- **ADR-Pflicht** bei Architektur-/Prozessentscheidungen und **jeder
dauerhaften Ausnahme von einer Regel** — eine Ausnahme nur zu
dokumentieren statt sie zu entscheiden, ist ein Fehler.
### Secrets & Credentials
- Token-/Secret-Werte **niemals anzeigen, loggen oder in Dateien
echoen** — anzeigen = Exposure = Rotation. Referenz nur über
Dateipfade (z. B. `~/.config/gitlab-lab/token`) oder maskierte
CI-Variablen. Die Trennung ist „Credential vs. Config":
nicht-geheime Konfiguration wird normal committet.
### Commit-Konventionen
- Nachrichten auf Englisch, Conventional-Stil; der Betreff sagt *was*,
der Rumpf *warum*.
- Autor- **und** Committer-Datum auf 12:00:00 UTC des laufenden Tages;
kanonische Autor-Identität. Historien-Rewrites nur mit
alt→neu-Zuordnung
([ADR-0009](decisions/0009-commit-konventionen-und-historien-anonymisierung.md),
Tabelle: [shared/commit-zuordnung-2026-08-07.md](shared/commit-zuordnung-2026-08-07.md)).
- ⚠️ Das schützt nur die Git-Historie; Plattform-Zeitstempel (Push,
Issues, Pipelines, Pakete) tragen die echte Uhrzeit (ADR-0009).
### Redlichkeit
- Verifiziert (Messung/Konsole) klar von Vermutung trennen;
Korrelation ≠ Kausalität — ein plausibler Verdacht ist kein Befund.
- Config-Dateien chirurgisch editieren, **nie re-dumpen**; vor dem Push
validieren (`docker compose config`, YAML-Parse).
- Fehlschläge und übersprungene Schritte benennen, nicht glätten —
„fertig" heißt verifiziert (deckungsgleich mit §1).
+1 -233
View File
@@ -1,233 +1 @@
# CLAUDE.md — übergreifende Arbeitskonventionen (kanonisch) Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
Diese Datei gilt für **jede Claude-/Agenten-Session in allen Projekten** der
Gruppe (axion1337.chat-Stack, ThreadNet-Repos, CFGMON/threadnet-operating,
Homelab). Projekt-Repos haben eigene CLAUDE.mds für ihre Spezifika (z. B. die
ESS-/Flux-Details in „ThreadNet Server Suite" = `axion1337.chat-gitops`) — bei
Widerspruch gilt für Arbeitsweise und Prozess **diese** Datei.
> **Für Sessions ohne Lab-Zugang** (CFGMON, MATRIX, …): dieses Repo ist als
> Push-Mirror unter `https://rohana.axion1337.de/sorb/management` von überall
> **lesbar** — dort diese Datei und die ADRs nachschlagen. Nur pushen ist tabu.
>
> 📋 **Kopierbare Kurzfassungen zum Voranstellen:**
> [verfahren/textbloecke.md](verfahren/textbloecke.md) — Session-Start, Host-Session,
> Deploy-Übergabe, Abschluss, Entscheidungsvorlage. Diese Datei hier bleibt die
> Quelle; die Bausteine verweisen nur darauf.
## Projektrealitäten (Stand 2026-08-01)
**Das Lab ist die Quelle der Wahrheit** ([ADR-0002](decisions/0002-issues-und-management-ins-lab.md)):
- Kanonische Repos liegen auf `git.lab/axion1337.chat/*` (nur im Lab/VPN
auflösbar). Gitea/rohana wird per **Push-Mirror** beliefert und bleibt
Flux-Source, Container-/npm-Registry und Release-Download
([ADR-0001](decisions/0001-gitlab-kanonisch-push-mirror.md)).
- **Warum überhaupt zwei Orte — und warum das kein Altbestand ist:** Auf git.lab
liegen die *Baupläne*, auf Gitea eine Kopie, die der Cluster **ohne verfügbares
Lab** erreicht. Der Hetzner-Cluster muss sich bauen und neu ausrollen lassen,
wenn das Homelab aus ist, im Umbau steckt oder niemand zu Hause ist — er darf
deshalb nicht von einem Host abhängen, der nur im Lab antwortet.
⚠️ **Die Flux-Quelle nicht „geradeziehen"** auf git.lab: Das sähe aufgeräumter
aus und würde die Verfügbarkeit der Produktion an das Lab koppeln — genau das,
was die Trennung verhindert.
- **Nie direkt zu Gitea pushen** (gespiegelte Repos) — der Mirror überschreibt
per Force.
- **Gespiegelt wird nur die Gruppe `axion1337.chat`** (die fünf Produkt-Repos und
`management`). Die Gruppe **`homelab`** (`docs`, `wiki`, `wiki-bookstack`) hat
bewusst **keine Mirrors**: Sie beschreibt und konfiguriert ausschließlich
Lab-Infrastruktur, und seit dem Site-to-Site-VPN
([ADR-0004](decisions/0004-site-to-site-vpn-hetzner-lab.md)) erreichen auch
Host-Sessions git.lab direkt — Tunnel einschalten genügt. Betriebslehren, die
von außen lesbar sein müssen, gehören deshalb in die **AARs** unter
`verfahren/aar/` (dieses Repo ist gespiegelt), nicht nur in die READMEs der
Lab-Repos.
- Landet doch ein Commit auf Gitea (z. B. aus einer Host-Session ohne Lab-Route):
**Kanonisierungs-Verfahren** in
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md) — `.patch`
von Gitea ziehen, `git am` (erhält Autorschaft), Push über git.lab.
- **Issues leben auf git.lab.** Die alten Gitea-Issues sind geschlossen und
verweisen dorthin. ⚠️ gitops-Nummern haben sich beim Umzug verschoben
(Gitea zählte PRs mit; z. B. Gitea#48 → GitLab#46) — alte „gitops#N"-Verweise
meinen die Gitea-Nummer; verbindlich ist der Migrations-Fußtext im Issue.
- **Ausnahme** (bewusst entschieden, nur noch eine): der
TURN-Rotations-CronJob schreibt weiter nach Gitea, weil er im Cluster läuft und
git.lab nicht erreicht.
**Die Rotation nicht von Hand nachziehen und den PR nie auf Gitea mergen**
das erledigt seit 2026-08-02 der geplante CI-Job `canonize_rotation` im
gitops-Repo täglich von git.lab aus. Scheitert er, bleibt die Pipeline rot;
diese rote Pipeline **ist** der Alarm, einen zusätzlichen Termin gibt es
bewusst nicht.
- **Dokumentation** ([ADR-0006](decisions/0006-wikis-konsolidieren-docusaurus.md)):
Das gitops-Wiki liegt seit 2026-08-02 auf git.lab (*Wiki*-Reiter im Projekt);
⚠️ der `wiki`-**Branch** im gitops-Repo ist ein überholter Mai-Abzug von `docs/`
und nicht die gepflegte Fassung. Alle Quellen zusammen erscheinen unter
**axionwiki.lab** ([`homelab/wiki`](https://git.lab/homelab/wiki), Docusaurus) —
Inhalte werden beim Bau geholt, **Änderungen gehören ins Quell-Repo**.
## Arbeitsframework ([ADR-0005](decisions/0005-pm-framework-kanban.md))
Kanban-Rückgrat mit leichten Scrum-Elementen:
- **Alles Offene ist ein Issue** — host-/infra-Scope hier im management-Projekt
(`host:`-Labels, alte IDs wie `CFGMON-01` bleiben im Titel), Projekt-Scope im
jeweiligen Projekt. Kein neues Backlog-Markdown anlegen; `hosts/`/`shared/`
sind nur Bestand + Historie.
- **Status über Labels**, genau eins pro Issue: `status:next` (die einzige
Zusage), `status:doing` (**WIP-Limit 2** — auch sessionübergreifend zu
verteidigen), `status:wartet` (nur mit benanntem Grund). Ohne Label = Backlog.
- **ADR-Pflicht** ([decisions/](decisions/)) bei Architektur-/Prozess-
entscheidungen und **jeder dauerhaften Ausnahme von einer Regel**. Eine
Ausnahme nur zu dokumentieren statt sie als Entscheidung vorzulegen, ist ein
Fehler.
- **Deploy-Übergaben** („einer baut, ein anderer rollt aus") laufen über das
Issue-Template und die Pflichtfelder in
[verfahren/deploy-uebergabe.md](verfahren/deploy-uebergabe.md) — das ist
unsere Definition of Done für Deployments. Nach Deploys mit Übergabe und nach
Incidents: **AAR** ([verfahren/aar/](verfahren/aar/), Vorlage liegt daneben).
- Prioritäten über `priority:*`; Zeitkritisches bekommt ein **Datum** im Issue,
nicht „bald".
- **Der Titel trägt keine Priorität.** Präfixe wie `[HIGH]`/`[MEDIUM]`/`[LOW]`
gehören nicht in den Titel — die Priorität steht im Label, und zwar nur dort.
Alte Kennungen wie `CFGMON-01` bleiben, die benennen den Gegenstand, nicht die
Dringlichkeit.
⚠️ Der Grund ist keine Ästhetik: Aus der Gitea-Migration trugen 34 Issues ein
Präfix, davon **zwei mit einer anderen Aussage als ihr Label** — wer nach Titel
sortierte, bekam ein anderes Bild als wer nach Label sortierte. Zwei Wahrheiten
über dieselbe Sache sind schlimmer als eine unvollständige. Bereinigt 2026-08-06.
- **Jedes Issue gehört zu genau einem Meilenstein** (Gruppen-Milestones M1M4,
siehe [roadmap.md](roadmap.md)). Label und Meilenstein beantworten verschiedene
Fragen: `priority:*` sagt **wie dringend**, der Meilenstein sagt **worauf es
einzahlt**. Ein Issue ohne Meilenstein taucht in keiner Roadmap-Ansicht auf und
ist damit praktisch unsichtbar — es existiert nur noch für den, der es angelegt
hat.
M1M4 haben **bewusst kein Enddatum**: Sie bündeln, sie simulieren keinen
Termindruck. Termindruck steht als Datum am einzelnen Issue.
## Secrets & Credentials
- **Token-/Secret-Werte niemals anzeigen, loggen oder in Dateien echoen** —
anzeigen = Exposure = Rotation. Echte Credentials tippt/legt sorb selbst an;
Sessions referenzieren sie nur über Dateipfade (z. B.
`~/.config/gitlab-lab/token`) oder maskierte CI-Variablen.
- Nicht-geheime Konfiguration wird direkt geschrieben und committet — die
Trennung ist „Credential vs. Config", nicht „alles über den Menschen".
## Commit-Konventionen (seit 2026-08-07)
Gilt für **alle** Repos der Gruppe `axion1337.chat` und die ThreadNet-Dienste.
- **Nachrichten auf Englisch**, Conventional-Commit-Stil: `feat:`, `fix:`,
`docs:`, `chore:`, `ci:`, `refactor:`. Der Betreff sagt *was*, der Rumpf *warum*.
- **Zeitstempel anonymisieren.** Autor- **und** Committer-Datum werden auf
**12:00:00 UTC des laufenden Tages** gesetzt, damit sich aus der Historie keine
persönlichen Arbeitszeiten ablesen lassen:
```bash
export GIT_AUTHOR_DATE="$(date -u +%Y-%m-%d)T12:00:00Z" \
GIT_COMMITTER_DATE="$(date -u +%Y-%m-%d)T12:00:00Z"
git commit -m "…"
```
⚠️ **Beide Variablen setzen.** Nur `GIT_AUTHOR_DATE` zu setzen bringt nichts —
`git log` zeigt zwar das Autordatum, das Committer-Datum bleibt aber im Objekt
und ist über `git log --format=%cd` und in jeder Weboberfläche sichtbar.
📎 Die Umstellung der Alt-Historie am 2026-08-07 hat 251 Commits neue SHAs
gegeben. Ältere Verweise bleiben über
[`shared/commit-zuordnung-2026-08-07.md`](shared/commit-zuordnung-2026-08-07.md)
auflösbar — **statt** geschriebene Issue-Kommentare nachträglich zu ändern. Wer
eine SHA nicht findet, hat einen Commit von vor der Grenze vor sich; der gilt
unverändert.
⚠️ **Das schützt nur die Git-Historie.** Push-Zeiten, Issue- und
Kommentar-Zeitstempel, Pipeline-Läufe und Paket-Veröffentlichungen tragen
weiterhin die echte Uhrzeit und liegen im selben GitLab bzw. auf dem
öffentlichen Gitea-Spiegel. Wer daraus wirklich keine Muster ableitbar haben
will, muss dort ansetzen — die Commit-Datumsregel allein reicht dafür nicht.
## Redlichkeit & gelebte Lehren
- **Aussagen mit Quelle:** Verifiziert (Messung/Konsole) klar von Vermutung
trennen; nicht selbst Geprüftes als solches kennzeichnen. Korrelation ≠
Kausalität — ein plausibler Verdacht ist kein Befund.
- **Config-Dateien textuell/chirurgisch editieren, nie re-dumpen** (YAML/JSON
neu serialisieren hat zweimal real Schaden angerichtet). Compose-/YAML-
Änderungen vor dem Push validieren (`docker compose config`, YAML-Parse).
- Fehlschläge und übersprungene Schritte werden benannt, nicht geglättet;
„fertig" heißt verifiziert.
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
*Der Karpathy-Block oben ist wortgleich aus der gitops-CLAUDE.md übernommen und
darf nicht bearbeitet werden (stehende Regel von sorb). Änderungen an dieser
Datei insgesamt: nur mit sorb abgestimmt — sie ist die gemeinsame
Arbeitsgrundlage aller Sessions.*
+1 -1
View File
@@ -34,7 +34,7 @@ GitLab-Issue-Template. **Alle Issues leben auf git.lab.**
| `vision/` | Eine Vision je Linie: Community (axion1337.chat), Tool (ThreadNet), Plattform (Homelab) | | `vision/` | Eine Vision je Linie: Community (axion1337.chat), Tool (ThreadNet), Plattform (Homelab) |
| `roadmap.md` | Linien, Meilenstein-Kandidaten, Kadenz — GitLab-Milestones halten den Stand | | `roadmap.md` | Linien, Meilenstein-Kandidaten, Kadenz — GitLab-Milestones halten den Stand |
| `decisions/` | ADRs — Pflicht bei Architekturentscheidungen **und dauerhaften Ausnahmen** | | `decisions/` | ADRs — Pflicht bei Architekturentscheidungen **und dauerhaften Ausnahmen** |
| `verfahren/` | Wie wir arbeiten: [Deploy-Übergabe/DoD](verfahren/deploy-uebergabe.md), [Refinement & Retro](verfahren/refinement.md), [AARs](verfahren/aar/), Werkzeuge | | `verfahren/` | Wie wir arbeiten: [Deploy-Übergabe/DoD](verfahren/deploy-uebergabe.md), [Refinement & Retro](verfahren/refinement.md), AARs (`verfahren/aar/`), Werkzeuge |
| `hosts/`, `shared/` | **Bestand + Historie** je Host/Thema — u. a. [Branding](shared/branding.md) (Marke, Paletten, wo welches Theme eingestellt ist); offene Punkte sind Issues | | `hosts/`, `shared/` | **Bestand + Historie** je Host/Thema — u. a. [Branding](shared/branding.md) (Marke, Paletten, wo welches Theme eingestellt ist); offene Punkte sind Issues |
Gelesen wird das alles auch gebündelt unter **[axionwiki.lab](https://axionwiki.lab)** — Gelesen wird das alles auch gebündelt unter **[axionwiki.lab](https://axionwiki.lab)** —
+24
View File
@@ -0,0 +1,24 @@
# STATUS
<!-- Generated by scripts/gen_status.py — do not edit. -->
## Issues (0 open, 0 closed)
_none open_
## Active design docs (1)
| Design | Gate | Title |
|---|---|---|
| [2026-08-11-neckbeard-migration](docs/design/2026-08-11-neckbeard-migration.md) | gate-4 | Design: Migration des Management-Systems auf neckbeard |
## ADRs (2)
| ADR | Status | Title |
|---|---|---|
| [0012](docs/adr/0012-issues-im-repo-gitlab-als-spiegel.md) | accepted | ADR-0012: Issues leben im Repo; GitLab wird deterministisch bespiegelt |
| [0013](docs/adr/0013-gruppenregeln-kanonisch-mit-pruefung.md) | accepted | ADR-0013: Gruppenregeln kanonisch im management-Repo, Komponenten zeigen und werden geprüft |
## Open AARs (0)
_none — nothing awaiting harvest_
+140
View File
@@ -0,0 +1,140 @@
# WORKFLOW.md — Gates, Sizing, and Rituals
Read this when a task begins, not preemptively. `AGENTS.md` holds the
always-on rules; this file holds the process.
## Size Classes
Propose one at task start; the human confirms — individually, or batched
at the next refinement session.
| Class | Scope | Process |
|---|---|---|
| S | One file / one small change, no design decisions | Direct. AGENTS.md rules only. The one-line go-ahead **before starting is the stop** — waived only if `PROJECT.md` grants the size-S exception. |
| M | Few files, minor decisions, fits one session | Slice plan in chat, no file. **STOP: plan approval before any code.** Then implement; each slice reports evidence and status inline. Gate 5 is a short AAR note in chat, filed to the wiki only if it produced a real learning. |
| L | New feature, multiple files or sessions, real decisions | Full design doc in `docs/design/` following Gates 15 below. |
When in doubt between two classes, pick the larger.
## Gate 0 — Project Initialization
Runs once per project, triggered by a missing `PROJECT.md`. Ask, never guess:
1. Response language? (e.g. de / en)
2. Size-S gate exception granted? (yes / no)
3. One-line project purpose?
4. Audience — who uses this besides the owner? (Drives which wiki areas
become mandatory later; see `docs/wiki/index.md`.)
Write the answers to `PROJECT.md` (frontmatter per `schema.yaml`), run
`validate.py`, and confirm the result with the human.
## Gates 15 (size L)
Each gate is a section of the design doc. A gate ends with **STOP**:
present the section, wait for explicit approval. Do not pre-fill later
sections.
### Gate 1 — Product
- Problem statement: what user problem, for whom.
- Verifiable acceptance criterion. A real number where one exists;
otherwise a concretely checkable outcome. "Works" is not a criterion.
- Non-goals: what this deliberately does not do.
- Announcement paragraph (35 sentences): what it is, who it's for, why
it's good. If you can't write it, the product isn't understood yet.
- UI involved? Plain-HTML mockups of the affected screens.
**STOP.**
### Gate 2 — Architecture
- Read first: the actual codebase, relevant ADRs, relevant AARs.
Past decisions and learnings are input, not trivia.
- How it fits the real system: endpoints, tables/schemas, query
outlines, the end-to-end flow (Mermaid).
- Constraints: non-functional requirements, proportional to the project.
- Options & trade-offs where more than one viable way exists: pro/contra
each, chosen option, and why. Feature-local decisions stay here.
- Lasting directional decisions discovered here become ADRs (one each),
linked from the design doc.
**STOP.**
### Gate 3 — Program Design
- File locations: exact paths, new and touched.
- Types and method signatures — no bodies.
- Call stack for the main flow(s).
- What the tests will assert.
- Boundaries: an explicit DO NOT CHANGE list.
- Shakiest calls: name the decisions you are least confident about.
**STOP.**
### Gate 4 — Vertical Slices
- Slice 1 is the tracer bullet: a thin end-to-end path that runs
(mocks and stubs allowed). Only then real logic, one testable slice
at a time. Never build layer-by-layer horizontally.
- Every slice lists its tasks; every task names **files, action,
verify, done**.
- Each slice ends with verification evidence, a status
(`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`),
and a **STOP** for human review before the next slice.
### Gate 5 — Closeout
- AAR section in the design doc: planned / actual / why the
difference / learnings.
- Harvest: learnings useful to future readers go to the wiki
(FAQ, Stolpersteine) with source links. A missing or wrong framework
rule becomes a framework issue or update.
- Good analyses produced along the way may be filed as wiki pages
(with citations) instead of dying in chat history.
- Move the design doc to `docs/design/done/`. Run `gen_status.py`.
## Debugging Path
For bugs and incidents, any size:
1. Reproduce first. No reproduction, no fix.
2. Hypothesize the root cause; verify the hypothesis with evidence
before changing anything.
3. Route the failure before fixing (diagnostic failure routing):
- **Intent issue** — we built toward the wrong goal → back to Gate 1.
- **Spec issue** — the design/plan was wrong → fix the spec
(Gate 2/3), then the code.
- **Code issue** — plan right, code wrong → fix in place.
4. Fix, plus a test that would have caught it.
5. Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`.
## Session Handoff
- When a slice completes, or context quality degrades, write the current
state into the design doc's **Handoff block** — done slices, open
decisions, next step — then start a fresh session that resumes from
the doc. The doc is the memory; the session is disposable.
- End every working session by answering: "Which choices did I make that
I'm least confident about?" File the answer in the design doc.
## Refinement Session
A recurring, human-triggered ritual. Agenda:
1. Batched confirmations: size classes and small approvals queued since
last time.
2. Backlog triage over `docs/issues/`: close, reprioritize, split.
3. AAR harvest: walk recent AARs; update the wiki (FAQ, Stolpersteine);
propose framework changes.
4. Wiki lint (content-level, beyond `validate.py`): contradictions
between pages, claims superseded by newer sources, orphan pages,
missing cross-references, gaps worth a new page or a web search.
5. STATUS review: anything stale or surprising in `STATUS.md`.
## Knowledge Handling (summary)
Full rules live in `docs/wiki/index.md`. The short version:
- Original sources live in `docs/sources/`, immutable — agents read
them, never modify them. Wiki pages cite the sources they draw on.
- Contradictions are resolved or explicitly flagged — never left
silently coexisting.
- If the wiki has no confident answer, say so. Never file a
low-confidence synthesis back as knowledge.
- Git is the changelog. No separate log file.
+34
View File
@@ -0,0 +1,34 @@
---
type: aar
status: open # open | harvested
date: YYYY-MM-DD
related: [] # design docs, issues, ADRs involved
---
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
Standalone AARs are for incidents and major deviations only —
normal undertakings get their AAR as Gate 5 inside the design doc. -->
# AAR: Title
## What was planned / expected
## What happened
<!-- Facts and timeline, not blame. -->
## Why the difference
<!-- Root cause. For failures, name the routing class:
intent issue / spec issue / code issue. -->
## Learnings
<!-- What future-you should know. Blunt beats polite. -->
## Actions
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
framework issues opened, tests added. When all actions are done,
set status: harvested. The refinement session walks all AARs
still marked open. -->
+37
View File
@@ -0,0 +1,37 @@
---
type: adr
id: "0000"
status: proposed # proposed | accepted | superseded
date: YYYY-MM-DD
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
superseded_by: null # filled in on the OLD adr when a new one replaces it
related: [] # optional: paths to design docs / issues
---
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
# ADR-0000: Title
## Context
<!-- The situation and the forces at play. Constraints upfront:
deadlines, scale, team knowledge, existing decisions. -->
## Options Considered
<!-- Name each option, even the one you lean toward. Pros/cons per
option; a small dimension table (complexity, cost, maintenance,
familiarity) where it helps. Keep proportional to the decision. -->
## Decision
<!-- The choice, in one or two sentences. -->
## Consequences
<!-- What becomes easier, what becomes harder, what we will need to
revisit. Honest cons included. -->
<!-- Rules: an accepted ADR is never edited — write a new ADR that
supersedes it and set superseded_by here. Lasting directional
decisions only; feature-local choices belong in the design doc. -->
+38 -1
View File
@@ -1,6 +1,6 @@
--- ---
type: design type: design
status: gate-3 status: gate-4
date: 2026-08-11 date: 2026-08-11
size: L size: L
related: related:
@@ -505,3 +505,40 @@ Hinweis von sorb bei der Gate-3-Freigabe, per Fetch und Live-API
Akzeptanzkriterium 2 = 11/11. Die Enums bleiben, wie in Annahme 2 Akzeptanzkriterium 2 = 11/11. Die Enums bleiben, wie in Annahme 2
benannt, aus dem management-Scope abgeleitet; `area:authentik` liegt benannt, aus dem management-Scope abgeleitet; `area:authentik` liegt
außerhalb (gitops) und wird erst bei dessen Adoption Schema-Thema. außerhalb (gitops) und wird erst bei dessen Adoption Schema-Thema.
## Gate 4 — Vertikale Slices
Jeder Slice endet mit Nachweis, Status und **STOP**.
**Slice 1 — Tracer Bullet: die Framework-Kette läuft Ende-zu-Ende.**
Baseline (`docs/sources/upstream/neckbeard-v0.1.1/` + `HERKUNFT.md`),
Karpathy-Block wortgleich nach `docs/sources/regelwerk/`, `AGENTS.md`
(§15 byte-treu + §6 Gruppenregeln), `CLAUDE.md`-Pointer, `WORKFLOW.md`,
Templates, erweitertes `schema.yaml`, `validate.py` (+3 Regeln),
`gen_status.py` (Fork), `pruefe_upstream_drift.py`, generiertes
`STATUS.md`, CI-Job `validate`, README-Verzeichnis-Link entschärft.
*Verify:* validate 0 Fehler · gen_status --check aktuell · Drift-Check
grün · vier Negativtests feuern · Baseline byte-identisch zur Referenz.
**Slice 2 — ADR-Port.** `decisions/0001…0011``docs/adr/` mit
Frontmatter, Verweise nachgezogen, `decisions/` entfällt.
*Verify:* 11/11 validieren, Duplikat-ID-Prüfung greift, validate grün.
**Slice 3 — Wiki, Sources, AARs.** `verfahren/`/`hosts/`/`vision/`/
`shared/` an ihre Zielorte, Wiki-Index, `pruefe_prosa.py`; Demos
Muster C (F-004-Punkte auf Vor-Stand) und D (6 verwaiste SHAs).
*Verify:* validate + pruefe_prosa grün auf Endstand, Demos feuern auf
Vor-Stand, alte Wurzelordner leer.
**Slice 4 — Issue-Import.** `import_issues.py` liest die offenen
management-Issues live (read-only), 26+ Dateien + 5 F-004-Issues,
`roadmap.md` verliert Zahlen an STATUS.md.
*Verify:* alle Issue-Dateien validieren (Pflicht-Meilenstein/-Priorität),
Import-Protokoll unter sources/migration, Muster-C-Endstand = 0.
**Slice 5 — Komponenten, Gruppenprüfung, Spiegel.** 8
Komponenten-Deklarationen, `gruppenpruefung.py` (+ CI-Job),
`spiegel_issues.py` (Dry-Run-Demo); Demos Muster A (eingefrorener
Export ↔ Alt-CLAUDE) und B (Hygiene über lokale Klone), Live-Befund
gitops#61.
*Verify:* Dry-Run-Ausgabe plausibel, Demos feuern, kein API-Write.
+110
View File
@@ -0,0 +1,110 @@
---
type: design
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
date: YYYY-MM-DD
size: L # this template is for size L
related: [] # issues, ADRs spawned or read
---
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
later gates. Advance `status` only after human approval. -->
# Design: Title
## Gate 1 — Product
**Problem.** <!-- What user problem, for whom. -->
**Acceptance criterion.** <!-- Verifiable. A real number where one
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
scope-creep brake there is. -->
**Announcement.** <!-- 35 sentences: what it is, who it's for, why
it's good. Can't write it? The product isn't understood yet. -->
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
> **STOP — awaiting Gate 1 approval.**
## Gate 2 — Architecture
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
why they matter here. -->
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
end-to-end flow as Mermaid. Against the actual codebase. -->
**Constraints.** <!-- Non-functional, proportional to the project:
performance, security, operations, compatibility. "None relevant"
is a valid answer — but say it. -->
**Options & trade-offs.** <!-- Where more than one viable way exists:
name the options, pro/contra each, state the chosen one and WHY.
This is the feature-local decision record. Only lasting, binding
decisions graduate to an ADR below. -->
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
linked. None is a valid answer. -->
> **STOP — awaiting Gate 2 approval.**
## Gate 3 — Program Design
**Files.** <!-- Exact paths, new and touched. -->
**Signatures.** <!-- Types and method signatures, no bodies. -->
**Call stack.** <!-- For the main flow(s). -->
**Test assertions.** <!-- What the tests will assert. -->
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
**Shakiest calls.** <!-- The decisions you are least confident about. -->
> **STOP — awaiting Gate 3 approval.**
## Gate 4 — Vertical Slices
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
Then real logic, one testable slice at a time. Per task:
files / action / verify / done. After each slice: evidence,
status, STOP. -->
### Slice 1 — Tracer bullet
- [ ] Task: … — files: … — action: … — verify: … — done: …
**Evidence:** <!-- command output, test run, screenshot ref -->
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
> **STOP — slice review.**
### Slice 2 — …
### Handoff
<!-- The single place session state lives. Overwrite on every handoff;
git keeps the history.
Done slices: …
Open decisions: …
Next step: … -->
## Gate 5 — Closeout (AAR)
**Planned vs. actual.** <!-- What was planned, what happened. -->
**Why the difference.** <!-- Root causes, honestly. -->
**Learnings.** <!-- What future-you should know. -->
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
links; framework issues opened, if a rule was missing or wrong. -->
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
did I make that I'm least confident about?" -->
<!-- After approval: set status: done, move this file to
docs/design/done/, run gen_status.py. -->
+29
View File
@@ -0,0 +1,29 @@
---
type: issue
id: "0000"
status: open # open | in-progress | done | rejected
created: YYYY-MM-DD
related: [] # design docs, ADRs, other issues
---
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
# Issue-0000: Title
## Problem / Motivation
<!-- What's wrong or missing, and why it matters. One paragraph. -->
## Acceptance
<!-- When is this issue done? Verifiable, like every other criterion
in this framework. -->
## Notes
<!-- Optional: context, links, findings gathered along the way.
Rules: status is the single source of truth and lives here in the
frontmatter — STATUS.md is generated, never edited. An issue that
starts real work links its design doc in `related`. Closed means
status: done (or rejected, with a one-line reason in Notes) —
the file stays; git is the history. -->
@@ -0,0 +1,68 @@
---
name: karpathy-guidelines
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
license: MIT
---
# Karpathy Guidelines
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
@@ -0,0 +1,103 @@
# AGENTS.md — Canonical Agent Instructions
Canonical instruction set for any coding agent working in this repository
(Claude Code, GPT-OSS harnesses, others). `CLAUDE.md` points here.
This file is loaded into every session — keep it short. Process details
live in `WORKFLOW.md`; read that when a task begins, not preemptively.
Tradeoff: these rules bias toward caution over speed. For trivial tasks,
use judgment — but say so.
## 1. Operating Rules
### Think before coding
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
### Simplicity first
- Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked. No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
- Test: "Would a senior engineer call this overcomplicated?" If yes, simplify.
- Before writing new code, stop at the first rung that holds:
needed at all? → codebase already has it? → stdlib? → platform-native?
→ installed dependency? → one line? → only then: the minimum that works.
(Ladder after ponytail, MIT.)
- Never cut, at any rung: trust-boundary validation, data-loss handling,
security, accessibility.
- Lazy about the solution, never about reading the code first.
### Surgical changes
- Touch only what you must. Match existing style, even if you'd differ.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- If you notice unrelated dead code, mention it — don't delete it.
- Remove imports/variables/functions that YOUR changes made unused;
leave pre-existing dead code alone unless asked.
- Every changed line must trace directly to the request.
### Goal-driven execution
- Transform tasks into verifiable goals:
"fix the bug" → "write a test that reproduces it, then make it pass".
- For multi-step work, state a brief plan: step → verify, step → verify.
- A task is well-defined only if it names all four:
**files, action, verify, done.** Missing one? The task is too vague — say so.
### Verification before completion
- Never claim something works without evidence: a test run, command
output, a rendered result. "Should work" is not a status.
- Report every task/slice with exactly one status:
`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`.
- Uncertainty is reported, never swallowed. Flag your shakiest calls.
## 2. Project Initialization (Gate 0)
At session start, read `PROJECT.md`. If it does not exist, initialization
is your first task: before anything else, ask the Gate 0 questions defined
in `WORKFLOW.md` — response language, size-S gate exception (yes/no),
one-line project purpose, audience — write the answers to `PROJECT.md`,
and have `validate.py` accept it. Never guess these answers; ask.
## 3. Workflow
For anything beyond a trivial change, read `WORKFLOW.md` and follow its
gates. At task start, propose a size class (S/M/L); the human confirms
(possibly batched later). **Never advance past a gate without explicit
human approval** — sole exception: size-S tasks, and only if `PROJECT.md`
explicitly grants that exception.
## 4. Repository Map
| Path | Purpose |
|---|---|
| `WORKFLOW.md` | Gate 0 (init) + Gates 15, size classes, debugging path, session handoff, refinement ritual |
| `PROJECT.md` | Per-project answers from Gate 0: language, size-S exception, purpose, audience |
| `STATUS.md` | Generated overview: open issues, active designs, recent ADRs — do not edit by hand |
| `schema.yaml` | Frontmatter schema — single source of truth for artifact structure |
| `docs/adr/` | Architecture Decision Records — binding; never edited, only superseded |
| `docs/design/` | One design doc per undertaking; completed ones move to `done/` |
| `docs/aar/` | Standalone After Action Reviews (incidents, major deviations only) |
| `docs/issues/` | In-repo issues, one file each; status lives in frontmatter |
| `docs/wiki/` | Wiki areas as folders, created on demand — rules in `docs/wiki/index.md` |
| `docs/sources/` | Immutable original sources; wiki pages cite them — read-only for agents |
| `scripts/` | Deterministic tooling: `validate.py`, `gen_status.py` |
Before proposing options (Gate 2), read the relevant ADRs and AARs first —
past decisions and learnings are input, not trivia.
## 5. Artifact Rules
- All artifacts are standard Markdown with YAML frontmatter conforming to
`schema.yaml`. Standard links only (`[text](path.md)`), no wikilinks.
Diagrams as Mermaid. This keeps every artifact portable across LLMs,
GitLab, and Obsidian.
- Never invent frontmatter fields or status values. `validate.py` is
authoritative; if it rejects your artifact, fix the artifact, not the
validator.
- Deterministic jobs (status generation, validation, link checks) are done
by scripts, not by you. If a deterministic job lacks a script, propose
one instead of doing it by inference.
@@ -0,0 +1 @@
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
@@ -0,0 +1,25 @@
# Herkunft dieser Baseline
Unveränderte Originale aus **neckbeard v0.1.1**, Commit
`823a08cac6b03a47d7e2f661200a49ac6e09d38d` (`main`, sauber), Origin
`https://git.lab/oss-projekte/ai/neckbeard.git` — der Stand, gegen den
der Feldtest (Branch `Neckbeard-v0.1.1-analyse-1`) gemessen hat und aus
dem die Migration übernommen wurde (Design:
`docs/design/2026-08-11-neckbeard-migration.md`).
Zweck: Byte-Baseline für `scripts/pruefe_upstream_drift.py`. Ein
Framework-Upgrade ersetzt diese Dateien bewusst und in einem eigenen
Commit — nie beiläufig.
| Datei hier | Arbeitskopie | Prüfung |
|---|---|---|
| `AGENTS.md` | `/AGENTS.md` | Präfix bis zur Marke `<!-- projektabschnitt -->` |
| `CLAUDE.md` | `/CLAUDE.md` | byte-identisch |
| `WORKFLOW.md` | `/WORKFLOW.md` | byte-identisch |
| `templates/adr-template.md` | `docs/adr/template.md` | byte-identisch |
| `templates/design-template.md` | `docs/design/template.md` | byte-identisch |
| `templates/aar-template.md` | `docs/aar/template.md` | byte-identisch |
| `templates/issue-template.md` | `docs/issues/template.md` | byte-identisch |
| `schema.yaml` | `/schema.yaml` | **erklärt projekterweitert** — nur Diff-Referenz |
| `scripts/validate.py` | `scripts/validate.py` | **erklärt projekterweitert** — nur Diff-Referenz |
| `scripts/gen_status.py` | `scripts/gen_status.py` | **erklärt projekterweitert** — nur Diff-Referenz |
@@ -0,0 +1,140 @@
# WORKFLOW.md — Gates, Sizing, and Rituals
Read this when a task begins, not preemptively. `AGENTS.md` holds the
always-on rules; this file holds the process.
## Size Classes
Propose one at task start; the human confirms — individually, or batched
at the next refinement session.
| Class | Scope | Process |
|---|---|---|
| S | One file / one small change, no design decisions | Direct. AGENTS.md rules only. The one-line go-ahead **before starting is the stop** — waived only if `PROJECT.md` grants the size-S exception. |
| M | Few files, minor decisions, fits one session | Slice plan in chat, no file. **STOP: plan approval before any code.** Then implement; each slice reports evidence and status inline. Gate 5 is a short AAR note in chat, filed to the wiki only if it produced a real learning. |
| L | New feature, multiple files or sessions, real decisions | Full design doc in `docs/design/` following Gates 15 below. |
When in doubt between two classes, pick the larger.
## Gate 0 — Project Initialization
Runs once per project, triggered by a missing `PROJECT.md`. Ask, never guess:
1. Response language? (e.g. de / en)
2. Size-S gate exception granted? (yes / no)
3. One-line project purpose?
4. Audience — who uses this besides the owner? (Drives which wiki areas
become mandatory later; see `docs/wiki/index.md`.)
Write the answers to `PROJECT.md` (frontmatter per `schema.yaml`), run
`validate.py`, and confirm the result with the human.
## Gates 15 (size L)
Each gate is a section of the design doc. A gate ends with **STOP**:
present the section, wait for explicit approval. Do not pre-fill later
sections.
### Gate 1 — Product
- Problem statement: what user problem, for whom.
- Verifiable acceptance criterion. A real number where one exists;
otherwise a concretely checkable outcome. "Works" is not a criterion.
- Non-goals: what this deliberately does not do.
- Announcement paragraph (35 sentences): what it is, who it's for, why
it's good. If you can't write it, the product isn't understood yet.
- UI involved? Plain-HTML mockups of the affected screens.
**STOP.**
### Gate 2 — Architecture
- Read first: the actual codebase, relevant ADRs, relevant AARs.
Past decisions and learnings are input, not trivia.
- How it fits the real system: endpoints, tables/schemas, query
outlines, the end-to-end flow (Mermaid).
- Constraints: non-functional requirements, proportional to the project.
- Options & trade-offs where more than one viable way exists: pro/contra
each, chosen option, and why. Feature-local decisions stay here.
- Lasting directional decisions discovered here become ADRs (one each),
linked from the design doc.
**STOP.**
### Gate 3 — Program Design
- File locations: exact paths, new and touched.
- Types and method signatures — no bodies.
- Call stack for the main flow(s).
- What the tests will assert.
- Boundaries: an explicit DO NOT CHANGE list.
- Shakiest calls: name the decisions you are least confident about.
**STOP.**
### Gate 4 — Vertical Slices
- Slice 1 is the tracer bullet: a thin end-to-end path that runs
(mocks and stubs allowed). Only then real logic, one testable slice
at a time. Never build layer-by-layer horizontally.
- Every slice lists its tasks; every task names **files, action,
verify, done**.
- Each slice ends with verification evidence, a status
(`DONE` | `DONE_WITH_CONCERNS` | `NEEDS_CONTEXT` | `BLOCKED`),
and a **STOP** for human review before the next slice.
### Gate 5 — Closeout
- AAR section in the design doc: planned / actual / why the
difference / learnings.
- Harvest: learnings useful to future readers go to the wiki
(FAQ, Stolpersteine) with source links. A missing or wrong framework
rule becomes a framework issue or update.
- Good analyses produced along the way may be filed as wiki pages
(with citations) instead of dying in chat history.
- Move the design doc to `docs/design/done/`. Run `gen_status.py`.
## Debugging Path
For bugs and incidents, any size:
1. Reproduce first. No reproduction, no fix.
2. Hypothesize the root cause; verify the hypothesis with evidence
before changing anything.
3. Route the failure before fixing (diagnostic failure routing):
- **Intent issue** — we built toward the wrong goal → back to Gate 1.
- **Spec issue** — the design/plan was wrong → fix the spec
(Gate 2/3), then the code.
- **Code issue** — plan right, code wrong → fix in place.
4. Fix, plus a test that would have caught it.
5. Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`.
## Session Handoff
- When a slice completes, or context quality degrades, write the current
state into the design doc's **Handoff block** — done slices, open
decisions, next step — then start a fresh session that resumes from
the doc. The doc is the memory; the session is disposable.
- End every working session by answering: "Which choices did I make that
I'm least confident about?" File the answer in the design doc.
## Refinement Session
A recurring, human-triggered ritual. Agenda:
1. Batched confirmations: size classes and small approvals queued since
last time.
2. Backlog triage over `docs/issues/`: close, reprioritize, split.
3. AAR harvest: walk recent AARs; update the wiki (FAQ, Stolpersteine);
propose framework changes.
4. Wiki lint (content-level, beyond `validate.py`): contradictions
between pages, claims superseded by newer sources, orphan pages,
missing cross-references, gaps worth a new page or a web search.
5. STATUS review: anything stale or surprising in `STATUS.md`.
## Knowledge Handling (summary)
Full rules live in `docs/wiki/index.md`. The short version:
- Original sources live in `docs/sources/`, immutable — agents read
them, never modify them. Wiki pages cite the sources they draw on.
- Contradictions are resolved or explicitly flagged — never left
silently coexisting.
- If the wiki has no confident answer, say so. Never file a
low-confidence synthesis back as knowledge.
- Git is the changelog. No separate log file.
@@ -0,0 +1,106 @@
# schema.yaml — single source of truth for artifact frontmatter.
# Stage 1 of ADR-0004: scripts/validate.py checks generically against this
# file. Extending the framework's metadata means editing THIS file, not code.
# Agents: never invent fields or status values; propose a schema change.
version: 1
scope:
# Files considered artifacts. Templates and raw sources are exempt.
include:
- "PROJECT.md"
- "docs/**/*.md"
exclude:
- "**/template.md"
- "docs/sources/**"
- "vendor/**"
# Files whose inline links are checked, but which need no frontmatter
# (root-level prose: README, AGENTS, WORKFLOW, generated STATUS, ...).
link_only:
- "*.md"
# Frontmatter fields whose values are links. Values starting with
# http://, https:// or mailto: are treated as external and only
# format-checked; everything else must be a repo-root-relative path
# to an existing file.
link_fields: [related, sources, supersedes, superseded_by]
types:
project:
dir: "."
filename: "^PROJECT\\.md$"
required: [type, language, size_s_exception, purpose, audience]
fields:
language: { enum: [de, en] }
size_s_exception: { kind: bool }
purpose: { kind: str }
audience: { kind: str }
adr:
dir: "docs/adr"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, date]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [proposed, accepted, superseded] }
date: { kind: date }
supersedes: { kind: link, nullable: true }
superseded_by: { kind: link, nullable: true }
related: { kind: links }
rules:
# status: superseded requires superseded_by to point at the successor.
- superseded_requires_pointer
design:
dir: "docs/design"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date, size]
fields:
status: { enum: [gate-1, gate-2, gate-3, gate-4, gate-5, done] }
size: { enum: [L] }
date: { kind: date }
related: { kind: links }
rules:
# status: done if and only if the file lives under docs/design/done/.
- done_iff_in_done_dir
aar:
dir: "docs/aar"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date]
fields:
status: { enum: [open, harvested] }
date: { kind: date }
related: { kind: links }
issue:
dir: "docs/issues"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, created]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [open, in-progress, done, rejected] }
created: { kind: date }
related: { kind: links }
wiki-page:
dir: "docs/wiki"
filename: "^[a-z0-9-]+\\.md$"
required: [type, area]
fields:
area:
enum:
- index
- architecture
- admin
- deployment
- user-guide
- requirements
- faq
- stolpersteine
sources: { kind: links }
related: { kind: links }
rules:
# Pages other than the index should be linked from somewhere
# (reported as WARNING, not error — see validate.py).
- warn_if_orphan
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
Writes STATUS.md (no timestamps output depends only on repo content, so
reruns are diff-clean). With --check, regenerates in memory and fails if
the committed STATUS.md is stale; CI uses this mode.
Usage:
python scripts/gen_status.py [repo-root] # write STATUS.md
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
def parse(path: Path):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "---":
return None, ""
for j in range(1, len(lines)):
if lines[j].strip() == "---":
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
body = "\n".join(lines[j + 1:])
return meta, body
return None, ""
def title(body: str, fallback: str) -> str:
match = H1_RE.search(body)
return match.group(1).strip() if match else fallback
def collect(root: Path, subdir: str, wanted_type: str):
items = []
base = root / subdir
if not base.is_dir():
return items
for path in sorted(base.rglob("*.md")):
if path.name == "template.md":
continue
meta, body = parse(path)
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
continue
rel = path.relative_to(root).as_posix()
items.append((rel, meta, title(body, path.stem)))
return items
def render(root: Path) -> str:
issues = collect(root, "docs/issues", "issue")
designs = collect(root, "docs/design", "design")
adrs = collect(root, "docs/adr", "adr")
aars = collect(root, "docs/aar", "aar")
out: list[str] = []
out.append("# STATUS")
out.append("")
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
out.append("")
open_issues = [i for i in issues
if i[1].get("status") in ("open", "in-progress")]
closed = len(issues) - len(open_issues)
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
out.append("")
if open_issues:
out.append("| Issue | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in open_issues:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none open_")
out.append("")
active = [d for d in designs if d[1].get("status") != "done"]
out.append(f"## Active design docs ({len(active)})")
out.append("")
if active:
out.append("| Design | Gate | Title |")
out.append("|---|---|---|")
for rel, meta, name in active:
out.append(f"| [{Path(rel).stem}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none active_")
out.append("")
out.append(f"## ADRs ({len(adrs)})")
out.append("")
if adrs:
out.append("| ADR | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in adrs:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none_")
out.append("")
open_aars = [a for a in aars if a[1].get("status") == "open"]
out.append(f"## Open AARs ({len(open_aars)})")
out.append("")
if open_aars:
for rel, _meta, name in open_aars:
out.append(f"- [{name}]({rel})")
else:
out.append("_none — nothing awaiting harvest_")
out.append("")
return "\n".join(out)
def main() -> int:
args = [a for a in sys.argv[1:] if a != "--check"]
check = "--check" in sys.argv[1:]
root = Path(args[0]) if args else Path.cwd()
content = render(root)
status = root / "STATUS.md"
if check:
current = status.read_text(encoding="utf-8") if status.is_file() else ""
if current != content:
print("gen_status --check: STATUS.md is stale — "
"run scripts/gen_status.py and commit the result")
return 1
print("gen_status --check: STATUS.md is current")
return 0
status.write_text(content, encoding="utf-8", newline="\n")
print(f"wrote {status}")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""validate.py — deterministic artifact validation against schema.yaml.
Checks (errors, exit 1):
* frontmatter present, parseable, `type` known
* file location and filename match the type's rules
* required fields, enums, patterns, dates
* link fields: repo-root-relative targets exist (http/https/mailto skipped)
* inline markdown links in bodies resolve (relative to the file)
* per-type rules: superseded_requires_pointer, done_iff_in_done_dir
Warnings (exit 0):
* wiki pages (except index) with no inbound link anywhere
Usage: python scripts/validate.py [repo-root]
"""
from __future__ import annotations
import datetime
import fnmatch
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("validate.py needs PyYAML: pip install pyyaml")
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
INLINE_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
HTML_SRC_RE = re.compile(r"(?:src|srcset)=\"([^\"]+)\"")
EXTERNAL_PREFIXES = ("http://", "https://", "mailto:")
errors: list[str] = []
warnings: list[str] = []
def err(path: Path, msg: str) -> None:
errors.append(f"ERROR {path}: {msg}")
def warn(path: Path, msg: str) -> None:
warnings.append(f"WARN {path}: {msg}")
def parse_frontmatter(text: str):
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return None, text
for j in range(1, len(lines)):
if lines[j].strip() == "---":
fm = "\n".join(lines[1:j])
body = "\n".join(lines[j + 1:])
return yaml.safe_load(fm) or {}, body
return None, text # unterminated
def is_date(value) -> bool:
if isinstance(value, datetime.date):
return True
return isinstance(value, str) and bool(DATE_RE.match(value))
def as_links(value):
"""Normalize a link field's value to a list of strings."""
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, list):
return [v for v in value if isinstance(v, str)]
return None # wrong shape
def discover(root: Path, scope: dict) -> list[Path]:
files: set[Path] = set()
for pattern in scope.get("include", []):
files.update(root.glob(pattern))
result = []
for f in sorted(files):
rel = f.relative_to(root).as_posix()
if any(fnmatch.fnmatch(rel, pat) for pat in scope.get("exclude", [])):
continue
if f.is_file():
result.append(f)
return result
def check_fields(path: Path, meta: dict, spec: dict, root: Path) -> None:
for field in spec.get("required", []):
if field not in meta or meta[field] is None:
err(path, f"missing required field '{field}'")
for field, rule in (spec.get("fields") or {}).items():
if field not in meta:
continue
value = meta[field]
if value is None:
if not rule.get("nullable"):
# required-check already covers required fields;
# a present-but-null optional field is fine unless typed link
pass
continue
if "enum" in rule and value not in rule["enum"]:
err(path, f"'{field}: {value}' not in enum {rule['enum']}")
if "pattern" in rule and not re.match(rule["pattern"], str(value)):
err(path, f"'{field}: {value}' does not match {rule['pattern']}")
kind = rule.get("kind")
if kind == "date" and not is_date(value):
err(path, f"'{field}: {value}' is not a YYYY-MM-DD date")
if kind == "bool" and not isinstance(value, bool):
err(path, f"'{field}: {value}' is not a boolean")
if kind == "str" and not isinstance(value, str):
err(path, f"'{field}' must be a string")
def check_links(path: Path, meta: dict, link_fields: list, root: Path,
inbound: set) -> None:
for field in link_fields:
if field not in meta:
continue
links = as_links(meta[field])
if links is None:
err(path, f"'{field}' must be a string or list of strings")
continue
for link in links:
if link.startswith(EXTERNAL_PREFIXES):
continue
target = (root / link)
if not target.is_file():
err(path, f"'{field}' link target missing: {link}")
else:
inbound.add(target.resolve())
def check_body_links(path: Path, body: str, root: Path, inbound: set) -> None:
# strip fenced code blocks and inline code spans so mermaid, code
# samples, and literal link examples in backticks aren't scanned
body = re.sub(r"```.*?```", "", body, flags=re.S)
body = re.sub(r"`[^`\n]*`", "", body)
candidates = [m.group(1) for m in INLINE_LINK_RE.finditer(body)]
for raw in (m.group(1) for m in HTML_SRC_RE.finditer(body)):
# srcset may list "path 2x, path2 1x" pairs — take each path token
for part in raw.split(","):
candidates.append(part.strip().split()[0])
for link in candidates:
if link.startswith(EXTERNAL_PREFIXES) or link.startswith("#"):
continue
link = link.split("#", 1)[0]
if not link:
continue
target = (path.parent / link).resolve()
if not target.is_file():
err(path, f"inline link target missing: {link}")
else:
inbound.add(target)
def apply_rules(path: Path, rel: str, meta: dict, spec: dict) -> None:
for rule in spec.get("rules", []):
if rule == "superseded_requires_pointer":
if meta.get("status") == "superseded" and not meta.get("superseded_by"):
err(path, "status 'superseded' requires 'superseded_by'")
elif rule == "done_iff_in_done_dir":
in_done = "/done/" in f"/{rel}"
if (meta.get("status") == "done") != in_done:
err(path, "status 'done' <-> file in docs/design/done/ mismatch")
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
schema = yaml.safe_load((root / "schema.yaml").read_text(encoding="utf-8"))
link_fields = schema.get("link_fields", [])
types = schema.get("types", {})
inbound: set = set()
wiki_pages: list[tuple[Path, dict]] = []
# Root documents: inline links must resolve; no frontmatter required.
for rel in schema.get("scope", {}).get("link_only", []):
path = root / rel
if not path.is_file():
continue # e.g. STATUS.md before first generation
text = path.read_text(encoding="utf-8")
_meta, body = parse_frontmatter(text)
check_body_links(path, body if _meta is not None else text,
root, inbound)
seen_ids: dict[tuple[str, str], Path] = {}
artifacts = discover(root, schema.get("scope", {}))
for path in artifacts:
rel = path.relative_to(root).as_posix()
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
err(path, "missing or unterminated YAML frontmatter")
continue
if not isinstance(meta, dict) or "type" not in meta:
err(path, "frontmatter has no 'type'")
continue
t = meta["type"]
if t not in types:
err(path, f"unknown type '{t}'")
continue
spec = types[t]
expected_dir = spec.get("dir", ".")
actual_dir = str(Path(rel).parent.as_posix())
if expected_dir == ".":
if actual_dir != ".":
err(path, f"type '{t}' must live in repo root")
elif not (actual_dir == expected_dir
or actual_dir.startswith(expected_dir + "/")):
err(path, f"type '{t}' must live under {expected_dir}/")
fn_pattern = spec.get("filename")
if fn_pattern and not re.match(fn_pattern, path.name):
err(path, f"filename does not match {fn_pattern}")
check_fields(path, meta, spec, root)
if "id" in (spec.get("fields") or {}) and meta.get("id") is not None:
artifact_id = str(meta["id"])
if not path.name.startswith(f"{artifact_id}-"):
err(path, f"id '{artifact_id}' does not match filename prefix")
key = (t, artifact_id)
if key in seen_ids:
err(path, f"duplicate {t} id '{artifact_id}' "
f"(also in {seen_ids[key].name})")
else:
seen_ids[key] = path
check_links(path, meta, link_fields, root, inbound)
check_body_links(path, body, root, inbound)
apply_rules(path, rel, meta, spec)
if t == "wiki-page" and meta.get("area") != "index":
wiki_pages.append((path, meta))
# link-only files: inline links are checked, frontmatter not required
already = {p.resolve() for p in artifacts}
for pattern in schema.get("scope", {}).get("link_only", []):
for path in sorted(root.glob(pattern)):
if not path.is_file() or path.resolve() in already:
continue
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
body = path.read_text(encoding="utf-8")
check_body_links(path, body, root, inbound)
for path, _meta in wiki_pages:
if path.resolve() not in inbound:
warn(path, "orphan wiki page — nothing links to it")
for line in errors + warnings:
print(line)
print(f"validate: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,34 @@
---
type: aar
status: open # open | harvested
date: YYYY-MM-DD
related: [] # design docs, issues, ADRs involved
---
<!-- Copy to docs/aar/YYYY-MM-DD-slug.md. Delete comments when filling in.
Standalone AARs are for incidents and major deviations only —
normal undertakings get their AAR as Gate 5 inside the design doc. -->
# AAR: Title
## What was planned / expected
## What happened
<!-- Facts and timeline, not blame. -->
## Why the difference
<!-- Root cause. For failures, name the routing class:
intent issue / spec issue / code issue. -->
## Learnings
<!-- What future-you should know. Blunt beats polite. -->
## Actions
<!-- Concrete: wiki pages updated (FAQ, Stolpersteine) with links,
framework issues opened, tests added. When all actions are done,
set status: harvested. The refinement session walks all AARs
still marked open. -->
@@ -0,0 +1,37 @@
---
type: adr
id: "0000"
status: proposed # proposed | accepted | superseded
date: YYYY-MM-DD
supersedes: null # path to older ADR, e.g. docs/adr/0002-old.md
superseded_by: null # filled in on the OLD adr when a new one replaces it
related: [] # optional: paths to design docs / issues
---
<!-- Copy to docs/adr/NNNN-slug.md. Delete all comments when filling in. -->
# ADR-0000: Title
## Context
<!-- The situation and the forces at play. Constraints upfront:
deadlines, scale, team knowledge, existing decisions. -->
## Options Considered
<!-- Name each option, even the one you lean toward. Pros/cons per
option; a small dimension table (complexity, cost, maintenance,
familiarity) where it helps. Keep proportional to the decision. -->
## Decision
<!-- The choice, in one or two sentences. -->
## Consequences
<!-- What becomes easier, what becomes harder, what we will need to
revisit. Honest cons included. -->
<!-- Rules: an accepted ADR is never edited — write a new ADR that
supersedes it and set superseded_by here. Lasting directional
decisions only; feature-local choices belong in the design doc. -->
@@ -0,0 +1,110 @@
---
type: design
status: gate-1 # gate-1 | gate-2 | gate-3 | gate-4 | gate-5 | done
date: YYYY-MM-DD
size: L # this template is for size L
related: [] # issues, ADRs spawned or read
---
<!-- Copy to docs/design/YYYY-MM-DD-slug.md. Delete comments when filling in.
Fill ONE gate at a time; each gate ends with STOP — do not pre-fill
later gates. Advance `status` only after human approval. -->
# Design: Title
## Gate 1 — Product
**Problem.** <!-- What user problem, for whom. -->
**Acceptance criterion.** <!-- Verifiable. A real number where one
exists; otherwise a concretely checkable outcome. "Works" is not one. -->
**Non-goals.** <!-- What this deliberately does NOT do. The cheapest
scope-creep brake there is. -->
**Announcement.** <!-- 35 sentences: what it is, who it's for, why
it's good. Can't write it? The product isn't understood yet. -->
**Mockups.** <!-- Only if UI is involved: plain-HTML mockups, linked. -->
> **STOP — awaiting Gate 1 approval.**
## Gate 2 — Architecture
**Inputs read.** <!-- Which ADRs and AARs were read; one line each on
why they matter here. -->
**System fit.** <!-- Endpoints, tables/schemas, query outlines,
end-to-end flow as Mermaid. Against the actual codebase. -->
**Constraints.** <!-- Non-functional, proportional to the project:
performance, security, operations, compatibility. "None relevant"
is a valid answer — but say it. -->
**Options & trade-offs.** <!-- Where more than one viable way exists:
name the options, pro/contra each, state the chosen one and WHY.
This is the feature-local decision record. Only lasting, binding
decisions graduate to an ADR below. -->
**New ADRs.** <!-- Lasting decisions discovered here → one ADR each,
linked. None is a valid answer. -->
> **STOP — awaiting Gate 2 approval.**
## Gate 3 — Program Design
**Files.** <!-- Exact paths, new and touched. -->
**Signatures.** <!-- Types and method signatures, no bodies. -->
**Call stack.** <!-- For the main flow(s). -->
**Test assertions.** <!-- What the tests will assert. -->
**Boundaries — DO NOT CHANGE.** <!-- Explicit list. -->
**Shakiest calls.** <!-- The decisions you are least confident about. -->
> **STOP — awaiting Gate 3 approval.**
## Gate 4 — Vertical Slices
<!-- Slice 1 is the tracer bullet: thin end-to-end, runs with mocks.
Then real logic, one testable slice at a time. Per task:
files / action / verify / done. After each slice: evidence,
status, STOP. -->
### Slice 1 — Tracer bullet
- [ ] Task: … — files: … — action: … — verify: … — done: …
**Evidence:** <!-- command output, test run, screenshot ref -->
**Status:** <!-- DONE | DONE_WITH_CONCERNS | NEEDS_CONTEXT | BLOCKED -->
> **STOP — slice review.**
### Slice 2 — …
### Handoff
<!-- The single place session state lives. Overwrite on every handoff;
git keeps the history.
Done slices: …
Open decisions: …
Next step: … -->
## Gate 5 — Closeout (AAR)
**Planned vs. actual.** <!-- What was planned, what happened. -->
**Why the difference.** <!-- Root causes, honestly. -->
**Learnings.** <!-- What future-you should know. -->
**Harvested.** <!-- Wiki pages updated (FAQ, Stolpersteine, …) with
links; framework issues opened, if a rule was missing or wrong. -->
**Open uncertainties.** <!-- Session-handoff answers to: "Which choices
did I make that I'm least confident about?" -->
<!-- After approval: set status: done, move this file to
docs/design/done/, run gen_status.py. -->
@@ -0,0 +1,29 @@
---
type: issue
id: "0000"
status: open # open | in-progress | done | rejected
created: YYYY-MM-DD
related: [] # design docs, ADRs, other issues
---
<!-- Copy to docs/issues/NNNN-slug.md. Delete comments when filling in. -->
# Issue-0000: Title
## Problem / Motivation
<!-- What's wrong or missing, and why it matters. One paragraph. -->
## Acceptance
<!-- When is this issue done? Verifiable, like every other criterion
in this framework. -->
## Notes
<!-- Optional: context, links, findings gathered along the way.
Rules: status is the single source of truth and lives here in the
frontmatter — STATUS.md is generated, never edited. An issue that
starts real work links its design doc in `related`. Closed means
status: done (or rejected, with a one-line reason in Notes) —
the file stays; git is the history. -->
+143
View File
@@ -0,0 +1,143 @@
# schema.yaml — single source of truth for artifact frontmatter.
# Stage 1 of ADR-0004: scripts/validate.py checks generically against this
# file. Extending the framework's metadata means editing THIS file, not code.
# Agents: never invent fields or status values; propose a schema change.
#
# PROJEKTERWEITERUNGEN gegenüber neckbeard v0.1.1 (Original:
# docs/sources/upstream/neckbeard-v0.1.1/schema.yaml; Design:
# docs/design/2026-08-11-neckbeard-migration.md, ADR-0012/0013):
# * issue: Pflichtfelder milestone (M1M5) + priority; Status-Enum um
# next/waiting erweitert; due/host/area/wartegrund/gitlab_iid;
# Regel waiting_requires_reason; globale Regel wip_limit (max. 2
# in-progress) in validate.py.
# * component: neuer Typ unter docs/components/ (Dateiname = Slug).
# * wiki-page: Area-Enum um "vision" erweitert.
version: 1
scope:
# Files considered artifacts. Templates and raw sources are exempt.
include:
- "PROJECT.md"
- "docs/**/*.md"
exclude:
- "**/template.md"
- "docs/sources/**"
- "vendor/**"
# Files whose inline links are checked, but which need no frontmatter
# (root-level prose: README, AGENTS, WORKFLOW, generated STATUS, ...).
link_only:
- "*.md"
# Frontmatter fields whose values are links. Values starting with
# http://, https:// or mailto: are treated as external and only
# format-checked; everything else must be a repo-root-relative path
# to an existing file.
link_fields: [related, sources, supersedes, superseded_by]
types:
project:
dir: "."
filename: "^PROJECT\\.md$"
required: [type, language, size_s_exception, purpose, audience]
fields:
language: { enum: [de, en] }
size_s_exception: { kind: bool }
purpose: { kind: str }
audience: { kind: str }
adr:
dir: "docs/adr"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, date]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [proposed, accepted, superseded] }
date: { kind: date }
supersedes: { kind: link, nullable: true }
superseded_by: { kind: link, nullable: true }
related: { kind: links }
rules:
# status: superseded requires superseded_by to point at the successor.
- superseded_requires_pointer
design:
dir: "docs/design"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date, size]
fields:
status: { enum: [gate-1, gate-2, gate-3, gate-4, gate-5, done] }
size: { enum: [L] }
date: { kind: date }
related: { kind: links }
rules:
# status: done if and only if the file lives under docs/design/done/.
- done_iff_in_done_dir
aar:
dir: "docs/aar"
filename: "^\\d{4}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md$"
required: [type, status, date]
fields:
status: { enum: [open, harvested] }
date: { kind: date }
related: { kind: links }
issue:
dir: "docs/issues"
filename: "^\\d{4}-[a-z0-9-]+\\.md$"
required: [type, id, status, created, milestone, priority]
fields:
id: { pattern: "^\\d{4}$" }
status: { enum: [open, next, in-progress, waiting, done, rejected] }
created: { kind: date }
milestone: { enum: [M1, M2, M3, M4, M5] }
priority: { enum: [high, medium, low] }
due: { kind: date, nullable: true }
host: { enum: [cfgmon, overmind, matrix, game], nullable: true }
area: { enum: [security, infrastructure, database, element], nullable: true }
wartegrund: { kind: str, nullable: true }
gitlab_iid: { pattern: "^\\d+$", nullable: true }
related: { kind: links }
rules:
# status: waiting requires a named reason (old rule: "nur mit
# benanntem Grund").
- waiting_requires_reason
component:
dir: "docs/components"
filename: "^[A-Za-z0-9.-]+\\.md$"
required: [type, slug, anzeigename, phase]
fields:
slug: { kind: str }
anzeigename: { kind: str }
phase: { enum: [active, staged, external] }
gitlab: { kind: str }
mirror: { kind: str, nullable: true }
related: { kind: links }
rules:
# The canonical slug is the filename — no second naming scheme.
- slug_matches_filename
wiki-page:
dir: "docs/wiki"
filename: "^[a-z0-9-]+\\.md$"
required: [type, area]
fields:
area:
enum:
- index
- architecture
- admin
- deployment
- user-guide
- requirements
- faq
- stolpersteine
- vision
sources: { kind: links }
related: { kind: links }
rules:
# Pages other than the index should be linked from somewhere
# (reported as WARNING, not error — see validate.py).
- warn_if_orphan
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""gen_status.py — generate STATUS.md deterministically from frontmatter.
PROJEKTERWEITERUNG gegenüber neckbeard v0.1.1 (Original unter
docs/sources/upstream/neckbeard-v0.1.1/scripts/): offene Issues sind
alles außer done/rejected (Status-Enum ist projektweit erweitert);
Tabelle zeigt Meilenstein/Priorität; Verteilungszeile je Meilenstein
ersetzt die früheren Hand-Zählungen der roadmap.md (F-001/F-010).
Writes STATUS.md (no timestamps output depends only on repo content, so
reruns are diff-clean). With --check, regenerates in memory and fails if
the committed STATUS.md is stale; CI uses this mode.
Usage:
python scripts/gen_status.py [repo-root] # write STATUS.md
python scripts/gen_status.py --check [repo-root] # verify, exit 1 if stale
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("gen_status.py needs PyYAML: pip install pyyaml")
H1_RE = re.compile(r"^#\s+(.*)$", re.M)
def parse(path: Path):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "---":
return None, ""
for j in range(1, len(lines)):
if lines[j].strip() == "---":
meta = yaml.safe_load("\n".join(lines[1:j])) or {}
body = "\n".join(lines[j + 1:])
return meta, body
return None, ""
def title(body: str, fallback: str) -> str:
match = H1_RE.search(body)
return match.group(1).strip() if match else fallback
def collect(root: Path, subdir: str, wanted_type: str):
items = []
base = root / subdir
if not base.is_dir():
return items
for path in sorted(base.rglob("*.md")):
if path.name == "template.md":
continue
meta, body = parse(path)
if not isinstance(meta, dict) or meta.get("type") != wanted_type:
continue
rel = path.relative_to(root).as_posix()
items.append((rel, meta, title(body, path.stem)))
return items
def render(root: Path) -> str:
issues = collect(root, "docs/issues", "issue")
designs = collect(root, "docs/design", "design")
adrs = collect(root, "docs/adr", "adr")
aars = collect(root, "docs/aar", "aar")
out: list[str] = []
out.append("# STATUS")
out.append("")
out.append("<!-- Generated by scripts/gen_status.py — do not edit. -->")
out.append("")
open_issues = [i for i in issues
if i[1].get("status") not in ("done", "rejected")]
closed = len(issues) - len(open_issues)
out.append(f"## Issues ({len(open_issues)} open, {closed} closed)")
out.append("")
if open_issues:
dist: dict[str, int] = {}
for _rel, meta, _name in open_issues:
m = str(meta.get("milestone", "?"))
dist[m] = dist.get(m, 0) + 1
out.append("Verteilung: " + " · ".join(
f"{m} {n}" for m, n in sorted(dist.items())))
out.append("")
out.append("| Issue | Status | Meilenstein | Priorität | Title |")
out.append("|---|---|---|---|---|")
for rel, meta, name in open_issues:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {meta.get('milestone')} "
f"| {meta.get('priority')} | {name} |")
else:
out.append("_none open_")
out.append("")
active = [d for d in designs if d[1].get("status") != "done"]
out.append(f"## Active design docs ({len(active)})")
out.append("")
if active:
out.append("| Design | Gate | Title |")
out.append("|---|---|---|")
for rel, meta, name in active:
out.append(f"| [{Path(rel).stem}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none active_")
out.append("")
out.append(f"## ADRs ({len(adrs)})")
out.append("")
if adrs:
out.append("| ADR | Status | Title |")
out.append("|---|---|---|")
for rel, meta, name in adrs:
out.append(f"| [{meta.get('id', '?')}]({rel}) "
f"| {meta.get('status')} | {name} |")
else:
out.append("_none_")
out.append("")
open_aars = [a for a in aars if a[1].get("status") == "open"]
out.append(f"## Open AARs ({len(open_aars)})")
out.append("")
if open_aars:
for rel, _meta, name in open_aars:
out.append(f"- [{name}]({rel})")
else:
out.append("_none — nothing awaiting harvest_")
out.append("")
return "\n".join(out)
def main() -> int:
args = [a for a in sys.argv[1:] if a != "--check"]
check = "--check" in sys.argv[1:]
root = Path(args[0]) if args else Path.cwd()
content = render(root)
status = root / "STATUS.md"
if check:
current = status.read_text(encoding="utf-8") if status.is_file() else ""
if current != content:
print("gen_status --check: STATUS.md is stale — "
"run scripts/gen_status.py and commit the result")
return 1
print("gen_status --check: STATUS.md is current")
return 0
status.write_text(content, encoding="utf-8", newline="\n")
print(f"wrote {status}")
return 0
if __name__ == "__main__":
sys.exit(main())
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""pruefe_upstream_drift.py — Byte-Vergleich gegen die gepinnte Baseline.
Schützt die übernommenen Framework-Dateien vor stillem Umschreiben
(Entscheidung 8 im Design 2026-08-11, Frage von sorb: wird die
AGENTS.md ggf. durch Agenten umgeschrieben?"). Die Baseline liegt unter
docs/sources/upstream/neckbeard-v0.1.1/ (siehe HERKUNFT.md dort); ein
Framework-Upgrade aktualisiert Baseline und Arbeitskopie im selben,
bewussten Commit.
Prüfungen (Fehler, Exit 1):
* byte-identische Paare laut PAARE
* AGENTS.md beginnt byte-identisch mit der Baseline-AGENTS.md und
trägt direkt danach die Marke des Projektabschnitts
Fehlt eine Baseline-Datei, ist das ein Fehler, kein Skip
(Stillstandsprüfungs-Regel: eine Prüfung ohne Gegenseite ist ungeprüft).
Usage: python scripts/pruefe_upstream_drift.py [repo-root]
"""
from __future__ import annotations
import sys
from pathlib import Path
BASELINE = "docs/sources/upstream/neckbeard-v0.1.1"
MARKE = "<!-- projektabschnitt -->"
# (Arbeitskopie, Baseline-Datei) — byte-identisch
PAARE = [
("CLAUDE.md", "CLAUDE.md"),
("WORKFLOW.md", "WORKFLOW.md"),
("docs/adr/template.md", "templates/adr-template.md"),
("docs/design/template.md", "templates/design-template.md"),
("docs/aar/template.md", "templates/aar-template.md"),
("docs/issues/template.md", "templates/issue-template.md"),
]
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
base = root / BASELINE
fehler: list[str] = []
for arbeit, original in PAARE:
a, b = root / arbeit, base / original
if not b.is_file():
fehler.append(f"Baseline fehlt: {BASELINE}/{original}")
continue
if not a.is_file():
fehler.append(f"Arbeitskopie fehlt: {arbeit}")
continue
if a.read_bytes() != b.read_bytes():
fehler.append(f"DRIFT: {arbeit} weicht von {BASELINE}/{original} ab")
agents, agents_base = root / "AGENTS.md", base / "AGENTS.md"
if not agents_base.is_file():
fehler.append(f"Baseline fehlt: {BASELINE}/AGENTS.md")
elif not agents.is_file():
fehler.append("Arbeitskopie fehlt: AGENTS.md")
else:
upstream = agents_base.read_bytes()
arbeit = agents.read_bytes()
if not arbeit.startswith(upstream):
fehler.append("DRIFT: AGENTS.md — Upstream-Teil (§15) ist "
"nicht mehr byte-identisch mit der Baseline")
else:
rest = arbeit[len(upstream):].decode("utf-8", "replace")
if MARKE not in rest.splitlines()[0:3]:
fehler.append(f"AGENTS.md: Marke '{MARKE}' fehlt direkt "
"nach dem Upstream-Teil")
for f in fehler:
print(f"FEHLER {f}")
print(f"pruefe_upstream_drift: {len(fehler)} Fehler")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main())
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env python3
"""validate.py — deterministic artifact validation against schema.yaml.
PROJEKTERWEITERUNG gegenüber neckbeard v0.1.1 (Original unter
docs/sources/upstream/neckbeard-v0.1.1/scripts/): drei Regeln
waiting_requires_reason, slug_matches_filename (je Typ) und das globale
WIP-Limit (max. 2 Issues in-progress, altes ADR-0005/F-014).
Checks (errors, exit 1):
* frontmatter present, parseable, `type` known
* file location and filename match the type's rules
* required fields, enums, patterns, dates
* link fields: repo-root-relative targets exist (http/https/mailto skipped)
* inline markdown links in bodies resolve (relative to the file)
* per-type rules: superseded_requires_pointer, done_iff_in_done_dir
Warnings (exit 0):
* wiki pages (except index) with no inbound link anywhere
Usage: python scripts/validate.py [repo-root]
"""
from __future__ import annotations
import datetime
import fnmatch
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError: # pragma: no cover
sys.exit("validate.py needs PyYAML: pip install pyyaml")
DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
INLINE_LINK_RE = re.compile(r"\]\(([^)\s]+)\)")
HTML_SRC_RE = re.compile(r"(?:src|srcset)=\"([^\"]+)\"")
EXTERNAL_PREFIXES = ("http://", "https://", "mailto:")
errors: list[str] = []
warnings: list[str] = []
def err(path: Path, msg: str) -> None:
errors.append(f"ERROR {path}: {msg}")
def warn(path: Path, msg: str) -> None:
warnings.append(f"WARN {path}: {msg}")
def parse_frontmatter(text: str):
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return None, text
for j in range(1, len(lines)):
if lines[j].strip() == "---":
fm = "\n".join(lines[1:j])
body = "\n".join(lines[j + 1:])
return yaml.safe_load(fm) or {}, body
return None, text # unterminated
def is_date(value) -> bool:
if isinstance(value, datetime.date):
return True
return isinstance(value, str) and bool(DATE_RE.match(value))
def as_links(value):
"""Normalize a link field's value to a list of strings."""
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, list):
return [v for v in value if isinstance(v, str)]
return None # wrong shape
def discover(root: Path, scope: dict) -> list[Path]:
files: set[Path] = set()
for pattern in scope.get("include", []):
files.update(root.glob(pattern))
result = []
for f in sorted(files):
rel = f.relative_to(root).as_posix()
if any(fnmatch.fnmatch(rel, pat) for pat in scope.get("exclude", [])):
continue
if f.is_file():
result.append(f)
return result
def check_fields(path: Path, meta: dict, spec: dict, root: Path) -> None:
for field in spec.get("required", []):
if field not in meta or meta[field] is None:
err(path, f"missing required field '{field}'")
for field, rule in (spec.get("fields") or {}).items():
if field not in meta:
continue
value = meta[field]
if value is None:
if not rule.get("nullable"):
# required-check already covers required fields;
# a present-but-null optional field is fine unless typed link
pass
continue
if "enum" in rule and value not in rule["enum"]:
err(path, f"'{field}: {value}' not in enum {rule['enum']}")
if "pattern" in rule and not re.match(rule["pattern"], str(value)):
err(path, f"'{field}: {value}' does not match {rule['pattern']}")
kind = rule.get("kind")
if kind == "date" and not is_date(value):
err(path, f"'{field}: {value}' is not a YYYY-MM-DD date")
if kind == "bool" and not isinstance(value, bool):
err(path, f"'{field}: {value}' is not a boolean")
if kind == "str" and not isinstance(value, str):
err(path, f"'{field}' must be a string")
def check_links(path: Path, meta: dict, link_fields: list, root: Path,
inbound: set) -> None:
for field in link_fields:
if field not in meta:
continue
links = as_links(meta[field])
if links is None:
err(path, f"'{field}' must be a string or list of strings")
continue
for link in links:
if link.startswith(EXTERNAL_PREFIXES):
continue
target = (root / link)
if not target.is_file():
err(path, f"'{field}' link target missing: {link}")
else:
inbound.add(target.resolve())
def check_body_links(path: Path, body: str, root: Path, inbound: set) -> None:
# strip fenced code blocks and inline code spans so mermaid, code
# samples, and literal link examples in backticks aren't scanned
body = re.sub(r"```.*?```", "", body, flags=re.S)
body = re.sub(r"`[^`\n]*`", "", body)
candidates = [m.group(1) for m in INLINE_LINK_RE.finditer(body)]
for raw in (m.group(1) for m in HTML_SRC_RE.finditer(body)):
# srcset may list "path 2x, path2 1x" pairs — take each path token
for part in raw.split(","):
candidates.append(part.strip().split()[0])
for link in candidates:
if link.startswith(EXTERNAL_PREFIXES) or link.startswith("#"):
continue
link = link.split("#", 1)[0]
if not link:
continue
target = (path.parent / link).resolve()
if not target.is_file():
err(path, f"inline link target missing: {link}")
else:
inbound.add(target)
def apply_rules(path: Path, rel: str, meta: dict, spec: dict) -> None:
for rule in spec.get("rules", []):
if rule == "superseded_requires_pointer":
if meta.get("status") == "superseded" and not meta.get("superseded_by"):
err(path, "status 'superseded' requires 'superseded_by'")
elif rule == "waiting_requires_reason":
if meta.get("status") == "waiting" and not meta.get("wartegrund"):
err(path, "status 'waiting' requires 'wartegrund'")
elif rule == "slug_matches_filename":
if meta.get("slug") is not None and str(meta["slug"]) != path.stem:
err(path, f"slug '{meta['slug']}' does not match filename")
elif rule == "done_iff_in_done_dir":
in_done = "/done/" in f"/{rel}"
if (meta.get("status") == "done") != in_done:
err(path, "status 'done' <-> file in docs/design/done/ mismatch")
def main() -> int:
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
schema = yaml.safe_load((root / "schema.yaml").read_text(encoding="utf-8"))
link_fields = schema.get("link_fields", [])
types = schema.get("types", {})
inbound: set = set()
wiki_pages: list[tuple[Path, dict]] = []
# Root documents: inline links must resolve; no frontmatter required.
for rel in schema.get("scope", {}).get("link_only", []):
path = root / rel
if not path.is_file():
continue # e.g. STATUS.md before first generation
text = path.read_text(encoding="utf-8")
_meta, body = parse_frontmatter(text)
check_body_links(path, body if _meta is not None else text,
root, inbound)
seen_ids: dict[tuple[str, str], Path] = {}
in_progress: list[Path] = []
artifacts = discover(root, schema.get("scope", {}))
for path in artifacts:
rel = path.relative_to(root).as_posix()
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
err(path, "missing or unterminated YAML frontmatter")
continue
if not isinstance(meta, dict) or "type" not in meta:
err(path, "frontmatter has no 'type'")
continue
t = meta["type"]
if t not in types:
err(path, f"unknown type '{t}'")
continue
spec = types[t]
expected_dir = spec.get("dir", ".")
actual_dir = str(Path(rel).parent.as_posix())
if expected_dir == ".":
if actual_dir != ".":
err(path, f"type '{t}' must live in repo root")
elif not (actual_dir == expected_dir
or actual_dir.startswith(expected_dir + "/")):
err(path, f"type '{t}' must live under {expected_dir}/")
fn_pattern = spec.get("filename")
if fn_pattern and not re.match(fn_pattern, path.name):
err(path, f"filename does not match {fn_pattern}")
check_fields(path, meta, spec, root)
if "id" in (spec.get("fields") or {}) and meta.get("id") is not None:
artifact_id = str(meta["id"])
if not path.name.startswith(f"{artifact_id}-"):
err(path, f"id '{artifact_id}' does not match filename prefix")
key = (t, artifact_id)
if key in seen_ids:
err(path, f"duplicate {t} id '{artifact_id}' "
f"(also in {seen_ids[key].name})")
else:
seen_ids[key] = path
check_links(path, meta, link_fields, root, inbound)
check_body_links(path, body, root, inbound)
apply_rules(path, rel, meta, spec)
if t == "wiki-page" and meta.get("area") != "index":
wiki_pages.append((path, meta))
if t == "issue" and meta.get("status") == "in-progress":
in_progress.append(path)
# link-only files: inline links are checked, frontmatter not required
already = {p.resolve() for p in artifacts}
for pattern in schema.get("scope", {}).get("link_only", []):
for path in sorted(root.glob(pattern)):
if not path.is_file() or path.resolve() in already:
continue
meta, body = parse_frontmatter(path.read_text(encoding="utf-8"))
if meta is None:
body = path.read_text(encoding="utf-8")
check_body_links(path, body, root, inbound)
if len(in_progress) > 2:
names = ", ".join(p.name for p in in_progress)
err(root / "docs/issues", f"WIP limit exceeded: "
f"{len(in_progress)} issues in-progress (max 2): {names}")
for path, _meta in wiki_pages:
if path.resolve() not in inbound:
warn(path, "orphan wiki page — nothing links to it")
for line in errors + warnings:
print(line)
print(f"validate: {len(errors)} error(s), {len(warnings)} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())