Mirror docs at v8.0.0

Wiki Mirror committed 2026-10-08 05:01:55 +00:00
1 parent 9c40ac7622
commit bc952b31bd
9 files changed
+1523

No files matched your search

+508
@@ -0,0 +1,508 @@
*Generated from [`INSTALL.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/INSTALL.md) at v8.0.0 – edit the repository, not this wiki.*
# Installation
Dieses Dokument richtet sich an Menschen. Es gibt genau einen Weg zu einer Chemenu-Instanz: Ein
Agent installiert das **neueste Release** in ein leeres Verzeichnis, das du vorgibst. Die
Schritte führt der Agent aus, nach `instructions/setup-instance.md` aus demselben Release. Hier
steht, was du vorher bereitstellst, welchen Satz du ihm gibst, was er dich fragt und was zu tun
ist, wenn er anhält. Die vollständige Kommandoreferenz steht in
[tools/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/tools/CONTRACT.md).
Den optionalen **MCP-Leseserver** installiert und betreibt
[INSTALL-MCP.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/INSTALL-MCP.md): derselbe Korpus, lesend, für einen Konsumenten, der kein
Terminal auf dieser Maschine ist.
<!-- dist:strip-start -->
Wer am Stack selbst arbeiten will, klont dieses Repo. Das ist eine Entwicklungsumgebung mit
Demo-Korpus und keine Instanz; sie steht in [DEVELOPMENT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/DEVELOPMENT.md).
<!-- dist:strip-end -->
## Was vorher da sein muss
Diese Programme prüft der Preflight, bevor irgendein `wikitool`-Befehl läuft. Installieren musst
du sie selbst - der Agent tut es nie, auch nicht mit deiner Zustimmung. Wo eines fehlt, nennt
der Preflight den Installationsbefehl für dein System. Die Liste wird aus
`tools/prerequisites.txt` erzeugt, derselben Datei, die der Preflight liest:
<!-- wikitool:prerequisites -->
- **Python** ≥ 3.11
- **Git**
- **ripgrep (rg)**
<!-- /wikitool:prerequisites -->
Dazu:
- **Ein Agent-Harness**: Claude Code, GitHub Copilot (in VS Code oder als CLI), Codex CLI oder
Mistral Vibe.
- **Ein leeres Verzeichnis**, in dem die Instanz liegen soll, und dein Harness darin geöffnet.
Leer heißt: nichts außer einem `.git`. Ein frisch geklontes, leeres Repo für deine Instanz ist
also genau richtig - liegt dein Repo `torben/nathan` etwa in `~/src/nathan`, installierst du
dorthin, und der Agent übernimmt dessen `origin` als Ziel für `publish`.
Unter Windows zusätzlich, ebenfalls vom Preflight geprüft:
<!-- wikitool:prerequisites-windows -->
- **PowerShell 7 (pwsh)** ≥ 7
<!-- /wikitool:prerequisites-windows -->
Und außerdem:
- **PowerShell 7 als Standardterminal in VS Code.** Windows PowerShell 5.1 reicht nicht, und WSL
ist nicht vorgesehen.
- **Execution Policy `RemoteSigned`** - auf vielen Rechnern ab Werk gesetzt
(`Get-ExecutionPolicy -List` zeigt es).
- **Git for Windows.** Es bringt Git Bash mit, in dem Claude Code seine Befehle ausführt.
- **Ein Installationsverzeichnis mit höchstens 95 Zeichen**, zum Beispiel `C:\Chemenu`. Windows
erlaubt ohne eingeschaltete lange Pfade nur 259 Zeichen je Pfad, und die Dateien des Wikis
brauchen den Rest. Wer Administratorrechte hat, kann stattdessen lange Pfade einschalten
(`LongPathsEnabled`); verlangt wird das nicht.
## Der Satz für den Agenten
Öffne das leere Verzeichnis in deinem Harness und gib dem Agenten diesen Satz:
> Richte in diesem Verzeichnis eine neue Chemenu-Instanz ein. Hol dazu das neueste Release von
> `https://gitea.nehmer.net/api/v1/repos/torben/chemenu/releases/latest` und folge dessen Asset
> `setup-instance.md`. Lies vor dem Start des Preflights das Asset `preflight.md` aus demselben
> Release.
Damit liest der Agent die Beschreibung des neuesten Releases und daraus die beiden Anleitungen.
Dann lädt er das passende Preflight-Skript (`preflight.ps1` für PowerShell, `preflight.sh` für
eine POSIX-Shell) mit einem Befehl seiner Shell ins Verzeichnis - nicht über den Browser, damit
Windows die Datei nicht als „aus dem Internet“ markiert - und startet es. Das Skript lädt den
Tarball desselben Releases, prüft dessen sha256, entpackt ihn in das Verzeichnis, löscht sich
selbst und prüft dann im entpackten Baum, ob alles da ist.
Die Liste aller Releases: <https://gitea.nehmer.net/torben/chemenu/releases>. Das Repo ist
öffentlich; der Download braucht weder Konto noch Token.
## Was der Agent dich fragt
Raten darf der Agent keine dieser Antworten, und keine übernimmt er aus einem anderen Repo:
- <!-- setup-question: identity --> **Autor-Identität** - Name und E-Mail für `git config`. Das ist
zugleich der Autorname jeder künftig angelegten Wiki-Seite (`$WIKI_AUTHOR` überschreibt ihn bei
Bedarf).
- <!-- setup-question: remote --> **Remote** - bei einem leeren Klon nur die Bestätigung, dass
`origin` stimmt; sonst eine URL, wenn du auf einen Server pushen willst. Ohne Remote bleibt die
Instanz lokal, und jedes `publish` läuft mit `--no-push`.
- <!-- setup-question: kb-language --> **Sprache und Ton der Seiten** - sie landen in
`kb/CONVENTIONS.md`, dazu je Collection `kb/<name>/COLLECTION.md`. Fertige Profile, darunter ein
vollständiges deutsches, hält `instructions/kb-profiles.md` bereit. Entscheide das **vor dem
ersten Ingest**: Danach ist ein Wechsel der Abschnittsnamen eine Migration jeder bestehenden
Seite. Titel, Wikilink-Ziele, Zitat-IDs, Schema-Werte, Tags, Befehle und Pfade folgen keiner
Sprache - `Act Runner` heißt in jeder Instanz `Act Runner`.
- <!-- setup-question: domain --> **Anwendungsgebiet** - woraus dieses Wiki seine Quellen zieht.
Daraus schlägt der Agent eine `source_type`-Liste vor (bei einem Verein etwa Satzung, Protokoll,
Spielbericht). Das ist ein Startpunkt, keine Festlegung: Später wird sie an echtem Bestand
korrigiert (`instructions/evolve-subtypes.md`).
- <!-- setup-question: personalization --> **Personalisierung** - wer diese Instanz bedient
(`USER.md`) und wie sie klingt (`SOUL.md`). Der Agent interviewt dich entlang der Vorlagen und
schreibt deine Antworten wörtlich mit. Zwei Fragen beantwortest nur du: den **Namen der Persona**
und die **Themen, die bewusst draußen bleiben**.
- <!-- setup-question: environment --> **Umgebung** (optional) - Harness, MCP-Server, Remotes,
damit spätere Sitzungen nicht erneut fragen. „Weiß ich nicht“ ist eine gültige Antwort.
- <!-- setup-question: telemetry --> **Telemetrie** - standardmäßig aus; der Agent fragt nur, ob du
sie einschalten willst.
- <!-- setup-question: task-tracker --> **Aufgaben-Tracker** (optional) - siehe
[Konfiguration](#konfiguration).
Am Ende legt der Agent den ersten Commit an. Dabei hält das Mass-Update-Gate an (Exit 42), weil
eine neue Instanz aus weit mehr als zehn Dateien besteht. Das ist erwartet: Der Agent zeigt dir
die Dateiliste und die `--confirm`-Zeile, und erst nach deiner Freigabe wird veröffentlicht.
Danach startest du die Agent-Sitzung im selben Verzeichnis neu, damit sie die Skills lädt.
## Wenn der Agent anhält
Der Preflight hält mit **Exit 42** an, wenn du etwas tun musst. Seine Ausgabe nennt in einem
nummerierten Block, was fehlt, warum, den Befehl, der es behebt, und wie es weitergeht. Der
Agent zeigt dir diesen Block unverändert, setzt eine Übersetzung höchstens darunter und wartet.
Sag ihm Bescheid, wenn du fertig bist; dann prüft er erneut. Ausweichen oder selbst installieren
darf er nicht.
**Beim Herunterladen und Entpacken** (das Skript aus dem Release, bevor es einen Baum gibt):
- **Werkzeuge zum Laden oder Entpacken fehlen** (Exit 42). Unter Linux und macOS braucht das
Skript `curl`, `tar` und `sha256sum` (oder `shasum`), unter Windows nur das `tar.exe` aus
Windows 10/11. Installiere, was die Ausgabe nennt; unter Windows liefert Git for Windows alles
für Git Bash mit.
- **Das Verzeichnis ist zu lang** (Exit 42, nur Windows ohne lange Pfade). Nimm ein kürzeres,
etwa `C:\Chemenu`, öffne es im Harness und gib den Satz dort noch einmal.
- **Das Verzeichnis ist nicht leer** (Exit 1). Es darf nichts enthalten außer dem Skript und
einem `.git`. Räume es selbst auf oder nimm ein anderes - der Agent löscht dort nichts.
- **Download fehlgeschlagen oder Prüfsumme falsch** (Exit 1). Es wurde nichts entpackt. Prüf die
Internetverbindung und lass es erneut versuchen; einen anderen Download-Weg sucht der Agent
nicht. Ohne direkten Download kannst du Tarball und `.sha256` von der Release-Seite selbst
nebeneinander ablegen; der Agent startet das Skript dann mit `--archive <tarball>`.
**Im entpackten Baum** (jeder weitere Lauf ist `tools/preflight.sh` bzw. unter PowerShell
`pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1`):
- **Ein Werkzeug fehlt** - Python, git, ripgrep, unter Windows auch PowerShell 7. Die Ausgabe
nennt den Installationsbefehl für dein System. Ist es schon installiert, nur woanders, nenn dem
Agenten den Pfad; er reicht ihn mit `--set <werkzeug>=<pfad>` weiter.
- **Eine Version ist zu alt** - zum Beispiel Python unter 3.11. Neuere Version installieren oder
deren Pfad nennen.
- **Ein genannter Pfad funktioniert nicht** - der Pfad muss auf das Programm selbst zeigen, nicht
auf seinen Ordner.
- **Die Python-Umgebung (`tools/.venv`) oder ihre Bibliotheken ließen sich nicht einrichten.** Die
Ausgabe zeigt, was Python oder pip gemeldet haben. Meist blockiert ein Proxy oder ein
Sicherheitsprogramm den Download; das klärt, wer deinen Rechner betreut.
- **Skripte tragen die Markierung „aus dem Internet“** (nur Windows). Das passiert, wenn das
Release im Browser geladen und im Explorer entpackt wurde. Einmal im Verzeichnis, in
PowerShell 7: `Get-ChildItem -Recurse -File | Unblock-File`. `doctor` zeigt den Stand unter
`script-marks`.
- **Die Execution Policy verbietet Skripte** (`Restricted` oder `AllSigned`, nur Windows). Die
Ausgabe nennt die eine Zeile für ein PowerShell-7-Fenster
(`Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned`). Setzt eine
Gruppenrichtlinie sie, hilft nur die IT - oder du arbeitest aus Git Bash mit `tools/wikitool`.
`doctor` zeigt den Stand unter `execution-policy`.
- **Das Installationsverzeichnis ist zu lang** (nur Windows ohne lange Pfade). Die Instanz muss
in ein kürzeres Verzeichnis umziehen, etwa `C:\Chemenu`.
Hält der Agent an einer anderen Stelle an und ist die Ursache nicht offensichtlich, bietet er dir
einen Fehlerbericht an - siehe [Troubleshooting](#troubleshooting).
## Version und Updates
Jede Instanz trägt die Version des **Stacks** (Werkzeuge, Typen, Instruktionen, Contracts) -
nicht die ihres Inhalts. Sie steht in `VERSION`, daneben `.wikitool-release.json` mit Herkunft
und Exportdatum des Releases.
```bash
tools/wikitool version # was läuft hier, und woher kommt es
tools/wikitool version check # gibt es ein neueres Release?
```
`version check`, `version notes` und `dist upgrade --latest` sind die einzigen Befehle, die
ins Netz gehen, und alle drei fragen denselben Release-Feed der Ursprungs-Instanz
(`$WIKITOOL_UPDATE_URL` überschreibt; sonst der Wert aus dem Stamp). `version check` ist dafür da;
`version notes` greift nur dann darauf zurück, wenn die lokale `CHANGES.md` den Eintrag nicht
hat - auf einer Instanz also immer, siehe unten - und sagt vorher auf stderr, welche URL es
fragt; `dist upgrade` fragt den Feed nur, wenn `--latest` dasteht, und lädt dann auch das Release
herunter (siehe „Eine Instanz aktualisieren"). Ein nicht erreichbarer Feed wird als Fehler
gemeldet - **nie** als „aktuell" und nie als „keine Notes".
**Was die Versionsnummer aussagt:** kompatibel ist, was in der *linkesten von Null
verschiedenen Stelle* übereinstimmt. `0.1.3 → 0.1.4` ist ein sicheres Update, `0.1.3 → 0.2.0`
nicht, und ab `1.0.0` liest sich dieselbe Regel als das gewohnte „MAJOR bricht". `version check`
sagt das direkt (`state: update` vs. `state: migration`).
Was diese Stelle beantwortet, ist **ob die neue Version ein Drop-in-Ersatz ist** - ob sich die
Maschinerie einfach darüberkopieren lässt und ob die alte danach noch zurückkann. Ob *Inhalt*
migriert werden muss, ist eine **zweite, unabhängige Frage**. Ein MAJOR-Sprung kann eine leere
Migrationskette haben und trotzdem Handarbeit verlangen: umbenannter Release-Feed, umbenanntes
Artefakt, umbenannter Import- oder Kommandoname, geänderte Envvar - `kb/` bleibt dabei
unangetastet, das Update ist trotzdem keins zum Drüberkopieren. Der Abschnitt „Sonderfall:
Update von 1.x auf 2.0.0" unten ist genau dieser Fall.
Deshalb stehen in den Release-Notes eines MAJOR zwei getrennte Zeilen, und beide sind vor dem
Update zu lesen: **Breaking Change:** sagt, was aufhört zu funktionieren und was diese Instanz
dagegen tun muss; **Migration:** sagt, ob und wie der Korpus umgeschrieben wird (`none required`,
wenn nicht). `tools/wikitool version notes` druckt beide Zeilen - im Ursprungs-Repo aus der dort
gefüllten `CHANGES.md`, auf einer ausgelieferten Instanz aus dem Release-Feed, weil die Instanz
die Datei nur als Stub bekommt und ein Update sie nie überschreibt. Der Befehl fragt dabei immer
das **neueste** Release: solange `VERSION` noch die alte Fassung nennt, antwortet er also mit
einer anderen Version als der eigenen und sagt das auf stderr dazu. Ist der Feed nicht
erreichbar, nennt die Fehlermeldung die Release-Seite, die `.wikitool-release.json` als
`release_url` führt; `--offline` verlangt diesen Weg von vornherein.
### Eine Instanz aktualisieren
Das Anwenden eines Updates schreibt in eine Instanz, die bereits Inhalt hat. Der Inhalt hat dabei
eine **eigene Version**: `.wikitool-kb.json` sagt, in welcher Form die Seiten vorliegen,
unabhängig davon, welche Maschinerie danebensteht. Genau dieser Unterschied ist der Zustand, in
dem sich jede Instanz mitten im Upgrade befindet.
**Die Durchführung selbst steht in `instructions/upgrade-instance.md`** - die Reihenfolge, was
jeder Schritt entscheidet, wo die Agent-Sitzung neu gestartet werden muss, und die beiden Stellen,
an denen heute Handarbeit nötig ist. Sie steht dort und nicht hier, weil sie von einer
Agent-Sitzung ausgeführt wird; eine zweite Fassung derselben Schrittfolge an dieser Stelle wäre
genau die Kopie, die irgendwann auseinanderläuft. Wer den Lauf selbst fahren will, liest dieselbe
Datei.
Was dieses Dokument beiträgt, ist die Entscheidung *davor* - welches Release, ob überhaupt, und
wem die Instanz als Quelle vertraut - und zwei Sonderfälle, die die Instruktion nicht abdecken
kann, weil es sie dort noch nicht gibt.
**Das Release kommt mit einem Befehl.** `tools/wikitool dist upgrade --latest --expect <version>`
fragt den Release-Feed, lädt Tarball und `.sha256` in ein Arbeitsverzeichnis, prüft den Tarball
gegen die Summe und wendet ihn an; das Arbeitsverzeichnis verschwindet bei jedem Ausgang wieder,
auch bei `--dry-run`. `<version>` ist die, die `version notes` gedruckt hat: der Feed kennt nur
sein *neuestes* Release, und `--expect` verweigert den Lauf **vor** dem Download, wenn inzwischen
ein neueres erschienen ist, statt es ungelesen einzuspielen. Beides zusammen entscheidet vor dem
Download über „schon aktuell", Downgrade und Vor-Release (`-beta.N` braucht `--pre`); fehlt dem
Release der Tarball oder die Summe, bricht der Befehl vor dem ersten Download ab und nennt die
Release-Seite. Wer offline arbeitet oder einen Tarball vom Betreiber bekommen hat, gibt statt
`--latest` weiter die Datei an (`dist upgrade <tarball>`).
**Was die Prüfsumme leistet - und was nicht.** Die `.sha256` liegt beim selben Feed wie der
Tarball. Sie schützt vor einer beschädigten Übertragung, nicht vor einem Feed, der selbst
kompromittiert ist: die Echtheit eines Releases beruht auf dem Vertrauen in den Host, dessen
Feed die Instanz fragt (`update_url` im Stamp). Es gibt keinen https-Zwang; wer einen
`http://`-Feed konfiguriert, tut das bewusst. `$WIKITOOL_UPDATE_TOKEN` geht nur an Downloads
auf demselben Host wie der Feed.
**Beim ersten Sprung auf ein Release, das `--latest` kennt, gibt es die Option in der Instanz
noch nicht** - die Instruktion, die dort steht, gehört zum Release, das die Instanz verlässt.
Dann Tarball und `.sha256` einmal von der Release-Seite holen, nebeneinander ablegen und
`dist upgrade <tarball>` geben; ab dem Release danach trägt die Instanz `--latest` selbst.
**Beim ersten Sprung auf `4.5.0` oder höher gibt es `dist upgrade` in der Instanz noch nicht** -
es kam erst mit `4.5.0`. Dann das Werkzeug aus dem entpackten *neuen* Tarball verwenden, gegen die
alte Instanz gerichtet:
```bash
tar -xzf chemenu-stack-<version>.tar.gz
CHEMENU_ROOT="$PWD" chemenu-stack-<version>/tools/wikitool \
dist upgrade chemenu-stack-<version>.tar.gz --dry-run
```
`CHEMENU_ROOT` sagt dem Paket, auf welchen Korpus es zeigen soll (siehe § Konfiguration); ohne die
Variable würde es den entpackten Tarball selbst für die Instanz halten. Ab dem zweiten Upgrade
trägt die Instanz Kommando und Instruktion selbst, und der normale Weg greift.
Was `dist upgrade` dabei genau tut, klassifiziert und verweigert, steht in
[tools/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/tools/CONTRACT.md) - einschließlich des vollständigen Fehlerkontrakts. Eine
lokal veränderte Stack-Datei ist damit sichtbar, statt von Hand gegen die sha256-Summen im
`files`-Block geprüft werden zu müssen - genau der Schritt, der vor `4.5.0` hier stand.
`doctor` warnt, solange `kb_version` hinter `VERSION` zurückliegt und noch Migrationen offen
sind. Einer Instanz, die älter ist als `.wikitool-kb.json`, fehlt die Datei ganz - dann einmalig
`tools/wikitool migrate baseline <version>` aufrufen; geraten wird nichts.
**Fallstricke.** Eine Instanz ohne lokale `.wikitool-release.json` (oder eine ohne `files`-Block,
aus der Zeit vor `4.5.0`) hat für `dist upgrade` keine Basis, gegen die es eine lokale Änderung
erkennen könnte, und verweigert den Tausch - dafür gibt es heute keine Reparatur.
Mit `<tarball-oder-verzeichnis>` lädt der Befehl selbst nichts herunter; die Datei muss vorher
von der Release-Seite geholt werden. Nur `--latest` lädt, und ein Tarball muss in beiden Fällen genau ein
Top-Level-Verzeichnis enthalten - die Form, in der `.gitea/workflows/release.yml` es baut.
Vor `4.5.0` stand hier ein rein manueller Ablauf (Maschinerie von Hand kopieren, `kb/CONTRACT.md`
eingeschlossen, sha256-Vergleich von Hand). `dist upgrade` ersetzt genau diesen Teil; wer ihn
dennoch von Hand nachvollziehen will oder muss (ein Werkzeug, das `wikitool` selbst nicht
ausführen kann), findet die Dateiliste im `files`-Block der `.wikitool-release.json` und die
Ausnahmen (`kb/CONVENTIONS.md`, `kb/*/COLLECTION.md`, `.wikitool-kb.json`) in
[tools/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/tools/CONTRACT.md)s `dist upgrade`-Datensatz (oder direkt:
`tools/wikitool dist upgrade -h`).
## Konfiguration
| Variable | Zweck | Fallback |
|----------|-------|----------|
| `WIKI_AUTHOR` | Override für den Autornamen neuer Source-Seiten | `git config user.name` - fehlt beides, bricht `new` mit `ERROR` ab |
| `WIKITOOL_SESSION_ID` | Scopt das Iteration-Budget-Gate auf eine Aufgabe statt auf ein Terminal | Eine vom Harness selbst gesetzte Sitzungs-Variable, wo eine bekannt ist (z. B. `CLAUDE_CODE_SESSION_ID`), sonst die Parent-Process-ID (siehe [instructions/session-setup.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/session-setup.md)) |
| `WIKITOOL_UPDATE_URL` | Release-Feed, den `version check` abfragt | Wert aus `.wikitool-release.json`, sonst der Feed der Ursprungs-Instanz |
| `WIKITOOL_UPDATE_TOKEN` | Gitea-Token für den Release-Feed | keiner - gegen `torben/chemenu` nicht nötig, nur gegen einen Feed in einem nicht öffentlichen Repo |
| `WIKITOOL_TASKS_CONFIG` | Pfad zu einer Tracker-Konfiguration, die `task`, `review` und `doctor` statt `.wikitool-tasks.json` lesen - um einen Checkout der Reihe nach gegen mehrere Tracker laufen zu lassen | die `.wikitool-tasks.json` im Repo-Root. Nennt die Variable eine Datei, die es nicht gibt, ist das ein Fehler und nie „kein Tracker konfiguriert" |
| `CHEMENU_ROOT` | Auf welchen Korpus das Paket zeigt - für einen Aufrufer, der nicht im Checkout selbst liegt | der Checkout, in dem das Paket liegt (`tools/wikitool` verhält sich ohne die Variable unverändert) |
| `WIKI_TRACE` / `WIKI_TRACE_DIR` | Telemetrie abschalten bzw. aus dem Arbeitsbaum heraus umlenken | Aus - siehe unten |
| `WIKI_TRACE_MAX_SESSION_BYTES` / `WIKI_TRACE_KEEP_SESSIONS` | Byte-Deckel je Session-Trace bzw. wie viele Session-Verzeichnisse die Retention behält | 5 MiB je Session, 250 Verzeichnisse |
**Gegen das Ursprungs-Repo braucht es kein Token.** `torben/chemenu` ist öffentlich lesbar;
`version check`, `dist upgrade --latest` und die Installation funktionieren ohne Konfiguration.
**Telemetrie ist in einer Instanz aus.** Jede Instanz trägt die `.wikitool-release.json` ihres
Releases, und daran erkennt `chemenu.telemetry.policy`, dass niemand Telemetrie bestellt hat.
Das gilt auch für jeden weiteren Klon der Instanz, weil die Datei mit dem ersten Commit ins Repo
kommt. Nur ein Klon des Ursprungs-Repos zur Entwicklung trägt keine; dort sind die Traces das
Messinstrument, mit dem der Stack sich selbst bewertet, und sie stehen auf **an**.
Wer den Default umdrehen will, legt `.wikitool-telemetry.json` im Repo-Root an (pro Checkout,
gitignored, kein `.template` - genau wie `.wikitool-remotes.json`):
```json
{ "enabled": true, "max_session_bytes": 5242880, "keep_sessions": 250 }
```
Alle drei Schlüssel sind optional. `WIKI_TRACE` überschreibt `enabled` weiterhin in beide
Richtungen und schlägt diese Datei. `wikitool doctor` meldet den aktuellen Zustand (an/aus,
warum, und die Menge gegen beide Deckel); mehr dazu in [EVALS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/EVALS.md) § "Whether it
runs at all".
**Tool-Pfade - schreibt der Preflight, nicht der Mensch.** `.wikitool-tools.json` im Repo-Root
hält die absoluten Pfade von Python, git und ripgrep, so wie der Preflight sie auf diesem Rechner
gefunden hat; `wikitool` startet git und rg von dort statt über `PATH`. Pro Checkout und
gitignored - ein Pfad auf einem Rechner sagt über den nächsten nichts. Fehlt die Datei oder ist
sie unvollständig, startet `tools/wikitool` nicht (Exit 42) und nennt den Preflight. Liegt ein
Tool woanders, als der Preflight sucht, nennt man den Pfad mit
`tools/preflight.sh --set rg=<pfad>` (PowerShell: `pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1 --set rg=<pfad>`); von Hand
bearbeitet wird die Datei nicht. `doctor` meldet
unter `tool-paths`, ob alle Pfade noch stimmen.
**Aufgaben-Tracker anbinden - optional.** Der Wochenrückblick (`tools/wikitool review`, Skill
`gtd-weekly-review`) gleicht die Projektseiten unter `kb/gtd/` gegen einen Aufgaben-Tracker ab. Welcher
das ist, steht in `.wikitool-tasks.json` im Repo-Root - der dritten Datei dieser Art neben
`.wikitool-telemetry.json` und `.wikitool-remotes.json`: pro Checkout, ohne `.template`, und
**gitignored, sobald ein Token darin liegt**. Fehlt sie, ist schlicht kein Tracker konfiguriert;
das ist ein gültiger Endzustand, kein Fehler. Kaputt ist sie dagegen ein `FAIL` - eine
unlesbare Konfiguration darf nicht als „kein Tracker" durchgehen.
```json
{
"schema": 1,
"provider": "superproductivity",
"thresholds": {
"stalled_waiting_days": 14,
"unpaged_project_weeks": 3,
"someday_stale_months": 5
},
"superproductivity": {
"access": "api",
"api_base_url": "http://127.0.0.1:3876",
"api_token": "<token aus den SP-Einstellungen>"
}
}
```
```json
"superproductivity": {
"access": "snapshot",
"backups_dir": "~/.config/superProductivity/backups"
}
```
`provider` wählt den Adapter - ausgeliefert werden `superproductivity` und `caldav`. Der
`thresholds`-Block trägt die drei Schwellwerte des Rückblicks (Konfiguration, nicht Schema): ab
wann ein Waiting-For überfällig ist, ab welchem Alter ein Tracker-Projekt ohne `kb/`-Seite
gemeldet wird, und ab wann ein Someday-Eintrag als verstaubt gilt. Ein Wert, den der Provider
gar nicht liefern kann - ein Waiting-Posten ohne Wiedervorlagedatum, ein Tracker-Projekt ohne
bestimmbares Anlagedatum - wird vom Rückblick nicht still übersprungen, sondern als eigene
Fundstelle gemeldet (`waiting_no_follow_up`/`project_age_unknown`).
Der gleichnamige Provider-Block trägt dessen Verbindungsangaben, und bei Super Productivity
entscheidet `access` **verpflichtend und ohne Rückfall**, welcher von zwei sich ausschließenden
Wegen das ist: eine headless bediente Instanz setzt `access: "snapshot"` und liest
ausschließlich den jüngsten Backup-Schnappschuss unter `backups_dir` (läuft auch ohne laufende
App, aber rein lesend - der Tracker ist von dort aus nicht schreibbar); eine Desktop-Instanz
setzt `access: "api"` und spricht ausschließlich die lokale REST-API an, die nur antwortet,
solange die App läuft, dafür aber auch den aktuellen Zustand liefert und den Schreibpfad trägt.
Der Block nennt nur die Felder seines eigenen Wegs - ein `backups_dir` neben `access: "api"` oder
ein `api_token` neben `access: "snapshot"` wird beim Lesen der Konfiguration abgelehnt, nicht
ignoriert. `api_token` ist bei `access: "api"` Pflicht, da jeder Endpunkt außer `GET /health`
`Authorization: Bearer <token>` verlangt.
`tools/wikitool new project` legt einen gleichnamigen Tracker-Eintrag nur auf einer
`access: "api"`-Instanz an (und auch dort nicht automatisch - siehe die Kommandotabelle). Auf
einer `access: "snapshot"`-Instanz verweigert das Kommando vollständig, exit 1: der Tracker ist
von dort aus nur lesbar. Dasselbe gilt für `tools/wikitool task new`, den zweiten Schreibweg:
es legt einen einzelnen Posten im Tracker an - ohne `kb/`-Seite - und existiert ebenfalls nur
auf einer `access: "api"`-Instanz. `tools/wikitool task close --id` ist der dritte und letzte
Schreibweg - er markiert einen Posten erledigt, löscht ihn nie - und verweigert auf
`access: "snapshot"` auf dieselbe Weise. `tools/wikitool task list --project` ist rein lesend
und beantwortet daher auf beiden Zugriffsarten.
**`caldav`** ist der standardbasierte zweite Adapter (RFC 4791/5545), gegen Nextcloud Tasks
verifiziert, mit iOS *Erinnerungen* als mobilem Client - gebaut nach dem Zuschnitt: auf dem
Telefon wird abgehakt, gepflegt wird am Schreibtisch. Anders als bei Super Productivity gibt es
nur einen Zugriffsweg - CalDAV ist immer ein Netzwerkzugriff, kein `access`-Feld nötig:
```json
"caldav": {
"url": "https://<host>/remote.php/dav/calendars/<user>/",
"username": "<login>",
"app_password": "<Nextcloud-App-Passwort>",
"inbox_list": "Inbox",
"someday_list": "Someday",
"exclude_lists": ["<vorhandene Liste, die kein Projekt ist>"]
}
```
`url` zeigt auf das CalDAV-Calendar-Home-Set des Kontos; sie darf ein Alias sein (Nextcloud
akzeptiert dort einen Kurznamen), da jede spätere Adresse ausschließlich aus den vom Server
gelieferten `href`s stammt, nie aus dieser URL und einem Namen zusammengesetzt wird. `username`
ist der Login-Name, der von der Benutzer-ID in der URL abweichen kann - ein
Nextcloud-App-Passwort wird empfohlen, nicht das Kontopasswort. `inbox_list`/`someday_list`
nennen die beiden festen Listen (je genau eine pro Instanz); `exclude_lists` nimmt vorhandene
reine Aufgabenlisten heraus, die keine Projekte sind - eine Liste mit `VEVENT`-Anteil zählt
ohnehin nie als Projekt.
Ein Projekt ist dort eine Liste, deren unterstützte Komponente ausschließlich `VTODO` ist; `tools/wikitool
new project` legt sie automatisch per `MKCALENDAR` an - anders als bei Super Productivity ohne
Rückfrage, weil CalDAV einen echten Anlage-Befehl für Listen kennt. Die Eindeutigkeitsprüfung
läuft gegen **jede** Liste im Konto, auch gegen ausgeschlossene, Inbox, Someday und gemischte
Kalender - kollidiert ein neuer Name mit einer davon, wird nichts angelegt und die Kollision
genannt; die vorhandene Liste in Nextcloud umzubenennen bleibt Handarbeit. `tools/wikitool task
new`/`task close` funktionieren auf einer `caldav`-Instanz uneingeschränkt - es gibt keinen
reinen Lesemodus wie `access: "snapshot"`. Beim Abhaken ändert `task close` ausschließlich
`STATUS`, `COMPLETED`, `PERCENT-COMPLETE`, `LAST-MODIFIED` und `DTSTAMP` an der bestehenden
`.ics`-Ressource; jede andere Eigenschaft - auch eine unbekannte `X-`-Eigenschaft oder ein Alarm
- bleibt unverändert erhalten, und eine seit dem Lesen veränderte Ressource (ETag-Konflikt)
schreibt nichts und bricht mit exit 1 ab. Gelöscht wird nie etwas.
Listen abgeschlossener Projekte bleiben nach dem Archivieren bestehen - der Adapter löscht nie
eine Liste; das übernimmt der Betreiber von Hand in Nextcloud, sobald gewünscht.
## Verifikation
```bash
tools/wikitool doctor
```
Prüft in einem Aufruf: Abhängigkeiten, Autor-Auflösung, Git-Identität/Branch/Remote,
publizierte Skills, Struktur (Collection-Contracts, generierte Dateien), Personalization
(`USER.md`/`SOUL.md` vorhanden **und** ausgefüllt), die optionale Umgebungsnotiz
(`ENVIRONMENT.md`), den Aufgaben-Tracker (`.wikitool-tasks.json` - fehlt sie, ist das `OK`; ist
ein Provider konfiguriert, zusätzlich ob sein Lesepfad bereitsteht und seine API gerade
antwortet, beides nie ein `FAIL`) und die Session-ID.
`OK`/`WARN` sind unbedenklich (ein fehlender Remote z. B. ist ein gültiger Endzustand); nur ein
`FAIL` bricht mit exit 1 ab, und jede Zeile nennt ihr eigenes Fix-Kommando.
Danach zusätzlich:
```bash
tools/wikitool docs verify
tools/wikitool instructions verify
```
## Troubleshooting
- **`tools/wikitool` endet mit `STOP - this checkout is not set up yet` (Exit 42)** - der
Preflight ist in diesem Checkout noch nicht durchgelaufen, oder seit dem letzten Update nicht
mehr: `tools/preflight.sh` ausführen, siehe
[instructions/preflight.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/preflight.md). In PowerShell 7 unter Windows heißt der
Aufruf `pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1`; das
`-ExecutionPolicy Bypass` gilt nur für diesen einen Prozess und ändert keine Einstellung. Was
seine Ausgabe dann bedeuten kann, steht unter [Wenn der Agent anhält](#wenn-der-agent-anhält).
- **Der Agent bietet keine Skills an (`wiki-ingest`, `wiki-query`, ...)** - `.agents/skills/`
und `.claude/skills/` sind generiert und nicht committet. `tools/wikitool instructions sync`
ausführen, dann die Agent-Session neu starten (Harnesses lesen Skills nur beim Start).
- **`doctor` meldet `personalization: FAIL`** - `USER.md`/`SOUL.md` fehlen, oder sie tragen
noch die Sentinel-Zeile aus dem Template (ein umbenanntes Template ist kein ausgefülltes).
Den Personalisierungs-Schritt aus `instructions/setup-instance.md` ausführen lassen; bei einem
weiteren Klon einer älteren Instanz ist das der einzige nachzuholende Schritt.
- **`doctor` meldet `environment: WARN`** - `ENVIRONMENT.md` existiert, trägt aber noch die
Sentinel-Zeile aus dem Template. Ausfüllen (Vorlage: `ENVIRONMENT.md.template`) und die Zeile
entfernen, oder die Datei löschen - sie ist optional, und `absent` ist ein gültiger
Endzustand.
- **`new` bricht mit "No author configured" ab** - weder `$WIKI_AUTHOR` noch
`git config user.name` sind gesetzt. `git config user.name "<Name>"` ausführen, oder
`WIKI_AUTHOR` exportieren.
- **`publish` endet mit Exit-Code 42 (Mass-Update-Gate)** - erwartetes Verhalten bei ≥10
gezählten Dateien (z. B. beim allerersten Commit einer neuen Instanz). Das ist kein Fehler,
sondern die Aufforderung, die Ausgabe einem Menschen zu zeigen: sie enthält die vollständige
Dateiliste und die exakte `--confirm <token>`-Zeile, die nach Freigabe veröffentlicht.
Details: [instructions/gates.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/gates.md).
- **`publish` endet mit Exit-Code 42 (Publish-Remote-Gate)** - dieser Checkout hat eine
`.wikitool-remotes.json`, und das angesteuerte Remote steht nicht darin. Ebenfalls kein
Fehler: Die Ausgabe nennt die Push-URL, an die geschrieben würde, und die erlaubten. Anders
als beim Mass-Update-Gate gibt es hier **keinen Token und keine Flagge** - stimmt das Ziel
wirklich, trägt der Mensch dessen URL selbst in die Datei ein. Ein Agent, der die Datei
anfasst, um an der Verweigerung vorbeizukommen, öffnet ein Gate aus eigenem Antrieb.
- **Setup oder Update schlägt fehl und ich will es melden** - den Agenten
[instructions/bug-report.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/bug-report.md) ausführen lassen, oder direkt
`tools/bugreport` aufrufen (aus Bash, Git Bash und PowerShell gleich). Der Aufruf sucht sich
selbst ein Python 3.8 oder neuer und überspringt dabei die Store-Aliase, auf die `python3`
unter Windows zeigt. Er braucht weder den Preflight noch ein funktionierendes `wikitool`, und
findet er kein Python, sagt er, warum. Das Skript schreibt ein Bündel nach
`reports/bugreport-<Zeitstempel>/` samt Zip. Geheimnisse werden entfernt, und aus allem, was das Skript selbst erzeugt, bleiben
Seitentitel draußen (`--titles` nimmt sie mit). Der Sitzungs-Trace ist standardmäßig dabei
(`--no-trace` lässt ihn weg) und kann wie Chronologie und Transkripte Seiteninhalt und Titel
enthalten; das Bündel enthält außerdem Maschinen-, Benutzer- und Pfadnamen. Mit `--pseudonymise`
ersetzt das Skript Benutzer-, Host-, Pfad-, Git- und Remote-Namen durch Platzhalter, die Länge,
Leerzeichen, Bindestriche und Pfadtiefe erhalten; der Agent kann danach weitere Namen
(Personen, Firmen, Kunden, Projekte) mit `--bundle … --candidates …` nachtragen. Das ist das
Urteil eines Modells und lässt einen Rest übrig - lies das Bündel vor dem Teilen. Die
Zuordnung, die Prüfliste und die Kandidatendatei enthalten Originale und liegen neben, nie im
Bündel. Es wird nirgends hochgeladen - den Kanal wählst du selbst.
+149
@@ -0,0 +1,149 @@
*Generated from [`docs/knowledge-and-commitment.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/knowledge-and-commitment.md) at v8.0.0 – edit the repository, not this wiki.*
# Why knowledge and commitments are two layers
Chemenu compiles knowledge into `kb/`, and it also tracks what its operator has committed to do.
Those look like one subject - both are "things about my projects" - and the stack deliberately
keeps them apart: `kb/gtd/` holds one page per initiative, an external task tracker holds the
open items, and the only thing that crosses between them is a name. This page is about why that
line was drawn there. The rules that follow from it live in [kb/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/kb/CONTRACT.md)
and the `review`, `new` and `task new` records in [tools/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/tools/CONTRACT.md).
<!-- wikitool:toc -->
## Contents
- [Different half-lives want different machinery](#different-half-lives-want-different-machinery)
- [Pattern 4: separate ownership, no synchronization](#pattern-4-separate-ownership-no-synchronization)
- [The join happens at read time, and stores nothing](#the-join-happens-at-read-time-and-stores-nothing)
- [One name, carrying the duties of an identifier](#one-name-carrying-the-duties-of-an-identifier)
- [Status has exactly one home](#status-has-exactly-one-home)
- [A finished initiative is a state, not a location](#a-finished-initiative-is-a-state-not-a-location)
- [Which tracker is a decision the stack does not make](#which-tracker-is-a-decision-the-stack-does-not-make)
<!-- /wikitool:toc -->
## Different half-lives want different machinery
`kb/` is a compiler for durable things, and every mechanism in it assumes durability: `raw/` is
immutable, a claim has to trace back to a source, a page's title is its identity, the indexes are
generated, and a large change stops at a gate so a human can look at it. All of that is the right
amount of ceremony for something that will still be true next year.
A next action is the opposite kind of fact. It is unsourced - nobody cites a reason for "call the
plumber". It changes several times a week. It is state, not knowledge: the interesting thing
about it is whether it is still open. And it is only correct *now*.
Running both through one layer does not produce a richer wiki; it produces a worse one. Every
task-shaped page carries `provenance: general` because there is no source to bind it to, which
drains that field of meaning for the pages where it matters. `kb/log.md` fills with "task
checked off" entries until the audit trail of what the *wiki* learned is unreadable. Lint findings
about orphans and stale claims start firing on pages that are supposed to be short-lived. And a
weekly pass over the task list trips the Mass-Update Gate every single time, which is how a gate
stops being read and starts being cleared reflexively.
The GTD method this borrows from draws the same line for its own reasons: of its horizons, `kb/`
covers the two slowest - project support material and reference - and nothing faster.
## Pattern 4: separate ownership, no synchronization
Four arrangements were on the table, and three of them fail in ways worth naming.
**One layer** is the case above. **Export** - the wiki writes a task list the tracker imports -
means a checkbox ticked in the tracker is a tick in a view, while the truth sits in a file the
operator was not editing; the two disagree immediately and silently. **Bidirectional sync** works,
at the cost of an id mapping to maintain, a conflict-resolution rule to design, and a deletion
semantics to decide - all of it machinery whose only job is to repair a split nobody needed.
What is left is **separate ownership with no sync at all**: the tracker owns the tasks, `kb/` owns
the project memory, and the single point of contact is the project's name. Nothing is mirrored,
so nothing can drift out of mirror.
## The join happens at read time, and stores nothing
Because there is no shared state, the connection between the two sides has to be made when
somebody actually asks - which is what `wikitool review` does: it reads both sides, matches them on
the case-normalized project name, prints what it found, and saves nothing. Not a cache, not a
mapping file, not even a `reports/` artifact.
That is the same posture `search` takes, and for the same reason: anything it wrote down would be
a third copy of a state the two sides already hold, stale the moment either side moved, and the
first thing to distrust in a report. A read-time join can be wrong about the present, but it
cannot be wrong about the past, because it does not remember one.
## One name, carrying the duties of an identifier
Reducing the coupling to a name is cheap, and it is not free. A name that joins two systems is an
identifier, whether or not anything enforces it, so the design had to pick up an identifier's
obligations explicitly: uniqueness is checked before a project is created rather than discovered
later; a rename is a deliberate, infrequent operation that touches both sides in one pass; and
nothing tries to re-match automatically behind the operator's back.
The last one is what makes the review's *both-directional* report matter. A tracker project with
no page and a page with no tracker project are reported separately, as two findings. They are
usually the two halves of one rename - and reporting them separately is exactly what turns a
silent decoupling into a visible event, at the cost of the review occasionally saying the same
thing twice.
## Status has exactly one home
The sharpest consequence of the split is a rule that feels like a restriction: a `kb/` page never
summarizes its own task list. No "3 open items", no "next: call the supplier".
Two places claiming to know the current status is the failure mode the whole arrangement exists
to avoid, and a summary is a copy with a slower clock. The page says what an initiative *is* -
its goal, its participants, its durable state, why it is worth doing. The tracker says what is
open right now. Anyone wanting the second reads the tracker, or runs the review.
This pays for itself somewhere unexpected: with the page carrying no task state, an agent has no
reason to read the task list at all outside the weekly review. That is what keeps the command
surface as small as it is - two read commands and three write commands - rather than growing a
full CRUD tree over somebody's todo list. The second creation command exists because a single
name is not always the whole story: a source can carry a piece of durable knowledge and a
commitment to follow up on it at the same time - a complaint arriving by email is both something
to file and something to chase - and the tracker-side half of that needs its own write path
alongside `new project`'s pairing of a page with a tracker project. `task new` creates only the
tracker item, never a page; a source that also carries knowledge gets that knowledge filed
through the ordinary page-creation commands, as a separate step. The two are never one
transaction the way `new project`'s tracker-then-page order is within a single command - they are
two independent writes a skill sequences, tracker first, so a failure creating the item leaves no
page and no promoted source material behind it. That holds for an ordinary source; a source large
or broad enough to run through the large-tree procedure instead promotes ahead of its own
per-unit commitment decision, because that procedure hands its units through a workshop directory
that needs them already promoted to address them at all - the same raw-file-without-page state
the ordinary case avoids becomes, there, the expected condition for as long as the run takes. And
a failure on the knowledge side afterwards is exactly the ordinary "a source without a page" state
`lint` already reports.
The write surface stops at *creating* an item and *marking one done* - it never moves a reminder
and never deletes anything. `task close` sets exactly the field the tracker's own "done" checkbox
sets, nothing more: reversible, and it leaves a record in the tracker rather than removing the
item's trace. A command that deleted would take the same shortcut through somebody's task list
that the whole split above exists to avoid - a write this stack cannot undo, made on behalf of a
tracker it does not own. `task list` is the one addition on the read side, and it changes nothing
about the join itself: it exists only because closing an item needs the tracker's own id for it,
and that id was never worth exposing before there was a write that consumed it.
## A finished initiative is a state, not a location
Archiving moves nothing. A completed initiative's page stays where it is and changes its `state:`
value, because the moment an initiative finishes is the moment its page is *most* valuable -
what was decided, what it cost, who was involved - and filing it away is how that gets lost.
The state field carries the distinction the review actually needs, which is not "open vs. done"
but "does silence here mean something is wrong". An initiative that is deliberately paused looks
identical, from the outside, to one that quietly stalled; only the operator knows which. Without a
value for "paused on purpose", the review reports the same untouched initiatives every week, and
a report that is mostly noise stops being read by the third week - which would cost more than the
findings are worth.
## Which tracker is a decision the stack does not make
The tracker is reached through a provider layer, and no instruction anywhere names which one it
is. An instruction that said "open Super Productivity" would bake one instance's tool choice into
the shared stack, and the next instance - a different context, a different employer, a different
set of constraints - would have to edit prose to change a setting.
So the provider lives in configuration (`.wikitool-tasks.json`), the adapters live behind one
protocol, and a capability the provider lacks surfaces as an ordinary tool error rather than as a
paragraph of instruction explaining what this particular tracker cannot do. A provider that
cannot create a project, for instance, stops and asks the operator to do it - the same posture the
gates take, and for the same reason: better a visible stop than an invented workaround.
+139
@@ -0,0 +1,139 @@
*Generated from [`docs/language-boundaries.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/language-boundaries.md) at v8.0.0 – edit the repository, not this wiki.*
# Language Boundaries
Two languages run through this repo at once. `kb/` is written in whatever language the instance
chose - German here, and the value lives in `kb/CONVENTIONS.md`'s `language:`. Everything that
tells an agent what to do - [AGENTS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md), every `CONTRACT.md`, everything under
`instructions/` - is written in English, in every instance, whatever the first value says.
The rule itself is in [AGENTS.md § File naming](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md#file-naming). This page holds the
part that is not a rule: why the line runs where it does, why the English half is not a setting,
and which argument for it turned out to be wrong.
<!-- wikitool:toc -->
## Contents
- [The axis is the reader, not the owner](#the-axis-is-the-reader-not-the-owner)
- [Why the control plane's language is English](#why-the-control-planes-language-is-english)
- [Why it is not a parameter](#why-it-is-not-a-parameter)
- [What the KB language still decides](#what-the-kb-language-still-decides)
- [Where the line runs around a page type](#where-the-line-runs-around-a-page-type)
- [What would put this back on the table](#what-would-put-this-back-on-the-table)
<!-- /wikitool:toc -->
## The axis is the reader, not the owner
For a long time the two halves could be told apart by asking who owned the file, and the answer
came out right every time: the stack owns `AGENTS.md` and the contracts, which are English; the
instance owns its pages and the templates that shape them, which are in the KB language. The
ownership boundary is a real and load-bearing thing - [ownership-and-templates.md](Ownership-and-templates)
is about what it buys - so it was easy to read the language split as one of its consequences.
It is not. The case that separates them is a page type an instance adds for itself. `types/`
takes a new type without a code change, so an instance can write one; that file is instance-owned
from the first line to the last, ships nowhere, and is nobody's to overwrite. Its authoring
guidance is still instruction addressed to an agent, and reads exactly like the guidance in the
four types the stack ships. Ownership says "yours"; the audience has not moved at all.
So the question a line answers is not *whose file is this* but *who reads this line*, which is
the same cut [kb/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/kb/CONTRACT.md#language-and-identifiers) already makes inside a
single page between prose and identifiers - applied one level up, to the halves of a document.
Ownership decides who may change a sentence. The reader decides what language it is in. The two
questions were answered together for as long as they happened to agree.
## Why the control plane's language is English
Not because English is better for the purpose, and not to be neutral: this instance's operator
reads German, and the pages are German for that reason.
- **The control plane is almost entirely about identifiers, and the identifiers are English.**
`base_dir`, `provenance: sourced`, `--confirm`, exit 42, `root: kb`. A sentence in another
language explaining when to set `page_ref_fields` is already half English by the time it
reaches the verb, and the prose/identifier boundary inside it becomes something a reader has to
work out line by line.
- **It quotes a body of material that is English and stays English.** The harness documentation
it has to agree with, the vendored skill-authoring sources under `commonplace/`, the tool's own
`--help`. A contract that translates their vocabulary makes its own claims harder to check
against them, not easier.
- **One language keeps instances comparable.** Two instances running the same stack version hold
the same control plane byte for byte, so a question about one is answerable from the other -
and anything an instance changes locally shows up as a difference in content rather than in
language.
## Why it is not a parameter
The natural next move, once `kb/CONVENTIONS.md` holds `language:`, is a second value beside it -
`control_plane_language:` - defaulting to English and settable by an instance that would rather
read its contracts in its own language. That option is deliberately not taken.
- **The knob's cost is paid by every file; its benefit lands on the few a human reads.** Every
rule about writing an instruction would have to name which of the two languages it means, every
example would need a note saying which one it is in, and every review of an instruction would
start by establishing which language it should have been in. The stack has one mechanism for
that class of problem - one rule, one place (AGENTS.md invariant 8) - and a second language
value forks it everywhere at once.
- **The document the knob is for is read by an agent.** An instruction, a contract, a type-spec's
guidance half: the reader is a model, and a model reads the English fine. What the *operator*
reads is unaffected by any of this - see the section below.
- **Today's local document is tomorrow's upstream candidate.** An instruction an instance wrote
for itself is the most likely thing it ever contributes back. Written in the KB language it
would have to be translated first, and the translation would have to re-derive the
prose/identifier boundary that the original author had in their head and did not write down.
- **Nothing would check it.** There is no mechanical test for what language a paragraph is in -
a stop-word scan flags the quoted vocabulary the rule deliberately keeps and misses a cleanly
translated paragraph. A setting nothing enforces produces drift that is visible only to whoever
next opens the file.
## What the KB language still decides
Making the control plane English does not make the instance's language an implementation detail.
`kb/CONVENTIONS.md`'s `language:` decides two things, and both are the ones an operator actually
experiences:
- **Page text.** Every page under `kb/`, and inside the page type-specs exactly the parts that
become page text - each one's `## Template` block, its `layout:` titles, and the subtype
templates `types/<name>.<value>.md` beside it, which are page text from the first line to the
last.
- **What an agent says.** An agent speaks the KB language, whatever the file it just read was
written in. An instruction that models a sentence for the operator writes that model in
English, like the rest of the control plane, and the agent delivers it in the instance's
language.
So an operator who reads no English gets German pages and German answers from an agent reading
English instructions. The English is what the machinery is written in, not what it says back.
## Where the line runs around a page type
A page type's contract is where the two languages meet most closely, and it is worth knowing
which part is which before editing any of it. Its authoring guidance addresses an agent and is
English; its `## Template` block, `layout:` titles and subtype templates become the literal
headings of pages and follow the KB language; its field names and enum values are identifiers and are translated in
neither direction.
The language line did not move when the *file* line did. A `root: kb` type-spec may now put its
authoring guidance in a separate, stack-owned `types/<name>.guidance.md` rather than carrying it
beside the template, but that split was made for ownership reasons - so an upgrade can improve
the guidance without overwriting what the instance chose - and it leaves this page's argument
untouched: each part is still written in the language its own reader needs, and a type-spec that
declares no `guidance:` keeps both halves in one file with exactly the same rule applying inside
it. [types/type-spec.md § Who owns a type-spec](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/types/type-spec.md#who-owns-a-type-spec) has
the split as a table, and [ownership-and-templates.md](Ownership-and-templates) § "Where the
file boundary used to strain" has what it cost to keep two audiences in one file for as long as
it did.
## What would put this back on the table
A `docs/` page goes stale when the reasoning stops holding rather than when the code changes, so
it is worth naming what that would look like here. Two things would:
- **A human starts reading the control plane directly and routinely** - not an operator checking
a rule now and then, which is the case today, but a workflow where people rather than agents
are the primary readers of `instructions/`. The second argument above is the one that fails
first, and it is the load-bearing one.
- **The identifiers stop being English.** If the tool's own vocabulary were ever localized, the
first argument would invert: the prose would then be the only English left in a file that is
otherwise not, which is the situation this page argues against.
Neither is close. Both are cheaper to notice here than to rediscover in an argument about a
single file.
+122
@@ -0,0 +1,122 @@
*Generated from [`docs/model-and-effort-selection.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/model-and-effort-selection.md) at v8.0.0 – edit the repository, not this wiki.*
# Choosing a Claude Code model and effort level
Claude Code exposes three choices this repo has an opinion on: which model a session itself
runs as, what model a spawned subagent gets, and which `/code-review` effort level to pick.
None of them are enforced anywhere - the gates in [instructions/gates.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/gates.md)
are code precisely because a model cannot be talked out of them
([why-gates-are-code.md](Why-gates-are-code) makes that argument for gates; this page applies
the same axis to who is holding the keyboard). What follows is a reference for making that choice
well, not a rule anything checks.
The axis worth tracking is not how important a task feels, but **what would catch a mistake in
it**. Work behind `pytest`, `docs verify`, `instructions verify` or CI surfaces a bad call within
one more round. Work behind nothing but a session reading prose does not surface at all - it
ships, and stays until someone happens to notice. That asymmetry, not task size, is what the
phase guide below is built on.
<!-- wikitool:toc -->
## Contents
- [Phase guide](#phase-guide)
- [Model per session, not per phase](#model-per-session-not-per-phase)
- [Subagent models](#subagent-models)
- [`/code-review` effort](#code-review-effort)
- [When it's unclear](#when-its-unclear)
- [Scope](#scope)
<!-- /wikitool:toc -->
## Phase guide
<!-- dist:strip-start -->
This repo's own stack-development work splits the axis into three phases, one skill each -
`stack-dev` (design), `stack-build` (build) and `stack-close` (closing) - handed over through
states in the issue tracker rather than inside one session:
<!-- dist:strip-end -->
| Phase / task | What would catch a mistake | Suggested model | Effort |
|---|---|---|---|
| `wiki-status`, simple `wiki-query` lookups | the answer is re-checkable against the corpus | Sonnet | default |
| `wiki-lint` | `lint` itself is the check | Sonnet | default |
| `wiki-ingest`, `wiki-manage`, judgment-heavy `wiki-query` | `lint` and `docs verify`, partly | Sonnet | high |
| Stack dev: design, the version part, a boundary-crossing judgment | nothing mechanical | Opus | high |
| Stack dev: code, tests, mechanical doc sync, waiting for CI | `pytest`, `docs verify`, `instructions verify`, CI | Opus (open - see below) | high; medium for a small change |
| Stack dev: closing an issue, `docs/` staleness, changelog prose | nothing, by construction | Opus | high |
The two unchecked rows are short - minutes, not hours - so keeping them on the strongest model
costs little and protects the only work that fails silently. The checked middle row is where the
tokens are, which makes it the tempting one to run cheaper. It is also the row with the least
settled answer: a build phase of this stack reads a lot of the tree, and a smaller context window
does not hold it - it runs into compaction, which costs more than the cheaper model saves. Sonnet
at high effort remains possible, but only as a session of its own (below).
**Effort is the cheaper lever than the model.** A reduced effort level is what gives up
multi-file consistency first, so `high` is a reasonable floor for anything touching more than one
file or a contract; `default` or `medium` suits a small mechanical change with a test behind it.
## Model per session, not per phase
A session cannot switch its own model - that is the user's `/model` - and it should not be asked
to mid-flow either. Two reasons:
- **A switch throws away the prompt cache.** A cache entry belongs to the model that wrote it,
so a new model starts the session's whole history from cold. The same holds for effort:
changing it always invalidates the cached message history - by far the largest part of a long
session - and, on some models, the tool and system prefix too (Anthropic's prompt-caching
documentation lists effort and the thinking configuration among what invalidates the cache). A
switch from one effort to another costs the same re-read of the whole session as a switch of
model.
- **An offered switch is rarely taken.** A sentence in the output at the moment a phase changes
is easy to read past - for the agent writing it and for the user reading it - and the session
just carries on in whatever it started as.
So the choice is made once, when a session starts, and phases that want different models or
effort levels are separated by a session boundary instead: `/clear`, then the next phase in a
session started the right way. That is only cheap if the next phase does not depend on the
previous session's context - which is why the handover has to live somewhere outside the session
(an issue body, a page) and be kept current at fixed points, not reconstructed at the end.
<!-- dist:strip-start -->
In this repo the handover points are a ready issue body (design → build) and a green CI run with
the body updated (build → closing); the two later skills can only be started by the user's slash
command, so each phase change is a real stop at which that choice is made.
<!-- dist:strip-end -->
## Subagent models
The `Agent` tool's `model:` parameter (`haiku`, `sonnet`, `opus`, `fable`) is a per-subagent
choice a session *can* make on its own:
- Read-only search/lookup (an `Explore` agent, or a `general-purpose` agent doing pure
retrieval): `haiku` - no judgment is being delegated, only retrieval.
- A subagent that writes pages, reviews code, or decides something: leave `model:` off so it
inherits the parent session's model.
- A fork (`subagent_type: "fork"`) always inherits the parent's model; a `model:` override on a
fork is ignored.
## `/code-review` effort
- A routine diff: `low` or `medium` - fewer, high-confidence findings are enough.
- Gate code, the compiler, or a change about to ship in a version bump: `high` and up - broader
coverage is worth it when the blast radius of a missed bug is a safety gate.
- `ultra` is user-triggered and billed separately - worth recommending, not assuming.
## When it's unclear
- A task spans both a mechanical step and a judgment call: weigh it by the judgment call, not the
mechanical one - the tooling carries the mechanical part regardless of which model supervises.
- No row fits cleanly: Sonnet at high effort is a safer default than the most capable model at
the highest effort. Under-provisioning where a check exists costs one worse answer once;
reflexively over-provisioning is a standing cost every session pays.
- Not sure whether a phase is checked: treat it as unchecked - a needless Opus phase costs money
once, an unchecked Sonnet phase can ship something nobody looks at again.
- The phase changed and the session runs on a model or effort the table would not pick: keep
working, and cut the session at the next handover rather than switching mid-flow - never block a
publish or an issue close on a choice the session cannot make itself.
## Scope
Specific to Claude Code: the model names, the `/code-review` dial and the `Agent` tool's `model:`
override have no equivalent in this repo's other supported harnesses (Codex CLI, GitHub Copilot
CLI, Mistral Vibe). Does not set the classifier model behind Claude Code's own `auto` permission
mode - that is a harness internal, not a per-task choice this repo controls.
+225
@@ -0,0 +1,225 @@
*Generated from [`docs/ownership-and-templates.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/ownership-and-templates.md) at v8.0.0 – edit the repository, not this wiki.*
# Ownership and Templates
Chemenu ships two kinds of files side by side, and at a glance they look the same: both are
plain markdown, both sit in the repo root or under `kb/`, both get read at session start. But a
stack upgrade treats them completely differently. Some - [AGENTS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md),
[kb/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/kb/CONTRACT.md), the per-stage contracts - are identical in every instance
that runs this stack and are the next release's to replace (with one caveat about local edits,
below). Others - `USER.md`,
`SOUL.md`, `kb/CONVENTIONS.md`, `ENVIRONMENT.md` - describe one particular instance, and
overwriting them would silently erase a choice someone made on purpose.
<!-- wikitool:toc -->
## Contents
- [Two different kinds of truth](#two-different-kinds-of-truth)
- [Why silent overwrite is the failure being designed against](#why-silent-overwrite-is-the-failure-being-designed-against)
- [Why the boundary is a predicate rather than a list](#why-the-boundary-is-a-predicate-rather-than-a-list)
- [Why a `.template`, not just an absent file](#why-a-template-not-just-an-absent-file)
- [Where the file boundary used to strain](#where-the-file-boundary-used-to-strain)
- [The consequence in practice](#the-consequence-in-practice)
- [Why an instance comes only from a release](#why-an-instance-comes-only-from-a-release)
<!-- /wikitool:toc -->
## Two different kinds of truth
The stack-owned files describe how the tool works. `kb/CONTRACT.md` opens by saying it holds
what `tools/wikitool` enforces or what follows mechanically from how it operates - see
[kb/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/kb/CONTRACT.md), lines 10-13. That kind of statement doesn't vary by
instance: the compiler behaves the same way regardless of who is running it, so the sentence
describing that behavior can be copied byte-for-byte into every checkout without becoming
wrong anywhere.
The instance-owned files describe a choice: which language pages are written in, what tone the
agent takes, who the operator is, which git remote is authoritative, which MCP servers are
reachable. None of that follows from the tool's mechanics - two instances of the identical
stack can answer all of these differently and both be correct. [AGENTS.md § Personalization](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md#personalization)
frames the split the same way for `kb/CONTRACT.md` versus `kb/CONVENTIONS.md`: "the split is by
who may change the sentence, not by what it is about." A rule about page structure could in
principle have been written per-instance too, but then every instance answering "not German" to
setup would be hand-editing a file the stack also ships, and the next release update would
hand the stack's file back to it, discarding the customization.
## Why silent overwrite is the failure being designed against
A stack update is meant to be a routine, low-risk operation: pull the latest release, get
whatever fixes and features shipped since the last one. That only stays low-risk if the update
knows which files it's allowed to touch. If `USER.md` or `kb/CONVENTIONS.md` were treated the
same as `AGENTS.md` - shipped and periodically re-copied - an upgrade would quietly replace a
description of *this* operator, in *this* language, with whatever placeholder or default the
stack maintainers wrote. The damage wouldn't be loud: nothing crashes, the files still parse,
the agent just starts acting on the wrong premises until someone notices the voice or the
language changed.
Keeping the boundary at the file level, rather than trying to merge changes within a shared
file, means an upgrade never has to guess which lines are "stack" and which are "instance" -
the file itself already answers that.
## Why the boundary is a predicate rather than a list
For a while the boundary was written down as a list of paths - once in `dist_cmd.py`, once in
the merge procedure a private instance was told to run by hand, and once in the check that
procedure ended with. Three copies of one fact, which is the shape [AGENTS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md)
invariant 8 exists to forbid, and they drifted exactly as predicted: the hand-run procedure was
still naming three paths after the collection contracts had moved to the instance's side of the
line, so it discarded upstream changes to files it had never heard of, while its own final check
excluded the same three paths and therefore reported success.
`chemenu/ownership.py` replaced the lists with one question - is this path, under a content
stage, the stack's or the instance's? - answered by shape rather than by enumeration:
`<stage>/CONTRACT.md`, and anything ending `.template`. Every consumer asks it - at the time,
`dist export` and a `wikitool upstream merge` that took the hand-run procedure's place; since
that path was removed, the export alone - so no two of them can disagree, and a machinery file
added under a content stage tomorrow is recognised without any of them being edited. The deeper point is not the
deduplication: a list has to be maintained by whoever remembers it exists, and the failure mode
when nobody does is silence, because a path the list has never heard of simply looks like
content.
## Why a `.template`, not just an absent file
The mechanism for instance-owned content is a `.template` file the distribution ships instead
of the real one - `USER.md.template`, `SOUL.md.template`, `kb/CONVENTIONS.md.template`,
`ENVIRONMENT.md.template`. An alternative would have been to ship nothing at all and let a
brand-new instance start from a blank page. The template exists because a blank page doesn't
tell [instructions/setup-instance.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/setup-instance.md) what shape the answer
should take, and it gives nothing for a validator to check afterward.
A template carries a placeholder value - a sentinel - in the fields that need a real answer.
Setup interviews the operator and replaces the sentinel with what they actually said. That
gives `doctor` a mechanical way to tell "personalized" from "not yet": a file that still
contains the sentinel hasn't been through setup, regardless of whether the file exists. That's
also why `ENVIRONMENT.md` only warrants a WARN rather than a FAIL when absent - see
[AGENTS.md § Environment](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md#environment) - while a missing or unfilled
`USER.md`/`SOUL.md`/`kb/CONVENTIONS.md` is a harder failure: `ENVIRONMENT.md` describes one
checkout among possibly several and is gitignored for that reason, so its absence is a normal
state rather than a sign setup was skipped.
## Where the file boundary used to strain
"The file itself already answers that" held for every file above except one shape: a `root: kb`
type-spec used to carry two audiences inside one file.
Its authoring guidance - when to use this type, what each frontmatter field means, how to cite -
was instruction to an agent. It read like the stack's own prose because it *was* the stack's own
prose: a later release that learned something about writing entity pages would want to improve it
everywhere. Its `## Template` block and its `layout:` titles were the opposite: they became the
literal headings of pages this instance writes, in the language this instance chose, and no
release had any business touching them.
The same file is where the language question comes apart from the ownership one, and for the same
reason: ownership decides who may change a line, its reader decides what language it is in -
which is why a `root: kb` type-spec still keeps English prose around a template block written in
its own language. [language-boundaries.md](Language-boundaries) has that argument; this page is
about ownership alone.
Ownership is per file, so a file carrying both audiences had to give both halves to whoever owned
it. The template half was correct that way. The guidance half paid for it: an instance that
adopted its type-specs at setup never received an improvement to the guidance again, because
`dist upgrade` wrote the `.template` beside the adopted file and never the file itself. Nothing
broke, and nothing reported it - the instance simply kept reading the guidance it was handed the
day it was created.
That was not an argument against the per-file boundary; the boundary is what makes an upgrade
safe at all, and merging inside a shared file is the failure the whole section above is about. It
was an argument that this particular file was cut in the wrong place - so it was cut again. A
`root: kb` type-spec may now declare `guidance:`, a repo-relative path to a second,
stack-owned file (`types/<name>.guidance.md`) holding exactly the half that used to be stranded:
when to use the type, when not to, and mechanism-level advice that holds for every instance. That
file ships verbatim and upgrades like any other machinery file, whether or not the type-spec that
links it has ever been adopted. `types/type-spec.md` §§ "Who owns a type-spec" and "Anatomy of a
type" hold the current shape; `tools/wikitool types describe <name>` composes the type-spec and its
guidance into one answer, so an agent asking for a type's contract never needs to know it comes from more than one
file. An instance that adopted its type-specs before this split existed takes it as an *offered*
migration rather than something an upgrade applies on its own - the same reasoning as any other
instance-owned file in the middle category below, spelled out for this one case because it is the
case that motivated the category existing at all.
A type-spec that declares no `guidance:` - one an instance writes entirely for itself - is
unaffected: it is still described from its own body alone, the way every type-spec worked before
`guidance:` existed. The split is optional exactly where there is no stack-owned improvement to
receive.
## The consequence in practice
An upgrade sorts every shipped path into three categories, not two - and the third one only
becomes visible once an upgrade is a command rather than a hand-run copy:
- **Verbatim files** - `AGENTS.md`, `kb/CONTRACT.md`, the per-stage contracts, everything under
`tools/`, `types/` and `instructions/` - are the release's to replace. This is where a `root:
kb` type-spec's optional `types/<name>.guidance.md` sits: verbatim, even though the type-spec
it documents (below) is not.
- **`.template`-sourced files** - `USER.md`, `SOUL.md`, `kb/CONVENTIONS.md`, each
`kb/<name>/COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs and their subtype
templates `types/<name>.<value>.md` - are never written by an upgrade at all. A subtype
template is the guidance split run the other way: the file boundary cut once more, this time
to give page material a file of its own, and page material belongs to the instance, so it
lands here rather than among the verbatim files - and because it is its own file, a release
can ship a new one without touching an adopted type-spec. The distribution ships only the `.template` beside them, so the filled file
is out of reach by construction rather than by a rule someone has to remember. The same
property has a second face on the way in: when a release ships a `.template` for a type or
collection the instance does not have *yet*, the upgrade writes the template and stops - it
cannot write the filled file without deciding the instance's own language and wording for it.
Adoption is therefore an act the instance performs, and where the stack *requires* that type
(the `source` idiom, and `project` since 7.0.0) an upgrade that skips it leaves a tree
`docs verify` refuses. That is the ownership boundary working rather than a gap in it, but it
is the one shape in which "the upgrade never writes this file" turns into work somebody has to
do; `instructions/upgrade-instance.md` carries the step.
- **Seeded-once files** - `.wikitool-kb.json`, `CHANGES.md`, `kb/log.md`, `raw/.gitkeep` - are
written into a *new* instance by `dist export` and belong to the instance from then on. They
are the awkward category: they sit in the release stamp's file list like any other shipped
file, so an upgrade has to exclude them deliberately (`chemenu.ownership.is_export_stub` and
`is_upgrade_preserved`). An upgrade that re-seeded them would reset the record of which
migrations ran, or erase the changelog the instance wrote for itself.
The first category carries a caveat that the word "verbatim" hides. It says who *decides* the
content, not that overwriting is always safe: an instance can still have edited a verbatim file
- a patched `tools/`, a locally adjusted instruction - and an upgrade assuming otherwise would
destroy that silently. Avoiding that assumption is the whole reason `dist export` records a
sha256 per shipped file in `.wikitool-release.json`. `wikitool dist upgrade` compares every
candidate path against the digest recorded when it was installed, overwrites only what still
matches, and refuses rather than overwrite what does not.
So the practical rule is narrower than "overwrite the verbatim files, leave the rest alone":
overwrite the verbatim files *this instance has not touched*, never write the other two
categories, and make a locally changed file a decision someone takes deliberately instead of
one an upgrade takes for them. The template-sourced files were filled in once, by a person, for
a reason, and nothing about a newer release of the stack's mechanics gives it standing to
override that.
The same rule runs in the other direction, for a file a release stops shipping, and there it has
a deadline the overwrite side does not. The stamp an upgrade writes is the new release's, and the
path is no longer in it - so the next upgrade cannot tell that file from one the instance wrote
itself, and an instance-owned file is exactly what an upgrade must never touch. Whatever happens
to a retired file therefore happens in the run that sees it go, or not at all. An unchanged one is
deleted as silently as an unchanged one is overwritten; a changed one is a local change like any
other and needs the same deliberate answer, where keeping it makes it the instance's own for
good. Leaving the decision optional, as it once was, did not defer it: it made it, the wrong way,
and left orphans behind - harmless under `tools/`, where nothing discovers modules by listing a
directory, and not harmless under `instructions/` or `types/`, where an orphaned skill keeps
being published and an orphaned type-spec keeps being a type.
## Why an instance comes only from a release
Everything above depends on one file every instance carries: the `.wikitool-release.json` its
release wrote. It is the base `dist upgrade` classifies against, the marker that turns telemetry
off for someone who never asked for it, and the record of which stack version the instance runs.
An instance that starts anywhere else starts without that base, and every later step has to
reconstruct the boundary by other means.
For a while there were four ways in: a release, a `dist export` from a checkout of the origin
repository, a clone of that repository, and a private clone that kept the origin as a git
`upstream` and took stack updates by merging. The last two never had the stamp, so they needed
the boundary a second way. The clone took the origin's demo corpus, demo persona and development
skills with it and had to be emptied by hand, and the instruction for doing so neither said what
had to survive nor fitted into the iteration budget. The merge path needed its own
ownership-aware command, `upstream merge`, which shipped two data-destroying bugs before it was
right, and still left a checkout with two remotes and no stamp. None of the four was in use when
they were cut down to one in 8.0.0.
What is left is a single shape. A release is an export packed as a tarball, installed into an
empty folder - or an empty clone of the instance's own repository - by a script attached to the
same release. `dist export` remains, as the tool that builds a release and tests what one would
ship, not as a way to install. A clone of the origin repository remains too, as the place the
stack is developed, and is never an instance.
+80
@@ -0,0 +1,80 @@
*Generated from [`docs/pipeline-rationale.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/pipeline-rationale.md) at v8.0.0 – edit the repository, not this wiki.*
# Why the pipeline has four stages
Chemenu could, in principle, be one directory: drop a file in, ask a question, get an answer
computed fresh each time. It isn't built that way. The pipeline in
[AGENTS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md#routing) - `raw/` -> `[types/ + tools/]` -> `kb/` -> `reports/`, with
`work/` alongside rather than inside it - separates *material* from *meaning* from
*byproduct*, and each seam exists because collapsing it costs something specific.
## Why raw material stays untouched
[raw/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/raw/CONTRACT.md) keeps a source exactly as it arrived. The reasoning is
simple once stated: the moment someone "cleans up" or reformats a source on the way in, the
thing later claims get checked against is no longer the thing that was actually said. An
immutable `raw/` means a citation always resolves to the original, not to somebody's tidied
memory of it. It also draws a trust boundary in one place instead of scattering it - everything
past `raw/` can be treated as reviewed, because nothing upstream of it silently already was.
`incoming/` sits entirely on the near side of that boundary: a file waiting there is
not yet reviewed and not yet a citation target, so its being gitignored and readable by an
ingest session does not weaken anything - the boundary is the promotion into `raw/` itself, not
the moment a human happened to drop a file somewhere.
Once a document can arrive from *outside* - the MCP server's optional `submit` tool - "a human
happened to drop a file somewhere" stops describing how everything reaches `incoming/`, so the
near side of the boundary gets a stage of its own rather than a second meaning. `mcp-upload/`
holds what nobody has looked at yet; `incoming/` holds what someone has. Two arrows, two
different things being granted: `upload accept` grants *trust* (a human read the material and
took responsibility for it), `raw accept` grants *immutability* (it becomes a citation target
and stops being editable). Collapsing them would have meant one of the two lying - either an
unreviewed stranger's file sitting in the same directory a human's own drop does, or the
promotion into `raw/` quietly doubling as the review step it cannot perform.
## Why extraction happens once, through a schema
[types/type-spec.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/types/type-spec.md) is what stands between a raw file and a `kb/` page:
a type-spec defines what a conforming instance of a page looks like, and the compiler
(`tools/wikitool`) applies it. The alternative - every query re-reading and re-interpreting the
source on demand - would mean paying the cost of understanding the material every single time,
and getting a slightly different answer each time depending on how the question was phrased.
Extracting once, against a fixed schema, turns "re-read and re-guess" into "look up what was
already compiled." That is the "never re-derive, always compile" principle from
[AGENTS.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md): understanding a source is expensive and worth doing exactly once,
after which it becomes a cheap, stable lookup.
## Why a `kb/` page has to stand on its own
[kb/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/kb/CONTRACT.md) sets the bar for the compiled layer: a page should answer a
future question without sending the reader back to the source it came from. That's the payoff
of compiling in the first place - if every answer still bottomed out in "go re-read the raw
file," the `kb/` layer would just be a pointer with extra steps, and the cost of extraction
would have bought nothing. A page that stands alone is what makes the corpus fast and
consistent to query: the work of understanding is already sitting there, done.
## Why `reports/` doesn't need to be maintained
[reports/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/reports/CONTRACT.md) treats most of what lands in `reports/` -
lint output, telemetry traces - as disposable. The structural content of a lint report can be
recomputed from the tree at any commit, so keeping an old copy around would just be a second
version of something the tool can already answer on demand, and a second copy is exactly the
kind of thing that quietly goes stale. Treating it as derived output rather than a fourth thing
to maintain means there is nothing there to fall out of sync - regenerating it is cheaper than
reconciling it. The one part that genuinely can't be recomputed - the judgment a pass produced -
is carried out into `kb/` or `kb/log.md` before the report itself is discarded, which is the
distinction between what's recomputable and what isn't.
## Where `work/` fits
[work/CONTRACT.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/work/CONTRACT.md) describes a workshop, not a fifth pipeline stage: a
place for the notes, extracts and open decisions of a task that spans more than one session, on
its way toward becoming a `kb/` page. It sits beside the raw -> kb -> reports flow rather than
inside it - closer in spirit to a desk than to a conveyor belt.
## The shape this produces
Four stages, each answering a different question: `raw/` - what was actually said; `types/` +
`tools/` - how to turn that into structured understanding; `kb/` - what is now known;
`reports/` - what a pass over the corpus noticed in passing. Keeping them separate is what lets
each one be trusted for what it is, instead of every layer having to double as all four at
once.
+139
@@ -0,0 +1,139 @@
*Generated from [`docs/version-model.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/version-model.md) at v8.0.0 – edit the repository, not this wiki.*
# Why the stack version splits compatibility from migration
A stack version number looks like it answers one question. It actually answers two, and the two
are independent of each other.
<!-- wikitool:toc -->
## Contents
- [Two questions, not one](#two-questions-not-one)
- [Why "kb/ untouched" is not proof of anything](#why-kb-untouched-is-not-proof-of-anything)
- [Reading compatibility off the leftmost non-zero component](#reading-compatibility-off-the-leftmost-non-zero-component)
- [Downgrade is half the promise](#downgrade-is-half-the-promise)
- [A promise made to a machine, not only to a person](#a-promise-made-to-a-machine-not-only-to-a-person)
- [The 2.0.0 story](#the-200-story)
- [Why a number is only spent by a release](#why-a-number-is-only-spent-by-a-release)
- [Where the procedure lives](#where-the-procedure-lives)
<!-- /wikitool:toc -->
## Two questions, not one
The first question is whether the new version is a drop-in replacement for the old one - whether
an existing instance can install it, and can also go back, without anyone doing hand-work. That
is what a version number *is*: a promise. The second question is whether the existing corpus in
`kb/` needs to change shape to keep working under the new version. These sound like the same
question, because most of the time a change that breaks compatibility also happens to touch
content, and most of the time a change that leaves content untouched also happens to be
compatible. The correlation is real; it just is not a law. `instructions/dev/version-parts.md`
carries the actual test for telling them apart and the steps that follow from it - this page is
about why the split exists at all.
## Why "kb/ untouched" is not proof of anything
The tempting shortcut is: if no page in `kb/` had to change, the bump can't be that serious. This
is exactly backwards for a class of changes that live entirely outside the corpus - a renamed
release artefact, a Python import path, an environment variable, the URL an instance's own
updater points at. None of those touch a single page. All of them can strand an existing
instance just as thoroughly as a rewritten type-spec would. The corpus is the part of the stack
that looks at itself; the compatibility question is about everything an instance depends on to
keep functioning, most of which the corpus never sees.
## Reading compatibility off the leftmost non-zero component
Semantic versioning gives every component a job, but only one of them is where an existing
instance's tooling actually looks to decide "is this safe." On a `2.x` stack that is MAJOR; on a
still-pre-1.0 `0.x` stack, by the same convention, it's MINOR - the leftmost slot that isn't
pinned to zero is the one an automated updater treats as the compatibility boundary. Bump
anything to its left, or bump that slot itself, and the promise changes. Everything to the right
of it can move as freely as the project likes without touching that promise. This is why the
question "is it boundary-crossing" always resolves to one specific digit, not to a feeling about
how big the change is.
## Downgrade is half the promise
It's natural to test compatibility by only asking "does the upgrade work." The other half -
"can an instance that upgraded put the old version back and land where it started" - carries
equal weight, and it's the half that's easy to forget because forward motion is what everyone is
testing for anyway. A state file the old version can no longer parse, a generated index in a new
shape, a stamp file that got renamed: none of these have to break the upgrade to break the
downgrade. An instance that can go forward but not back has already lost the property a
compatible version number is supposed to guarantee.
## A promise made to a machine, not only to a person
A human reading a changelog can absorb "this technically isn't compatible but it's fine, just
update those two things by hand." An instance's own update mechanism cannot. It reads a version
number, decides whether to pull the new release, and has no channel for nuance - which is exactly
why the update path itself is one of the sharpest ways to cross the boundary invisibly: if the
new version moves where updates come from, the very channel that would have told an instance to
adjust is the channel that just broke. The version number isn't documentation aimed at a reader;
it's an input consumed by code that has no other way to ask.
## The 2.0.0 story
This isn't hypothetical for this stack. The rebranding that produced Chemenu renamed the repo,
the release artefact, and the Python package - and left every page in `kb/` untouched. The first
instinct was a MINOR bump, on the reasoning that nothing in the corpus needed migrating. That
reasoning was correct on its own terms and answered the wrong question. Three things broke
underneath it: every existing instance's `update_url` pointed at a repo path that no longer
existed and, because it's a machine-written file, couldn't be hand-repaired; the release artefact
name changed, breaking every download script and pin against it; and the import name changed,
breaking anything importing the package from outside the shipped tree. The corpus had nothing to
say about any of this, because none of it lived in the corpus.
What caught the mistake was a person looking at the diff and asking whether it really was a
drop-in replacement, not a validator. No check in `docs verify` or anywhere else confirms that a
version part was chosen correctly - it only confirms that a boundary-crossing bump documents
what it breaks. The 2.0.0 entry in `CHANGES.md` carries the corrected reasoning in full, and the
version bump that shipped it was `--major --no-migration`: boundary-crossing and untouched
corpus, at the same time, which is precisely the combination the two-question split exists to
make visible.
## Why a number is only spent by a release
Everything above is about what a version number *promises*. A separate question turned out to
matter just as much in practice: how many numbers get handed out along the way to making one
release. For a while the answer was "one per bump," and that turned out to be the wrong grain
entirely.
Two mechanisms decide when a number gets minted, and they answer different questions. CI's
version gate asks a *commit*-level one: has this tree changed since the last push, and if so
has `VERSION` moved with it. A release asks something else entirely: is this a state worth
handing to someone, under a number they will pin against. Tying the second to the first - every
`VERSION` move firing the release workflow - answers the gate correctly and the release question
by accident, because it treats every bump as if it were about to ship when most bumps are steps
toward a release that has not happened yet.
The failure mode is not phantom numbers; every one of those releases was real, tagged and
downloadable. It is that "real" stopped meaning anything. On 2026-09-03 this repository cut four
releases in six hours - `4.3.0` through `4.3.3` - for one continuous arc of work, two of them for
prose changes alone. Someone tracking the feed saw four upgrades and had no way to tell which, if
any, was a moment worth stopping for. A release is a promise addressed to a consumer, and a
promise made four times an afternoon is not a smaller promise, it is a less legible one.
The fix is not to slow the gate down - it still wants `VERSION` to move every time, and it still
gets that. It is to stop treating every movement as a number worth publishing. Between two
releases the stack now carries one running candidate, escalating through `-beta.N` as bumps
accumulate, and only `version release` spends the number for real by fixing it and closing its
changelog entry. A number is proposed by a bump and spent by a release; conflating the two was
the actual defect, not the arithmetic of any single bump.
This is also why a candidate never gets to a distributed instance. The promise a released version
makes - "install this, and it is exactly what its number says" - has no equivalent for something
still being decided during a single dev checkout's session. `release.yml`'s only job with respect
to this is refusing to act on a suffixed `VERSION` at all: not because a beta is unsafe, but
because there is nothing yet to promise.
## Where the procedure lives
The drop-in test, the catalogue of changes that cross the boundary with no page touched, and the
steps for a boundary-crossing bump - the `--breaking` lines, the migration document or
`--no-migration` reason, talking to the user before bumping - are one procedure, kept at one
place: `instructions/dev/version-parts.md`.
Named as a plain path rather than linked, because it is not here to link to. `dist export`
prunes `instructions/dev/` wholesale, so that file exists only in the origin repo - the place
where a version is bumped at all. An instance reads this page to understand what a version
number promises it; it never runs the procedure.
+151
@@ -0,0 +1,151 @@
*Generated from [`docs/why-gates-are-code.md`](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/docs/why-gates-are-code.md) at v8.0.0 – edit the repository, not this wiki.*
# Why gates are code
Chemenu has five hard limits - the Mass-Update Gate, the Publish-Remote Gate, the Upload Review
Gate, the Guideline Push Gate, and the Iteration Budget Gate - and all five live inside
`tools/wikitool`, not in a
paragraph of instructions an agent reads and follows. The rules themselves, and what to do when
one trips, are in [AGENTS.md § Gates](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/AGENTS.md#gates) and
[instructions/gates.md](https://gitea.nehmer.net/torben/chemenu/src/tag/v8.0.0/instructions/gates.md). This page is only about the design choice
underneath them: why code, and why these mechanisms in particular.
<!-- wikitool:toc -->
## Contents
- [A suggestion an agent can talk itself past](#a-suggestion-an-agent-can-talk-itself-past)
- [Why different mechanisms, not one](#why-different-mechanisms-not-one)
- [Exit 42 is a posture, and it outgrew the gates](#exit-42-is-a-posture-and-it-outgrew-the-gates)
- [A gate in code still has to be reachable](#a-gate-in-code-still-has-to-be-reachable)
- [Numbers that come from measurement, not intuition](#numbers-that-come-from-measurement-not-intuition)
<!-- /wikitool:toc -->
## A suggestion an agent can talk itself past
An instruction like "don't publish too much at once" or "don't loop forever" lives in the same
place as every other piece of guidance a session is holding - alongside the task, the user's
last message, and whatever context made the moment feel urgent. Under pressure, or with a
plausible-sounding reason ("this batch is different, it's mechanical"), that guidance can be
reasoned around without anyone deciding to break a rule. Nothing enforces it; it just competes
for attention with everything else in the context window, and sometimes loses.
A check compiled into the tool doesn't have that problem, because it isn't part of the
conversation at all. It runs before the command dispatches, regardless of how convincing the
case for skipping it seemed a moment earlier. The difference isn't that code is smarter than a
well-written instruction - it's that code doesn't get talked into anything.
## Why different mechanisms, not one
The five gates ask four different kinds of question, and each one's shape follows from what kind
of question it is.
The Mass-Update Gate asks *is this change too large to publish unreviewed* - a judgment that
varies changeset by changeset, so it clears with a `--confirm` token tied to the specific
output the user just read. Approval is scoped to that one publish.
The Publish-Remote Gate asks something underneath that: *is this even the right repository*.
That's not a per-push judgment, it's a standing property of the checkout - true or false for
every publish that checkout will ever attempt, not just this one. A confirm token would let an
agent clear it once and then treat the answer as settled, which is exactly backwards for a
question whose answer shouldn't move at all mid-session. The only way past it is the user
editing `.wikitool-remotes.json` directly, outside the gate's own flow.
The Upload Review Gate asks the Mass-Update Gate's own question - *is this change right?* - at
the opposite end of its size range: one file from a stranger instead of a changeset from the
session's own work. That similarity is exactly why it reuses the same shape (a `--confirm` token
digesting the thing being approved) rather than inventing a fourth one: the two gates differ in
*who* produced the change and *how much* of it there is, not in what kind of question either one
is answering, so nothing about the mechanism needed to change - only the boundary it sits behind
did, since the submission lives in a quarantine the ordinary pipeline never reads at all rather
than in the working tree `publish` is about to commit.
The Guideline Push Gate asks the same question once more, at the boundary facing outwards: *is
this content right for these repositories, now?* `export guidelines --push` writes a generated
file straight onto another repository's branch, with no review on the receiving side, and a
public target publishes it the moment it lands. That is a per-run judgment - the guidelines
change, the set of opted-in repositories changes, a branch moves - so it takes the token shape,
digesting exactly what would be written where. It deliberately does not take the Publish-Remote
Gate's shape, although it too pushes to a remote: which repositories are targets is already a
standing, committed declaration - the capture manifests under `raw/` - and asking for the same
URLs in an allowlist as well would be a second declaration of one fact. What is left to ask is
the per-run question, and that is the one a token answers.
The Iteration Budget Gate asks a fourth kind of question - not "is this instance correct" but
"has this session stopped making progress." That's read from the shape of the call history
itself (call count, repeated identical calls), not from anything about the content of any one
call.
## Exit 42 is a posture, and it outgrew the gates
Those five are the named gates, and they are not the only thing that exits 42 any more. When the
task-tracker provider layer arrived, it brought a case that looks like a gate from the outside and
is not one: a provider whose API cannot create a project (Super Productivity's local REST API
reads projects but does not write them) raises `HumanInterventionRequired`, and the command prints
what a human has to do and exits 42.
Reusing the code was deliberate, and so was not calling it another gate. What the named gates share
is a *refusal*: the operation was possible and the tool declined to perform it unreviewed. This is
the opposite situation - the operation is not possible at all, and no token could make it
possible. What the two have in common is only what the exit code actually communicates: **stop,
show this to a human, do not improvise a way around it.** That sentence is the whole meaning of
42 here, and it is worth more as a shared convention than as a number reserved for one mechanism.
The alternative was worse in a specific way. A provider that cannot do something could have been
described in the instruction layer instead - "if you are on this tracker, create the project by
hand first" - which is exactly the prose-shaped rule this page argues against, with the added cost
that every instruction would then have to know which provider an instance runs. The capability
gap belongs where the capability is, and reaches the session as an exit code rather than as a
paragraph it has to remember to apply.
The preflight is the second such case, and the one closest to this page's own argument. A machine
without Python or ripgrep is not something the tool can fix, and an install that met that gap
with only a setup instruction to go on showed what follows: the agent worked around each missing
piece - another environment, a hand-made configuration - and kept going. So `tools/preflight.sh` (and its PowerShell twin, `tools/preflight.ps1`)
exits 42 with the command a human has to run, and the launcher in front of every `wikitool` call
exits 42 until the preflight has passed. "Check first, stop, let the user act" lives in two places
a session cannot read past, not in a step it can skip.
## A gate in code still has to be reachable
Code beats prose for the reason above, but on its own it buys less than it looks like: a check
that runs on every call is only as good as the thing it counts under. The Iteration Budget Gate
scopes its counter to a session, and "session" was approximated by the parent process id whenever
nothing set an explicit one. On a harness that runs every tool call in a freshly initialised
shell, that approximation hands out a new session per call - so a traced run of thirty-three calls
arrived as twenty-one sessions of one to three calls each, the ceiling of sixty was never
approached, and the loop-breaker's window never held three calls at once to compare. The gate ran
on every one of those calls, exactly as written, and refused nothing.
That failure has no symptom of its own. A gate that fires announces that it exists; a gate that
*cannot* fire looks identical to a gate nobody happened to need - the same clean runs, the same
silence - and what finally told the two apart was reading a trace for an unrelated reason. So
there is a third property to keep alongside living in code and carrying measured numbers: each
gate has to leave evidence that it can still fire. The ones that clear by token or by a
deliberate edit have it by construction, because clearing one is a visible event in somebody's
terminal. The budget gate, whose ordinary outcome is silence, is the one that had to be given
it - which is why its session id now carries where it came from, into both the trace and
`budget status`, so a session's own record answers the question instead of an investigation
having to.
## Numbers that come from measurement, not intuition
The iteration ceiling didn't start where it sits now. It used to run 15-25, borrowed from a
general rule of thumb, until four real ingest runs measured 24, 26, 29 and 30 calls apiece -
every one of them an ordinary workflow doing nothing wrong, and every one of them at or past
where the old ceiling would have refused it. A limit that the normal case keeps tripping stops
functioning as a limit; it becomes background noise a session learns to route `--override-budget`
around as a matter of course, and the whole point of a hard-coded check is that it isn't supposed
to feel routine.
That's the deeper reason these numbers live in a tool rather than in prose: prose is read once
and remembered loosely, but a threshold enforced every call is tested by every call, and a
threshold that fails its own test gets noticed and re-measured rather than quietly ignored.
The suite's coverage floor is the same argument run forwards instead of backwards. The ceiling
above was wrong first and measured afterwards; the floor was withheld on purpose until the number
existed - measured, then watched across 38 runs while the code grew by a quarter, and only then
written down as 85 against an observed 87.0%. The two points of daylight are the same
consideration as the ceiling's headroom: a limit the ordinary case keeps tripping stops being a
limit. A coverage floor set at the measured number goes red on the next thin command wrapper,
which is not a regression, and a threshold that goes red for a non-reason gets lowered rather
than earned - the failure mode above, reached from the other direction.
+10
@@ -0,0 +1,10 @@
* [Chemenu - Personal IT Knowledge Base](Home)
* [Installation](Install)
* Documentation
* [Why knowledge and commitments are two layers](Knowledge-and-commitment)
* [Language Boundaries](Language-boundaries)
* [Choosing a Claude Code model and effort level](Model-and-effort-selection)
* [Ownership and Templates](Ownership-and-templates)
* [Why the pipeline has four stages](Pipeline-rationale)
* [Why the stack version splits compatibility from migration](Version-model)
* [Why gates are code](Why-gates-are-code)