From d34924d640d4b9b2ae3fa35e8ce996f9577f398e Mon Sep 17 00:00:00 2001 From: Torben Nehmer Date: Fri, 4 Sep 2026 15:30:09 +0200 Subject: [PATCH] stack-dev/stack-close skill split; publish stack-machinery note; model-selection fix (#47 Block 2) 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 --- CHANGES.md | 67 +++++++++++++ VERSION | 2 +- instructions/claude-code-model-selection.md | 8 +- instructions/dev/stack-close/SKILL.md | 102 ++++++++++++++++++++ instructions/dev/stack-dev/SKILL.md | 87 ++++++++--------- tools/CONTRACT.md | 2 +- tools/chemenu/commands/git_publish.py | 37 +++++++ tools/chemenu/tests/test_git_publish.py | 40 ++++++++ 8 files changed, 293 insertions(+), 52 deletions(-) create mode 100644 instructions/dev/stack-close/SKILL.md diff --git a/CHANGES.md b/CHANGES.md index 3a8e210..701d9e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,73 @@ dev-checkout concern - readable here, never shipped as something to parse. --- +## 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 + + +- stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2) + + +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 `/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 diff --git a/VERSION b/VERSION index 4404a17..6016e8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.5.1 +4.6.0 diff --git a/instructions/claude-code-model-selection.md b/instructions/claude-code-model-selection.md index 7c03cf2..0c52a36 100644 --- a/instructions/claude-code-model-selection.md +++ b/instructions/claude-code-model-selection.md @@ -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. - 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. 2. **Pick a spawned subagent's model by what it does**, via the `Agent` tool's `model:` diff --git a/instructions/dev/stack-close/SKILL.md b/instructions/dev/stack-close/SKILL.md new file mode 100644 index 0000000..80543d9 --- /dev/null +++ b/instructions/dev/stack-close/SKILL.md @@ -0,0 +1,102 @@ +--- +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: there is no longer a "next step" to skip past, only a skill nobody has +invoked yet. 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. diff --git a/instructions/dev/stack-dev/SKILL.md b/instructions/dev/stack-dev/SKILL.md index ed02b4c..1c1071c 100644 --- a/instructions/dev/stack-dev/SKILL.md +++ b/instructions/dev/stack-dev/SKILL.md @@ -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 `/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). diff --git a/tools/CONTRACT.md b/tools/CONTRACT.md index 42b6d62..6c966be 100644 --- a/tools/CONTRACT.md +++ b/tools/CONTRACT.md @@ -58,7 +58,7 @@ tools/wikitool --help | `sources trace --raw ` \| `--page ""` | 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 any `<stage>/CONTRACT.md` - the same scope a stack version bump itself covers - 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) | diff --git a/tools/chemenu/commands/git_publish.py b/tools/chemenu/commands/git_publish.py index 528a0f2..33890fd 100644 --- a/tools/chemenu/commands/git_publish.py +++ b/tools/chemenu/commands/git_publish.py @@ -423,6 +423,41 @@ 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 `version-parts.md`'s + scope for the stack version - `tools/`, `types/`, `instructions/`, + `AGENTS.md`, or any `<stage>/CONTRACT.md`. 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.""" + 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 +1173,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) diff --git a/tools/chemenu/tests/test_git_publish.py b/tools/chemenu/tests/test_git_publish.py index 271c94c..26de025 100644 --- a/tools/chemenu/tests/test_git_publish.py +++ b/tools/chemenu/tests/test_git_publish.py @@ -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: