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