Skip to content

feat(garden): Duplikate zusammenführen, Widersprüche als Thread, Regressionsschutz, Herkunftstest (REQ-457) - #149

Merged
NurRobin merged 6 commits into
devfrom
feat/garden-consolidate-regression
Oct 20, 2026
Merged

NurRobin merged 6 commits into
devfrom
feat/garden-consolidate-regression

Conversation

@NurRobin

@NurRobin NurRobin commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Pull request

What changed?

REQ-457, Teil A von zwei: Job 3 (Konsolidieren), Job 5 (Regressionsschutz) und der Herkunftstest (Kriterium 3). Teil B (Job 4 Ledger, Job 1 Ghosts) ist nicht angelegt.

Grundregel unverändert: Der Gärtner schlägt vor, ein Mensch entscheidet. Alles, was er schreibt, trägt Harness gardener und Confidence staged; jede Änderung an anderen Einträgen ist ein Knopf auf der Review-Seite, den ein Mensch mit den nötigen Rechten drückt.

Duplikate zusammenführen (Job 3a). Eine Duplikat-Notiz auf /ui/review hat jetzt „Zusammenführen“. Die Karte zeigt vorher, welcher Eintrag bleibt (der neueste) und welche ersetzt werden. Der Knopf ruft dieselbe Store-Funktion wie context_supersede (SupersedeKnowledge) mit dem Grund „Gärtner-Vorschlag #, bestätigt von “ und schließt die Notiz als bestätigt. Er wird abgelehnt (409), wenn ein Eintrag nicht mehr gilt, die Scopes verschieden sind oder der bleibende Eintrag noch nicht vertrauenswürdig ist. Die Duplikat-Notiz hat kein einfaches „Bestätigen“ mehr, und „Alle freigeben“ überspringt sie. Verwerfen bleibt wie vorher.

Widersprüche als Thread (Job 3b). --propose öffnet je validiertem Widerspruch einen Thread statt einer Notiz: verknüpft mit allen betroffenen Einträgen, Titel als Frage mit „[Gärtner]“, erster Beitrag mit Begründung und wörtlichen Zitaten, geschrieben vom Konto des Gärtners. Der Client konnte das schon (CreateThread, LinkThread, ThreadsForObject, SendCoordMessage); ergänzt habe ich nur die schmale Schnittstelle garden.ThreadSink ohne jeden Aufruf, der einen Thread-Zustand setzt. Der Gärtner meldet sich dafür als Agent gardener:<projekt> im Projektraum an, weil Threads ohne Web-Sitzung nur so lesbar und eröffnbar sind. Kann ein Thread nicht geöffnet werden, entsteht wie bisher eine gestagte Notiz.

Idempotenz der Threads. Die Frage des Threads enthält eine Zeile „Stand der Einträge beim Fund: #12 (hash), #15 (hash)“ (HashOf, Wortlaut der Einträge). Vor dem Öffnen werden die Threads an allen Einträgen gesucht:

  • ein offener Thread an denselben Einträgen: kein zweiter;
  • ein vom Menschen geschlossener, geparkter oder archivierter Thread: wird nicht wieder geöffnet und nicht mit einem neuen beantwortet, solange die Hashes gleich sind; ändert sich ein Eintrag, darf ein neuer Thread fragen, der alte bleibt unberührt;
  • ein Thread von Menschen an allen Einträgen (ohne Stand-Zeile) zählt in jedem Zustand;
  • eine Gärtner-Notiz zur selben Beziehung (in jedem Zustand) zählt als „gesehen“;
  • sind die vorhandenen Threads nicht abfragbar, wird nichts geöffnet.

Regressionsschutz (Job 5). Im selben Paket, ohne zusätzliche Sessions, bekommen offene Pitfalls (ohne Urteil in context_regression_cover, ohne stehenden Vorschlag des Gärtners, höchstens 8 je Session) eine Zusatzfrage: ein vorhandener Test (Datei und Name; func TestX muss in einer getrackten _test.go-Datei stehen, die die Session gelesen hat; bei anderen Sprachen der Name als Wort), eine Skizze (Datei, Name, 3 bis 6 Zeilen; Datei-Verzeichnis muss existieren, der Name darf es noch nicht geben) oder „nicht testbar“ mit Grund. Was der Checkout nicht belegt, wird verworfen und im Bericht genannt. Je Antwort entsteht eine gestagte Gärtner-Notiz. Auf der Review-Seite setzen „Als abgesichert vermerken“ (vorhandener Test) und „Als nicht testbar vermerken“ über SetRegressionCover covered mit Testname bzw. not_applicable, mit dem Recht zu verifizieren (wie das Regressions-Werkzeug); Skizzen haben nur Bestätigen/Verwerfen und stehen zusätzlich in einem eigenen Abschnitt „Testskizzen“ im Morgenbericht (--out). Das Antwortformat wächst nur um das optionale Feld regression.

Herkunftstest (Kriterium 3). cmd/ctx/garden_provenance_test.go lässt den Gärtner im Agent-Modus mit einem Claude-Stand-in gegen den echten Server (Store im Speicher, server.New) laufen und zeichnet jeden Schreibzugriff auf. Er belegt (a) jede Notiz hat Harness gardener und Confidence staged und ersetzt nichts, (b) Thread und erster Beitrag gehören dem Konto des Gärtners und sind markiert, (c) nur POST auf Wissen, Raum-Anmeldung, Threads, Verknüpfungen und Beiträge, nichts, was ändert, löscht, schließt, ablöst, zurückzieht, ein Regressionsurteil oder Kriterium setzt oder einen Request abschließt (Wissen, Relationen, Auftrag danach unverändert), ein zweiter Lauf schreibt nichts Neues, ein vom Menschen geschlossener Thread bleibt geschlossen, (d) das Konto des Gärtners (Rolle member) wird vom Server für alle diese Schritte abgelehnt. Die Review-Aktionen sind in internal/web/review_garden_actions_test.go mit Lead, Reviewer, Mitglied, Gast und Fremdem getestet; dazu ein Test, der die Methodenliste von garden.Sink und garden.ThreadSink festhält.

Rechte der neuen Review-Aktionen.

Aktion Recht
Zusammenführen (POST /ui/review/{id}/merge) Ändern aller beteiligten Einträge (wie context_supersede), dazu Verifizieren der Notiz. Praktisch Lead/Owner; ein Mitglied nur an eigenen Einträgen und mit Reviewrecht.
Als abgesichert / nicht testbar vermerken (POST /ui/review/{id}/cover) Verifizieren des Pitfalls (wie context_regression_cover) und der Notiz: Lead, Owner, Mitglied mit Reviewrecht.
Gast, Fremder 404 (sie sehen die gestagte Notiz nicht), Mitglied ohne Rechte 403. Ohne Formular-Token passiert nichts.

Prompt-Zuwachs. Die Frage steht in der Aufgabe eines Pakets mit offenen Pitfalls, nicht in der Standardanweisung: einmal rund 1.000 Zeichen je Session plus ein Marker je Frage. Gemessen im Test an einem Paket mit 30 Pitfalls zu je 2.000 Zeichen: 73.644 auf 74.677 Zeichen, +1,4 Prozent (bei realen Texten von 3.000 Zeichen eher +1 Prozent). Dazu kommen höchstens 8 Antworten (rund 500 Ausgabe-Tokens) und möglicherweise zusätzliche Nachschlage-Turns je Pitfall; das ist geschätzt, nicht gemessen. Die Obergrenze (--max-usd 3, Teilergebnis bei Limit) bleibt.

Herkunft der Notizen (zwei Sicherheitsbefunde der Review, Confused Deputy). Ohne weitere Prüfung hätte jedes Konto per POST /api/knowledge (oder context_remember) mit harness: gardener und passender session_ref eine Notiz anlegen können, die wie ein Gärtner-Vorschlag aussieht; die Karte hätte ein anderes Ziel gezeigt, als „Zusammenführen“ bzw. „Als abgesichert vermerken“ trifft. Gelöst so:

  1. Das Gärtner-Konto ist eine Konto-Nummer, kein Name. Ein erster Entwurf löste es über den Kontonamen auf; der Review-Befund dazu: Wer seinen Anzeigenamen selbst ändern kann (Profil, Name des Identitätsanbieters), bekäme mit dem Namen die Rolle, und ein umbenanntes Konto verschöbe sie still. Deshalb steht jetzt die Nummer in org_state (vorhandene Tabelle, keine Schemaänderung), und nur der Betreiber des Server-Hosts setzt sie, mit ctx account set-gardener <name|person:N|N> --db <pfad> (wie die übrigen ctx account-Befehle: Datenbankzugriff statt Token). Der Befehl löst einmal auf und speichert die Nummer; --clear nimmt die Rolle weg, ctx account list markiert das Konto mit gardener=true, ctx serve loggt beim Start gardener_account. Der laufende Server liest die Einstellung bei jeder Prüfung, ein Neustart ist nicht nötig. Umgebungsvariable und Flag mit dem Namen gibt es nicht; es bleibt kein Weg über Namen, also braucht der Name auch keine Reservierung für SetOwnName und den Provider-Abgleich (das Umbenennen auf den alten Namen weist der Store ohnehin ab). Nichts, was über HTTP erreichbar ist, schreibt die Einstellung (ein Test liest server, web, mcpserver und client darauf). Konto-Nummern ändern sich nicht und werden nicht wieder vergeben (INTEGER PRIMARY KEY, es gibt keinen Löschpfad).
  2. Schreib-Gate am Server (fail-closed). Harness gardener und gardener:-Verweise (jede Schreibweise) darf nur dieses Konto mit einem Token schreiben, auch über supersedes und POST /api/migrated-knowledge. Alle anderen, Owner eingeschlossen, bekommen 403 gardener_reserved; ist kein Konto gesetzt, bekommt es niemand, und die Meldung nennt den Befehl.
  3. Autor. Die Review-Aktionen handeln nur bei einer Notiz, die dieses Konto geschrieben hat (Konto-Nummer aus knowledge_authors, nie das Label), die seitdem niemand bearbeitet hat (kein Eintrag in der Historie) und nur im Zustand gestaged/aktiv. Ohne gesetztes Konto sind die Aktionen aus.
  4. Ziel. Der Titel (#<id>) muss dieselben Einträge nennen wie die session_ref, sonst Hinweis „Herkunft nicht bestätigt: keine Aktion“ und 403. Die Karte zeigt die Einträge der session_ref mit ID, Titel und Link („Betrifft“ bzw. „Bleibt“/„Wird ersetzt“), nie die des Titels.
  5. Testzeile. Ein erneutes Prüfen gegen das Repo geht am Server nicht, er hat keinen Checkout; ctx hat es vor dem Anlegen getan. Stattdessen: feste Form (relativer/pfad:Funktionsname, keine Leerzeichen, kein .., kein .git, höchstens 300 Zeichen), die Zeile steht ganz auf der Karte, wird so eingetragen, wie die Notiz sie hält, und die Anfrage trägt keinen eigenen Test (Zusatzfelder im Formular wirken nicht). Wer die Zeile bearbeitet, entzieht der Notiz damit die Aktion.

Negativtests für jede Prüfung: Autor Reviewer/Mitglied/Lead/Owner/unbekannt, kein Konto gesetzt, anderes Konto gesetzt, Umbenennen des Gärtner-Kontos ändert nichts an seinen Rechten, ein Konto mit dem alten Namen bekommt keine (über SetOwnName und über direkte Namensänderung), fremder Titel, bearbeitete Notiz, 11 Formen einer unmöglichen Testzeile, Zusatzfelder, Laufzeit-Store mit Writer und ein zweiter Prozess. Mutationsproben (jede Prüfung einzeln ausgebaut, dazu eine namensbasierte Identität) lassen den jeweiligen Test fehlschlagen.

Betrieb vor dem nächsten Lauf. Auf jedem Server, der den Gärtner betreut, einmal ctx account set-gardener <Kontoname des Gärtner-Tokens> --db <Datenbank> ausführen, mit dem Benutzer, dem die Datenbank gehört (Beispiel mit transienter Unit in deploy/README.md); sonst lehnt der Server die Vorschläge mit 403 gardener_reserved ab. Alte Notizen ohne Autor oder von einem anderen Konto bekommen keine Aktion mehr (Bestätigen/Verwerfen bleiben).

Nicht enthalten / bekannte Grenzen. Der Server erzwingt staged für das Konto des Gärtners selbst nicht; das hält der Befehl ein und der Herkunftstest belegt es. Das Konto sollte die Rolle member haben, nicht Lead.

Verification

gofmt -l .                      # leer
go vet ./...
go build ./...
go test ./internal/garden/... ./cmd/ctx/... ./internal/client/...
go test ./internal/store/... -timeout 30m
go test ./internal/web/... -timeout 55m

Ergebnisse: siehe Kommentar unten. Manuell geprüft: Aufruf ctx garden zeigt die neue Hilfe; Mutationsprobe (Notiz mit trusted statt staged schreiben) lässt den Herkunftstest fehlschlagen.

Release impact

  • major — incompatible after 1.0
  • minor — compatible feature, or incompatible before 1.0
  • patch — compatible fix
  • none — no release impact

Checklist

  • This pull request targets dev (or is an approved release/hotfix promotion).
  • Tests cover changed behavior.
  • I did not add credentials, private infrastructure details, or internal planning documents.
  • User-facing behavior and the changelog are updated where needed.
  • I have read and agree to the Contributor License Agreement, and I am authorized to make this contribution.

…essionsschutz, Herkunftstest (REQ-457)

Teil A von REQ-457 (Job 3 Konsolidieren, Job 5 Regressionsschutz,
Kriterium 3 Herkunft). Der Gärtner schlägt weiter nur vor; alles, was er
schreibt, ist gestagt und trägt Harness gardener.

- Duplikat-Notizen haben auf /ui/review "Zusammenführen": der neueste
  Eintrag löst die älteren ab (SupersedeKnowledge, Recht wie
  context_supersede, Grund "Gärtner-Vorschlag #<notiz>, bestätigt von
  <person>"), die Karte zeigt vorher, was bleibt und was ersetzt wird.
- Ein validierter Widerspruch wird ein Thread (verknüpft mit allen
  Einträgen, Titel als Frage, erster Beitrag mit Begründung und
  Zitaten) statt einer Notiz. Idempotent über die Stand-Zeile in der
  Frage: kein zweiter offener Thread, ein geschlossener bleibt zu,
  solange die Einträge gleich sind. Fällt der Thread aus, entsteht die
  gestagte Notiz.
- Offene Pitfalls bekommen im selben Paket die Regressionsfrage (vorhandener
  Test, Skizze oder nicht testbar; höchstens 8 je Session, vom Checkout
  geprüft). Review-Aktionen "Als abgesichert vermerken" und "Als nicht
  testbar vermerken" rufen SetRegressionCover; Skizzen stehen im Bericht.
- Herkunftstest gegen den echten Server, der jeden Schreibzugriff des
  Gärtners aufzeichnet; Tests der Review-Aktionen mit Lead, Reviewer,
  Mitglied, Gast und Fremdem.
@NurRobin
NurRobin force-pushed the feat/garden-consolidate-regression branch from 85a7e0b to 095f5fd Compare October 19, 2026 22:52
…n Autor, Titel und Notiz (REQ-457)

Befund (Confused Deputy): Wer per POST /api/knowledge mit harness gardener
und passender session_ref schrieb, legte eine Notiz an, die wie ein
Gärtner-Vorschlag aussah; "Zusammenführen" und "Als abgesichert vermerken"
hätten dann auf ein Ziel gewirkt, das die Karte nicht zeigt.

- Der Server benennt das Gärtner-Konto (GHOSTTREE_GARDENER_ACCOUNT bzw.
  --gardener-account, im Store neben dem Zugriffsmodus, nachgeschlagen bei
  jedem Schreiben). Harness gardener und gardener:-Verweise (jede
  Schreibweise) darf nur dieses Konto schreiben, auch über supersedes und
  /api/migrated-knowledge; alle anderen und alle bei fehlender Einstellung
  bekommen 403 gardener_reserved.
- Die Review-Aktionen handeln nur bei einer Notiz, die dieses Konto
  geschrieben hat (Konto-Nummer aus knowledge_authors, nicht das Label),
  die seitdem niemand bearbeitet hat und deren Titel dieselben Einträge
  nennt wie die session_ref; sonst Hinweis auf der Karte, 403 und keine
  Änderung.
- Die Karte zeigt die Einträge der session_ref mit ID, Titel und Link.
- Die Testzeile einer Regressions-Notiz hat eine feste Form (relativer Pfad,
  Doppelpunkt, Funktionsname), wird ganz angezeigt und so eingetragen, wie
  die Notiz sie hält; die Anfrage trägt keinen eigenen Test. Eine
  Wiederholung der Prüfung gegen den Checkout geht am Server nicht, er hat
  keinen.
- Negativtests für jede Prüfung, mit Mutationsprobe; das Markdown der
  deploy/README.md besteht markdownlint wieder.
… ctx account set-gardener (REQ-457)

Zweiter Befund der Review: Die Gärtner-Identität wurde über den Kontonamen
aufgelöst. Wer seinen Anzeigenamen selbst ändern kann (Profil, Name des
Identitätsanbieters), hätte mit dem Namen die Rolle bekommen; ein
umbenanntes oder fehlendes Konto hätte sie still verschoben.

- Die Einstellung ist die Konto-Nummer, gespeichert in org_state (keine
  Schemaänderung), gesetzt nur mit `ctx account set-gardener <name|person:N|N>
  --db <pfad>` auf dem Server-Host (wie die übrigen ctx-account-Befehle:
  Datenbankzugriff statt Token). Der Befehl löst einmal auf und speichert die
  Nummer; --clear nimmt die Rolle weg; ctx account list markiert das Konto
  mit gardener=true; ctx serve loggt gardener_account beim Start. Der
  laufende Server liest die Einstellung bei jeder Prüfung, ein Neustart ist
  nicht nötig.
- Die Umgebungsvariable und das Flag mit dem Namen entfallen. Es gibt keinen
  Weg über Namen mehr, also braucht der Name auch keine Reservierung.
  Nichts, was über HTTP erreichbar ist, schreibt die Einstellung (Test liest
  die Pakete server, web, mcpserver und client darauf).
- Ohne gesetztes Konto darf niemand harness gardener schreiben (403
  gardener_reserved, die Meldung nennt den Befehl), und die Review-Aktionen
  für Gärtner-Notizen sind aus.
- Negativtests: Umbenennen des Gärtner-Kontos ändert seine Rechte nicht, ein
  Konto mit dem alten Namen bekommt keine (über SetOwnName, das der Store für
  alte Namen schon abweist, und über direkte Namensänderung), Konto
  wechseln verschiebt die Rolle mit der Nummer; Mutationsprobe mit einer
  namensbasierten Identität schlägt fehl.
- Die Startzeile gardener_account steht in cmdServe neben writer_config und
  nicht im Handler-Aufbau, den TestServerConcurrentTelemetrySmoke zeilenweise
  zählt.
@NurRobin

Copy link
Copy Markdown
Contributor Author

Lokale Ergebnisse (Branch feat/garden-consolidate-regression, Stand 086f209, gemergt mit origin/dev 96c8e17):

  • gofmt -l .: leer
  • go vet ./...: sauber
  • go build ./...: ok
  • go test ./internal/garden/... ./cmd/ctx/... ./internal/client/...: ok (zusätzlich ./internal/server/... und ./internal/mcpserver/...: ok)
  • go test ./internal/web/... -timeout 55m: ok (auf e6953af; danach nur noch Store-Leseweg und Inventar)
  • go test ./internal/store/... -timeout 30m: ein Lauf auf e6953af mit zwei Fehlern. TestRuntimeStoreWriterInventory hat die neuen öffentlichen Store-Methoden bemängelt; behoben in 7b471c0 (klassifiziert, Leseweg über den Reader), danach grün. TestRuntimeWriterCrashRestartMatrix ("child did not reach crash boundary") ist der bekannte flaky Test; er scheitert unter derselben Last auch auf unverändertem origin/dev (geprüft).
  • markdownlint-cli2 lokal auf deploy/README.md und CHANGELOG.md: 0 Funde.
  • Mutationsproben: jede Herkunftsprüfung einzeln ausgebaut (Autor, Titel gegen Ref, unbearbeitete Notiz, Form der Testzeile, Schreib-Gate an beiden Wegen) sowie eine namensbasierte Identität des Gärtner-Kontos: jeweils scheitert der zugehörige Test.

@NurRobin
NurRobin merged commit 4584fb7 into dev Oct 20, 2026
7 checks passed
@NurRobin
NurRobin deleted the feat/garden-consolidate-regression branch October 20, 2026 00:43
NurRobin added a commit that referenced this pull request Oct 20, 2026
…te überall, Hinweis auf ctx db vacuum (REQ-526) (#151)

# Pull request

## What changed?

**Problem (REQ-526, Folgeauftrag zu #150).** Ein bekanntes, geleaktes
Passwort steht in den gespeicherten Sitzungen an vielen Stellen ohne
Beschriftung: in Befehlszeilen wie `sed 's/<wert>/…/'`, in grep-Ausgaben
und als einzelne Wortfragmente (der Volltextindex führt die Teile eines
zusammengesetzten Werts als eigene Wörter). Die Schwärzung nach
Beschriftung aus #150 erfasst das nicht. Außerdem nannte der
Schlusshinweis von `ctx redact-sessions` noch `sqlite3 <db> VACUUM`, und
`skills/ghosttree-setup/references/local-server.md` empfahl `sqlite3 …
"VACUUM INTO …"` als Sicherung; beides scheitert an
`ghosttree_valid_utf8` (Pitfall #3146).

**Aufruf.**

```bash
ctx redact-sessions --db <pfad> [--dry-run] [--batch N] [--sleep D] [--after-id N] [--skip-optimize] --literals-file <pfad>
```

Die Datei enthält einen bekannten Wert je Zeile. Leerzeilen und Zeilen
mit `#` am Anfang werden ignoriert (ein Wert, der selbst mit `#`
beginnt, wird `\#` geschrieben), Leerraum um den Wert wird
abgeschnitten. Jedes Vorkommen jedes Werts wird durch
`[REDACTED:literal]` ersetzt, mit oder ohne Beschriftung. Ohne den
Schalter bleibt alles, wie es ist.

**Was ersetzt wird** (jeweils in dem Lauf, den der Befehl ohnehin
macht):

| Ort | Verhalten |
| --- | --- |
| `session_chunks.text` | Ersetzen, `chunks_fts` mit `delete` (alter
Text) und neuem `INSERT` nachgeführt |
| `session_chunks.raw` | Ersetzen auch in JSON-escapter Schreibweise
(der Vergleich läuft auf den dekodierten Zeichen: Und-Zeichen als
Unicode-Escape, `\"`, `\\`, Surrogatpaare, beide Hex-Schreibweisen);
`raw` bleibt gültiges JSON, ein Treffer beginnt nur innerhalb eines
JSON-Strings und enthält nie ein unescaptes Anführungszeichen, eine Zahl
oder die Struktur wird nie ersetzt |
| `chunk_index`, `sess_fts` | neu aus dem geschwärzten `raw` abgeleitet
(Denkblöcke, Werkzeugeingabe, Werkzeugergebnis), `delete` mit den
gespeicherten Feldern |
| `sessions.title` | neu aus dem geschwärzten Chunk abgeleitet, übrige
Titel tabellenweit geschwärzt |
| `knowledge_evidence.quote`, `request_sightings.quote` | Zitate
geänderter Chunks wie bisher; neu: alle Zitate tabellenweit, auch wenn
ihr Chunk nicht mehr da ist |
| danach | `optimize` beider Volltextindizes, `secure_delete`,
WAL-Checkpoint `TRUNCATE` wie bisher |

**Nur gezählt, nicht geändert** (Bericht: Zeilen je Tabelle und Nummern
der Einträge): `knowledge`, `knowledge_versions`, `requests`. Ein
sauberes Ersetzen gibt es dort nicht: derselbe Text steht auch in
`knowledge_versions` (Verlauf), `search_documents` und beiden
Volltextindizes, in Kriterien, Aktivitäten, Erklärungen,
Koordinationsnachrichten, Dokumenten und in den unveränderlichen
Schnappschüssen (Trigger verbieten `UPDATE`, kein Löschweg). Ein
Teil-Ersetzen ließe den Rest als falsche Sicherheit stehen; was mit
diesen Texten geschieht, entscheidet der Mensch.

**Regeln.**

- Ein Wert passt in der angegebenen und in der kleingeschriebenen
Schreibweise (die FTS-Spalten sind case-insensitive, Fragmente stehen in
Ausgaben klein); andere Schreibweisen nicht, dafür eine eigene Zeile.
Länger vor kürzer.
- Werte unter 4 Zeichen werden abgelehnt (Exit 2), ebenso ungültiges
UTF-8, Steuerzeichen, eine leere Liste und ein Wert, der Teil eines
Platzhalters (`[REDACTED:…]`) ist (sonst ersetzte ein zweiter Lauf den
Platzhalter wieder).
- Die Datei ist selbst ein Geheimnis: Gruppe oder andere dürfen sie
nicht erreichen, sonst Exit 2 mit Hinweis `chmod 600` (der Modus wird am
geöffneten Handle gelesen). Das geschieht vor dem ersten Zugriff auf die
Datenbank.
- Ein Wert erscheint nie in Ausgabe, Logs oder Fehlermeldungen (nur
Zeilennummern, Gründe und Zählungen) und gelangt nie in eine
SQL-Anweisung.
- Der Abgleich läuft in Go auf den Zeilen, die der Lauf ohnehin liest,
nicht mit `instr` oder `MATCH` in SQL: `raw` trägt Inhalt, der in `text`
fehlt, und der Wert steht dort auch escapt, was `instr` nicht findet.
- Die Beschriftungsregel läuft weiter, aber nach den bekannten Werten:
`Passwort: <wert>` wird `Passwort: [REDACTED:literal]`.

**Korrektur der Hinweise.** Der Schlusshinweis nennt nur noch `ctx db
vacuum --db <pfad>` (Dienst gestoppt). `local-server.md` sichert eine
laufende Datenbank jetzt mit `sqlite3 … ".backup '…'"` (kopiert Seiten
und wertet keine CHECK-Bedingung aus; gegen eine mit `ctx` angelegte
Datenbank mit sqlite3 3.53.4 geprüft, `VACUUM INTO` scheitert dort und
lässt eine leere Zieldatei zurück) und prüft die Kopie mit `ctx db
check`. `deploy/README.md` prüft eine kopierte Datenbank ebenfalls mit
`ctx db check` statt `PRAGMA integrity_check` und beschreibt den neuen
Schalter.

**Messung** (`TestRedactSessionsLiteralsScale`, nur mit
`GHOSTTREE_REDACT_SCALE_CHUNKS=200000`, in CI übersprungen; 200.000
erfundene Chunks, 1,6 GB, 800 davon mit den erfundenen Werten in fünf
Formen):

| Lauf | Dauer |
| --- | --- |
| Probelauf, nur Beschriftung | 11,6 s (17.300 Chunks/s) |
| Probelauf, mit Werten | 13,6 s (14.700 Chunks/s), +17 % |
| echter Lauf, 800 Chunks geändert, mit `optimize` und Checkpoint | 33,3
s |
| zweiter Lauf | 13,1 s, 0 Änderungen |

Danach liefert `MATCH` auf alle Fragmente in `chunks_fts` und `sess_fts`
nichts, `integrity_check` und FTS-`integrity-check` sind ok. Der
Literal-Scan allein schafft rund 1,2 GB/s (`BenchmarkLiteralsApplyRaw`),
die Beschriftungsregel rund 160 MB/s.

## Verification

- `gofmt -l .`: leer. `go vet ./...`: ok. `go build ./...`: ok.
- `go test ./internal/redact/... ./cmd/ctx/...`: ok (`internal/redact`
auch mit `-race`; `cmd/ctx` mit `-race` vor dem Rebase auf #149 ok).
- `go test ./internal/store/... -timeout 30m`: alles ok bis auf
`TestRuntimeWriterCrashRestartMatrix` („child did not reach crash
boundary“), der unter Last bekannt flaky ist; er scheitert unter
derselben Last (Load 8) auch auf unverändertem `origin/dev` (4584fb7),
und die Phase `before_admission` allein läuft durch.
- Tests, alle Werte erfunden:
- `internal/redact/literal_test.go`: exakte und kleingeschriebene
Schreibweise, länger vor kürzer, JSON-escapte Formen (Go-Encoder mit und
ohne HTML-Escape, `ensure_ascii`-Stil, große und kleine Hex-Ziffern,
Surrogatpaare), Zahl und Struktur in JSON bleiben unberührt, kein
Treffer mitten in einem Escape, 20.000 Zufallstexte (JSON- und Text-Weg
gleich, gültiges JSON, idempotent, kein Wert übrig), Format der Liste,
Ablehnungen ohne Wert in der Meldung.
- `internal/store/redactliterals_test.go`: Wert in `raw` (roh und
escapt), `text`, Denkblock, Werkzeugausgabe, kleingeschriebenes
Fragment, Titel (auch mitten im Wert abgeschnitten und ein Titel ohne
Chunk), Zitate (auch ohne Chunk); danach kein `MATCH`, kein Segmentblock
und keine Datei samt WAL mit einem Fragment, `raw` gültiges JSON,
`integrity_check`; `--dry-run` ändert nichts und zählt wie der echte
Lauf; zweiter Lauf ändert nichts; ohne Schalter bleibt alles; Wissen,
Fassungen und Aufträge werden gezählt und nicht angefasst. Gegenprobe:
vier gezielte Fehler im Code (Titel, tabellenweite Zitate,
Kleinschreibung, JSON-Weg) lassen den Test scheitern.
- `cmd/ctx/redactliterals_test.go`: Aufruf, Datei mit
0644/0640/0604/0620/0666/0660/0606 wird abgelehnt (Exit 2, Datenbank
unverändert), 0600 und 0400 gehen; Wert unter 4 Zeichen, leere Liste,
ungültiges UTF-8, Platzhalterteil, fehlende Datei, Verzeichnis: Exit 2;
stdout und stderr enthalten nie einen Wert (roh, klein, escapt).

## Release impact

- [ ] `major` — incompatible after 1.0
- [x] `minor` — compatible feature, or incompatible before 1.0
- [ ] `patch` — compatible fix
- [ ] `none` — no release impact

## Checklist

- [x] This pull request targets `dev` (or is an approved release/hotfix
promotion).
- [x] Tests cover changed behavior.
- [x] I did not add credentials, private infrastructure details, or
internal planning documents.
- [x] User-facing behavior and the changelog are updated where needed.
- [x] I have read and agree to the [Contributor License
Agreement](../CLA.md), and I am authorized to make this contribution.
NurRobin added a commit that referenced this pull request Oct 20, 2026
…s Vorschlag (REQ-457) (#152)

# Pull request

## What changed?

REQ-457, Teil B des Gärtners (Teil A ist #149): Job 4, das Ledger
pflegen, und Job 1, Ghost-Beschreibungen pflegen. Der Gärtner schlägt
weiterhin nur vor. Alles, was er ablegt, ist eine gestagte Notiz mit
harness `gardener`; geändert wird erst, wenn ein Mensch auf `/ui/review`
bestätigt. Die Sessions sind unverändert schreibgeschützt
(Read/Grep/Glob im Checkout, `git_log`/`git_show`, nur Loopback,
`--no-mcp`); Daten über Requests und Ghosts bekommen sie nur im Prompt,
den ctx baut.

**Job 4, Ledger** (`internal/garden/ledger.go`,
`cmd/ctx/garden_jobs.go`)

- ctx holt die offenen Requests samt offenen Kriterien über die
Client-API (Suche, dann je Request die Details) und gibt die fälligen
(`--requests N`, Standard 8, 0 = aus; älteste zuerst) in einer eigenen
Session mit eigener Standardanweisung (höchstens 8 je Session) mit.
Rotation über den vorhandenen Zustand (`garden_checks` oder
`--state-file`) unter einem eigenen Schlüsselraum `ledger|<projekt>`;
ein Request ist wieder fällig, wenn sich seine offenen Kriterien ändern
oder der Checkout weitergerückt und der letzte Blick älter als
`--min-age` ist.
- Belege sind Commits, Testfunktionen oder Dateien. ctx prüft jeden
einzeln: der Commit existiert, liegt im Verlauf von `HEAD` und wurde von
einem Werkzeug der Session ausgegeben (Squash-Commits behalten ihr
`(#149)`); die Testfunktion steht in einer Datei, die die Session
gelesen hat; die Datei ist versioniert und gelesen. Ohne einen gültigen
Beleg wird nichts vorgeschlagen, und der Bericht nennt, was verworfen
wurde.
- Je Kriterium eine gestagte Notiz (`[Gärtner] Erfüllt? #<Request>
Kriterium <n>: …`, Referenz `gardener:criterion:<Request>:<Kriterium>`).
Die Review-Aktion **Kriterium als erfüllt vermerken** ruft
`SetCriterionState` auf, dieselbe Store-Funktion wie
`request_record_progress criterion_met`; der Beleg endet mit
„Gärtner-Vorschlag #Notiz, bestätigt von Person“. Das Abschließen ganzer
Requests schlägt der Gärtner nicht vor und hat keinen Aufruf dafür.
- **Liegengeblieben:** Abschnitt im Morgenbericht, rein aus Daten, ohne
Modellaufruf (auch bei `--dry-run` und `--mode api`). Grenze 30 Tage
ohne sichtbare Aktivität (`--idle-days`, 0 = aus). Begründung: Arbeit am
Ledger kommt in Schüben von Tagen bis wenigen Wochen; eine kürzere
Grenze listet jeden Request zwischen zwei Schüben, eine längere lässt
Vergessenes ein Quartal liegen. Gezählt wird, was das Gärtner-Konto
sehen darf; Arbeit in privaten Sessions anderer Konten fehlt, der
Abschnitt sagt das.

**Job 1, Ghosts** (`internal/garden/ghosts.go`,
`cmd/ctx/garden_jobs.go`)

- Kandidaten: Dateien mit Zustand `stale`, `unknown` (geändert, Ausmaß
unbekannt), dann `undescribed`, dann `drifted`, in dieser Reihenfolge,
höchstens `--ghosts N` (Standard 8, 0 = aus). Die Frische rechnet ctx
auf dem Client mit `ghost.Judge`/`HashFile` (mit dem gehärteten Git des
Gärtners). Ausgelassen: Lockfiles, `*.sum`, `testdata`, Generiertes,
Binäres, Lizenzen, Dateien über 150 KB (kleine Vorgabeliste im Code,
dazu `ghost.IsIncidental`) und ausdrücklich leer gelassene Pfade
(`ReviewedEmpty`). REQ-433 ist nicht gebaut; es zählt nur, was es gibt.
Verzeichnisse sind nicht dabei, nur Dateien.
- Eigenes Paket statt Anhang an die Wissens-Pakete: Dateien eines
Verzeichnisses kommen zusammen in eine Session (gleichmäßig auf
höchstens 8 je Session geteilt). Die Wissens-Sessions wachsen dadurch um
kein Byte. Die Session bekommt die alte Beschreibung und zwei
Nachbarbeschreibungen als Stilvorlage im Prompt, liest die Datei und
schlägt eine neue Beschreibung vor oder sagt `keep`.
- Prüfung in ctx: Pfad ist eine versionierte reguläre Datei, die Session
hat sie mit `Read` gelesen, die Datei hat sich seit dem Lesen nicht
geändert (Hash), Text 40 bis 3.500 Zeichen ohne Steuer- oder unsichtbare
Formatzeichen, nicht der bestehende Text.
- Je Datei eine gestagte Notiz (`[Gärtner] Beschreibung? <Pfad>`,
Referenz `gardener:ghost:<Pfad>`) mit Text, Inhaltshashes der gelesenen
Fassung und Hash der ersetzten Beschreibung. **Beschreibung übernehmen**
zeigt alte und neue Beschreibung nebeneinander und ruft `PutGhostFile`
auf, dieselbe Funktion wie `context_describe_file`. Ist inzwischen eine
andere Beschreibung gesetzt als die, gegen die die Notiz geschrieben
wurde, gibt es 409 und nichts wird überschrieben (die ersetzte Fassung
bleibt wie immer in der Historie).
- Herkunft: Die Ghost-Revision hat `Harness`, `SessionRef` und `Person`:
`gardener`, `gardener:ghost-note:<Notiz>`, bestätigende Person. Der
Ghost-Baum schreibt „vom Gärtner vorgeschlagen, bestätigt von Person“.
`POST /api/ghosts` verweigert allen (auch dem Gärtner-Konto) harness
`gardener` und `gardener:`-Referenzen mit 403 `gardener_reserved`; nur
die Review-Seite setzt die Marke.

**Sicherheit der Review-Aktionen:** Jede Aktion handelt nur auf Daten,
die der Server selbst ableitet: Nummern oder Pfad aus der Referenz einer
Notiz, die nur das per `ctx account set-gardener` gesetzte Konto
geschrieben hat (Kontonummer aus `knowledge_authors`) und die niemand
bearbeitet hat (leere Historie); der Titel muss dasselbe nennen wie die
Referenz (Request und Kriteriumsnummer, bzw. Pfad); Beleg oder Text samt
Hashes kommen aus der Notiz und werden erneut auf eine strenge Form
geprüft (der Server hat keinen Checkout); die Rechte kommen vom
Betrachter. Der Request, der den Knopf drückt, trägt nur das CSRF-Token;
Formularfelder werden nicht gelesen (getestet mit gefälschten Feldern).
Lesen und Arbeiten am Request werden vor dem Vergleich von Titel und
Projekt der Notiz geprüft, damit eine Notiz, die auf einen Request eines
verborgenen Projekts zeigt, wie eine unbekannte Nummer antwortet (404,
getestet).

**Optionen und Standards:** `--requests 8`, `--ghosts 8`, `--idle-days
30`; `--max-usd 3` bleibt der Deckel je Lauf. Die beiden neuen Jobs
laufen vor den Wissens-Sessions; eine Session von ihnen darf höchstens
ein Viertel des Laufs ausgeben, ihr Scheitern stoppt den Wissens-Job nie
(nach zwei fehlgeschlagenen Zusatz-Sessions entfallen die übrigen).
Hilfe von `ctx garden`, `CHANGELOG.md` (Unreleased) und
`deploy/README.md` sind aktualisiert.

**Mehrkosten je Nacht (Schätzung, nicht gemessen):** Gemessen ist der
erste Nachtlauf mit 120 Einträgen in 4 Sessions: 1,27 $, also rund 0,32
$ je Session. Die beiden Zusatz-Sessions (8 Aufträge, 8 Dateien)
bestimmt wie dort die Zahl der Werkzeugaufrufe; ich erwarte je 0,25 bis
0,40 $, zusammen etwa 0,5 bis 0,8 $, im Mittel also knapp 2 $ je Nacht,
solange der Bestand noch durchläuft. Harte Obergrenze: je Zusatz-Session
`--max-usd`/4 = 0,75 $, zusammen 1,5 $; den Wissens-Sessions bleiben
dann mindestens 1,5 $, mehr als die gemessenen 1,27 $. Ist nichts fällig
(alles angesehen, nichts geändert), laufen keine Zusatz-Sessions und die
Mehrkosten sind null. Gemessen habe ich nur die Prompts (`--dry-run`
listet sie je Session): Standardanweisung Ledger 2.056 und
Beschreibungen 1.641 Zeichen, Aufgabe für 8 Aufträge mit je 4 Kriterien
und langen Texten etwa 15.900 Zeichen, für 8 Dateien mit je 1.500
Zeichen alter Beschreibung etwa 15.400 Zeichen (grob ein Drittel davon
in Tokens). Die Wissens-Sessions: 0 Byte Zuwachs. Die Probleme „flach
geprüft“ der beiden Wissens-Sessions betreffen diesen PR nicht; die
Zusatz-Sessions verlangen im Prompt mindestens eine Suche je Kriterium
und das Lesen jeder Datei, und der Bericht warnt wie bisher bei weniger
Werkzeugaufrufen als Einträgen.

**Grenzen:** Nur Dateien, keine Verzeichnisse (`dirchanged`). Das
Gärtner-Konto ist Mitglied und darf damit technisch Kriterien setzen und
Beschreibungen schreiben; der Server kann ihm das nicht verweigern, ohne
Mitgliedern das Recht zu nehmen. Garantiert ist es durch das Programm:
`gardenStore` hat keinen solchen Aufruf, und der Herkunftstest zeichnet
jeden schreibenden Aufruf eines Laufs gegen den echten Server auf und
erlaubt nur gestagte Notizen, die Registrierung im Raum und Threads; die
Gärtner-Marke auf Ghosts verweigert der Server allen. Ein echter Lauf
mit Claude Code ist hier nicht gemacht worden (die Sessions sind im Test
durch ein Stand-in ersetzt, das durch den echten Server und den echten
Store läuft).

## Verification

- `gofmt -l .` leer, `go vet ./...`, `go build ./...` ohne Befund.
- `go test ./internal/garden/... ./cmd/ctx/... ./internal/client/...
./internal/ghost/... ./internal/server/...`: grün.
- `go test ./internal/store/... -timeout 30m`: grün (935 s).
- `go test ./internal/web/... -timeout 55m`: der erste Lauf fand genau
einen Fehler (`TestCatalogTextsAreEnglishAndNotEmpty`: zwei neue
Meldungen mit mehreren Verben ohne nummerierte Argumente), korrigiert;
der vollständige Lauf auf dem Endstand ist grün (1.180 s).
- Neue Tests: Validierung (gefälschter Commit, nicht im Verlauf von
`HEAD`, nicht von der Session gesehen, nicht vorhandener Test, nicht
vorhandene oder nicht versionierte Datei, Pfad außerhalb des Checkouts:
verworfen), Rotation, Liegengeblieben, Auswahl und Prüfung der Ghosts,
Zusatz-Sessions im Agent-Lauf (Reihenfolge, eigene Anweisung,
Teilergebnis, Scheitern, Manipulation des Checkouts, Trockenlauf),
Herkunftstest erweitert um Job 1 und 4 (weiterhin kein ändernder Aufruf,
7 statt 5 Notizen), Review-Aktionen mit Lead, Owner, Reviewer, Mitglied,
Gast und Fremdem, gefälschte Notizen (anderer Autor, bearbeitet, Titel,
Referenz, Beleg- und Texteform), Formularwerte ohne Wirkung, verborgene
Projekte, `POST /api/ghosts` mit Gärtner-Marke. Mutationsproben: Beleg-
und Verlaufsprüfung, Titelprüfung und Überschreibschutz einzeln
ausgebaut lassen je einen Test fehlschlagen.

## Release impact

- [ ] `major` — incompatible after 1.0
- [x] `minor` — compatible feature, or incompatible before 1.0
- [ ] `patch` — compatible fix
- [ ] `none` — no release impact

## Checklist

- [x] This pull request targets `dev` (or is an approved release/hotfix
promotion).
- [x] Tests cover changed behavior.
- [x] I did not add credentials, private infrastructure details, or
internal planning documents.
- [x] User-facing behavior and the changelog are updated where needed.
- [x] I have read and agree to the [Contributor License
Agreement](../CLA.md), and I am authorized to make this contribution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant