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
This commit is contained in:
2026-09-04 15:30:09 +02:00
parent 87a47cc237
commit d34924d640
8 changed files with 293 additions and 52 deletions
+37
View File
@@ -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)
+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: