From f5a3643da896fd29dc7b0fc4b272346962802a35 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 2 Sep 2026 00:38:50 -0400 Subject: [PATCH] =?UTF-8?q?refactor(plugin):=20retire=20what=20the=20hand-?= =?UTF-8?q?bump=20scheme=20left=20behind=20=E2=80=94=20the=20README=20that?= =?UTF-8?q?=20taught=20it,=20the=20floor=20test,=20the=20stale=20rationale?= =?UTF-8?q?=20(#3328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3127 checklist 19. The step's own deletion list turned out to be largely spent: `check_version_bump()` came out with #3327, and the machinery the step expected to delete alongside it is load-bearing for its replacement. `manifest_version(ref=…)`, `--base`, `--no-version` and the `origin/main` resolve path all STAY. Derivation makes the value right; it does not make the comparison unnecessary. `check_version_is_minted` still has to ask "did the version move when the shipped content did?", and that is a base-branch question no matter who chose the number. The step was planned before #3327 landed, when the assumption was that these died with the guard. What was actually still standing, all of it teaching or asserting the retired scheme: - `plugin/README.md` told the reader to "set a `version` bump per release." A shipped file, instructing the exact act the mint replaced — this is how a deleted control gets re-added by someone following the docs. Now says not to hand-edit the field, names `make mint-plugin`, and says what a forgotten mint costs. (`make` is not installed on every workstation, so the direct script invocation is given too.) - `test_plugin_version_bumped_with_the_hook` asserted `version >= (0,1,31)` as a tuple of ints. Under a minted value it passes vacuously — every date clears a floor of 0.1.31 — and `int("0415")` silently eats the padding the format exists to keep. Superseded by `test_the_shipped_manifest_carries_a_minted_version`, which asserts the canonical shape instead of an ordering the comparator does not perform. Removed whole (rule 22). - The module preamble still ended on "a written rule that depends on being remembered is not a control; this is" — true of the bump guard, and read as a stronger claim than the mint can support. Replaced with what the change did and did not remove: choosing a number is gone, running the mint is not, and the difference is that forgetting is now loud rather than silent. - An orphaned `# --- the version bump ---` section header with nothing under it, and a test docstring still naming `check_version_bump`. `--no-version` keeps its one legitimate case — on `main` the version is measured against itself — and now says so in both the usage block and its `--help`, so it does not read as an escape hatch. `check_session_context_ reports_its_version` stays untouched: a different check with a different job, and the only thing that makes step 6 readable from a transcript (#2220). Version minted 2026.09.02.0415 -> 2026.09.02.0438 for the README change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TcCs1CcQ1ormdnzSshKqvN --- plugin/.claude-plugin/plugin.json | 2 +- plugin/README.md | 9 +++++++-- scripts/check_plugin.py | 33 +++++++++++++++++++++++-------- tests/test_plugin_shipped_set.py | 2 +- tests/test_write_path_trigger.py | 8 -------- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index ae00a4f..c462aa9 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "2026.09.02.0415", + "version": "2026.09.02.0438", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/README.md b/plugin/README.md index 14a1fdc..adbd6b6 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -78,8 +78,13 @@ On install you'll be asked for: ## Notes -- Set a `version` bump in `.claude-plugin/plugin.json` per release so clients - pick up changes. +- **Do not hand-edit `version` in `.claude-plugin/plugin.json`.** It is minted + from the clock — run `make mint-plugin` (or `python3 + scripts/mint_plugin_version.py`) after changing anything under `plugin/`, and + commit the result. The installer decides whether to refresh the cache it + executes from by comparing that string, so content that ships without a new + version reaches the repo and stops there (#2209). CI fails the lane if you + forget. - The session-start, auto-inject and prior-art hooks need only a **read**-scoped key; the MCP tools need **write** scope to create/update. Every hook is a GET for that reason — a read key cannot POST. diff --git a/scripts/check_plugin.py b/scripts/check_plugin.py index 9066a7a..dd381ba 100755 --- a/scripts/check_plugin.py +++ b/scripts/check_plugin.py @@ -13,9 +13,20 @@ separate defects have reached a live install through that path: install, because `plugin.json`'s version wasn't bumped and the installer compares versions to decide whether to refresh its cache. -The rule for the second one was already written down and was still missed. A -written rule that depends on being remembered during a long session is not a -control; this is. +Both were fixed. The second was fixed TWICE — once by bumping the number, and +then properly, by removing the class it came from: `plugin.json`'s version is +no longer a value anybody chooses. `scripts/mint_plugin_version.py` derives it +from the clock (`make mint-plugin`), and `check_version_is_minted` below fails +the lane when shipped content moved and the version did not. + +State exactly what that did and did not remove, because a rationale that +overstates its own control is how the control gets trusted past its limit, and +because the paragraph this replaces was itself read that way. Gone: having to +remember which NUMBER to write, and the whole question of whether a chosen +number was the right one. Not gone: the mint still has to be RUN, and +forgetting to run it is still possible. What changed is that forgetting is now +LOUD — a red lane on the batch that forgot, instead of a silent no-op found +weeks later when somebody says "I don't think it updated" (#2220). shellcheck and jq are NOT in `ci-python` (verified against CI-runner's Dockerfile and scripts/install-common.sh, not from memory — rule #37). CI installs both @@ -31,8 +42,15 @@ itself loudly, because a check that quietly no-ops is the failure mode this whole file exists to prevent. Usage: - python3 scripts/check_plugin.py # all checks - python3 scripts/check_plugin.py --no-version # skip the version check + python3 scripts/check_plugin.py # all checks + python3 scripts/check_plugin.py --no-version # on `main` only — see below + +`--no-version` exists for ONE case. The version is measured against +`origin/main`, so on `main` itself the comparison is against itself and answers +nothing; the syntax, pattern and marker checks are the only ones that mean +anything there. It is NOT a way past a red lane — see +`check_version_is_minted`, whose whole design is shaped by keeping this flag +out of anyone's muscle memory. """ from __future__ import annotations @@ -217,8 +235,6 @@ def check_patterns() -> None: ok(f"{rel}: no known-bad patterns") -# --- the version bump ------------------------------------------------------ - # --- shellcheck ------------------------------------------------------------ def check_shellcheck() -> None: @@ -668,7 +684,8 @@ def check_version_is_minted(base: str = "origin/main") -> None: def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--no-version", action="store_true", - help="skip the minted-version check") + help="skip the minted-version check; for `main`, where " + "it would be measured against itself") parser.add_argument("--base", default="origin/main", help="branch the version is measured against") args = parser.parse_args() diff --git a/tests/test_plugin_shipped_set.py b/tests/test_plugin_shipped_set.py index 0a8f46e..2587f6e 100644 --- a/tests/test_plugin_shipped_set.py +++ b/tests/test_plugin_shipped_set.py @@ -203,7 +203,7 @@ def test_shipped_content_changed_reports_a_version_only_commit_as_unchanged(monk """End to end through the git seam, with git stubbed. The unit above proves the comparison; this proves it is actually WIRED to - the path that `check_version_bump` reads. A correct helper nobody calls + the path that `check_version_is_minted` reads. A correct helper nobody calls would leave the circular check exactly as it was. """ from scripts import check_plugin diff --git a/tests/test_write_path_trigger.py b/tests/test_write_path_trigger.py index f84b2cd..c2c47c1 100644 --- a/tests/test_write_path_trigger.py +++ b/tests/test_write_path_trigger.py @@ -867,14 +867,6 @@ def test_hook_skips_prose_and_data_files(): assert '/scribe_defs.sh"' in src # sourced, not copied -def test_plugin_version_bumped_with_the_hook(): - """The #1040 lesson: a plugin change clients can't see is a change that didn't - ship.""" - manifest = json.loads((PLUGIN / ".claude-plugin" / "plugin.json").read_text()) - version = tuple(int(p) for p in manifest["version"].split(".")) - assert version >= (0, 1, 31) - - def test_hook_keeps_sync_and_reuse_dedup_apart(): """#2708's dedup audit, pinned: the hook holds TWO per-session id files and feeds each its own class — sync ids (snippets recording the edited file) to