diff --git a/Install.md b/Install.md new file mode 100644 index 0000000..96422dd --- /dev/null +++ b/Install.md @@ -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. + + +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). + + +## 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: + + +- **Python** ≥ 3.11 +- **Git** +- **ripgrep (rg)** + + +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: + + +- **PowerShell 7 (pwsh)** ≥ 7 + + +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: . 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: + +- **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). +- **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`. +- **Sprache und Ton der Seiten** - sie landen in + `kb/CONVENTIONS.md`, dazu je Collection `kb//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`. +- **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`). +- **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**. +- **Umgebung** (optional) - Harness, MCP-Server, Remotes, + damit spätere Sitzungen nicht erneut fragen. „Weiß ich nicht“ ist eine gültige Antwort. +- **Telemetrie** - standardmäßig aus; der Agent fragt nur, ob du + sie einschalten willst. +- **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 `. + +**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 =` 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 ` +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`. `` 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 `). + +**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 ` 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-.tar.gz +CHEMENU_ROOT="$PWD" chemenu-stack-/tools/wikitool \ + dist upgrade chemenu-stack-.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 ` 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 `` 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=` (PowerShell: `pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1 --set rg=`); 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": "" + } +} +``` + +```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 ` 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:///remote.php/dav/calendars//", + "username": "", + "app_password": "", + "inbox_list": "Inbox", + "someday_list": "Someday", + "exclude_lists": [""] + } +``` + +`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 ""` 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 `-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-/` 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. diff --git a/Knowledge-and-commitment.md b/Knowledge-and-commitment.md new file mode 100644 index 0000000..238c4ac --- /dev/null +++ b/Knowledge-and-commitment.md @@ -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). + + +## 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) + + +## 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. diff --git a/Language-boundaries.md b/Language-boundaries.md new file mode 100644 index 0000000..0868d03 --- /dev/null +++ b/Language-boundaries.md @@ -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. + + +## 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) + + +## 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/..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/.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. diff --git a/Model-and-effort-selection.md b/Model-and-effort-selection.md new file mode 100644 index 0000000..134051f --- /dev/null +++ b/Model-and-effort-selection.md @@ -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. + + +## 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) + + +## Phase guide + + +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: + + +| 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. + +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. + + +## 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. diff --git a/Ownership-and-templates.md b/Ownership-and-templates.md new file mode 100644 index 0000000..c2b69e7 --- /dev/null +++ b/Ownership-and-templates.md @@ -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. + + +## 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) + + +## 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: +`/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/.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 ` 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/.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//COLLECTION.md`, `ENVIRONMENT.md`, the `root: kb` type-specs and their subtype + templates `types/..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. diff --git a/Pipeline-rationale.md b/Pipeline-rationale.md new file mode 100644 index 0000000..304f48a --- /dev/null +++ b/Pipeline-rationale.md @@ -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. diff --git a/Version-model.md b/Version-model.md new file mode 100644 index 0000000..5cac7f2 --- /dev/null +++ b/Version-model.md @@ -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. + + +## 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) + + +## 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. diff --git a/Why-gates-are-code.md b/Why-gates-are-code.md new file mode 100644 index 0000000..d7e27e5 --- /dev/null +++ b/Why-gates-are-code.md @@ -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. + + +## 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) + + +## 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. diff --git a/_Sidebar.md b/_Sidebar.md new file mode 100644 index 0000000..ec5d867 --- /dev/null +++ b/_Sidebar.md @@ -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)