Nutzer-FYI: Node.js einzuführen ist eine Dimension schwerer als eine Backend-Abhängigkeit und muss dem Entwickler deutlich gemacht werden — als Regel in CLAUDE.md. Aus den nachgefragten Beweggründen folgt: kein lokaler Prozess. Der MCP-Server lebt im Backend (Streamable HTTP, Bearer-Token), führt die Frontend-Module per GraalJS aus, jedes Werkzeug hat eine text- und eine document-Form; das Node-Paket ist verworfen, seine Tool-Schicht lebt als frontend/src/inspect.js, guard.js, edit.js. Backend-Abhängigkeiten GraalJS + Spring-AI-Starter zugestimmt; der GraalJS-Spike steht vor allem anderen. RFC neu geschrieben (§12 hält die drei Runden fest), Plan-Teilbaum #ai.mcp neu, D93-Nachtrag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
637 lines
40 KiB
Markdown
637 lines
40 KiB
Markdown
# RFC 001 — MCP-Server: Werkbaum für KI-Agenten außerhalb des Editors
|
||
|
||
| | |
|
||
|---|---|
|
||
| Status | **Abgestimmt** (drei Runden am 2026-09-02, Entscheidungen in §11 und D93) — nichts gebaut |
|
||
| Plan-Knoten | `#ai.mcp` in `docs/examples/werkbaum.werkbaum` |
|
||
| Entscheidung | D93 in `docs/DECISIONS.md` |
|
||
| Berührt | Backend (`de.werkbaum.integration.mcp`, Security, Build), `frontend/src/*` (headless-Module: eine Verschiebung, drei neue Dateien), `tools/pull-doc`, README, `.mcp.json` |
|
||
| Berührt nicht | Notation (SPEC), `llms.md`, `frontend/src/app.js` im Verhalten, Deploy-Skripte außer einer Umgebungszeile |
|
||
| Neue Technologie | **keine** — die JVM bleibt die einzige Laufzeit (§6, Runde 3) |
|
||
|
||
## 1. Zusammenfassung
|
||
|
||
Werkbaum bekommt einen **MCP-Server** (Model Context Protocol) **im
|
||
Backend**: Unter `/api/v1/mcp` bietet die Spring-Boot-Anwendung KI-Agenten
|
||
außerhalb des Editors — Claude Code, OpenCode, Codex, IDE-Agenten,
|
||
gehostete Agenten — einen Werkbaum-Plan als **Ressource** (Text) und als
|
||
**Werkzeuge** (Baum, Warnungen, günstigster Pfad, Knoten-Verben,
|
||
Schreiben) an, über Streamable HTTP mit einem Bearer-Token.
|
||
|
||
Das Backend **parst nicht selbst**: Es führt die headless-Module des
|
||
Frontends (`parser.js`, `model.js`, `live.js` und drei neue) **unverändert
|
||
in der JVM aus** — per GraalJS, gebündelt beim Bauen. Damit bleibt D14
|
||
gewahrt (genau ein Parser), und ein Agent bekommt wörtlich dieselben
|
||
Warnungen, effektiven Status und Stationen, die das Diagramm zeigt.
|
||
|
||
Zwei Arten von Plänen, ein Satz Werkzeuge: **Server-Dokumente** bearbeiten
|
||
die Tools direkt — als Zeilen-Diff gegen Basisversion und Prüfsumme (D76),
|
||
Konflikte werden abgelehnt, jede Änderung steht in der Historie mit dem
|
||
Namen des Agenten. **Lokale `.werkbaum`-Dateien** liest und schreibt der
|
||
Agent mit seinen eigenen Werkzeugen und reicht den **Text** an dieselben
|
||
Tools — zur Analyse, zur Prüfung und für die Knoten-Verben, die dann Text
|
||
zurückgeben. Es gibt **keinen lokalen Prozess** und **keine neue
|
||
Laufzeit**: kein Node-Paket, nichts zu installieren beim Entwickler, nur
|
||
eine URL und ein Token in der Host-Konfiguration.
|
||
|
||
Zwei Leitplanken: **keine beschädigte Notation** und **kein `[^]` von einem
|
||
Agenten** — auf Server-Dokumenten als Sperre, auf Text als Prüfergebnis.
|
||
Der Git-Spiegel eines geteilten Plans ist das vorhandene
|
||
`tools/pull-doc --git-commit` (D88), das künftig nennt, wer welche Version
|
||
geändert hat — vom Cron oder vom Agenten selbst aufgerufen.
|
||
|
||
Das ist die **Gegenrichtung** zu `#ai.dialog`: Dort käme ein Modell in den
|
||
Editor, hier geht der Plan zu Agenten, die schon irgendwo laufen. Beides
|
||
teilt sich den Boden (`llms.md`, D43) und dieselben Module — die drei
|
||
neuen Dateien in `frontend/src` sind genau die, die `#ai.dialog` später
|
||
auch braucht.
|
||
|
||
## 2. Motivation
|
||
|
||
Die Beweggründe, wie sie in Runde 3 benannt wurden (§11): Ein oder mehrere
|
||
**Entwickler arbeiten parallel an geteilten Werkbaum-Plänen** und setzen
|
||
dabei **agentische KI** ein — vor allem, um einen Plan **im Dialog zu
|
||
erstellen** und den **Feature-Fortschritt zu tracken**. Der Agent läuft
|
||
meist lokal beim Entwickler (OpenCode, Claude Code, Codex); später soll
|
||
ein Agent auch in das Werkbaum-Frontend kommen (`#ai.dialog`). Die Pläne
|
||
liegen **beides**: als Datei neben dem Code und als geteiltes
|
||
Server-Dokument.
|
||
|
||
Heute kann ein Agent damit dreierlei tun — und alles davon nur halb:
|
||
|
||
1. **Eine `.werkbaum`-Datei lesen und schreiben.** Das geht mit Claude
|
||
Code bereits, `llms.md` erklärt die Notation (D43). Aber der Agent muss
|
||
die **Semantik** selbst nachrechnen: Ist der Plan warnungsfrei? Welcher
|
||
Knoten ist effektiv wie weit (§4)? Was ist die nächste Station auf dem
|
||
günstigsten Pfad (§9)? Für all das gibt es im Frontend geprüfte
|
||
Funktionen — der Agent bekommt sie nicht zu sehen und rät. Beim
|
||
Größen-Konflikt (D62) oder der Closure-Rechnung (D42) ist Raten
|
||
nachweislich falsch.
|
||
2. **Ein Server-Dokument (`?live=`) anfassen.** Nur per `curl` gegen die
|
||
REST-API — mit Volltext-`PUT`, also ohne das Zeilen-Diff-Protokoll
|
||
(D76). Ein Agent, der so schreibt, überschreibt, was ein Mensch in der
|
||
Zwischenzeit getippt hat; genau der Verlust, gegen den D89 vier Netze
|
||
gespannt hat.
|
||
3. **Den Plan im Editor ändern lassen** — `#ai.dialog` ist eine Idee ohne
|
||
Entwurf.
|
||
|
||
MCP ist inzwischen die Verkehrssprache dafür: Claude Code, OpenCode,
|
||
Codex, Claude Desktop, IDE-Agenten und die Anthropic-API selbst
|
||
(MCP-Connector) sprechen es. Ein Server, der Werkbaum darüber anbietet,
|
||
macht Punkt 1 und 2 richtig — für **alle** diese Hosts, nicht nur für
|
||
einen.
|
||
|
||
## 3. Ziele und Nicht-Ziele
|
||
|
||
**Ziele**
|
||
|
||
- Ein Agent kann einen Plan **lesen** (Server-Dokument per Ressource; eine
|
||
Datei liest er selbst) und den Notationsleitfaden dazu bekommen.
|
||
- Ein Agent kann einen Plan **befragen**, ohne die SPEC nachzuimplementieren:
|
||
Baum mit allem, was das Diagramm weiß; Warnungen; günstigster Pfad samt
|
||
Stationen und Personen-Last — für einen Text wie für ein Dokument.
|
||
- Ein Agent kann einen Plan **ändern**, ohne fremde Arbeit zu
|
||
überschreiben; jede Änderung an einem Dokument ist als Agenten-Änderung
|
||
erkennbar.
|
||
- Kein zweiter Parser, keine zweite Semantik (D14). **Keine neue
|
||
Laufzeit** (D93-Nachtrag, CLAUDE.md).
|
||
- Kein Geheimnis läuft durch den Server, das er nicht braucht.
|
||
|
||
**Nicht-Ziele (Abgrenzung — wird nicht gebaut, siehe §9)**
|
||
|
||
- **Kein LLM im Server.** Der Server ruft kein Modell auf und kennt keinen
|
||
API-Schlüssel. Das Modell sitzt im Host; `#ai.key` bleibt ein eigener
|
||
Knoten.
|
||
- **Keine neue Notation.** Nichts an SPEC oder `llms.md` ändert sich.
|
||
- **Kein lokaler Prozess.** Kein Node-Paket, kein stdio-Server, kein
|
||
Jar-Aufruf beim Entwickler (§6).
|
||
- **Kein Taiga über MCP** in der ersten Fassung (§9).
|
||
- **Keine Rechteverwaltung** — ein Token je Installation; das
|
||
Dokument-Zugriffsmodell (unerratbare UUID, D76) bleibt.
|
||
- **Kein Ersatz für `#ai.dialog`** und kein Chat im Editor.
|
||
|
||
## 4. Begriffe (soweit hier nötig)
|
||
|
||
MCP trennt **Hosts** (die Anwendung mit dem Modell) von **Servern**
|
||
(Prozesse oder Endpunkte, die Fähigkeiten anbieten). Ein Server bietet
|
||
drei Dinge an:
|
||
|
||
- **Resources** — lesbare Inhalte mit URI, vom Host in den Kontext geholt
|
||
(„zeig mir den Plan“).
|
||
- **Tools** — aufrufbare Funktionen mit JSON-Schema, die das Modell selbst
|
||
wählt („prüfe den Plan“, „setze den Status“).
|
||
- **Prompts** — vorgefertigte Anweisungen, die der Nutzer auswählt
|
||
(„zerlege diesen Knoten“).
|
||
|
||
Transporte: **stdio** (der Host startet den Server als Kindprozess) und
|
||
**Streamable HTTP** (ein Netzwerk-Endpunkt; hat 2025 das ältere
|
||
SSE-Verfahren abgelöst). MCP verlangt **keine bestimmte Sprache** — es
|
||
gibt offizielle SDKs für TypeScript, Python, Kotlin, Java, C#; Werkbaum
|
||
nimmt den Spring-AI-Starter für Java (§11). Die Protokollrevision trägt
|
||
das SDK; dieses RFC legt keine fest.
|
||
|
||
## 5. Vorschlag
|
||
|
||
### 5.1 Architektur
|
||
|
||
```
|
||
Host beim Entwickler Werkbaum-Backend (Spring Boot, JDK 21)
|
||
(Claude Code, OpenCode, Codex, …) ┌───────────────────────────────────────┐
|
||
┌──────────────┐ Streamable HTTP + Bearer │ /api/v1/mcp │
|
||
│ Modell │◄────────────────────────────►│ de.werkbaum.integration.mcp │
|
||
│ + Werkzeuge │ (Apache reicht /api/ durch) │ ├─ resources (Guide, Dokumente) │
|
||
│ + eigene │ │ ├─ tools (inspect, check, │
|
||
│ Datei- │ │ │ write, verbs) │
|
||
│ Zugriffe │ │ └─ WerkbaumCore: GraalJS-Kontext │
|
||
└──────────────┘ │ führt werkbaum-core.mjs aus — │
|
||
│ liest/schreibt │ das Bündel aus frontend/src: │
|
||
▼ .werkbaum-Dateien selbst │ parser · model · live · │
|
||
Repo des Entwicklers │ inspect · guard · edit │
|
||
(Git-Spiegel: tools/pull-doc) │ DocumentService / LiveEditingService │
|
||
└───────────────────────────────────────┘
|
||
```
|
||
|
||
Es gibt **einen** Dienst und **ein** Artefakt: das Backend-Jar. Neu darin
|
||
sind ein Paket, ein Bearer-Filter und ein gebündeltes JS-Modul als
|
||
Ressource. Alles Entscheidbare kommt aus `frontend/src`; Kotlin ist
|
||
Transport, Zugang, Ablage und die Brücke in die JVM.
|
||
|
||
### 5.2 Zwei Formen je Werkzeug: Text und Dokument
|
||
|
||
Weil es keinen lokalen Prozess gibt, unterscheidet der Server nicht nach
|
||
Ablage, sondern nach **Eingabeform**. Jedes Werkzeug nimmt **entweder**
|
||
`text` (der Agent hat die Datei selbst gelesen) **oder** `document` (die
|
||
UUID eines Dokuments **dieses** Backends):
|
||
|
||
| Form | Lesen | Ergebnis eines Schreib-Werkzeugs | Leitplanken (§5.5) |
|
||
|---|---|---|---|
|
||
| `text` | der Agent liefert den Text mit | der **neue Text** samt Prüfergebnis — der Agent schreibt ihn selbst in die Datei | **Prüfergebnis**: `findings` mit `blocking: true/false`; der Text wird trotzdem zurückgegeben |
|
||
| `document` | `DocumentService` | der neue Stand (`version`, `checksum`) — geschrieben per `PATCH /content`-Weg (D76) | **Sperre**: blockierende Befunde ⇒ nicht geschrieben |
|
||
|
||
`document` ist eine **UUID, keine URL**: Der Server fasst nur seine eigenen
|
||
Dokumente an. Fremde Backends gibt es für ihn nicht — das nimmt die
|
||
SSRF-Frage aus dem Entwurf, bevor sie gestellt ist, und ein Agent, der
|
||
mit zwei Werkbaum-Servern arbeitet, konfiguriert zwei MCP-Server.
|
||
|
||
Der `?live=`-Link, den Menschen weitergeben, enthält die UUID; ein Agent
|
||
bekommt sie vom Menschen oder aus der Ressourcen-Liste (§5.3).
|
||
|
||
### 5.3 Resources
|
||
|
||
| URI | Inhalt | MIME |
|
||
|---|---|---|
|
||
| `werkbaum://guide` | `llms.md` — der Notationsleitfaden (D43) | `text/markdown` |
|
||
| `werkbaum://doc/<uuid>` | Notationstext des Dokuments; `version` und `checksum` als Metadaten | `text/plain` |
|
||
|
||
Die Ressourcen-**Liste** nennt die Dokumente dieses Backends mit Titel
|
||
und UUID — für den Agenten, der fragt „welche Pläne gibt es hier?“. Das
|
||
ist dieselbe Auskunft wie `GET /documents`, die heute hinter dem
|
||
Master-Passwort steht (D76-Nachtrag 6), und sie steht hier hinter dem
|
||
Bearer-Token (§5.6): Wer das Token hat, ist Entwickler dieser
|
||
Installation, nicht ein Fremder mit einer erratenen Adresse. Das
|
||
Master-Passwort selbst geht **nie** über MCP.
|
||
|
||
Resources sind bewusst **nur der Text**: Er ist das führende Format (D14),
|
||
und ein Agent, der ihn im Kontext hat, kann mit `llms.md` schon alles
|
||
lesen. Die Semantik kommt aus den Tools.
|
||
|
||
### 5.4 Tools
|
||
|
||
Wenige Verben, sprechend benannt; jedes nimmt `text` **oder** `document`
|
||
(§5.2), die Dokument-Form beim Schreiben zusätzlich `base`
|
||
(`{version, checksum}` vom letzten Lesen — Pflicht):
|
||
|
||
| Tool | Eingabe | Ausgabe | Nutzt |
|
||
|---|---|---|---|
|
||
| `werkbaum_read` | `document` | Text, `version`, `checksum`, Zeilenzahl | `DocumentService` |
|
||
| `werkbaum_inspect` | `text`\|`document`, optional `id` (Teilbaum), `include: [tree, warnings, path]` | JSON: Baum je Knoten mit `id`, `label`, `line`, `gate`, `optional`, `status`, `effectiveStatus`, `size`, `assumedSize`, `tags`, `deps`, `desc`, `taigaSlug`, `ticketRef`; Warnungen als `{type, line, …}` **plus** englischer Klartext; Pfad als nötige Knoten, Stationen in Dokumentreihenfolge, `assigneeLoads`, `overloadedAssignee`, `exact` (D42) | `parse`, `effectiveStatus`, `computeCheapPlan`, `assigneeLoads`, `taigaSlugs`, `warningText`; `inspect.js` |
|
||
| `werkbaum_check` | `text`\|`document`, optional `against` (Vorher-Text) | `findings`: Warnungs-Delta und Leitplanken-Befunde mit `blocking` — das, was ein Schreib-Werkzeug prüfen würde, als Auskunft. Für Agenten, die eine Datei gleich selbst schreiben | `guard.js` |
|
||
| `werkbaum_set_status` | …, `id`, `status` (Code der §4-Tabelle) | Text-Form: neuer Text + `findings`; Dokument-Form: neuer Stand + `findings` | `setStatusBox` (parser.js) |
|
||
| `werkbaum_apply_ops` | …, `ops[]` (`insert`/`replace`/`delete` mit `index`, `count`, `lines` — **dasselbe Schema wie `LineOperation` der API**) | wie oben | `applyOps` (live.js) |
|
||
| `werkbaum_write` | …, `newText` (Volltext) | wie oben; Dokument-Form: Diff per `computeOps` | `computeOps` (live.js) |
|
||
| `werkbaum_add_node` | …, `parent` (ID), `line` (Knotenzeile ohne Einrückung), optional `after` (Geschwister-ID) | wie oben | `addNodeLine` (edit.js) |
|
||
| `werkbaum_move_node` | …, `id`, `parent`, optional `after` | wie oben; Teilbaum samt Beschreibungs- und Fortsetzungszeilen wandert mit, Einrückung wird angepasst | `moveSubtree` (edit.js) |
|
||
| `werkbaum_set_size` | …, `id`, `size` (`XS`…`XXL` oder `null`) | wie oben | `setSizeToken` (edit.js) |
|
||
| `werkbaum_remove_node` | …, `id`, `discard: true\|false` | wie oben — `discard` setzt `[-]` (§4), `false` löscht den Teilbaum samt Blöcken | `setStatusBox` bzw. `removeSubtree` (edit.js) |
|
||
|
||
Die **Knoten-Verben** (Runde 1, §11) sind Bequemlichkeit für das Modell:
|
||
`apply_ops` könnte jedes davon ausdrücken, aber ein Agent, der
|
||
Zeilenindizes ausrechnet, verrechnet sich — ein Verb mit ID ist die
|
||
robustere Schnittstelle. Damit dabei **keine zweite Stelle mit
|
||
Zeilenformat-Wissen** entsteht, liegen die Text→Text-Regeln als
|
||
`frontend/src/edit.js` neben `setStatusBox`/`setFoldMark` (parser.js) und
|
||
`appendToken` (taiga.js) — headless, getestet, und für `#ai.dialog` schon
|
||
da (§7.1). Kotlin verdrahtet sie nur.
|
||
|
||
**Warnungen tragen Typ und Text.** Der Typ (`sizeConflict`, `unknownDep`,
|
||
…) ist für den Agenten das Verlässliche; der englische Text kommt dazu,
|
||
weil das Modell ihn ohne Nachschlagen versteht. Die Texte sind die
|
||
**Editor-Texte** (`warningText`, D33-Nachtrag) in Englisch — nicht
|
||
nacherzählt (§7.1).
|
||
|
||
### 5.5 Leitplanken
|
||
|
||
Auf jedem Schreib-Werkzeug, vor dem Schreiben (Dokument) bzw. als Befund
|
||
(Text):
|
||
|
||
1. **Keine beschädigte Notation.** Der Ergebnis-Text wird geparst; trägt
|
||
er **Struktur-Warnungen**, die der Ausgangstext nicht hatte —
|
||
`mixedGate`, `unknownStatus`, `descStray`, `duplicateId` —, ist das
|
||
**blockierend**: Ein Dokument wird nicht geschrieben, die Antwort nennt
|
||
sie; ein Text kommt mit `blocking: true` zurück. Das sind die Warnungen,
|
||
bei denen der Parser eine Zeile anders liest, als sie gemeint war.
|
||
Schalter `allowWarnings: true` je Aufruf erlaubt es ausdrücklich.
|
||
**Inhaltliche Widersprüche** — `sizeConflict`, `assigneeOverload`,
|
||
`xorConflict`, `unknownDep`, `unknownDesc` — werden **geschrieben und
|
||
gemeldet**: Ein Agent arbeitet in Zwischenschritten (das Ziel einer
|
||
Abhängigkeit entsteht oft erst im nächsten Zug), und im Diagramm sieht
|
||
ein Mensch sie als bernsteinfarbene Marke — wie bei einer eigenen
|
||
Änderung (Runde 1; die strengere Fassung war der Vorschlag).
|
||
Verschwindende Warnungen sind immer erlaubt, jede Antwort trägt das
|
||
Warnungs-Delta.
|
||
2. **Kein `[^]` von einem Agenten.** `in Produktion` ist die Aussage eines
|
||
Deploys (D30), nicht die eines Modells. Blockierend wie oben; Schalter
|
||
`allowProd: true` je Aufruf — ausdrücklich, nie still.
|
||
3. **Konflikt ist ein Fehler, keine Entscheidung.** Bei veralteter `base`
|
||
(Dokument) kommt der **aktuelle** Text mit zurück; der Agent — und im
|
||
Zweifel der Mensch hinter ihm — entscheidet. Der Server rebased nichts
|
||
über das hinaus, was der `LiveEditingService` ohnehin tut (D76: nicht
|
||
überlappende Ops verschiebt er; das bleibt). Für Dateien gilt dasselbe
|
||
auf der Seite des Agenten: Er hat den Text gelesen und schreibt ihn
|
||
selbst — `werkbaum_check` mit `against` sagt ihm vorher, ob sein
|
||
Ergebnis Befunde hat.
|
||
|
||
Der Preis der Text-Form ist benannt und war Teil der Entscheidung
|
||
(Runde 3): Bei Dateien ist die Leitplanke **Auskunft**, keine Sperre. Ein
|
||
Agent, der `blocking: true` ignoriert und die Datei trotzdem schreibt,
|
||
hat den Fehler dann wenigstens im Transkript stehen — und der nächste
|
||
`inspect` meldet ihn wieder.
|
||
|
||
### 5.6 Zugang und Identität
|
||
|
||
**Zugang: ein statisches Bearer-Token je Installation**
|
||
(`WERKBAUM_MCP_TOKEN`, in der Umgebungsdatei des Dienstes wie das
|
||
Master-Passwort, D77), geprüft von einem Spring-Security-Filter für
|
||
`/api/v1/mcp/**`; ohne Token 401. Hosts geben es als Header mit
|
||
(Claude Code: `claude mcp add --transport http werkbaum <url> --header
|
||
"Authorization: Bearer …"`; der API-MCP-Connector: `authorization_token`).
|
||
Das Token reist über HTTPS in der Host-Konfiguration, nie im Transkript.
|
||
**OAuth 2.1**, das die MCP-Spezifikation für HTTP vorsieht, kommt, sobald
|
||
es einen Identity Provider gibt — die Taiga-Instanz wechselt auf OIDC
|
||
(D91-Nachtrag 1), das wäre der naheliegende Anker; ohne einen
|
||
Authorization Server wäre OAuth ein Bau weit über den Anlass hinaus
|
||
(Runde 2). Ohne konfiguriertes Token ist der Endpunkt **gesperrt, nicht
|
||
offen** — dieselbe Voreinstellung wie beim Master-Passwort
|
||
(D76-Nachtrag 6), und `GET /info` meldet `mcp: false`.
|
||
|
||
**Identität:** Jeder Schreibvorgang an ein Dokument geht mit `clientId`
|
||
`mcp-<sitzung>` (laufende `seq` je Sitzung, D76-Nachtrag 7) und einem
|
||
`displayName`, der den **Host nennt** — den Namen aus dem MCP-Handshake
|
||
(`clientInfo.name`, etwa „Claude Code“), Rückfall „Agent“ (Runde 1). In
|
||
jedem offenen Editor steht damit „geändert von: Claude Code“ (D86); der
|
||
Name bleibt eine Behauptung, kein Nachweis — wie beim Anzeigenamen eines
|
||
Menschen. Das Token ist zugleich die Schicht, über die später das
|
||
**Owner-Passwort** (`#col.live.owner`) kommt: je Dokument eine Zuordnung
|
||
in der Umgebung des Dienstes, **nie** als Tool-Parameter.
|
||
|
||
### 5.7 Geteiltes Dokument **und** Git — beides zugleich
|
||
|
||
Ein Agent soll auf dem **geteilten** Dokument arbeiten (dort sitzen die
|
||
Menschen, dort greift das Konfliktprotokoll), und derselbe Plan soll
|
||
**unter Git-Kontrolle** stehen (Diff, Blame, Review, ein Netz außerhalb
|
||
des Servers — D89). Die Antwort ist eine **Richtung**: Der Server ist die
|
||
Quelle, Git ist das Archiv.
|
||
|
||
**Der Weg dafür existiert:** `tools/pull-doc --git-commit` (D88,
|
||
`#col.git.pull`) holt ein Server-Dokument in ein Worktree und committet es
|
||
datiert, nur bei Änderung. Zwei Aufrufer:
|
||
|
||
- **Der Cron** auf dem Server-Host oder beim Entwickler — archiviert alle
|
||
Änderungen, egal von wem.
|
||
- **Der Agent selbst**, nach seinem Schreibvorgang: Er läuft lokal beim
|
||
Entwickler und hat eine Shell; `pull-doc --git-commit --with-history
|
||
<uuid> <datei>` ist ein Befehl wie jeder andere. Ohne lokalen
|
||
MCP-Prozess (Runde 3) ist das der Weg, den Spiegel **sofort** nach der
|
||
eigenen Änderung zu ziehen; der Cron fängt den Rest. Das Prompt
|
||
`werkbaum/decompose` (§5.8) endet mit genau diesem Hinweis.
|
||
|
||
**Wer steht im Commit?** Ein Spiegel-Commit enthält den **ganzen**
|
||
Serverstand — die Zeilen des Agenten und alles, was Menschen seit dem
|
||
letzten Commit geändert haben. Die Form der Urheber-Angabe entscheidet,
|
||
ob `git blame` später lügt:
|
||
|
||
| Form | Aussage | Preis |
|
||
|---|---|---|
|
||
| a) `git commit --author="Claude Code <…>"` | Der Agent ist Git-**Autor** — `blame` schreibt ihm **jede** Zeile darin zu, auch die, die Anna getippt hat | Falsche Blame-Auskunft; Git verlangt eine E-Mail-Adresse, die es nicht gibt |
|
||
| b) `Co-Authored-By:`-Trailer | Die Repo-Konvention für Claude-Commits: „hat beigetragen“, ohne Autor zu sein | Braucht ebenfalls `Name <email>`; für Menschen aus der Server-Historie gibt es nur den Anzeigenamen |
|
||
| c) Freitext im Betreff (`… (Version 8, via Claude Code)`) | Lesbar im `git log --oneline` | Nicht maschinenlesbar; nennt nur den Auslöser |
|
||
| **d) Historie im Rumpf + eigene Trailer** | Der Rumpf listet aus `GET /history`, **wer welche Version** seit dem letzten Spiegel-Commit geändert hat („v7 Anna · v8 Claude Code“); je Beteiligtem `Werkbaum-Changed-By: <Anzeigename>`, dazu `Werkbaum-Version: 8` | Ein zweiter `GET` je Commit; eigene Trailer statt der GitHub-Konvention |
|
||
|
||
**Entschieden (Runde 2): d, mit c im Betreff.** Git-Autor bleibt, wer den
|
||
Prozess betreibt (buchstäblich richtig — er hat committet), die
|
||
Beteiligten stehen mit Namen im Rumpf, maschinenlesbar per
|
||
`git interpret-trailers`, und `Werkbaum-Version:` gibt `pull-doc` beim
|
||
nächsten Lauf die Untergrenze für „seit dem letzten Commit“. Der
|
||
Cron-Commit wird damit im selben Zug ehrlicher. Der Schalter
|
||
(`--with-history`) kommt ins Skript (D77-Nachtrag). Die Anzeigenamen
|
||
bleiben Behauptungen (D86) — deshalb „Changed-By“, nicht „Author“.
|
||
|
||
### 5.8 Prompts (optional, Phase 5)
|
||
|
||
Zwei vorgefertigte Anweisungen, die ein Host dem Nutzer anbietet:
|
||
|
||
- `werkbaum/review` — „Prüfe diesen Plan: Warnungen, Größen, fehlende
|
||
Zerlegung ab M, unklare Zuständigkeiten“ (nutzt `inspect`, schreibt
|
||
nichts).
|
||
- `werkbaum/decompose` — „Zerlege Knoten `#id` in Teilpakete mit Größen“
|
||
(endet in den Verben, mit den Leitplanken; bei einem Dokument mit dem
|
||
Hinweis auf `pull-doc`).
|
||
|
||
Beides sind Texte, keine Logik; sie leben im Backend-Paket und nicht in
|
||
`llms.md` (das bleibt Notation, D43).
|
||
|
||
### 5.9 Konfiguration
|
||
|
||
Serverseitig (Umgebungsdatei des Dienstes, D77):
|
||
|
||
- `WERKBAUM_MCP_TOKEN` — das Bearer-Token; fehlt es, ist der Endpunkt
|
||
gesperrt.
|
||
- Kein weiterer Schalter: Es gibt keine Dateiwurzeln (kein Dateizugriff),
|
||
keinen Spiegel (§5.7 läuft beim Aufrufer), keine Fremdserver (§5.2).
|
||
|
||
Beim Entwickler: die URL und das Token in der Host-Konfiguration. Im
|
||
Repository liegt eine `.mcp.json`, die auf `${WERKBAUM_MCP_URL}` und
|
||
`${WERKBAUM_MCP_TOKEN}` aus der Umgebung zeigt — damit Claude Code hier
|
||
den Server von selbst hat, gegen die stabile Instanz oder gegen ein
|
||
lokal gestartetes Backend (`./gradlew bootRun`; der Entwickler-Rechner
|
||
hat das JDK ohnehin). Der Plan `docs/examples/werkbaum.werkbaum` ist damit
|
||
das erste Dokument, an dem der Server benutzt wird (Dogfooding, D27).
|
||
|
||
### 5.10 Die JVM führt die JS-Module aus
|
||
|
||
Der Kern der Entscheidung (Runden 2 und 3): Das Backend braucht die
|
||
Werkbaum-Logik, darf sie nicht nachbauen (D14) und soll keine zweite
|
||
Laufzeit neben sich haben. Also führt es die Module **selbst** aus —
|
||
dieselbe Technik, die der Plan für das IDE-Plugin vorsieht
|
||
(`#idea.drift.js`: „den einen JS-Parser im IDE laufen lassen“).
|
||
|
||
- **GraalJS** (`org.graalvm.polyglot:polyglot` + `js-community`) läuft auf
|
||
dem Stock-OpenJDK 21 der Zielumgebung (D77) im Interpreter-Modus — ohne
|
||
Graal-Compiler langsamer, aber ein Plan hat Dutzende Kilobyte, keine
|
||
Megabyte; zu messen, nicht anzunehmen.
|
||
- **Gebündelt beim Bauen:** Gradle ruft das vorhandene Frontend-Tooling
|
||
(esbuild, das Vite mitbringt) und bündelt `frontend/src/inspect.js`
|
||
samt allem, was es importiert, zu **einem** ES-Modul
|
||
`werkbaum-core.mjs` in den Jar-Ressourcen. So braucht GraalJS keine
|
||
Import-Auflösung und kein virtuelles Dateisystem; Node bleibt, was es
|
||
ist — Build-Werkzeug, nie Laufzeit. Der Build bricht, wenn das Bündel
|
||
fehlt: dieselbe Zusage wie bei der OpenAPI-Generierung.
|
||
- **Was in der JVM anders ist:** `crypto.subtle` (die Prüfsumme in
|
||
`live.js`) gibt es in GraalJS nicht — die Prüfsumme rechnet Kotlin
|
||
(`MessageDigest`), `checksum()` wird dort nicht aufgerufen; alles
|
||
übrige (`TextEncoder`, `Map`, `Set`, Unicode-Regex) ist ECMAScript und
|
||
vorhanden. Ein Polyglot-`Context` ist **nicht nebenläufig**: ein kleiner
|
||
Pool von Kontexten mit gecachter `Source`, je Aufruf einer — die
|
||
Kosten je Kontext (Speicher, Aufbauzeit) sind Teil des Spikes.
|
||
- **Der Spike vor Phase 1** misst: Laufen `parser.js`, `model.js`,
|
||
`live.js`, `inspect.js`, `guard.js`, `edit.js` als Bündel in GraalJS
|
||
und liefern für den mitgelieferten Plan dieselben Zahlen wie Vitest
|
||
(231 Knoten, 0 Warnungen, Stationen)? Was kostet ein Kontext an RSS auf
|
||
der Zielumgebung (D76-Nachtrag 3: rund 300 MB frei)? Reicht der Apache
|
||
den SSE-Strom langer Werkzeugaufrufe ungepuffert durch (Long Polling ist
|
||
gemessen, D76-Nachtrag 2 — ein Strom nicht)? Die Ergebnisse gehören als
|
||
Nachtrag zu D93. **Fällt der Spike durch**, ist die Antwort nicht Node,
|
||
sondern eine neue Frage an den Entwickler (CLAUDE.md-Regel).
|
||
|
||
## 6. Alternativen
|
||
|
||
### A — Kotlin-Server mit eigenem Parser
|
||
|
||
*Pro:* keine JS-Ausführung in der JVM.
|
||
*Contra:* **genau die zweite Grammatik, die D14 verbietet** — und die
|
||
Drift-Frage, die `#idea.drift` für das IDE-Plugin schon stellt.
|
||
**Verworfen** (`#ai.mcp.kotlin` als `[-]`).
|
||
|
||
### B — Node-Paket, das die Frontend-Module importiert (Entwurf der Runden 1–2)
|
||
|
||
*Pro:* ein Parser; stdio ist der Transport, den alle lokalen Hosts
|
||
können; Dateien und Git-Spiegel direkt am Prozess.
|
||
*Contra:* **Node.js würde Laufzeit** — beim Entwickler als Paket, auf dem
|
||
Server als zweiter Dienst. Bisher ist Node im Repo Build-Werkzeug. Runde 3
|
||
hat das als das erkannt, was es ist: die Einführung einer Technologie,
|
||
eine Dimension schwerer als eine Abhängigkeit (CLAUDE.md, D93-Nachtrag).
|
||
Und die Beweggründe brauchten es nicht: Der Text darf zum eigenen Server.
|
||
**Verworfen** (`#ai.mcp.node` als `[-]`); die Tool-Schicht, die dafür
|
||
geplant war, wandert unverändert nach `frontend/src`.
|
||
|
||
### C — Das Backend-Jar als lokaler stdio-Server (`java -jar … --mcp-stdio`)
|
||
|
||
*Pro:* keine neue Laufzeit, Dateien und Spiegel lokal.
|
||
*Contra:* verlangt ein JDK beim Entwickler und startet in Sekunden; und
|
||
sobald der Text ohnehin zum Server darf, ist der lokale Prozess ohne
|
||
Anlass. **Nicht gewählt**, bleibt als Rückfall benannt, falls die
|
||
Text-Form sich als unpraktisch erweist — sie kostete dann keinen
|
||
Technologie-Wechsel.
|
||
|
||
### D — Nur im Backend, Text-Form für Dateien (gewählt)
|
||
|
||
*Pro:* eine Laufzeit, ein Artefakt, ein Deployment; die vorhandene
|
||
Zugangsschicht; nichts zu installieren beim Entwickler; die Module bleiben
|
||
einmal vorhanden.
|
||
*Contra:* Netz auch für lokale Dateien; die Leitplanken sind dort Auskunft
|
||
statt Sperre; der GraalJS-Spike steht vor allem anderen.
|
||
**Gewählt** (Runde 3).
|
||
|
||
### E — Nichts bauen: Agenten lesen `llms.md` und editieren Dateien
|
||
|
||
*Pro:* funktioniert heute mit Claude Code.
|
||
*Contra:* keine Semantik, keine Server-Dokumente außer per `curl`,
|
||
Schreiben ohne Konfliktschutz — der Ausgangszustand aus §2.
|
||
**Bleibt als Rückfall bestehen** — der Server ersetzt `llms.md` nicht.
|
||
|
||
### F — OpenAPI der REST-API den Agenten geben
|
||
|
||
*Contra:* dieselbe Semantik-Lücke wie E, und die Prüfsumme müsste das
|
||
Modell selbst rechnen. **Verworfen.**
|
||
|
||
### G — `#ai.dialog` zuerst (Modell im Editor)
|
||
|
||
**Nicht konkurrierend** — D liefert die Module, die G später braucht.
|
||
|
||
## 7. Impact auf den bestehenden Code
|
||
|
||
### 7.1 `frontend/src/` — eine Verschiebung, drei neue Dateien
|
||
|
||
| Datei | Was | Warum |
|
||
|---|---|---|
|
||
| `app.js` → neu `i18n.js` | Das `I18N`-Objekt (ab `app.js:3287`) in ein headless-Modul verschieben; `app.js` importiert es. | Der Server braucht `warningText(w, t)` mit **englischem** `t`. Eine Kopie im Backend wäre die Drift, vor der D33-Nachtrag warnt. Reine Verschiebung; der Vite-Bundle bleibt gleich. |
|
||
| **neu** `inspect.js` | Baum → JSON (`id`, `label`, `line`, `status`, `effectiveStatus`, `assumedSize`, …), Warnungen → `{type, line, text}`, Pfad → Stationen und Lasten. Das Einstiegsmodul des Bündels. | Die Sicht für Agenten — und für `#ai.dialog`. Headless, Vitest-getestet gegen SPEC §10 und den mitgelieferten Plan. |
|
||
| **neu** `guard.js` | Leitplanken (§5.5): Warnungs-Delta nach Klasse (strukturell/inhaltlich), `[^]`-Befund; Text → `findings`. | Eine Stelle, die weiß, was blockiert. |
|
||
| **neu** `edit.js` | Text→Text-Regeln der Knoten-Verben: `addNodeLine`, `moveSubtree`, `setSizeToken`, `removeSubtree` — zeilenweise, zeichengenau, mit Fortsetzungs- und Beschreibungszeilen (§1), Kommentare bleiben stehen. | Die eine Stelle für „Zeile eines Knotens umbauen“; bewusst nicht in `parser.js` (Sammelbecken). |
|
||
| `parser.js`, `model.js`, `live.js`, `taiga.js`, `warnings.js`, `render.js` | **unverändert** | Alles Nötige ist exportiert; `render.js` ist headless bis zum Aufruf (`esc`). |
|
||
|
||
Die drei neuen Dateien sind reine ES-Module ohne Browser- und ohne
|
||
Node-APIs — die Bedingung dafür, dass sie in GraalJS laufen, und ohnehin
|
||
die Hausregel (D54-Nachtrag 3).
|
||
|
||
### 7.2 Backend
|
||
|
||
| Ort | Was |
|
||
|---|---|
|
||
| `build.gradle.kts` | Abhängigkeiten `spring-ai-starter-mcp-server-webmvc` und `org.graalvm.polyglot:polyglot` + `js-community` (Runde 3, Rückfrage gestellt und beantwortet — die Boot-4-Verträglichkeit des Starters ist beim Bauen zu prüfen; fällt sie durch, das MCP-Java-SDK direkt mit seinem WebMVC-Transport). Eine Gradle-Task `bundleWerkbaumCore`, die per `npx esbuild --bundle --format=esm` aus `frontend/src/inspect.js` die Ressource `werkbaum-core.mjs` erzeugt; `processResources` hängt davon ab. |
|
||
| **neu** `de.werkbaum.integration.mcp` | `WerkbaumCore` (GraalJS-Kontext-Pool, gecachte `Source`, Aufrufe `inspect/check/edit` als Kotlin-Funktionen mit JSON hinein und heraus), `McpServerConfiguration` (Resources, Tools, Prompts am Starter registriert), `McpTools` (die Verben aus §5.4, je mit Text- und Dokument-Form; Dokument-Form über `DocumentService`/`LiveEditingService` — derselbe Weg wie `PATCH /content`, mit Prüfsumme aus `MessageDigest`). |
|
||
| `api/SecurityConfiguration.kt` | Bearer-Filter für `/api/v1/mcp/**` gegen `werkbaum.mcp.token`; ohne Wert `denyAll` (D76-Nachtrag 6). |
|
||
| `api/DocumentsController.kt` (`getInfo`) | `mcp: true/false` |
|
||
| `openapi/api.yaml` | `ServiceInfo.mcp`; der MCP-Endpunkt selbst steht **nicht** in der OpenAPI — er spricht JSON-RPC, nicht REST, und der Starter trägt seinen Vertrag selbst (dieselbe Grenze wie beim Long-Polling-Endpunkt, D76-Nachtrag 5: was der Generator nicht ausdrücken kann, bleibt draußen — hier ohne Verlust, denn die Tool-Schemata prüft das SDK) |
|
||
| `application.yaml` | `werkbaum.mcp.token: ${WERKBAUM_MCP_TOKEN:}` |
|
||
| `scripts/deploy-backend.sh` | zieht `WERKBAUM_MCP_TOKEN` aus der `.env` idempotent in die Server-Umgebung — dieselbe Zeile wie für `TAIGA_API_URL` (D91-Nachtrag 4) |
|
||
| `scripts/prod.htaccess` | **unverändert** — `/api/` ist proxied; der SSE-Strom ist zu messen (§5.10) |
|
||
| Tests | `WerkbaumCoreTest` (Bündel läuft, dieselben Zahlen wie Vitest), Cucumber-Szenarien über einen MCP-Client gegen den laufenden Kontext (Token fehlt ⇒ 401; `set_status` auf Dokument mit veralteter `base` ⇒ Konflikt; `[^]` ⇒ abgelehnt; `sizeConflict` ⇒ geschrieben und gemeldet) |
|
||
|
||
Perspektivisch berührt: `#col.live.owner` — das Owner-Passwort kommt
|
||
über die Umgebung des Dienstes (§5.6), die Endpunkte sind so zu schneiden,
|
||
dass die Prüfung dazukommt, ohne die Signatur zu brechen (backend/CLAUDE.md).
|
||
|
||
### 7.3 Übriges
|
||
|
||
| Ort | Änderung |
|
||
|---|---|
|
||
| `tools/pull-doc` | `--with-history` (§5.7): holt `GET /history`, listet im Rumpf, wer welche Version seit dem letzten Spiegel-Commit geändert hat, setzt die Trailer `Werkbaum-Changed-By:` und `Werkbaum-Version:`; der Schalter kommt ins Skript, nicht in den Aufrufer (D77-Nachtrag) |
|
||
| `.mcp.json` (Repo-Wurzel) | HTTP-Server-Eintrag mit URL und Token aus der Umgebung (Dogfooding) |
|
||
| `README.md` / `README.de.md` | Abschnitt „Für KI-Agenten: der MCP-Server“ — URL, Token, die zwei Formen, die Leitplanken, `pull-doc` |
|
||
| `frontend/public/llms.txt` | ein Link auf den Endpunkt (Wegweiser, D43-Nachtrag 2); `llms.md` **unverändert** |
|
||
| `docs/CHANGELOG.md` | je Phase eine Zeile |
|
||
| `tools/remote` | **unverändert** — es ist derselbe Dienst |
|
||
| `frontend/package.json` | keine neue Abhängigkeit: esbuild bringt Vite mit |
|
||
|
||
## 8. Sicherheit
|
||
|
||
- **Plan-Text ist Fremddaten.** Beschreibungen (`---`-Teil) können
|
||
Anweisungen enthalten („ignoriere die Leitplanken“). Der Server führt
|
||
nichts aus, was im Text steht — GraalJS läuft **ohne** Host-Zugriff
|
||
(kein `HostAccess`, kein Dateisystem, kein Netz im Kontext); der Text
|
||
ist Eingabe einer reinen Funktion. Resources sind als Inhalt deklariert,
|
||
nie als Anweisung. Der Host trägt die Prompt-Injection-Frage — der
|
||
Server macht sie nur nicht schlimmer.
|
||
- **Kein Geheimnis im Transkript.** Master-Passwort und Taiga-Token gehen
|
||
**nie** über MCP; das Bearer-Token steht in der Host-Konfiguration. Ein
|
||
späteres Owner-Passwort kommt als Umgebungsvariable (§5.6).
|
||
- **Kein Dateizugriff, keine Fremdserver.** Der Server liest keine Pfade
|
||
und ruft keine URLs — `document` ist eine UUID des eigenen Backends
|
||
(§5.2). Die einzige Netzverbindung des MCP-Endpunkts ist der Aufrufer.
|
||
- **Rechenkosten gedeckelt:** Textgröße je Aufruf begrenzt (dasselbe
|
||
Limit wie `PATCH /content`, D76), Kontext-Pool begrenzt, ein
|
||
Werkzeugaufruf mit Timeout — ein hängender Kontext wird verworfen, nicht
|
||
wiederverwendet.
|
||
- **Schreiben nur mit `base`** auf Dokumenten; kein Volltext-`PUT`. Was
|
||
der Editor an Netzen hat (Konflikt-Band, Rettungs-Sicherung, D89), hat
|
||
der Agent in Form des zurückgelieferten aktuellen Textes.
|
||
|
||
## 9. Abgrenzung — was ausdrücklich nicht gebaut wird
|
||
|
||
| Nicht gebaut | Warum | Wo es hingehört |
|
||
|---|---|---|
|
||
| Modellaufrufe, API-Schlüssel, Provider-Wahl | Der Host hat das Modell; ein Server mit eigenem Schlüssel wäre `#ai.key` unter falschem Namen | `#ai.key`, `#ai.dialog` |
|
||
| Ein lokaler MCP-Prozess (Node-Paket oder Jar-Modus) | Neue Laufzeit bzw. ohne Anlass, sobald der Text zum Server darf (Runde 3) | `#ai.mcp.node` (`[-]`); Jar-Modus als benannter Rückfall (§6 C) |
|
||
| Dateizugriff des Servers | kein Nutzer-Dateisystem am Dienst; ein Pfad über HTTP wäre die Einladung, die §8 verbietet | der Agent liest und schreibt selbst |
|
||
| Fremde Backends per URL | SSRF; ein Server, ein Dokumentenbestand | zwei MCP-Server konfigurieren |
|
||
| Taiga-Tools (Ticket anlegen, Status schreiben) | Braucht das Taiga-Token — das lebt im Browser (D91) und dürfte nicht durch ein Transkript; außerdem eine eigene Entscheidung, ob ein Modell Tickets anlegen darf | eigener Nachtrag: über den vorhandenen Proxy, Token aus der Umgebung |
|
||
| Anlegen/Löschen/Umbenennen von Dokumenten | Verwaltung; wartet auf `#col.live.owner` | nach dem Owner-Passwort |
|
||
| Falten (`fold`/`unfold`) als Verb | Faltmarken sind Darstellung für Menschen (§9); ein Agent liest den ganzen Text | — |
|
||
| Änderungsfeed / Long Polling über MCP | Ein Agent arbeitet in Zügen; `read` vor jedem Schreiben genügt | — |
|
||
| Git als **Quelle** (Server folgt dem Repo) | Umkehrung von §5.7; Konfliktmarker mitten in der Notation | `#col.git.pr`, `#col.git.auto` |
|
||
| OAuth 2.1 | kein Identity Provider; Bearer-Token bis dahin | `#ai.mcp.auth`, wenn ein IdP da ist |
|
||
| Ein Kotlin-Parser | D14 | `#ai.mcp.kotlin` (`[-]`) |
|
||
|
||
## 10. Umsetzungsreihenfolge
|
||
|
||
Jede Phase ist für sich abgeschlossen und bekommt ihre CHANGELOG-Zeile;
|
||
Plan-Knoten gehen beim Mergen auf `[x]`.
|
||
|
||
0. **Spike** (`#ai.mcp.spike`): GraalJS-Bündel läuft in der JVM, dieselben
|
||
Zahlen wie Vitest für den mitgelieferten Plan; Speicher je Kontext auf
|
||
der Zielumgebung gemessen (RSS vorher/nachher, wie in D77); SSE durch
|
||
den Apache mit einem absichtlich langen Werkzeugaufruf gemessen (wie
|
||
D76-Nachtrag 2). Ergebnis als D93-Nachtrag. **Erst danach wird gebaut.**
|
||
1. **Lesen und befragen** (`#ai.mcp.core`, `#ai.mcp.read`,
|
||
`#ai.mcp.inspect`, `#ai.mcp.auth`): `i18n.js`, `inspect.js`, das
|
||
Bündel im Jar, `WerkbaumCore`, Bearer-Filter, Resources, `read`,
|
||
`inspect`, `check` in beiden Formen. Nachweis: Claude Code liest über
|
||
die `.mcp.json` den mitgelieferten Plan und bekommt 231 Knoten,
|
||
0 Warnungen, dieselben Stationen wie der Editor; ohne Token 401.
|
||
2. **Schreiben** (`#ai.mcp.write`, `#ai.mcp.guard`): `guard.js`, die drei
|
||
Basis-Schreib-Werkzeuge in beiden Formen. Nachweis gegen das lokale
|
||
Backend mit offenem Editor: Agenten-Änderung erscheint mit
|
||
„Claude Code“ in der Historie; veraltete `base` ⇒ Fehler mit aktuellem
|
||
Text, nichts überschrieben; `[^]` abgelehnt; `mixedGate` abgelehnt,
|
||
`sizeConflict` geschrieben und gemeldet; Text-Form liefert
|
||
`blocking: true` und trotzdem den Text.
|
||
3. **Knoten-Verben** (`#ai.mcp.verbs`): `edit.js` mit Tests (Gegenprobe
|
||
per Mutation wie bei `setFoldMark`), dann die vier Verben. Nachweis:
|
||
`move_node` nimmt Beschreibungsblock und Fortsetzungszeilen mit und
|
||
lässt den Rest zeichengenau stehen.
|
||
4. **Spiegel** (`#ai.mcp.mirror`): `pull-doc --with-history`. Nachweis in
|
||
einem Wegwerf-Worktree: ein Agenten-Zug ergibt genau einen Commit mit
|
||
Serverstand, Beteiligten im Rumpf und beiden Trailern; ein
|
||
unveränderter Zug keinen.
|
||
5. **Prompts** (§5.8).
|
||
|
||
## 11. Entscheidungen
|
||
|
||
Drei Multiple-Choice-Runden am 2026-09-02, jede Frage mit der Option
|
||
„zunächst offen halten“ — keine wurde gewählt.
|
||
|
||
**Runde 1**
|
||
|
||
| Frage | Entschieden | Anmerkung |
|
||
|---|---|---|
|
||
| Strenge der Warnungs-Leitplanke (§5.5) | **Nur Struktur-Warnungen blockieren** | Vorgeschlagen war die strenge Fassung; der Nutzer hat die Zwischenschritte eines Agenten höher gewichtet |
|
||
| `displayName` (§5.6) | **Host-Name aus dem Handshake** | Rückfall „Agent“ |
|
||
| Geteiltes Dokument und Git (§5.7) | **Beides**: Server ist Quelle, Git Archiv über `pull-doc` | Nutzer-Idee, mitten in der Ausarbeitung |
|
||
| Schreib-Tools (§5.4) | **Zusätzlich Knoten-Verben** | Regeln in `frontend/src/edit.js` |
|
||
| Verteilung | **Nur aus dem Repo** | `.mcp.json`; npm-Frage entfällt mit Runde 3 ohnehin |
|
||
| SDK-Abhängigkeit für das Node-Paket | Ja | **überholt** durch Runde 3 (kein Node-Paket) |
|
||
| Ort der Baum-Serialisierung | im MCP-Paket | **überholt**: es gibt kein Paket; sie liegt als `frontend/src/inspect.js` — dort, wo `#ai.dialog` sie braucht |
|
||
|
||
**Runde 2** (Nutzer: Architektur-Entscheidungen jetzt diskutieren)
|
||
|
||
| Frage | Entschieden | Anmerkung |
|
||
|---|---|---|
|
||
| Urheber im Spiegel-Commit (§5.7) | **Historie im Rumpf + Trailer** | `Werkbaum-Changed-By:`, `Werkbaum-Version:`; Git-Autor bleibt der Betreiber |
|
||
| HTTP: Ort | **Im Backend, das die JS-Module per GraalJS ausführt** | Die Frage „Kotlin kann das nicht?“ hat den Entwurf korrigiert: MCP braucht kein Node, nur der Parser erzwang es |
|
||
| HTTP: Zugang (§5.6) | **Statisches Bearer-Token** | OAuth 2.1 erst mit einem IdP |
|
||
| HTTP: Umfang | **Nur Dokumente** | kein Dateizugriff, kein Spiegel am Dienst |
|
||
|
||
**Runde 3** (Nutzer: neue Technologien sind eine Dimension schwerer als
|
||
Abhängigkeiten; erst die Beweggründe, dann die Entscheidung)
|
||
|
||
| Beweggrund | Antwort |
|
||
|---|---|
|
||
| Für wen? | Entwickler, die parallel an geteilten Plänen arbeiten und agentische KI einsetzen — Pläne im Dialog erstellen, Fortschritt tracken; der Agent läuft meist lokal (OpenCode, Claude Code, Codex); später ein Agent im Frontend |
|
||
| Welche Pläne? | **Beides**: `.werkbaum`-Dateien im Repo und geteilte Server-Dokumente |
|
||
| Darf der Text eines lokalen Plans zur Analyse an den eigenen Server? | **Ja** |
|
||
| Startzeit eines lokalen Prozesses? | Egal |
|
||
|
||
| Entscheidung | Entschieden | Anmerkung |
|
||
|---|---|---|
|
||
| Node als Laufzeit? | **Nein — nur im Backend**, Text-Form für Dateien (§5.2) | Alternative C (Jar als stdio) als Rückfall benannt |
|
||
| Backend-Abhängigkeiten | **GraalJS + Spring-AI-Starter** (`spring-ai-starter-mcp-server-webmvc`) | Boot-4-Verträglichkeit beim Spike prüfen; Rückfall das MCP-Java-SDK direkt |
|
||
|
||
**Weiterhin offen** — Messungen und Kleinigkeiten, die sich erst beim
|
||
Bauen stellen: der Spike (§5.10, Phase 0) mit seinen drei Messungen; die
|
||
Prompts (§5.8).
|
||
|
||
## 12. Revisionsgeschichte
|
||
|
||
- **Runde 1** — Node-Paket `mcp/` mit stdio, Dateien und Server-Dokumente
|
||
als Ablagen; SDK, Leitplanken, Verben, Spiegel entschieden.
|
||
- **Runde 2** — Urheber im Commit; Streamable HTTP im Backend per GraalJS
|
||
statt als zweiter Node-Dienst — der Entwurf hatte angenommen, MCP
|
||
erzwinge Node.
|
||
- **Runde 3** — Der Nutzer benennt Node als **neue Technologie**, nicht
|
||
als Abhängigkeit (CLAUDE.md-Regel, D93-Nachtrag). Aus den Beweggründen
|
||
folgt: kein lokaler Prozess, alles im Backend, Text-Form für Dateien.
|
||
Das Node-Paket ist verworfen; seine Tool-Schicht lebt als
|
||
`frontend/src/inspect.js`, `guard.js`, `edit.js` weiter — unverändert
|
||
im Zuschnitt, nur ohne eigenes Paket.
|