Compare commits

..

5 Commits

Author SHA1 Message Date
torben 72b2b4424f docs verify: DEVELOPMENT.md in STAGE_READMES; self-labelling release notes; prose corrections to 4.6.0 (#47 Block 3)
CI / verify (push) Successful in 54s
Release / release (push) Successful in 36s
Files changed:
- .gitea/workflows/release.yml
- CHANGES.md
- VERSION
- instructions/dev/stack-close/SKILL.md
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/tests/test_docs_verify.py
2026-09-04 15:55:04 +02:00
torben 91bd430ac8 docs: DEVELOPMENT.md - stack-dev/stack-close split (stale since #47 Block 2)
CI / verify (push) Successful in 58s
Files changed:
- DEVELOPMENT.md
2026-09-04 15:31:24 +02:00
torben d34924d640 stack-dev/stack-close skill split; publish stack-machinery note; model-selection fix (#47 Block 2)
CI / verify (push) Successful in 58s
Release / release (push) Successful in 38s
Files changed:
- CHANGES.md
- VERSION
- instructions/claude-code-model-selection.md
- instructions/dev/stack-close/SKILL.md
- instructions/dev/stack-dev/SKILL.md
- tools/CONTRACT.md
- tools/chemenu/commands/git_publish.py
- tools/chemenu/tests/test_git_publish.py
2026-09-04 15:30:09 +02:00
torben 87a47cc237 instructions/dev/issue-tracking.md: destructive-step invariants, comment-vs-body authority, rename sweep (#47 Block 1, #29)
CI / verify (push) Successful in 56s
Release / release (push) Successful in 35s
Files changed:
- CHANGES.md
- VERSION
- instructions/dev/issue-tracking.md
2026-09-04 14:59:30 +02:00
torben d4cbeca5a8 docs: INSTALL.md - erster Sprung auf 4.5.0 nutzt das Werkzeug aus dem neuen Tarball (#7)
CI / verify (push) Successful in 55s
Files changed:
- INSTALL.md
2026-09-04 12:52:43 +02:00
14 changed files with 556 additions and 54 deletions
+21
View File
@@ -102,11 +102,32 @@ jobs:
- name: Release notes from CHANGES.md
# `version notes` fails when the changelog has no entry for this
# version, which is the last place that mistake can still be caught.
#
# The footer below settles Gitea #47's second side-finding: a release
# note is written once, at tag time, and a later correction to
# CHANGES.md never reaches it - `gitea-mcp` has no release-edit method,
# and delete-and-recreate would destroy the attached tarball assets that
# INSTALL.md and `version check` point at. That happened for real to
# v4.4.0, whose note carried a fact that the corpus had already
# corrected. Rather than build a correction path for a text nobody can
# edit, the snapshot says it is one and names where the maintained
# version lives. A stale note then costs a reader one click instead of
# a wrong belief. Appended here rather than inside `version notes`,
# which is a general-purpose extractor whose other callers (a local
# preview, a pipe) should not inherit a release-page footer.
if: steps.version.outputs.skip != 'true'
run: |
set -eu
tools/wikitool docs verify
tools/wikitool version notes > /tmp/release-notes.md
cat >> /tmp/release-notes.md <<'EOF'
---
*This note is a snapshot of the `CHANGES.md` entry as it stood when the tag was cut, and
is never edited afterwards. The maintained version of this text - including any later
correction - is the entry for this version in `CHANGES.md` in the repository.*
EOF
cat /tmp/release-notes.md
- name: Build the distribution tarball
+173
View File
@@ -35,6 +35,179 @@ dev-checkout concern - readable here, never shipped as something to parse.
---
## 4.6.1 - 2026-09-04 - DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
<!-- /wikitool:bumps -->
Block 3 aus #47 - die beiden Nebenbefunde entschieden und umgesetzt - plus drei Prosa-Korrekturen
an `4.6.0`, die eine Bewertung des eigenen Ergebnisses gefunden hat.
**`DEVELOPMENT.md` gehört in `docs_verify.STAGE_READMES`** (entschieden: ja). Das Gegenargument
bei der Aufnahme war, die Liste führe bisher nur ausgelieferte Dokumente, und `DEVELOPMENT.md`
wird von `dist_cmd.ROOT_FILES` bewusst nicht ausgeliefert. Beim Hinsehen löst es sich auf:
`check_readmes_have_no_command_table` überspringt eine Datei, die nicht existiert. In einer
ausgelieferten Instanz ist der Eintrag damit schlicht wirkungslos, im Entwicklungs-Checkout - dem
einzigen Ort, an dem die Datei existiert und also driften kann - greift er. Dafür spricht der
Anlass: genau diese Datei trug einmal eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
beschrieb, was er prüft, und sie musste von Hand entfernt werden, weil nichts sie mit etwas
verglich. Zwei Tests: einer, der die Tabelle in `DEVELOPMENT.md` meldet, und einer, der
festhält, dass eine fehlende gelistete Datei übersprungen und nicht als Fund gemeldet wird - der
Instanz-Fall, an dem die Entscheidung hing. Der Konstantenname ist jetzt enger als sein Inhalt;
das steht als Kommentar daneben, statt eine Umbenennung durch zwei Aufrufstellen zu ziehen.
**Veröffentlichte Release-Notes veralten weiter - sie sagen es jetzt selbst** (entschieden:
Schnappschuss akzeptieren, statt einen Korrekturweg zu bauen). Eine nach dem Tag korrigierte
`CHANGES.md` erreicht die Release-Seite nicht: `gitea-mcp` kennt kein Release-Edit, und
Löschen-und-neu-Anlegen würde die angehängten Tarball-Assets vernichten, auf die `INSTALL.md` und
`version check` zeigen. Bei `v4.4.0` ist das real eingetreten. Statt eines Korrekturwegs für einen
Text, den niemand editieren kann, trägt der Schnappschuss jetzt eine Fußzeile, die sagt, dass er
einer ist und wo die gepflegte Fassung liegt - eine veraltete Notiz kostet einen Leser damit einen
Klick statt einer falschen Überzeugung. Angehängt in `release.yml` und nicht in `version notes`:
das Kommando ist ein allgemeiner Extraktor, dessen andere Aufrufer (lokale Vorschau, eine Pipe)
keine Release-Seiten-Fußzeile erben sollen. `.gitea/`-Änderung, also ohne eigenen Bump-Anspruch -
sie fährt hier mit.
**Drei Prosa-Korrekturen an `4.6.0`.** Der `4.6.0`-Eintrag und der Docstring von
`touches_stack_machinery` behaupteten, das Prädikat prüfe „denselben Pfad-Umfang, den der
CI-Versions-Gate selbst verwendet". Das stimmt nicht: CI matcht `[^/]+/CONTRACT\.md$`, also genau
eine Pfadebene, das Prädikat matcht `CONTRACT.md` in jeder Tiefe. Folgenlos im Verhalten - ein
Über-Match druckt eine Zeile zu viel, nie eine zu wenig -, aber es war eine behauptete Äquivalenz,
die keine ist, geschrieben in genau der ungeprüften Prosa-Phase, um die #47 sich dreht. Docstring
und `tools/CONTRACT.md` benennen die Differenz jetzt und begründen sie (bei einer Erinnerung ist
Über-Matchen die richtige Richtung). Drittens: `stack-close` beschrieb den eigenen Skill-Schnitt
zu stark („es gibt keinen nächsten Schritt mehr, an dem vorbei zu rutschen wäre"). Wahr für die
*Prozedur*, die nicht mehr im Kontext steht; nicht wahr für den *Auslöser* - `stack-dev`s „invoke
it now" ist weiterhin ein Satz, und die `publish`-Notiz nennt den Skill bewusst nicht beim Namen.
Zwei der drei Kettenglieder bleiben Selbstdisziplin. Der Skill sagt das jetzt selbst, statt sich
als Garantie zu verkaufen, die er nicht ist.
Verifiziert: `tools/wikitool docs verify`, `tools/wikitool instructions verify`,
`.venv/bin/python -m pytest -q` (969 passed, 2 davon neu), `release.yml` gegen den YAML-Parser
und das Heredoc als Trockenlauf gegen eine Beispiel-Notiz.
---
## 4.6.0 - 2026-09-04 - stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
<!-- /wikitool:bumps -->
Block 2 aus #47 (Vorschlag E, am 2026-09-04 entschieden): die ungeprüfte Schlussphase einer
Stack-Sitzung - Issue-Body-Rewrite, `docs/`-Veralterung, Changelog-Prosa - hatte bisher keinen
eigenen Haltepunkt, sondern einen Prosa-Break in `stack-dev` Schritt 6. Der ist zweimal
hintereinander verschluckt worden (#42, #30), beide Male mit echtem Fund im nachgeholten
Durchgang. Ein dritter Prosa-Haltepunkt hätte dieselbe Wette verloren, die
`docs/why-gates-are-code.md` für Gates schon verliert - also keine Prosa-Lösung mehr, sondern ein
struktureller Schnitt.
**Neuer Skill `stack-close`**, dev-only wie `stack-dev`. `stack-dev` endet nach `tools/wikitool
publish` mit einem Stop statt mit einem sechsten Schritt; die Schlussphase existiert nur noch als
eigener Skill, den eine Sitzung aufrufen muss - es gibt keinen „nächsten Schritt" mehr, an dem
vorbei sie rutschen könnte. `stack-close` trägt drei Dinge: den Modell-Rückwechsel-Hinweis (wie
zuvor), die Body-Rewrite-Disziplin aus `issue-tracking.md` Schritte 2-3 und 7, und neu die
**Handover-Pflicht über die ganze Sitzung**: benannt wird das Modell für Design/Versionsstelle
(Schritt 3), für die mechanische Mitte, und für diese Schlussphase - alle drei, auch wenn sie
identisch sind. Eine Handover-Zeile, die nur eine billige Schlussphase meldet, schweigt genau
dann, wenn die ebenso ungeprüfte Design-Phase auch billig lief und niemand dort gewechselt hat.
Ein Agenten-Zuschnitt (Schlussphase als eigener Subagent mit eigenem Modell) wurde geprüft und
verworfen: ein Fork erbt in Claude Code zwingend das Elternmodell, ein frischer Subagent den
Sitzungskontext nicht - die Kombination, die der Zuschnitt bräuchte, gibt es nicht, und selbst
wenn: der Input der Schlussphase *ist* das akkumulierte Sitzungswissen, das ein kalter Agent aus
Diff und Issue neu ableiten müsste. Volle Begründung im Body von #47.
**`instructions/claude-code-model-selection.md` korrigiert**, im dist-strip-Block: die
Übersicht „stack-dev Schritt 3 und 6" ist falsch geworden, seit Schritt 6 nicht mehr existiert.
Sie benennt jetzt beide Haltepunkte an ihrem tatsächlichen Ort - Schritt 3 in `stack-dev`,
der zweite am Anfang von `stack-close`.
**`stack-dev` Schritt 3 ehrlicher formuliert** (Vorschlag C): nicht mehr „ab hier alles
mechanisch", sondern mit benannter Ausnahme - Changelog-Prosa (Schritt 4), eine berührte
`docs/`-Seite, neue Menschendoku, der Prosa-Anteil einer Instruction. Dazu die Einschränkung aus
#30: „durch Tests abgedeckt" gilt nur für das, was die Tests *treffen* - zwei
datenvernichtende Bugs in `upstream merge` liefen an einem grünen `pytest`/`docs
verify`/`instructions verify`/CI vorbei, weil kein Test den Fall traf, nicht weil ein
schwächeres Modell schlechteren Code für den getesteten Fall geschrieben hätte.
**Neu: `tools/wikitool publish` selbst erinnert an die Phasengrenze.** Berührt das Changeset
`tools/`, `types/`, `instructions/`, `AGENTS.md` oder ein `<stage>/CONTRACT.md` - derselbe
Umfang, den ein Versions-Bump selbst abdeckt -, druckt `publish` nach der Erfolgsmeldung eine
Zeile, dass die folgende Phase von keinem der drei Checks abgedeckt ist. Kein Gate, keine
Änderung am Exit-Code, für eine gewöhnliche Content-Publish stumm; harness- und
instanzneutral formuliert, ohne jede Erwähnung eines Trackers, weil `publish` von jedem
Skill genutzt wird, nicht nur von `stack-dev`. `git_publish.touches_stack_machinery()` plus
vier neue Tests (`test_git_publish.py`): zwei für die reine Klassifikationsfunktion
(positiv/negativ), zwei Integrationstests gegen einen echten Publish - die Notiz erscheint genau
einmal bei einer `instructions/`-Änderung und bleibt aus bei einer gewöhnlichen `kb/`-Änderung.
`tools/CONTRACT.md`s `publish`-Zeile trägt die Kurzfassung, absichtlich ohne den Dateinamen
`version-parts.md` zu nennen - die Datei liegt unter `instructions/dev/` und würde in einer
ausgelieferten Instanz ins Leere zeigen, während `tools/CONTRACT.md` selbst ausgeliefert wird.
Verifiziert: `tools/wikitool instructions sync` (7 Skills, `stack-close` neu), `tools/wikitool
docs verify`, `tools/wikitool instructions verify`, `.venv/bin/python -m pytest -q` (967
passed, 4 davon neu).
#47 bleibt offen für Block 3 (`DEVELOPMENT.md` in `STAGE_READMES`, veraltete Release-Notes).
---
## 4.5.1 - 2026-09-04 - issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
**Author:** Torben Nehmer
<!-- wikitool:bumps -->
- issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
<!-- /wikitool:bumps -->
Block 1 aus #47 (gemeinsam mit #29): drei Ergänzungen an
`instructions/dev/issue-tracking.md`, ausgelöst durch zwei Fehlerklassen, die
in derselben Sitzung am Stack aufgetreten waren.
- **Schritt 1** trägt jetzt, dass destruktive Schritte im Body die Invariante
nennen müssen, die sie nicht verletzen dürfen, und dass ein
Akzeptanzkriterium eine prüfbare Eigenschaft ist, keine Tätigkeit. Auslöser
war #30: der Body schrieb wörtlich "Arbeitsverzeichnis entfernen" für den
`upstream merge`-Ablauf, und genau das wurde zum datenvernichtenden Bug -
ein `shutil.rmtree` auf eine Stage mit gitignorierten, nicht
rekonstruierbaren Daten.
- **Schritt 2** trägt jetzt die Lesesicht auf Body und Kommentare, die es
bisher nur aus Autorensicht gab: der Body ist der Stand, Kommentare sind
Historie; ein erkennbar veralteter Body wird richtiggestellt statt
umgangen; widersprüchliche Kommentare werden nach Beleg aufgelöst, nicht
nach Datum. Auslöser war ebenfalls #30 (ein Kommentar empfahl das Gegenteil
dessen, was der Body später festlegte) und #10 (ein seit Tagen veralteter
Body gegen drei widersprechende Kommentare, zwei davon sich selbst
widersprechend).
- Neuer Abschnitt **"Renames and other decay in the tracker"** nach Schritt 7:
ein Rename ist erst fertig, wenn auch die offenen Issues nachgezogen sind,
weil `wikitool` diesen Tracker nicht kennt und nicht kennenlernen soll. Mit
der Wegweiser-vs-Beleg-Unterscheidung aus #29 und dem Hinweis, dass auch
verschwundene `kb/`-Seiten und private Infrastrukturangaben Issue-Texte
altern lassen. Ein neuer Trigger in "When to run" verweist darauf.
Keine der drei Ergänzungen verschiebt die bestehende Nummerierung der
Schritte 1-7 - die Querverweise darauf (u. a. aus
`instructions/dev/stack-dev/SKILL.md` auf Schritt 7, aus
`kb/concepts/Issue Label Scheme.md` auf Schritt 2) bleiben also gültig, ohne
angefasst zu werden.
Verifiziert: `tools/wikitool instructions verify`, `tools/wikitool docs
verify`, beide grün (Prosa-only, kein Interface geändert, PATCH).
#47 bleibt offen (Block 2: der Skill-Schnitt aus Vorschlag E; Block 3: die
beiden Nebenbefunde). #29 bleibt ebenfalls offen: dieser Block deckte nur den
Regelabsatz, nicht den noch ausstehenden Pfad-Durchgang durch #4, #5, #15,
#21, #16, #26 - der war nicht Teil des Auftrags für diesen Block.
---
## 4.5.0 - 2026-09-04 - Beide Update-Wege in Code: upstream merge fuer Clones, dist upgrade fuer Tarball-Instanzen
**Author:** Torben Nehmer
+4 -1
View File
@@ -92,6 +92,9 @@ gegen einen Commit. `.gitea/workflows/release.yml` ist Schritt 6 oben.
Der `stack-dev`-Skill (`instructions/dev/`, nur in diesem Ursprungs-Repo vorhanden) fasst die
Regeln für eine Sitzung, die den Stack selbst statt Wiki-Inhalt bearbeitet: wann
Quellenbindung nicht gilt, wo Design endet und die mechanische Phase beginnt (mit dem
Modellwechsel-Hinweis), und dass Issue-Abschluss ein Body-Rewrite ist, kein Kommentar. Siehe
Modellwechsel-Hinweis), und endet mit dem Publish. Die Schlussphase - Issue-Body als Rewrite
statt Kommentar, `docs/`-Veralterung, die Modell-Handover-Zeile über die ganze Sitzung - liegt
seit `4.6.0` in einem eigenen Folge-Skill, `stack-close`, den `stack-dev` an dieser Stelle
übergibt statt sie als weiteren eigenen Schritt zu führen. Siehe
[instructions/dev/issue-tracking.md](instructions/dev/issue-tracking.md) für den
Issue-Tracker selbst.
+14
View File
@@ -195,6 +195,20 @@ dem sich jede Instanz mitten im Upgrade befindet.
tools/wikitool dist upgrade <tarball-oder-verzeichnis> --dry-run
```
**Beim ersten Sprung auf `4.5.0` oder höher gibt es dieses Kommando 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 das Kommando selbst und die kurze Form oben genügt.
Klassifiziert jede Datei aus dem `files`-Block der neuen `.wikitool-release.json`:
unverändert seit der Installation, lokal verändert oder gelöscht, neu im Release, oder aus dem
Release entfallen - und druckt die Migrationskette, die nach dem Tausch aussteht, ohne sie
+1 -1
View File
@@ -1 +1 @@
4.5.0
4.6.1
+6 -2
View File
@@ -74,8 +74,12 @@ to *make*, not a setting to apply.
switch at the moment the phase changes - once, without arguing about it, and never as a reason
to stop work that is already underway.
<!-- dist:strip-start -->
In this repo those moments are named: the `stack-dev` skill breaks for them at its steps 3
(design settled, work turns mechanical) and 6 (publish done, the unchecked tail begins).
In this repo those moments are named, one per skill rather than both in one: `stack-dev`'s own
step 3 breaks for the first (design settled, work turns mechanical), and `stack-dev` itself
ends at the publish rather than asking the same session to break out of its own momentum a
second time. The second switch lives at the opening of `stack-close`, the skill `stack-dev`
hands off to once the publish succeeds (the unchecked tail begins) - a session has to invoke
it to reach that step at all, which is the point: nothing left to skip past mid-flow.
<!-- dist:strip-end -->
2. **Pick a spawned subagent's model by what it does**, via the `Agent` tool's `model:`
+69
View File
@@ -34,6 +34,8 @@ issues at that URL, which is exactly why `dist export` excludes
moved.
- Closing one: the body is rewritten to its final state first, and only then
closed (step 7).
- A rename or move ships: sweep the open issues for text that assumed the old
name or path (§ Renames and other decay in the tracker).
## Steps
@@ -42,6 +44,26 @@ issues at that URL, which is exactly why `dist export` excludes
specific files or commands involved. An issue that only makes sense to
whoever wrote it is a note, and notes were the problem.
**Destructive steps carry the invariant they must not violate.** A body
that prescribes a mechanism gets built as prescribed - including its
bugs. Where a step deletes, overwrites, resets or moves, name the
property that must still hold afterwards, not only the command that gets
there. "Remove the working directory, then `git checkout HEAD --
<stage>`" is a mechanism; "the content stages must afterwards match
`HEAD` exactly, without any untracked or ignored file being touched" is
the same instruction plus its test - a build instruction and an
acceptance criterion at once, so the defect surfaces while the test is
written rather than in review afterwards. #30's `upstream merge` body
wrote the mechanism and got exactly that bug: a working-directory removal
that took a stage's gitignored, unrecoverable data with it.
**An acceptance criterion states a checkable property, not an activity.**
"Implement X" is done when someone says so; "after `upstream merge`,
`reports/` still holds every file it held before" is done when it is
true. This is not a ban on imperative steps - a numbered procedure can
still produce a correct control flow, and that is its merit - it binds
the destructive steps, and every box in the criteria list.
2. **The body is the working state, not a historical first post - keep it
current as you go.** It is this stack's plan file: the same thing a harness's
own plan document is, and it is maintained the same way. Not written once,
@@ -73,6 +95,32 @@ issues at that URL, which is exactly why `dist export` excludes
Body rewrites and comments are an LLM session's job. A human normally
touches only labels and metadata directly.
**Reading an issue, the body is the state and comments are history.** A
session picking an issue up reads the body as the spec; comments are read
for provenance - why something was decided, what was tried - never as
the current instruction. A recommendation in a comment can be older than
the body's decision and read just as convincingly: on #30 an earlier
comment recommended a smaller, `verify`-only command, while the body had
since settled on building the full `merge` command. A session trusting
the comment would have built the wrong thing, with a plausible
justification out of this repo's own tracker.
**A body that is demonstrably wrong is corrected first, not worked
around.** "Body beats comment" is a rule of precedence, not a licence to
execute a stale spec. Where a comment or the tree proves a claim in the
body false, the body is rewritten before the work starts - the rewrite
above is the fix; leaning on the comments as the "real" state is not.
#10 is the case: its body claimed coverage had never been measured while
three comments carried a percentage, a statement count and a CI run
number.
**Where two comments contradict each other, evidence decides, not
recency.** On #10, one comment showed a retrieved artifact with zero
items on a finished run - the report was not actually retrievable - and
a later comment declared the same criterion met without re-checking. The
later comment is not the newer truth, only the unchecked one. Resolve it
into the body with the evidence named, or mark the point open.
3. **Comment a changelog, never a copy.** A body rewrite gets one short comment
naming only what changed against the previous state - what is new, what is
gone, what was corrected. Do not snapshot the old body into a comment: a full
@@ -180,6 +228,27 @@ issues at that URL, which is exactly why `dist export` excludes
shipped. Nothing mechanical catches it (see below), which is why it is a step
rather than a habit.
## Renames and other decay in the tracker
A rename is not finished when the tree is green. Renaming a package, a path,
a command, a flag or the repository itself moves text that lives outside the
working tree, and the open issues are the largest such text. Nothing catches
them - `wikitool` does not know this tracker exists and must not learn (see
"What no tool checks" below) - so a pass over the open issues is part of the
rename, in the session that did it, not a follow-up someone remembers.
Distinguish a wayfinder from a piece of evidence: a path meant to point at
where something *is* gets pulled through; a path quoted for what was true at
a time is left standing and dated. Note per corrected body what was pulled
through and when, so the next pass can tell a checked body from one that
merely looks right. Closed issues are out of scope - they guide nobody.
Renames are not the only thing that ages an issue text. A page a body cites
can vanish from `kb/` (`wikitool search` against the cited titles is the
second pass), and an old body can carry private infrastructure detail into
what is now a public tracker - both found in the same issue, both worth the
same look.
## What no tool checks
`wikitool` does not know this tracker exists, and should not learn. It ships to
+111
View File
@@ -0,0 +1,111 @@
---
name: stack-close
description: Close out a stack-dev work package after its publish has landed - rewrite the issue body to its final state, check for docs/ staleness, and name which model ran which phase of the session. Use right after a stack-dev session's tools/wikitool publish succeeds, or when resuming a package that was published but never closed.
---
# Stack Close
**Purpose:** Carry out the unchecked closing phase of a stack-development work package, as its
own skill rather than a break `stack-dev` has to remember to ask for mid-flow.
**Trigger:** A `stack-dev` session's `tools/wikitool publish` just succeeded - `stack-dev` ends
there and hands off here rather than continuing into this phase in the same breath. Also: `publish`
printed its stack-machinery note ("this publish touched stack machinery...") and nothing has
closed the work package it belongs to yet; or a package was published in an earlier session and
never went through this skill (the gap this split exists to make impossible to skip past
silently - see [issue-tracking.md](../issue-tracking.md)'s note that a closed body is the version
everyone reads afterwards and nobody revisits).
**This directory is dev-only.** Same boundary as `stack-dev`
([its own note](../stack-dev/SKILL.md) has the full reasoning) - `dist export` prunes
`instructions/dev/` wholesale, so this skill never reaches a distributed instance.
## Why this is a separate skill, not `stack-dev`'s step 6
The two phases around the mechanical middle of a stack-dev session have no mechanical guard at
all - `pytest`, `docs verify` and `instructions verify` cover the code and tests in between, and
nothing covers a changelog entry's accuracy, a `docs/` page's staleness, or an issue body's final
state (see [claude-code-model-selection.md](../../claude-code-model-selection.md)). Asking the
same session to notice it has crossed into that second unchecked stretch - as a prose break inside
`stack-dev`'s own step 6 - failed twice in a row on this stack (Gitea #42, then #30): both times
the session knew the rule and skipped past it anyway, because nothing in the moment forced the
question. Splitting the phase into its own skill does not add a check either - `wikitool` still
does not know this tracker exists and must not learn (see
[issue-tracking.md](../issue-tracking.md) § What no tool checks) - but it removes the thing that
was actually failing: the closing *procedure* is no longer sitting in the session's context as a
next step to run past - it exists only inside a skill someone has to invoke.
**Be precise about what that does and does not buy**, because the honest version is weaker than
"now it cannot be skipped". What did **not** change is the trigger: `stack-dev`'s "invoke it now"
is still a sentence, and `publish`'s stack-machinery note is deliberately generic enough not to
name this skill at all. Two of the three links in that chain remain self-discipline. The split
narrows the failure, it does not close it - treat a session that reaches this text as the
mechanism having worked *this time*, not as proof that it always will.
See Gitea #47 for the full incident history and the rejected alternative (a
model-switched subagent - not buildable in Claude Code, where a fork inherits the parent's model
and a fresh subagent starts without the session's context).
## Steps
1. **Offer the model switch back up, once, and keep working either way.**
> Ab hier greift kein maschineller Check mehr - Issue-Body, `docs/`-Veralterung und
> Changelog-Prosa prüft nichts. Wenn du zurück auf Opus willst, ist jetzt der Moment.
**Never block on the answer.** The change is already published; a session that stops here
leaves exactly the state this skill exists to prevent.
2. **Rewrite the issue body to its final state, then close.** The test is what a reader who
opens the closed issue tomorrow would conclude:
- every acceptance criterion ticked, or struck with the reason it was dropped
- proposals that were decided read as decided; a "to decide" section has become the decision
and its reasoning
- nothing left in the present tense about a defect that no longer exists
- what was verified is named - which checks ran, which CI run - not a commit hash alone
Then one short comment naming what changed against the previous state, and nothing else -
[issue-tracking.md](../issue-tracking.md) steps 2-3 and 7 have the full shape; this is that
procedure, run at the point this skill exists to guarantee it actually gets run.
**A closing report in a comment does not satisfy this**, however thorough: it reads as
complete to whoever writes it and leaves a body still phrased as open work. Nothing mechanical
catches it, which is why this is a step - and now a whole skill - rather than a habit. #44 and
#45 both closed exactly this way on the old, single-skill shape, the second an hour after the
rule was first written down.
3. **Check whether a `docs/` page or new human doc went stale.** A `docs/` page carries no
normative sentence, so nothing verifies it by construction (AGENTS.md § File naming) - the
same is true of `README.md`/`INSTALL.md`/`DEVELOPMENT.md` prose and a new instruction's own
wording, which `instructions verify` checks structurally but never for what it claims. If the
change this package shipped moved the reasoning one of these pages documents, update it now;
if none did, say so rather than leaving the question unasked.
4. **Name which model ran which phase - not only this one.** This is the handover in full, not
a note about the tail alone: state the model for the design/version-part/boundary-judgment
phase (`stack-dev` step 3), for the mechanical middle (code, tests, the version bump), and for
this closing phase - all three, even when they are all the same model. A handover that only
flags a cheap-model *closing* phase stays silent exactly when the earlier, equally unchecked
design phase also ran cheap and nobody offered the switch back then either; naming all three
every time is what keeps that omission from being the quiet default.
## Decision points
- **The work package spans several sessions?** Run this skill once, at the point the package is
actually finished and its last publish has landed - not after every individual publish. A
package still open across sessions keeps its body current per
[issue-tracking.md](../issue-tracking.md) step 2 in the meantime; that is maintenance, not
closing.
- **Resuming a package whose publish landed in an earlier, already-ended session?** Run this
skill now, on whatever model the current session is - do not reopen the earlier session to run
it "correctly." The handover in step 4 names the earlier phases from the historical record
(the issue's comments, `CHANGES.md`) rather than from memory.
- **Nothing to close - the session's own exploration, no publish happened?** This skill does not
apply; there is no package to rewrite a body for.
## Scope
Follows a `stack-dev` session's publish. Not for wiki content work - use
`wiki-ingest`/`wiki-query`/`wiki-manage`/`wiki-lint`/`wiki-status` for that, whose own closing
conventions (`kb/log.md`, page provenance) are unrelated to this tracker-body procedure.
+39 -48
View File
@@ -46,9 +46,10 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
package, labelled `area/`, `kind/`, `prio/` and `size/`. There is no `TODO.md`. **The body
of the issue you are working on is this session's plan file:** keep it current as the state
moves, so an interrupted session leaves a body the next one can resume from, *and* rewrite it
to its final state before closing. Both halves bind; the second is step 6 below. Read it
before filing something for later, before editing or closing an issue, or before deciding
what to pick up next.
to its final state before closing. Both halves bind; the second is what
[`stack-close`](../stack-close/SKILL.md) carries out once this skill's own work is published -
see step 5 below. Read it before filing something for later, before editing or closing an
issue, or before deciding what to pick up next.
[testing-conventions.md](../testing-conventions.md) - the suite runs against a deliberately
empty machine; what the autouse fixture already neutralizes, and what a test still has to
establish itself. Read it before adding or changing a test.
@@ -63,14 +64,18 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
list grows without needing this skill file to change shape.
3. **Settle the design before building - and break there for the model switch.** These are two
different kinds of work, and the split is not stylistic: design, the version part and any
boundary judgment have **no** mechanical guard, while the code and tests that follow have
`pytest`, `docs verify`, `instructions verify` and CI behind them.
boundary judgment have **no** mechanical guard, while the code and tests that follow are mostly
covered - `pytest`, `docs verify`, `instructions verify` and CI catch a mistake **in what they
cover**.
So when the design is settled - the issue body says what will be built, the open questions are
answered - stop and say so, in one sentence:
answered - stop and say so, in one sentence that names what the mechanical stretch does **not**
cover:
> Der Plan steht, ab hier ist die Arbeit mechanisch und durch Tests/CI abgedeckt. Wenn du auf
> Opus bist, ist jetzt der Moment für `/model sonnet` bei Effort `high`.
> Der Plan steht, ab hier ist die Arbeit größtenteils mechanisch und durch Tests/CI abgedeckt -
> mit Ausnahme der Changelog-Prosa (Schritt 4), einer berührten `docs/`-Seite, neuer
> Menschendoku oder des Prosa-Anteils einer Instruction. Wenn du auf Opus bist, ist jetzt der
> Moment für `/model sonnet` bei Effort `high`.
**You cannot make this switch yourself** - the session's model is the user's `/model`, not a
setting an agent applies. Offer it once and keep working either way; a session that argues
@@ -78,6 +83,15 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
be settled after all - a boundary crossing surfaces, an assumption breaks - that is a reason to
offer the switch back up, not to decide it alone.
**"Covered by tests" means covered by the tests that exist, not by the tests that should
exist.** Whether the right test was written is itself a judgment call with no mechanical
guard: two data-destroying bugs in `upstream merge` (Gitea #30) shipped past a green
`pytest`/`docs verify`/`instructions verify`/CI because no test exercised the case, not
because a weaker model wrote worse code for the case that *was* tested. This is not a third
break - it is a caveat on this one: the middle phase stays the cheaper phase to run on, but its
test suite is only as complete as the judgment that wrote it, and that judgment is unchecked
the same way the design phase is.
Effort is the cheaper lever than the model, and `high` is the floor for anything touching more
than one file or a contract. Full table and reasoning:
[claude-code-model-selection.md](../../claude-code-model-selection.md).
@@ -119,47 +133,23 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
Prose-only changes (`README.md`, `INSTALL.md`, `EVALS.md`) and the workflows under `.gitea/`
do not need a bump - CI's version gate is scoped to what changes behaviour.
5. **Verify before publishing.** `tools/wikitool docs verify`, `tools/wikitool instructions
verify`, and the relevant `pytest` run in `tools/` - the same checks any stack change must
pass, run explicitly rather than assumed. CI (`.gitea/workflows/ci.yml`) runs these plus a
full `setup-instance.md` replay against a fresh `dist export`; a push to `main` that moves
`VERSION` additionally triggers a tagged release. **CI does the tagging** - a session never
creates a tag, which is what keeps AGENTS.md invariant 5 intact.
5. **Verify, then publish.** `tools/wikitool docs verify`, `tools/wikitool instructions verify`,
and the relevant `pytest` run in `tools/` - the same checks any stack change must pass, run
explicitly rather than assumed. CI (`.gitea/workflows/ci.yml`) runs these plus a full
`setup-instance.md` replay against a fresh `dist export`; a push to `main` that moves `VERSION`
additionally triggers a tagged release. **CI does the tagging** - a session never creates a
tag, which is what keeps AGENTS.md invariant 5 intact.
6. **Close the issue with a body rewrite, not a comment.** The last act of a session that
finished a work package, and the one most easily skipped: by here the change is published and
the issue feels done. It is not. The body is the version everyone reads afterwards and nobody
revisits, so it is the one place the debt comes due at the worst moment.
Publish with `tools/wikitool publish`. When the changeset touches `tools/`, `types/`,
`instructions/`, `AGENTS.md` or a `<stage>/CONTRACT.md`, `publish` itself prints a one-line
reminder that the phase past this point is not covered by any of the checks above - that line
is the cue that this skill's own job just ended.
**Break here too, in the other direction.** Everything left in the session - this rewrite,
whether a `docs/` page's reasoning went stale, the changelog prose - is the unchecked kind of
work again, the mirror of step 3. If the session dropped to Sonnet there, say so now:
> Ab hier greift kein maschineller Check mehr - Issue-Body, `docs/`-Veralterung und
> Changelog-Prosa prüft nichts. Wenn du zurück auf Opus willst, ist jetzt der Moment.
Then **do the work regardless of the answer.** Never block a close on a model switch: the
change is already published, and a session that stops here leaves exactly the state this step
exists to prevent. If it ran on the cheaper model, name that in the handover rather than
leaving it silent.
Rewrite it to its final state *first*, then close. The test is what a reader who opens the
closed issue tomorrow would conclude:
- every acceptance criterion ticked, or struck with the reason it was dropped
- proposals that were decided read as decided; a "to decide" section has become the decision
and its reasoning
- nothing left in the present tense about a defect that no longer exists
- what was verified is named - which checks ran, which CI run - not a commit hash alone
Then one short comment naming what changed against the previous state, and nothing else.
**A closing report in a comment does not satisfy this**, however thorough: it reads as
complete to whoever writes it and leaves a body still phrased as open work. Nothing
mechanical catches it - `wikitool` does not know this tracker exists and must not learn it,
since it ships to instances that have no board - so this step is the only enforcement there
is. #44 and #45 both closed exactly this way, the second an hour after the rule was written.
[issue-tracking.md](../issue-tracking.md) step 7 has the full shape.
**This skill stops here.** The closing phase - rewriting the issue body to its final state,
checking for `docs/` staleness, and naming which model ran which phase of the session - lives
in [`stack-close`](../stack-close/SKILL.md), not in a further step of this one. Invoke it now;
do not fold its work into this session under this skill's rules, and do not treat "the change
is published" as this work package being done.
## Decision points
@@ -180,4 +170,5 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
Not for wiki content work - use `wiki-ingest`/`wiki-query`/`wiki-manage`/`wiki-lint`/
`wiki-status` for that. Not for setting up a new instance (`instructions/setup-instance.md`) or
a fresh clone of this repo (`instructions/bootstrap.md`).
a fresh clone of this repo (`instructions/bootstrap.md`). Not for closing a work package after
its publish has landed - that is [`stack-close`](../stack-close/SKILL.md).
+1 -1
View File
@@ -58,7 +58,7 @@ tools/wikitool <command> --help
| `sources trace --raw <path>` \| `--page "<Title>"` | Trace provenance in either direction: raw file -> source page(s) -> citing pages, or page -> its sources -> their raw files |
| `sources rebuild-index [--dry-run]` | Regenerate the `kb/provenance.md` reverse index (raw file -> source page -> citing pages) |
| `sync [--remote origin] [--branch main] [--confirm-rebase TOKEN]` | Fetch `<remote>/<branch>` and bring the local branch up to date with it: fast-forward when the remote is simply ahead, rebase local commit(s) on top when both sides moved but touch disjoint files (a content conflict is then impossible by construction), and exit **42** for review when they touch the same file (the **rebase-review gate** - see `publish` below). Never commits, never pushes, never force-anything - no remote configured, or one that cannot be reached, is reported and skipped, not a failure. Meant to run once at the start of a writing session (`instructions/session-setup.md`) so the rest of it works against a current tree instead of discovering the drift at the final `publish` |
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree |
| `publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` | Reconcile with `<remote>/<branch>` exactly like `sync` (skipped for `--no-push`), then stage all changes, commit, and push. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous `publish` whose push failed no longer strands it. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. **Mass-Update Gate:** when >= `--threshold` (default 10) *counted* files would be committed, exits **42 (`EXIT_NEEDS_CLEARANCE`)** instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn, generated files split out as needing no review. The token digests each counted path **and its contents** plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated *before* anything is staged, so a refused publish leaves the working tree untouched. **Publish-Remote Gate:** when this checkout carries a `.wikitool-remotes.json` and the resolved push URL of `--remote` is not listed in it, exits **42** before the reconcile step even fetches - the URL is read from `git remote get-url --push`, so a repointed remote does not pass on its name. Unlike the other two gates it has **no token and no flag**: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See [instructions/gates.md](../instructions/gates.md) `--yes`/`-y` are gone and now fail with an explicit error. `--path` (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. **Stack-machinery note:** after a successful commit/push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending in `CONTRACT.md` - roughly the scope a stack version bump covers, deliberately a shade broader than CI's version gate, which matches only a `CONTRACT.md` one segment deep - prints one reminder line that the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish |
| `work new (--input <raw path> \| --key <run key>) [--again] [--dry-run]` | Scaffold `work/<runkey>/` for one workshop run: refuses a collision instead of suffixing it, and writes the required `README.md` + `plan.md`. `--input` derives the run key from the path below `raw/` (an ingest); `--key` names it outright for a run with no raw input - a migration or a sweep across `kb/` - and may not start with `ingest-`, which stays reserved for derived keys. Exactly one of the two. `--again` opens a dated second pass over a tree that has itself changed. See [work/CONTRACT.md](../work/CONTRACT.md) |
| `work close --run-key <name> [--yes] [--dry-run]` | Delete a finished workshop. Lists what would be lost and requires `--yes`, because nothing in it is recoverable from the rest of the repo - the durable conclusions must already be in `kb/` |
| `budget status` | Show the current session's `wikitool` call count and recent command history (never counted against the budget) |
+11 -1
View File
@@ -136,7 +136,17 @@ ROOT_README = config.ROOT / "README.md"
# tools/README.md is exactly the file it drifted in. INSTALL.md is here for the
# same reason: it is human-facing prose about installing an instance, and the
# command reference lives exactly once, in tools/CONTRACT.md.
STAGE_READMES = ("tools/README.md", "INSTALL.md")
#
# DEVELOPMENT.md joined them after it drifted the same way (Gitea #47): it grew
# a table describing what each verify command checks, which had to be removed by
# hand because nothing compared it to anything. It is not shipped - dist_cmd
# .ROOT_FILES excludes it - and that is not an argument against listing it here:
# `check_readmes_have_no_command_table` skips a file that does not exist, so in
# a distributed instance this entry is simply inert, while in the dev checkout
# (the only place the file exists, and the only place it can drift) it is
# checked. The name is now narrower than the tuple - these are the human-facing
# prose docs that must not re-list commands, stage README or not.
STAGE_READMES = ("tools/README.md", "INSTALL.md", "DEVELOPMENT.md")
# Docs that must not re-introduce the pre-migration bare-enum `type:` form.
# The per-collection contracts are appended at call time, since which ones exist
+42
View File
@@ -423,6 +423,46 @@ def counted_files_of(
return [change for change in changes if not is_exempt(change.path, prefixes)]
# The same scope `version-parts.md` names for the stack version itself -
# "tools/, types/, instructions/, AGENTS.md and the contracts" - reused here
# to decide whether a publish's changeset falls under it. Not a copy of that
# rule: version-parts.md states the scope in prose for a human choosing a
# version part, this instantiates the same boundary in code for a different
# question (does this publish deserve the closing-phase reminder below).
STACK_MACHINERY_PREFIXES = ("tools/", "types/", "instructions/")
STACK_MACHINERY_NAMES = ("AGENTS.md",)
STACK_MACHINERY_NOTE = (
"Note: this publish touched stack machinery. What a stack-dev session "
"does next - closing prose, a changelog entry's accuracy, whether a "
"docs/ page went stale - is not covered by docs verify, instructions "
"verify, or pytest. No tool checks it; a session has to."
)
def touches_stack_machinery(changed_files: list[str]) -> bool:
"""Whether `changed_files` includes a path under the stack version's own
scope - `tools/`, `types/`, `instructions/`, `AGENTS.md`, or a path ending
in `CONTRACT.md` at any depth. A publish in this class is, by construction
of the `stack-dev`/`stack-close` split, always followed by the unchecked
closing phase - `STACK_MACHINERY_NOTE` times a reminder to land exactly
there, for any session, not only one that read the skill that names it.
Deliberately a shade broader than CI's version gate, which matches
`<one-segment>/CONTRACT.md` only: this decides whether to print a sentence,
so over-matching costs a reminder nobody needed, while under-matching costs
the reminder in the one case it was built for. The two are not the same
predicate and should not be described as one."""
for path in changed_files:
if path in STACK_MACHINERY_NAMES:
return True
if path.endswith("CONTRACT.md"):
return True
if path.startswith(STACK_MACHINERY_PREFIXES):
return True
return False
YES_REMOVED_MESSAGE = (
"--yes no longer exists. The Mass-Update Gate is cleared with `--confirm <token>`, and the "
"token comes from the gate's own refusal output - run this command without it first to see "
@@ -1138,3 +1178,5 @@ def publish_command(
},
)
success(f"Published changes to {remote}/{branch}." if push else "Committed changes (not pushed).")
if touches_stack_machinery(changed_files):
typer.echo(STACK_MACHINERY_NOTE)
+24
View File
@@ -100,6 +100,30 @@ def test_install_md_is_checked_too(tmp_path, monkeypatch):
assert any("`doctor`" in issue for issue in issues)
def test_development_md_is_checked_too(tmp_path, monkeypatch):
"""DEVELOPMENT.md drifted exactly this way once (Gitea #47): a table
describing what each verify command checks, removed by hand because nothing
compared it to anything."""
root = tmp_path
(root / "DEVELOPMENT.md").write_text(
"| Command | Purpose |\n| `docs verify` | checks docs |\n", encoding="utf-8"
)
monkeypatch.setattr(docs_verify.config, "ROOT", root)
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md") # doesn't exist here
issues = docs_verify.check_readmes_have_no_command_table()
assert any("`docs verify`" in issue for issue in issues)
def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
"""The distributed-instance case: DEVELOPMENT.md is not shipped, so listing
it must stay inert where the file does not exist rather than failing a tree
that is correct."""
root = tmp_path
monkeypatch.setattr(docs_verify.config, "ROOT", root)
monkeypatch.setattr(docs_verify, "ROOT_README", root / "README.md")
assert docs_verify.check_readmes_have_no_command_table() == []
def test_legacy_type_blocks_are_absent():
assert docs_verify.check_legacy_type_blocks() == []
+40
View File
@@ -10,6 +10,7 @@ from chemenu.commands._util import EXIT_NEEDS_CLEARANCE
from chemenu.commands.git_publish import (
DEFAULT_MASS_UPDATE_THRESHOLD,
GATE_EXEMPT_PREFIXES,
STACK_MACHINERY_NOTE,
YES_REMOVED_MESSAGE,
FileChange,
attention_notes,
@@ -29,6 +30,7 @@ from chemenu.commands.git_publish import (
rerun_command,
scale_line,
sync_command,
touches_stack_machinery,
)
@@ -290,6 +292,26 @@ def test_gate_message_names_generated_files_as_their_own_reason():
assert "kb/provenance.md" not in message
def test_stack_machinery_detects_the_version_parts_scope():
"""The same boundary version-parts.md names for the stack version: tools/,
types/, instructions/, AGENTS.md, and any <stage>/CONTRACT.md."""
assert touches_stack_machinery(["instructions/dev/issue-tracking.md"])
assert touches_stack_machinery(["tools/chemenu/commands/git_publish.py"])
assert touches_stack_machinery(["types/instruction.md"])
assert touches_stack_machinery(["AGENTS.md"])
assert touches_stack_machinery(["kb/CONTRACT.md"])
assert touches_stack_machinery(["raw/CONTRACT.md"])
def test_stack_machinery_excludes_ordinary_content():
"""kb/ content, docs/ prose and work/ scratch carry no normative sentence
and are not what the closing-phase reminder is about."""
assert not touches_stack_machinery(["kb/entities/systems/Foo.md"])
assert not touches_stack_machinery(["docs/why-gates-are-code.md"])
assert not touches_stack_machinery(["work/ingest-x/extract-0.md"])
assert not touches_stack_machinery([])
# --- publish_command integration: a real git repo + a local bare remote ---
@@ -356,6 +378,24 @@ def test_below_threshold_publish_goes_straight_through(repo):
assert _git(repo, "status", "--porcelain", "-uall").stdout == ""
def test_publish_notes_stack_machinery_after_success(repo, capsys):
"""The closing-phase reminder lands exactly once, after the OK line, and
only when the changeset actually falls under version-parts.md's scope."""
(repo / "instructions").mkdir()
(repo / "instructions/example.md").write_text("x\n", encoding="utf-8")
_publish(message="touch instructions")
out = capsys.readouterr().out
assert STACK_MACHINERY_NOTE in out
assert out.count(STACK_MACHINERY_NOTE) == 1
def test_publish_stays_quiet_for_ordinary_content(repo, capsys):
_write_files(repo, 1)
_publish(message="ordinary content")
out = capsys.readouterr().out
assert STACK_MACHINERY_NOTE not in out
def test_at_threshold_publish_asks_for_clearance_and_stages_nothing(repo):
_write_files(repo, 10)
with pytest.raises(typer.Exit) as excinfo: