docs(6210): acceptance sweep — src, scripts and frontend leftovers described in the present tense
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 34s

The acceptance greps found what the per-area passes missed:

- src/scribe: "Phase 7/8" removal notes in projects, settings and
  user_profile; a paragraph promising an auto-summary side effect that no
  longer exists; user_profile's claim that "upcoming MCP profile tools" read
  the profile, which nothing does; an instance count and instance rule
  titles in plugin_context; "MILESTONE 323 DECIDED" in rulebooks.
- scripts/: check_plugin's header and inline comments were a history of
  the incidents behind each guard, with issue numbers in two failure
  messages; check_dangling_styles named this app's classes as its examples;
  check_design_tokens, measure_duplication and mint_plugin_version cited
  issues and checklists.
- frontend/src: forty-odd comments with four-digit issue numbers that the
  earlier frontend scan filtered out as hex colours, plus "used to" and
  "Phase N" notes.

Kept: illustrative ids (`#4181` as a bare-id example, `PR #198`), migration
Create Date headers and one migration's dated data floor, and the plugin
skills' worked examples of how to cite a record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:54:38 -04:00
co-authored by Claude Opus 5.5
parent 4c150cbc2f
commit 067aa58fbc
40 changed files with 199 additions and 257 deletions
+10 -12
View File
@@ -8,13 +8,13 @@ like. Three ways it goes wrong, all of them silent:
1. A rule is deleted and its `:hover` / modifier survives. The selector still
exists, so nothing reads as unused, but the element renders with no base
styling at all. `.btn-workspace:hover` outlived `.btn-workspace` and a
router-link rendered as raw browser blue for days.
styling at all — `.btn:hover` outliving `.btn` leaves a link in raw
browser blue.
2. The parent's layout rule is deleted while the children keep theirs.
`.milestone-header` was a flex row; its children still declare `flex: 1`
and `flex-shrink: 0`. Without the parent they stack vertically, and a
milestone that was one line becomes five. Nothing errors — the page just
A header that is a flex row loses its rule; its children still declare
`flex: 1` and `flex-shrink: 0`. Without the parent they stack vertically,
and a row that was one line becomes five. Nothing errors — the page just
wastes space, which reads as a design decision.
3. A rule is removed from a comma-separated group, leaving `.a,` dangling in
@@ -35,9 +35,8 @@ Descendant selectors count as a base — `.panel .row {}` styles `.row` — beca
from the element's side there is no difference. Only the LAST compound of a
selector is what it styles, and a compound's whole class SET is what it
requires: `.pane.empty` and `td.num` are base rules for the element carrying
those classes, not modifiers. Reading them as modifiers cost this check four
false reports on its first run, and a check with false reports is one that gets
skimmed.
those classes, not modifiers. Reading them as modifiers produces false
reports, and a check with false reports is one that gets skimmed.
REPORT, NOT FAIL. Bare wrappers with no styling of their own are legitimate,
so this cannot be a gate without a suppression mechanism nobody would maintain.
@@ -46,11 +45,10 @@ say so in a comment beside its descendant rules — the remaining reports here
all carry one, so a new entry means something changed.
KNOWN BLIND SPOT: a class with NO rule anywhere is invisible to this, because
it cannot be told from a semantic-only hook. `.systems-list` had lost its
entire rule and was rendering with browser bullets; it was found by reading the
file next to a class that WAS half-styled, not by this check.
it cannot be told from a semantic-only hook. A list whose entire rule is gone
renders with browser bullets, and only reading the file finds it.
INSTANCE-AGNOSTIC (rule #115). Nothing here knows a class name, a component, or
INSTANCE-AGNOSTIC. Nothing here knows a class name, a component, or
a convention; point it at any Vue tree.
"""
from __future__ import annotations
+10 -11
View File
@@ -1,19 +1,18 @@
#!/usr/bin/env python3
"""Check the frontend's CSS against the tokens its stylesheet declares.
The gap this closes: `services/design_stylesheet.check_code_against_tokens` has
always been able to answer "does this code use the sheet correctly?", but the
only thing ever fed to it was recorded SNIPPETS. The app's own components — where
sixteen unresolvable references were found living quietly (#2319) — were checked
by nothing at all.
`services/design_stylesheet.check_code_against_tokens` answers "does this code
use the sheet correctly?", but inside the app it is fed only recorded SNIPPETS.
The app's own components, where unresolvable references live quietly, need a
check of their own.
That was structural rather than an oversight. The drift panel runs in the browser
and cannot read source files, and the server has no repo access. CI is the only
place holding both the component sources and the ability to run the check, and it
only became cheap once `theme.css` became a generated artifact — so the source of
truth is a local file, with no network and no credentials.
That is structural. The drift panel runs in the browser and cannot read source
files, and the server has no repo access. CI is the only place holding both the
component sources and the ability to run the check, and `theme.css` is a
generated artifact — so the source of truth is a local file, with no network
and no credentials.
INSTANCE-AGNOSTIC ON PURPOSE (rule #115). Nothing here knows what a token should
INSTANCE-AGNOSTIC ON PURPOSE. Nothing here knows what a token should
be called or which literals are discouraged. Both come from the stylesheet: the
declarations, and the `SUPERSEDES` block the generator emits. Point it at a
different install's sheet and it checks that install's rules.
+89 -113
View File
@@ -4,47 +4,35 @@
WHY THIS EXISTS. `plugin/` is not built into the Docker image. Installs fetch it
from this git repo via `.claude-plugin/marketplace.json`, so a push IS the
release for plugin content: no build, no gate, immediately fetchable. Two
separate defects have reached a live install through that path:
failures reach an install through that path without a sound:
- #2198 — all four hook scripts were inert (wrong env-var case, line-oriented
`jq -rR`, line-oriented `cut -c`). No CI ran, because `plugin/**` wasn't in
the workflow's `paths:` filter at all.
- #2209 — the fix for #2198 shipped to `main` and still couldn't reach an
install, because `plugin.json`'s version wasn't bumped and the installer
compares versions to decide whether to refresh its cache.
- inert hook scripts (a wrong env-var case, a line-oriented `cut -c`) — so
`plugin/**` is in the workflow's `paths:` filter and these checks run on it;
- a fix that ships and cannot reach an install, because `plugin.json`'s
version did not move and the installer compares versions to decide whether
to refresh its cache.
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.
`plugin.json`'s version is not 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).
What that does and does not remove, stated exactly, because a rationale that
overstates its own control is how the control gets trusted past its limit.
Gone: choosing which NUMBER to write, and the question of whether a chosen
number was the right one. Not gone: the mint still has to be RUN. What the
check adds is that forgetting is 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".
shellcheck is NOT in `ci-python` (verified against CI-runner's Dockerfile and
scripts/install-common.sh, not from memory — rule #37). CI installs it per-job,
which is what CI-runner's own docs/process.md prescribes for a dep with a
single consumer: "If only one project needs the dep, prefer that project
installing it per-job in their workflow — at least until a second consumer
arrives." Promotion into the image is filed as an issue there rather than
assumed here.
shellcheck is NOT in the `ci-python` image; CI installs it per-job, the usual
practice for a dependency with a single consumer. It is optional at runtime:
without shellcheck the lint step is SKIPPED and says so. A skipped check
announces itself loudly, because a check that quietly no-ops is the failure
mode this whole file exists to prevent.
It is optional at runtime: without shellcheck the lint step is SKIPPED and says
so. A skipped check announces itself loudly, because a check that quietly
no-ops is the failure mode this whole file exists to prevent.
jq USED TO BE IN THAT SAME SENTENCE, and the smoke tests below skipped
themselves without it. Since jq is not in `ci-python` either, that meant three
of them announced a skip on every CI run and had never actually run there. The
hooks need no jq now (#4107), so those checks run unconditionally — removing a
dependency from the product removed a permanent hole in its verification.
The hooks need no jq (it is not in `ci-python` either), so the smoke tests
below run unconditionally rather than skipping wherever it is absent.
Usage:
python3 scripts/check_plugin.py # all checks
@@ -85,7 +73,7 @@ MANIFEST = PLUGIN_DIR / ".claude-plugin" / "plugin.json"
# ── What ships, and what decides what it says about itself ─────────────────
#
# ONE definition (#3127 §3, milestone 334 step 2). It has TWO consumers that
# ONE definition. It has TWO consumers that
# need different granularities, and conflating them is the bug:
#
# the workflow's `paths:` trigger whole paths should CI run at all?
@@ -103,15 +91,14 @@ MANIFEST = PLUGIN_DIR / ".claude-plugin" / "plugin.json"
# carries description, mcpServers and userConfig, all of which reach an
# install and all of which matter. Excluding the file wholesale would mean a
# userConfig-only edit computes an unchanged version and never refreshes —
# #2209 again with a narrower trigger.
# the unreached fix again, with a narrower trigger.
SHIPPED_PATHS = ("plugin", ".claude-plugin")
# Files that decide what a published artifact SAYS ABOUT ITSELF — kept as a
# table so the next artifact is a one-line addition rather than a third
# bespoke guard (#3127 §3). The membership test is NOT "is this copied into
# the artifact?" but "can changing this file change the published bytes, or
# what the artifact says about itself?" — FC learned that twice in four days
# (#3156, #3202), and a deriver is never in the COPY list.
# bespoke guard. The membership test is NOT "is this copied into the
# artifact?" but "can changing this file change the published bytes, or what
# the artifact says about itself?" — a deriver is never in the COPY list.
#
# Note what is absent: a CHECKER does not belong here. Whatever validates a
# version decides whether the lane goes red, not what any artifact reports,
@@ -119,10 +106,10 @@ SHIPPED_PATHS = ("plugin", ".claude-plugin")
# the plugin version is.
DERIVERS: dict[str, tuple[str, ...]] = {
# The "Generate image tags and version" step computes the server image's
# name, ordering key and channel (#3298).
# name, ordering key and channel.
".forgejo/workflows/ci.yml": ("server-image",),
# Decides the plugin's version FORMAT, so it decides what every future
# manifest says about itself (milestone 334 step 3).
# manifest says about itself.
"scripts/mint_plugin_version.py": ("plugin",),
}
@@ -130,7 +117,7 @@ DERIVERS: dict[str, tuple[str, ...]] = {
def version_relevant_paths() -> tuple[str, ...]:
"""Everything a change to which must produce a NEW plugin version.
Wider than `SHIPPED_PATHS`, and #3127 §3's asymmetry is why it has to be:
Wider than `SHIPPED_PATHS`, and this asymmetry is why it has to be:
A change to how the VERSION is computed is compared against nothing at
all. Left out, the published artifact goes on reporting the OLD value
@@ -140,7 +127,7 @@ def version_relevant_paths() -> tuple[str, ...]:
and a diff over the shipped paths alone reports "no content change, the
version need not move". The manifest then keeps a value in the old format
forever and nothing ever says so. The mint script reaches no install and
belongs here anyway; that is #3156's exact shape.
belongs here anyway.
A CHECKER is deliberately not here. Whatever validates the version decides
whether the lane goes red, not what any artifact reports — so this file is
@@ -165,7 +152,7 @@ def ok(msg: str) -> None:
def skip(msg: str) -> None:
# Loud on purpose. A check that quietly does nothing is indistinguishable
# from a check that passed — the exact confusion that let #2198 survive.
# from a check that passed.
print(f"SKIP {msg}")
@@ -191,7 +178,7 @@ def check_syntax() -> None:
# --- known-bad patterns ----------------------------------------------------
# Each entry: (compiled pattern, short label, why it's wrong).
# These are the exact classes from #2198. They are deliberately specific — a
# These are the classes that make a hook inert. They are deliberately specific — a
# broad shell linter belongs in the image, not hand-rolled here.
PATTERNS: list[tuple[re.Pattern, str, str]] = [
(
@@ -202,15 +189,11 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
"hook then does nothing, silently.",
),
(
# NEITHER OF THESE MAY COME BACK (#4107). This replaced a narrower rule
# about `jq -R` being line-oriented, which is now moot in the only way
# that rule could become moot: there is no jq left to pass flags to.
#
# The wider rule is the one worth having. jq is absent by default on
# NEITHER OF THESE BELONGS IN A HOOK. jq is absent by default on
# macOS, on the Debian/Ubuntu slim images, on Alpine and in most CI
# containers; `tac` is GNU-only and absent on macOS. Every hook used to
# guard itself with `command -v jq || exit 0`, so a machine without it
# got no context, no rules, no prior art and no process sync, silently —
# containers; `tac` is GNU-only and absent on macOS. A hook that guards
# itself with `command -v jq || exit 0` gives a machine without it no
# context, no rules, no prior art and no process sync, silently —
# `exit 0` is indistinguishable from "nothing to say". A hook may use
# only what POSIX guarantees; scribe_defs.sh has the readers.
re.compile(r"(?<![\w./-])(jq|tac)(?![\w./-])"),
@@ -219,15 +202,15 @@ PATTERNS: list[tuple[re.Pattern, str, str]] = [
"by default anywhere the plugin is likely to land, and tac is GNU-only. "
"Read JSON with scribe_json_flat / _pick / _list / _len, write it with "
"scribe_json_out, encode with scribe_urlenc; to take the LAST match of "
"a filter, pipe to `tail -1` instead of reversing with tac (#4107).",
"a filter, pipe to `tail -1` instead of reversing with tac.",
),
(
# `$(producer | head -N) || var=""` — the fallback OUTSIDE the
# substitution (#4042). Every hook runs under `pipefail`; once `head`
# substitution. Every hook runs under `pipefail`; once `head`
# (or `grep -m`/`-q`) has what it needs it exits, a producer still
# writing dies of SIGPIPE, the substitution fails, and the fallback
# wipes the output that was already captured. Silent, and only on big
# inputs — which for the duplicate arm meant the most-duplicated names.
# inputs — which for the duplicate arm means the most-duplicated names.
re.compile(r"\|\s*(?:head\b|grep\s+-[A-Za-z]*[mq])[^)]*\)\s*\|\|\s*\w+=\"\""),
"early-exit consumer with the fallback outside the substitution",
"Under pipefail, `$(x | head -N) || v=\"\"` discards head's output whenever x "
@@ -298,7 +281,7 @@ def check_shellcheck() -> None:
# always emits a static behavioural floor that needs no credentials and no
# network, so "silent" would be the wrong assertion for it.
#
# UNREACHABLE is different for the two write-path hooks since #2932: a call
# UNREACHABLE is different for the two write-path hooks: a call
# that was owed and did not come back is SAID, once per outage ("> Scribe did
# not answer …"), so a session can tell "checked, nothing there" from "never
# checked". That line — or silence, when the once-per-outage marker in
@@ -306,19 +289,17 @@ def check_shellcheck() -> None:
# is the only output allowed with no working instance; anything else is a hook
# speaking on data it cannot have.
#
# This is the contract that made #2198 invisible for weeks, so it is worth
# pinning: the bug and the healthy no-results case looked identical from
# outside. #2932 is what finally makes the failure visible at the write; this
# check makes sure the fail-open behaviour stays deliberate rather than
# accidental.
# The contract is worth pinning because an inert hook and the healthy
# no-results case look identical from outside. The outage line makes the
# failure visible at the write; this check makes sure the fail-open behaviour
# stays deliberate rather than accidental.
# A symbol that exists nowhere, ASSEMBLED rather than written literally.
# The prior-art hook's local arm (#2280) fires with no credentials, so the
# silence assertion below needs a name the repo genuinely lacks. Two traps,
# both hit while writing this:
# - `def f` matched real code, so the hook spoke and "silent" was asserting
# the wrong thing;
# - spelling the replacement out in full put `def <name>(` INTO this file,
# so the smoke event defined the very symbol it claimed was absent.
# The prior-art hook's local arm fires with no credentials, so the silence
# assertion below needs a name the repo genuinely lacks. Two traps:
# - a short name like `def f` matches real code, so the hook speaks and
# "silent" asserts the wrong thing;
# - spelling the replacement out in full puts `def <name>(` INTO this file,
# so the smoke event defines the very symbol it claims is absent.
# Concatenating keeps the contiguous string out of the source.
_ABSENT_SYM = "zz" + "_absent_" + "9f3a2b"
@@ -331,7 +312,7 @@ SMOKE_EVENTS: dict[str, str] = {
"tool_input": {"file_path": "src/x.py",
"new_string": f"def {_ABSENT_SYM}():\n pass\n"}}
),
# The pre-tool rule arm (#3476). A real Bash call, and one whose whole
# The pre-tool rule arm. A real Bash call, and one whose whole
# point is that it looks harmless: reaching for curl against the forge API
# is the reflex the arm exists to catch. With no instance it must stay
# SILENT — it is deliberately not an OUTAGE_SPEAKER, because a Bash call is
@@ -341,7 +322,7 @@ SMOKE_EVENTS: dict[str, str] = {
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
"tool_input": {"command": "curl -s https://example.invalid/api/v1/runs"}}
),
# The moment arm (milestone 458). A command the shipped mappings send to
# The moment arm. A command the shipped mappings send to
# work.deliver, so a configured run reaches the moment-tools fetch and the
# moment request; with no instance both fail and it must stay SILENT,
# since it fires before every call that can reach a mounted rule.
@@ -351,7 +332,7 @@ SMOKE_EVENTS: dict[str, str] = {
),
"scribe_sync_processes.sh": json.dumps({"source": "startup"}),
"scribe_session_context.sh": json.dumps({"source": "startup"}),
# The after-write hook (#2901) diffs the working tree; on CI's clean
# The after-write hook diffs the working tree; on CI's clean
# checkout there is nothing to report, so silence is the right assertion.
# (On a dirty local tree with a definition just written it may speak —
# that is the hook working, not a failure of the contract.)
@@ -359,27 +340,27 @@ SMOKE_EVENTS: dict[str, str] = {
{"session_id": "smoke", "cwd": ".", "tool_name": "Bash",
"tool_input": {"command": "true"}, "tool_response": {}}
),
# The Stop-hook report check (milestone 409 step 5). A transcript that does
# The Stop-hook report check. A transcript that does
# not exist is the smoke case: nothing to read, so it must stay silent and
# never block, configured or not.
"scribe_report_check.sh": json.dumps(
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
),
# The reply moment (milestone 458). No transcript, so no reply: silent and
# The reply moment. No transcript, so no reply: silent and
# never a block, configured or not.
"scribe_reply_check.sh": json.dumps(
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
),
# The end-of-turn shape check (milestone 439). With no ledger for the
# The end-of-turn shape check. With no ledger for the
# session there is nothing to ask about, and with no instance there is
# nobody to ask: silent, and never a block, configured or not.
"scribe_shape_check.sh": json.dumps(
{"session_id": "smoke", "transcript_path": "/nonexistent/smoke.jsonl",
"cwd": ".", "hook_event_name": "Stop", "stop_hook_active": False}
),
# The PreCompact preserver (#3680). Its whole contract is the inverse of
# The PreCompact preserver. Its whole contract is the inverse of
# every other hook's: it must exit 0 AND print, because stdout is what
# becomes the summarizer's custom instructions. A silent success here is
# the failure mode, and it would look like every other hook's success.
@@ -387,7 +368,7 @@ SMOKE_EVENTS: dict[str, str] = {
{"session_id": "smoke", "cwd": ".", "hook_event_name": "PreCompact",
"trigger": "manual", "custom_instructions": None}
),
# The opened-ledger recorder (#4100). It writes to TMPDIR and prints
# The opened-ledger recorder. It writes to TMPDIR and prints
# nothing — a PostToolUse hook that emitted output would put a line in
# front of every get_rule call, which is the opposite of its purpose. The
# smoke case is a well-formed open: it must exit 0 and stay silent.
@@ -396,18 +377,17 @@ SMOKE_EVENTS: dict[str, str] = {
"tool_name": "mcp__scribe__get_rule", "tool_input": {"rule_id": 1},
"tool_response": {}}
),
# The acted-ledger recorder (#4216). Same shape and same reasoning as its
# sibling above: TMPDIR only, and silent, because a PostToolUse hook that
# spoke would put a line after every rule_outcome call and turn recording
# an outcome into something with a cost. A skip here would have left the
# newest of the three ledgers as the only hook the lane never runs.
# The acted-ledger recorder. Same shape and same reasoning as its sibling
# above: TMPDIR only, and silent, because a PostToolUse hook that spoke
# would put a line after every rule_outcome call and turn recording an
# outcome into something with a cost.
"scribe_record_outcome.sh": json.dumps(
{"session_id": "smoke", "cwd": ".",
"tool_name": "mcp__scribe__rule_outcome",
"tool_input": {"rule_id": 1, "outcome": "applied"},
"tool_response": {}}
),
# The claim-session binder (milestone 381). Silent like the ledgers above —
# The claim-session binder. Silent like the ledgers above —
# a PostToolUse hook that spoke would add a line after every task write —
# and with no instance configured it must exit before reaching for one.
"scribe_claim_session.sh": json.dumps(
@@ -416,7 +396,7 @@ SMOKE_EVENTS: dict[str, str] = {
"tool_input": {"task_id": 1, "content": "smoke"},
"tool_response": {}}
),
# The SessionEnd claim release (milestone 381 step 4). Silent: nobody reads
# The SessionEnd claim release. Silent: nobody reads
# a SessionEnd hook's output, and with no instance it must exit first.
"scribe_session_end.sh": json.dumps(
{"session_id": "smoke", "cwd": ".", "hook_event_name": "SessionEnd",
@@ -435,14 +415,14 @@ STATIC_EMITTERS = {
# The two-tier SessionStart design: the bundled static tier ships whatever
# the instance does, and that floor is the whole point of the split.
"scribe_session_context.sh": "static floor present",
# PreCompact's contract is INVERTED (#3680). Its stdout becomes the
# PreCompact's contract is INVERTED. Its stdout becomes the
# summarizer's custom instructions, and what must survive a summary is
# known without asking anything — so it needs no instance, and silence is
# the failure mode rather than the success one. Read as a generic hook it
# would look like a leak; it is the opposite.
"scribe_precompact_preserve.sh": "preservation instructions present",
}
# The hooks that say so when a configured instance does not answer (#2932).
# The hooks that say so when a configured instance does not answer.
OUTAGE_SPEAKERS = {"scribe_prior_art.sh", "scribe_after_write.sh"}
OUTAGE_LINE = "> Scribe did not answer the prior-art check"
@@ -458,13 +438,9 @@ def _run_hook(script: Path, event: str, env_extra: dict[str, str]) -> subprocess
def check_fail_open() -> None:
# No jq gate any more (#4107). This check used to skip itself when jq was
# missing, because without it every hook bailed at line 1 and the check
# would have passed while exercising nothing. jq is NOT in `ci-python`, so
# what that actually meant is that this smoke test announced a skip on
# every CI run and never once ran there. The hooks now need only POSIX
# tools, so it runs everywhere — which is the point of the change it is
# testing.
# No jq gate: the hooks need only POSIX tools, so this runs everywhere,
# CI's `ci-python` image included. A gate on a tool the image lacks would
# announce a skip on every run and never exercise the hooks there.
scenarios = [
("unconfigured", {}),
# Connection refused immediately — exercises the unreachable-instance
@@ -514,7 +490,7 @@ def check_fail_open() -> None:
def check_local_prior_art_needs_no_instance() -> None:
"""The prior-art hook's local arm must answer with no credentials (#2280).
"""The prior-art hook's local arm must answer with no credentials.
The other arms ask Scribe what was RECORDED. This one asks the repo what
EXISTS, which needs no instance — and that is the whole reason it catches
@@ -562,8 +538,8 @@ def check_session_context_reports_its_version() -> None:
pulls on its own, while the CACHE is what executes and refreshes only when
the manifest version changes. So a shipped fix can sit unreached while
inspecting the clone shows it present — the obvious debugging move
misleads, and twice the only detector was a human saying "I don't think it
updated" (#2209, #2220).
misleads, and otherwise the only detector is a human saying "I don't think
it updated".
Asserted WITHOUT credentials on purpose. The state most needing diagnosis
is the one where the token never arrives, and a marker that vanished there
@@ -643,8 +619,8 @@ def manifest_differs_beyond_version(a: str | None, b: str | None) -> bool:
or mcpServers-only edit demanding a new version.
Unreadable input answers True. The conservative direction is "demand a new
version": a spurious bump costs one cache refresh, while a missed one is
#2209 — the fix reaches the repo and stops there.
version": a spurious bump costs one cache refresh, while a missed one means
the fix reaches the repo and stops there.
"""
def without_version(text: str | None):
if text is None:
@@ -667,8 +643,8 @@ def shipped_content_changed(base: str) -> tuple[bool | None, list[str]]:
"""Has anything that REACHES AN INSTALL changed against `base`?
Returns `(changed, paths)`. `changed` is **None** when the question could
not be answered — a caller must never read that as "no", which is the
distinction #2663 cost weeks of zeroed telemetry to learn.
not be answered — a caller must never read that as "no": an unanswered
question read as "nothing changed" is a failure that reports success.
The manifest is special-cased, not excluded: if it is the ONLY thing that
moved and the only difference is `version`, nothing that reaches an
@@ -693,17 +669,17 @@ def shipped_content_changed(base: str) -> tuple[bool | None, list[str]]:
def check_version_is_minted(base: str = "origin/main") -> None:
"""THE control (#3127 checklist 4), replacing "somebody remembers".
"""THE control, replacing "somebody remembers".
The checklist asks, of any hand-set component: *say what happens the
release somebody forgets it.* This is the answer — the lane goes red,
deterministically, because CI can compute whether the value should have
moved. Its predecessor could only ask "did the number move at all", which
any bump satisfied and which therefore proved nothing.
moved. Asking only "did the number move at all" would be satisfied by any
bump and would therefore prove nothing.
Four verdicts:
content changed, version did not FAIL — this is #2209, exactly
content changed, version did not FAIL — the fix would not reach an install
version not in canonical shape FAIL — see below
version implausibly in the future FAIL — a bad clock or a hand-edit
version moved, content did not pass, and say so
@@ -718,10 +694,10 @@ def check_version_is_minted(base: str = "origin/main") -> None:
A malformed version is worth failing on even though the installer would
accept it. `K4` returns the manifest string verbatim, and `H == "unknown"`
sets `forceOverwrite`, so a broken value either sorts as a normal string
or reinstalls the plugin every single session (#3325). Neither is loud.
or reinstalls the plugin every single session. Neither is loud.
Stated against the BASE BRANCH rather than the last commit, as its
predecessor was: a per-commit rule would demand a fresh mint from every
Stated against the BASE BRANCH rather than the last commit: a per-commit
rule would demand a fresh mint from every
commit in a batch, when what matters is that whatever reaches an install
differs from what is cached. One mint per batch, which is also how a person
would do it.
@@ -745,8 +721,8 @@ def check_version_is_minted(base: str = "origin/main") -> None:
if not VERSION_RE.match(here):
fail(
f"the manifest version is {here!r}, which is not YYYY.MM.DD.HHMM.\n"
f" One shape for every version in the family (#3127 checklist "
f"10), zero-padded so the midnight case renders 2026.01.05.0000.\n"
f" One shape for every version in the family, zero-padded so "
f"the midnight case renders 2026.01.05.0000.\n"
f" Run `make mint-plugin`."
)
return
@@ -780,7 +756,7 @@ def check_version_is_minted(base: str = "origin/main") -> None:
f" The installer decides whether to refresh its cache by "
f"comparing this string, so an unchanged version means these edits "
f"reach the repo and stop there — the marketplace clone updates, the "
f"cache that actually executes does not (#2209, #1040, #2220).\n"
f"cache that actually executes does not.\n"
f" Run `make mint-plugin`.\n"
f" Changed:\n {files}"
)
+1 -2
View File
@@ -7,8 +7,7 @@ A DRY audit made of several passes ends with a question no single pass can
answer: did the audit as a whole reduce duplication, and did it create any? The
second half is the one that matters. A pass that shortens two statements can
leave them byte-identical to a third, and the new copy is invisible to that
pass's own scan because it did not exist when the scan ran (#4745: a retro over
a 17-pass audit found exactly this, and folded it into a view). This script is
pass's own scan because it did not exist when the scan ran. This script is
the close-out measure the DRY Pass process names, so the measure is a tool and
not something each audit improvises.
+4 -5
View File
@@ -11,7 +11,7 @@ Installs fetch it straight from this git repo via `.claude-plugin/
marketplace.json`, so **a push IS the release** — there is no build between
you committing and a user fetching, and therefore no moment at which CI could
stamp a version in. Every other artifact in the family derives its version
during a build (note #3127 §2). This one has no build to derive during.
during a build. This one has no build to derive during.
WHICH CLOCK, AND WHY IT DIFFERS FROM THE SERVER IMAGE — the divergence is
deliberate, and it lives one directory away from its opposite, so it is
@@ -27,8 +27,7 @@ one lane and no build, so that reason does not reach it and paying its cost
buys nothing. What is given up is reproducibility-from-history: you cannot
recompute this value later, only verify that it moved when it had to.
That trade is acceptable ONLY because of what #3325 established by reading the
installer's code: the refresh test is `P.version === H`, plain string
That trade is acceptable ONLY because of what the installer's code does: the refresh test is `P.version === H`, plain string
equality, with no ordering comparison anywhere. Where a comparator ORDERS, an
unreproducible version is dangerous — nothing can check it is right. Where it
only tests equality, "did it change when it should have" is the entire
@@ -52,8 +51,8 @@ ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "plugin" / ".claude-plugin" / "plugin.json"
# Four dot-separated numeric fields, zero-padded, and nothing else — one shape
# for every human-readable version in the family (#3127 checklist 10). The
# padding is load-bearing for the midnight case the checklist names by hand:
# for every human-readable version in the family. The padding is
# load-bearing for the midnight case:
# 2026.01.05.0000, which an unpadded `%-H%M` would render as `0` and silently
# shorten. Harmless while nothing orders these, wrong the moment anything does.
VERSION_RE = re.compile(r"^\d{4}\.\d{2}\.\d{2}\.\d{4}$")