Three published documents went out with 0.7.0 unread. SKILL.md told a reader what a SKIP FINDING means and stopped there. Complete before 0.7.0, not now: a repo can headline `OK · 3 not checked`, and a session reporting the status word alone would report a green pass over three unjudged findings. Status and count are now required together. Shipping without this made the plugin whose purpose is catching stale published documents ship one. README's example output cited commits older than the behaviour they demonstrate —5884a64(0.6.0) beside a `notChecked` that did not exist in it, anda7276e6two releases back. The version names a file; only the sha names the code. Both now cite10ad125. README also now states the unit: `notChecked` counts un-judged FINDINGS, not checks. portfolio-optimiser's `11 not checked` is almost entirely LINK-OUTSIDE-REPO, so the bare number reads worse than the repo is. Named as a known limitation, not corrected in the string — the fix is splitting "deliberately not judged" from "could not run", which is the next release.
24 KiB
Changelog
All notable changes to this project are documented here. Format follows Keep a Changelog; versioning is Semantic Versioning.
[0.7.1] — 2026-08-09
Fixed
-
The skill told a reader what a
SKIPfinding means and stopped there, which was complete before 0.7.0 and is not now: a repo can headlineOK · 3 not checked, and a session reporting only the status word would report a green pass over three unjudged findings.SKILL.mdnow requires the status and the count to be reported together. Shipping 0.7.0 without this made the plugin whose purpose is catching stale published documents ship one. -
README's example output carried commits older than the behaviour they demonstrate — one from 0.6.0 beside a
notCheckedthat did not exist in it, one two releases back. This repo's own invariant is that the version names a file and only the sha names the code; the examples now cite10ad125, the commit the feature actually shipped in.
Added
- README states the unit of
notChecked: it counts un-judged findings, not checks. A repo with many links leaving it scores a high count without a single check having failed to run, so the bare number reads worse than the repo is. Named as a known limitation rather than corrected in the string — the fix is splitting deliberately not judged from could not run, which is its own release.
[0.7.0] — 2026-08-09
Changed
-
A repo's
statusis the worst judged finding, andSKIPonly when nothing was judged.SKIPused to rank betweenOKandWARN, so one un-runnable check spoke for every check that ran: a repo with 0 ERROR, 0 WARN and a dozenOKheadlined as "skipped". Reported by org-ops against five repos in census 05 —ktg-plugin-marketplace,okr,playground-design-system,portfolio-optimiser,commons— withokrholding the mostOKin the org and reading as unread. A skip is not a severity; it is the absence of a verdict, and it cannot be the worst of a set that contains real ones. "SKIPis never a pass" survives intact in the half of the rule that keeps it: an unregistered repo, or an empty finding set, still saysSKIP, because there is nothing else to be worst of.Measured across all 21 local clones from one saved sweep, so before and after are computed from the same findings rather than from two sweeps of a moving org: 343 findings before, 343 after, none added or removed. 8 repos moved, every one
SKIP→OK. The counts that decide whether a repo needs work did not move at all — 1ERRORbefore and after, 3WARNbefore and after.Consumers reading
.statussee a changed value domain: a repo that reportedSKIPin censuses 03–07 may reportOKfrom 08 on with an identical finding set. Raw census files already carryengineVersionandengineCommit, which is what distinguishes the two readings.
Added
notCheckedbesidestatus, in--jsonand as a· N not checkedqualifier on the summary line. Fixing the status alone would have traded "clean repos look skipped" for "skipped checks look clean", which is the worse direction — so coverage gets its own axis rather than being folded into the verdict, the same split the engine already makes between a finding's level and its bucket. The count is the number ofSKIPfindings, which is what the existingNOT CHECKED — these are not passessection has always listed. An absentnotCheckedmeans an older engine and prints the pre-0.7.0 line; it is not zero.
[0.6.0] — 2026-08-09
Added
-
titlesin the register: an optional per-repo README title. Set, the H1 is compared against it and a match isOK; unset, theWARNstands exactly as before. A human title was already a valid choice —docs/rds-v1.mdprescribes1. # Titlefor every class, not# repo-name— but a decided YES had nowhere to live, so the same 6README-H1WARNs were reported in census 05, 06 and 07 and would have been reported forever. "We decided this is correct" and "nobody has looked at it" now produce different outcomes. Five titles registered, each H1 read from the repo rather than copied from the census;ai-psychosisis deliberately left unregistered, so the one repo where a reader cannot connect title to name stands alone. Measured across all 21 local clones: 6 WARN before, 1 after, no other finding moved. -
engineCommitalongsideengineVersion, in the header line (short) and the--jsonoutput (full). The version names a FILE; only the sha names the CODE. A sweep stamped 18 raw files0.4.0while four carried findings from a check that only exists in 0.5.0 — the feature and the version bump are two commits, so the worktree held new code under an old number for a window, and the stamp lied without being broken. Derived from this checkout, no network call. The field is present-and-nullwhen underivable, never absent: an absent key means an older engine, an explicitnullmeans this engine ran and had no HEAD to read, and a consumer sorting raw files by stamp needs those to differ.
Changed
README-DESCno longer demands byte equality for theorg-profileclass, viareadme_desc_match: falseon the class — data in the register, not a hardcoded name. For an ordinary repo the README opening and the forge description describe the same subject and equality is right; for this one class they do not — the README is the organisation's landing page, the forge text describes the repo. Both are correct about their own subject, so it is the equality that does not apply. A landing page's opening line also carries a markdown link while the description field renders as plain text, so passing the old check would have meant putting raw markdown on a real surface. The class has one member, so coverage is lost nowhere else, and the exemption is still RECORDED as anOKfinding naming the reason rather than dropped. Measured:.profilewent fromERRORto 0 ERROR / 0 WARN, and the same README under apluginclass is still anERROR.
[0.5.0] — 2026-08-09
Added
LINK-INTERNAL-FIXTURE: a dead link inside atest/,tests/,fixtures/, or*golden*path now reports asSKIPinstead ofWARN. These paths are presumed to break their own links on purpose —nav-golden-escape/bundle/index.md's deliberate../../../../etc/passwdescape pops the whole base path instead of resolving outside the repo, so it read as a genuine dead link against three repos in the org. The finding still fires with file and line; only the judgement changes. Measured before shipping: 16LINK-INTERNAL-*findings before, 16 after, across all 20 local clones — every one converted 1:1, none disappeared.
[0.4.0] — 2026-08-09
Added
LINKS-OPEN-REFS: theopen/reference check now reports its own result instead of only its failures. Emitting nothing on success made "no dead references" and "the check never ran" identical in the output — a sweep across the org could not tell 19 clean repos from 19 unread ones (measured: org-ops census 03b). The OK line carries the count it resolved, because an OK that cannot say how many it checked is the same silence in a different colour. Zero enumerated files is nowLINKS-OPEN-REFS-UNAVAILABLE(SKIP), which is the honest name for the case that used to look like a pass.portfolio-optimiser-commonsregistered asshared-asset, the org's 20th repo. Class verified structurally rather than accepted: absent from the catalog'smarketplace.json, no.claude-plugin/,README.md+LICENSEpresent, consumed by both reference implementations as agit subtree— sovendoris the right install form and## Installwould be wrong to require. Its absence was producing three falseLINK-DEADERRORs inportfolio-optimiseragainst a repo that exists, and leaving repo #20 with zero coverage. Verified after the fact:portfolio-optimisernow has no ERROR, and the commons repo measures as WARN with 9 checks passed.
Fixed
LINK-DEAD/LINK-NON-REPOfalse negative on a schemaless host.URL_REFrequired://or@host:, sogit.fromaitochitta.com/open/<name>— the form a subtree instruction routinely uses — resolved to nothing at all (measured: llm-security/V3-UPGRADE.md:343). What makes a name resolvable is its position after a host, not the scheme in front of it. Two guards keep the widening from becoming noise: the host must end in a TLD-shaped label, and a candidate preceded by/is a path segment that merely contains a dot, not a host — sodocs/v1.2/open/andtest/nav.golden/open/stay silent, and the API-endpoint rule from 0.3.0 is untouched. Measured before shipping across 1501 tracked Markdown files in 20 local clones: 14 lines changed verdict, 13 of them references that were previously invisible.- The 14th was a defect the widening introduced, caught by that same measurement and fixed here: a markdown link whose display text repeats its own URL matched on both halves, which prints one dead reference twice and inflates the count the new OK line offers as evidence. References are now deduplicated per name-and-line — two different names on one line, or the same name on two lines, still count as two.
[0.3.0] — 2026-08-04
Added
BADGE-DEAD-LINK: a linked badge making a run claim ("tests", "build", "CI", "passing") is now resolved against the repo's tracked files when the link target is relative. Being wrapped in a link previously ended all scrutiny — the link-target capture group was missing from the regex outright, so a badge linked to a dead relative path passed silently. External targets (the ordinary case — a CI provider) still need the network and stay out of scope, same precedent ascheckInternalLinks.
Fixed
LINK-DEADfalse positive on API endpoint paths:URL_REFmatchedopen/<name>anywhere in a URL, so a Forgejo API call like.../api/v1/orgs/open/reposread as a dead reference to a repo named "repos".openmust now be the first path segment after the host, matching how every real repo URL is shaped. Measured twice against the catalog's ownRUNBOOK.md.- 429 backoff was tuned for a hard IP ban that does not exist. Measured
directly against the live forge: nginx never sends
Retry-After(the branch handling it is dead code in practice), the limit is a leaky bucket that can take up to ~15s to drain, and it is IP-based rather than token-quota-based — a valid token made no difference.fetchWithRetrynow defaults to a 23s worst-case budget (retries: 5,maxDelayMs: 8000), up from 7s.CLAUDE.md's "13 calls in a loop" explanation corrected to match. - Two tests asserted facts about their own local test fixture instead of the
real
register/repos.json— a typo in the real register would have drifted past them silently. Both now read the live register vialoadRegister().
[0.2.2] — 2026-08-04
Fixed
- The engine never printed its own version, which is how a stale plugin cache
went unnoticed: two repos independently proved the
/repo-standardskill had resolved${CLAUDE_PLUGIN_ROOT}to a cached0.1.1while0.2.0was installed and the catalog pinned it — same instruction, wrong engine, and0.1.1has neitherBADGE-COUNTnorREADME-LANGUAGE, so the run looked clean instead of incomplete. The header line and--jsonoutput now both carryrepo-standard v<version>;SKILL.mdtells the reader to confirm it against the catalog pin before trusting a green result. The cache resolution itself is the harness's, not this repo's, and is not fixed here.
[0.2.1] — 2026-08-04
Fixed
- The gate makes two anonymous forge calls per invocation (org listing +
catalog
marketplace.json), not one — this repo's ownCLAUDE.mdsaid "one" from beforeINSTALL-TRUTHadded the second, and stayed wrong long enough that a 13-repo shell loop trusted the count and tripped an HTTP 429 at 26 requests. Both calls now go throughfetchWithRetry, which honorsRetry-Afterand retries instead of silently reportingSKIPon a transient rate limit.CLAUDE.mdcorrected to match.
0.2.0 — 2026-08-04
Added
Two presentation checks, both from adversarially-verified research rather than
taste — docs/presentation-research-2026-08-03.md records what the evidence
supports and, more usefully, what it refuses to support.
BADGE-COUNT(WARN) — more than five badges. Trockman et al., ICSE 2018 (n=294,941 npm packages) measured a non-linear relationship with popularity inflecting at five, motivated by surveyed maintainers calling over-badged READMEs cluttered and "trying too hard".WARNand neverERROR: the coefficient sits in an appendix without CI or p-value, so it carries "more is not better" and cannot carry a hard limit. Counting uses a narrower rule than the existing claim check — a screenshot or architecture diagram must not be counted as clutter. Measured: 8 of 18open/repos are past it.README-LANGUAGE(WARN) — the prose is not in the language this repo's readers were declared to speak, via a newlocalesaxis in the register. English is the default;ms-ai-architectandokrare declarednbas Norway-only in audience. Detection is a stopword-frequency comparison over prose with code stripped, so a Norwegian flag name in a shell example cannot decide the document. Evenly bilingual prose is aSKIP— the question is live and unanswered. No running prose is anOK: nothing claims a language, and a thin README ischeckFirstScreen's business. Measured: fires on exactly the two declared repos, silent on all sixteen English ones.
Deliberately not built, because the evidence does not reach: any rule about
images, diagrams, screenshots or terminal recordings (every such claim was
refuted 0-3); a README length bound (no evidence-based target exists); a section
count (would fire on 9 of 18 — textbook "suspect the CHECK"); Mermaid source
length and #gh-dark-mode-only (zero occurrences, and the instance's actual
limit is not readable via the API, so any threshold would be a guess).
Changed
- The skill now carries diagram guidance, deliberately as judgement and not as a
check. Mermaid is verified to render on this forge; that is capability, not
justification, and every claim that visuals improve a README was refuted. What
the guidance covers is a diagram that already exists — height, node labels,
the silent degradation past
MERMAID_MAX_SOURCE_CHARACTERS, and the fact that#gh-dark-mode-onlyis unavailable below Gitea 1.26.0. The org's own measured example renders at 1,459 px and wraps node text mid-word while its neighbour sits at 191 px: rendering and rendering well are separate questions.
0.1.3 — 2026-08-03
Fixed
Three more measured false positives, all reported via coord the same day 0.1.2 shipped, from repos running the gate for the first time.
extractChangelogToptruncated a PEP 440 pre-release (0.5.0a2) to0.5.0, soVERSION-CHANGELOGdisagreed withVERSION-TAG— which compares the untruncated tag and does not have this problem — over a version the manifest and the tag already agreed on. Reported byllm-ingestion-okfwith a repro. An affected repo could reach 0 ERROR only by lying about its version; now the pre-release suffix is kept.BADGE-STATIC-CLAIMtreated a barestatusbadge (status-alpha,status-experimental) as a run claim, the same astests-34-passing. A self-declared maturity label asserts no run — the same class as version, licence and platform, already exempt. Reported byllm-ingestion-pipeline-security.build/ci/passingstill catch the run-asserting compounds ("build status", "CI status"); only the bare word is dropped.BOILERPLATEflagged a bareFIXMEeven when it named the convention rather than an instance of it — a scanner's own docs describing what it detects ("Flag TODO/FIXME markers…", a table row listing "TODO/FIXME comments" as a finding type) read the same as a forgotten marker. Reported byconfig-audit, whose product literally is a marker scanner. "TODO/FIXME" named together is now read as the convention; a loneFIXMEis still caught.
0.1.2 — 2026-08-03
Fixed
checkInternalLinkscompared a link's resolved target only againstpresent— the set of tracked files — so a link to a directory ([x](dir/)) was reportedLINK-INTERNAL-MISSINGeven when every file under that directory was tracked. A directory was never a member of that set to begin with. Reported byportfolio-optimiser-claude(coord 20260803T194933Z) with a minimal repro; confirmed and traced to the same defect invoyage,linkedin-studioandportfolio-optimiser— 12 of the 71 ERROR findings measured across the org on 2026-08-03 were this one check, not twelve separate repo problems. Fixed by deriving the set of directories that actually contain a tracked file frompresentitself, and letting a link that resolves to one of them pass.
0.1.1 — 2026-08-03
Documentation only. No behaviour change: scripts/ and register/ are
unchanged from 0.1.0 — this release carries version strings and the correction
below.
Fixed
- The 0.1.0 entry said the gate has eleven checks. It has twelve, which is what both the code and the README table already said. The entry is corrected in place, and this release exists so the catalog's pinned tag serves the corrected text instead of the wrong count. The count is drift of exactly the kind this plugin was built to catch, and it was sitting in the plugin's own repository.
0.1.0 — 2026-07-27
First release. Covers the checks that a single repository can answer on its own.
Added
- The gate (
scripts/repo-standard-check.mjs) — pure classifiers with all I/O resolved into their input, findings taggedERROR/WARN/SKIP/OK, exit 1 onERROR. Twelve checks: README first screen, install block, install truth, required headings, files required by the repo's class and traits,open/<name>references, relative links, licence claim, badges, boilerplate, version consistency and description length. - Taxonomy register (
register/repos.json) — one central file mapping each repository to its class, plus the per-class file and install requirements.--refreshcompares it against the live org listing. - Three-outcome reference check — "matches no repository" (
ERROR) is a separate finding from "matches something that is deliberately not a repository" (WARN). Sharing an outcome would let real dead links hide among correct text. Only names in URL position are treated as references; the.gitsuffix is normalised first. - Two-axis findings — every
ERROR/WARNcarries a bucket alongside its level:broken(a stranger is blocked or misled),missing(an expected artefact is absent),weakening(present, but it reads as amateur). Output is grouped by bucket, because that is the order findings get acted on. - Traits, a second axis beside class — class is structural and readable off
the catalog; a trait is about what the code does.
securityattaches aSECURITY.mdrequirement and a## Known limitationssection. - Required README headings per class —
## Install,## Non-goals,## Changelog. A heading present at the wrong depth is its own finding, not "missing". - In-repo version consistency — manifest, README version badge, newest
CHANGELOG entry and the git tag must agree. An untagged repo
SKIPs the tag comparison rather than failing it. - Badge honesty — a static badge asserting a test, build or coverage run is a claim dressed as evidence. Version, licence and platform badges assert no run and are fine.
- Install truth, not just install syntax — the marketplace URL in the install
block must be the real one (offline, against the register), and the plugin must
actually be pinned in the catalog (one call;
SKIPif unreachable). A perfectly formedclaude plugin install x@mktfails silently whenxwas never pinned, and that is the single most disqualifying thing a repo can publish. - Boilerplate, licence-claim and relative-link checks.
- The skill (
skills/repo-standard/) — the judgement the script cannot encode: what a description should say, why the summary card must never be used to verify one, what not to retrofit, and where the per-repo boundary is. - Link level follows the reader. A dead relative link in a root document —
README, CHANGELOG, SECURITY — is an
ERROR: it is in the shop window. The same link below the root is aWARN. Measured across seventeen repositories: 30 of 43 findings sat inshared/,docs/plan/and.claude/— session plans, agent working files, and path-traversal test fixtures whose targets are invalid on purpose. All 30 wereERRORs, which is how a gate gets switched off. Only the level moves; every finding is still reported with its file and line. - The repo name comes from the remote, not the directory.
catalog/is the working directory of the repo namedktg-plugin-marketplace. Reading the basename left itREPO-UNREGISTEREDwith zero checks run — against the one repo every catalog rule depends on. The scp form the forge's clone button hands out is handled, and a bare host is not a repo name. - 84 tests over the pure classifiers, using measured false positives as reference fixtures.
Notes
- Descriptions are measured in codepoints — not bytes, and not UTF-16 units. The same string measures 248 / 249 / 253 across those three yardsticks when it contains an astral character.
enabledPluginsinsettings.jsonis treated as a legitimate second install form. The gate requires a CLI command as well, and never reports the JSON form as the defect.- Two network calls, both anonymous: the org listing and the catalog manifest.
Per-repo fetching trips the forge's rate limiter, so neither is ever done per
repository.
--offlineskips both, and the checks that needed them reportSKIPrather than passing. CONTRIBUTING.md,CODE_OF_CONDUCT.mdandMAINTAINERS.mdare required by no class: the project is solo-maintained and publishes that stance. This is not a rule against having them — files already present are a cleanup decision, not a gate finding.SECURITY.mdis unaffected; it exists for the outsider who finds a hole.- There is no CI badge because there is no CI: the forge has no Actions runner registered. The substitute is one command from a clean clone, stated honestly. A badge asserting the test count would be the very anti-pattern the gate flags, and an earlier draft of this README carried exactly that.
- The link and boilerplate checks ignore fenced blocks and inline code spans. Documentation about regexes and placeholders is full of text that is a markdown link to a naive scanner — the first version produced about thirty findings against a real repository and every one was noise.
- Relative links resolve against the file they appear in, not the repo root.
- Indented text counts as a code block only when a blank line opens it, and then continues while the indent holds. The first rule alone made links inside nested list items invisible; the second alone let everything after a block's first line leak back in. Both were silent false passes, which is worse than noise — the second was caught by the gate on this plugin's own SKILL.md.
- A README H1 that differs from the repo name is a
WARN, not anERROR. The thread that has to hold is description == catalog == opening line, and the H1 is none of those three; a human title is a naming choice the operator owns. A missing H1 is still anERROR. - No hook ships in this release. A blocking gate has to be precise enough not to fail a correct repository first.