Same precedent as v0.2.0: a new check is a minor bump, not a patch.
Bundles this session's three fixes (LINK-DEAD false positive,
widened 429 backoff, BADGE-DEAD-LINK) and the tautological-test
correction.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
Missed updating this alongside the previous commit, which added 3
tests (113 -> 116) — same stale-count mistake this repo's history
already caught once (103 -> 111, ca81db5).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
Two remaining minor findings from STATE.md:
checkBadges treated being wrapped in a markdown link as proof of a
real run and skipped BADGE-STATIC-CLAIM entirely — but nothing ever
checked the link actually resolved. The link-target capture group was
missing from the regex outright. Now a linked run-claim badge with a
relative target is resolved against `present` (new BADGE-DEAD-LINK,
ERROR/broken): a dead link is worse than a static badge because it
looks verified. External targets (the ordinary case — a CI provider)
still need the network and stay out of scope, same precedent as
checkInternalLinks.
The "four pre-existing tautological tests" note undercounted on
re-measurement: only two exist ('no class requires a ROADMAP', 'CONTRIBUTING
and CODE_OF_CONDUCT are required by no class'). Both read
`required_files` off the test file's OWN local REGISTER fixture, so
they could only ever check the fixture against itself — a typo in the
real register/repos.json would drift past them silently. Switched
both to `loadRegister()` and verified the fix is real: temporarily
added ROADMAP.md to a class in the live register and confirmed the
test goes red, then restored it clean.
113 -> 116 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
Measured directly against the live forge: nginx never sends a
Retry-After header on its 429s (the branch handling it is dead code
in practice), the limit is a leaky bucket rather than a fixed ban (a
20-25 request burst took up to ~15s to drain), it is IP-based rather
than token-quota-based (a valid FORGEJO_TOKEN made no difference to a
reproduced burst), and it triggers well below "13 calls in a loop" —
20 concurrent anonymous requests reproduced it directly. The old
default (retries: 3, ~7s worst case) was tuned for a hard ban that
doesn't exist. fetchWithRetry now defaults to retries: 5 with a
maxDelayMs: 8000 cap (23s worst case), covering the measured drain
time without one attempt blocking for a full uncapped exponential
step. CLAUDE.md's explanation is corrected to match; test count in
README/CLAUDE.md updated for the two new tests (111 -> 113).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
URL_REF matched `open/<name>` anywhere in a URL path, so a Forgejo API
call like `.../api/v1/orgs/open/repos` read as a dead reference to a
repo named "repos" — "open" there is the org argument to the API, not
a repo reference. Restrict the host segment to exclude `/`, so `open`
must be the first path segment after the host, matching how every
real repo URL is shaped. Measured twice against the catalog's own
RUNBOOK.md:39 and :114.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1ZJFViVYpr8cvf4fs91j1
npm test currently reports 111, not 103 — the number went stale as
tests were added. Caught by catalog's coord message after measuring
the v0.2.2 tag directly (git-archive extraction, not the worktree).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WB6C8cJLHo3PF2BLPVb5qt
Measured 2026-08-04: two repos (llm-security, config-audit) independently
proved the /repo-standard skill had resolved ${CLAUDE_PLUGIN_ROOT} to a
cached 0.1.1 while 0.2.0 was installed and the catalog pinned it — same
instruction, same variable, wrong engine. 0.1.1 has neither BADGE-COUNT nor
README-LANGUAGE, so a broadcast recommending the skill produced clean-looking
runs that could not have found what they were sent to find.
The header line and --json output now carry `repo-standard v<version>`
(headerLine/withEngineVersion, both pure and unit tested). SKILL.md tells the
reader to confirm it against the catalog pin before trusting green. The
stale-cache resolution itself is the harness's, not this repo's — not fixed
here, only made visible instead of silent.
TDD: 4 new tests. 111/111 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01496ZWasKPnA627crFBXWhe
Measured 2026-08-04: 13 script invocations in one shell loop tripped an
anonymous Forgejo rate limit at 26 requests, because each invocation makes
TWO calls (org listing + catalog marketplace.json), not the ONE this repo's
own CLAUDE.md claimed. That line went stale when INSTALL-TRUTH added the
second call and nobody updated the count it depended on.
fetchWithRetry wraps both calls, honoring Retry-After on HTTP 429 instead of
silently falling back to SKIP. TDD: 5 new tests inject a fake fetch and sleep
to drive the retry/backoff/give-up paths without touching the network.
A sweep across every repo still doesn't belong in this engine — that's
org-ops's job by this file's own header — but a single repo's self-check
should not read as broken just because the forge was briefly busy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01496ZWasKPnA627crFBXWhe
BADGE-COUNT and README-LANGUAGE are additions, so this is a minor rather than a
patch. The catalog ref still sat on v0.1.1 against a released v0.1.3; bumping it
to v0.2.0 clears that lag in the same move, because a ref points, it does not
queue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf1zhujv5QjYuAn1a9HcgW
Every claim about visual content failed adversarial verification, so this adds
no engine check. What it adds is judgement about a diagram that already exists:
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.
Also records what the platform actually permits — Mermaid verified live on
15.0.6+gitea-1.22.0, silent degradation past MERMAID_MAX_SOURCE_CHARACTERS, and
no #gh-dark-mode-only until Gitea 1.26.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf1zhujv5QjYuAn1a9HcgW
Adversarial deep research (25 sources, 124 claims extracted, 25 verified:
11 confirmed / 14 refuted) plus direct measurement of all 18 cloned open/
repos. The useful half of the result is what it REFUSED to support, so the
research is recorded in docs/presentation-research-2026-08-03.md rather than
being spent and forgotten.
- BADGE-COUNT (WARN) — past 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". WARN and never ERROR: the
coefficient sits in an appendix with no CI or p-value. Counting deliberately
uses a NARROWER rule than the existing claim check, so a screenshot or an
architecture diagram is never counted as clutter. Fires on 8 of 18.
- README-LANGUAGE (WARN) — prose not in the language this repo's readers were
declared to speak, via a new `locales` axis in the register. Class is
structural, a trait is what the code DOES, a locale is who it is FOR — the
standard's own "who the reader is decides what is required". English is the
default; ms-ai-architect and okr are declared nb, named by the operator as
Norway-only in audience. Stopword-frequency comparison over prose with code
stripped: a Norwegian flag name in a shell example cannot decide the
document. Fires on exactly those two, silent on all sixteen English repos.
One design correction found mid-implementation: the first version returned
SKIP when a README had too little prose to judge, which broke a passing
fixture and would have stopped any terse repo from ever reaching OK. SKIP is
for a check that could not RUN; this one ran, saw everything and found no
prose to be in the wrong language — the same shape as "no licence claim to
back". Insufficient prose is now OK, and evenly bilingual prose is the SKIP,
because there the question is live and unanswered.
Deliberately NOT built, because the evidence does not reach: any rule about
images, diagrams or terminal recordings (every such claim 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 limit is not readable
via the API, so any threshold would be a guess).
Verified live against the operator's own forge (15.0.6+gitea-1.22.0): Mermaid
DOES render in README.md — two div.mermaid-block iframes carrying real SVG —
while #gh-dark-mode-only landed only in Gitea 1.26.0 and is unavailable here.
103 tests green, up from 92. No version bump: the catalog ref still trails at
v0.1.1 against 0.1.3, and starting a second release chain over that is the
operator's call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCAGKZT8h9F46ygzSkhEee
- extractChangelogTop truncated PEP 440 pre-release versions (0.5.0a2 -> 0.5.0),
so VERSION-CHANGELOG disagreed with VERSION-TAG over a version everything
already agreed on. Reported by llm-ingestion-okf with a repro.
- BADGE-STATIC-CLAIM treated a bare `status` badge as a run claim, same as
tests/build/CI. A self-declared maturity label asserts no run, same class
as version/licence/platform. Reported by llm-ingestion-pipeline-security.
- BOILERPLATE flagged FIXME when a scanner's own docs named the TODO/FIXME
convention rather than an instance of it. Reported by config-audit.
92 tests green, up from 86.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwwfdmrfnp7FuGQ4z25RKH
`checkInternalLinks` compared a link's resolved target only against `present`
(tracked files), so `[x](dir/)` was always LINK-INTERNAL-MISSING even when
every file under it was tracked. Reported by portfolio-optimiser-claude with a
minimal repro; the same defect inflated ERROR counts in voyage, linkedin-studio
and portfolio-optimiser — 12 of the org's 71 measured ERRORs were this one
check, not twelve repo problems.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uwwfdmrfnp7FuGQ4z25RKH
The 0.1.0 entry said eleven checks; there are twelve, and both the code and
the README table already said so. 2963f4f fixed the entry in place but landed
after the tag, so the catalog's pin kept serving the wrong count.
Documentation only: scripts/ and register/ are unchanged from v0.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Wuy8mxQj8Zt4z23hiRjiN
The 0.1.0 entry says "Eleven checks" and then lists twelve. classifyRepo spreads
exactly twelve check* calls, and the README table has twelve rows. Found when
catalog measured the tag independently and reported twelve against a "fourteen"
this repo had put in a coord message -- a number that was never measured here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeNsGsWukggfmLQ926WZPx
Two defects that only org-wide measurement exposes. Both were found by running
the gate against seventeen repositories, not by reading the code.
Link level follows the reader. 30 of 43 LINK-INTERNAL-MISSING findings sat in
`shared/`, `docs/plan/` and `.claude/` -- session plans, agent working files,
and path-traversal fixtures whose targets are invalid on purpose. All 30 were
ERRORs. portfolio-optimiser scored 19 ERRORs and 15 of them were noise. A gate
that wrong gets switched off, so root documents stay ERROR and everything below
becomes WARN. Only the level moves; the finding keeps its file and line.
The OK line had to move with it: it asserted "every resolvable relative link
resolves" whenever no ERROR was present, which would have printed it beside a
pile of WARNs saying the opposite.
The repo name comes from `git remote get-url origin`. `catalog/` is the working
directory of `ktg-plugin-marketplace`; the basename left it REPO-UNREGISTERED
with zero checks run -- against the one repo every catalog rule depends on. The
scp form is handled because the forge's clone button hands it out, and a bare
host is not a repo name (that test caught a real bug: `https://host/` parsed as
a repo named after the host).
Re-measured: portfolio-optimiser 19 ERROR -> 4, all four genuine. catalog now
reports as `ktg-plugin-marketplace [catalog]` and runs its full rule set.
Still v0.1.0: nothing is pushed, no tag exists, no consumer has seen it.
77 -> 84 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeNsGsWukggfmLQ926WZPx
'Five checks' is eleven, and 'one call' is two. Both were true when
written and stopped being true in the same session - which is the drift
the version-consistency check exists to catch and the reason this file
gets re-read before every release.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Install truth is the brief's first control and the gate only checked
syntax. Now: the marketplace URL must be the real one (offline, from the
register), and the plugin must actually be pinned in the catalog (one
call, SKIP if unreachable). A well-formed `claude plugin install x@mkt`
fails silently when x was never pinned.
This makes the gate block ITSELF until publication finishes - the run
against this repo now has exactly one ERROR, and it is true: repo-standard
is not in the catalog yet. That is the post-publish acceptance test,
enforced mechanically instead of remembered.
Badge honesty no longer keys on img.shields.io. A self-hosted SVG asserts
the same unverified thing, and the README claimed the general rule while
the code checked one host.
Two stripCode bugs, both silent false passes:
- 4-space indent treated as code unconditionally made links inside nested
list items invisible. Fixed by requiring a blank line to OPEN a block.
- That fix alone ended the block after line 1, so multi-line indented
templates leaked back into scanning. Caught by the gate on this repo's
own SKILL.md, which shows a README template containing a CHANGELOG link.
A block now opens on a blank line and continues while the indent holds.
Also corrected two claims in this README: it said "one network call"
when there are two, and it still argued against a CONTRIBUTING using
reasoning the solo-maintainer section had already replaced.
77 tests. llm-security regression: still zero link and boilerplate noise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Measured this build against a documentation brief for public repos. The
five original checks covered roughly one of its ten sections, so this
adds what a single repo can answer on its own.
New: required README headings per class (Non-goals is the cheapest
trust-builder there is), in-repo version consistency across manifest /
badge / CHANGELOG / tag, badge honesty, boilerplate, licence-claim,
and relative links. Findings now carry a BUCKET beside the level -
broken / missing / weakening - and output is grouped by it, because
that is the order the work gets done in.
Traits are a second axis beside class: class is structural and readable
off the catalog, a trait says what the code does. `security` attaches
SECURITY.md and a Known limitations section. The two names carrying it
are proposed, not measured - that list is the operator's.
Solo-maintained settles a category: CONTRIBUTING, CODE_OF_CONDUCT and
MAINTAINERS are required by no class. Consumer-facing documents are
untouched by that; SECURITY.md exists for the stranger who finds a hole.
Three bugs found by running against llm-security, not by reading:
- ~30 link findings, all noise. Regexes inside code spans are
`[...](...)` to a naive scanner. Strip code first.
- `file:` and other schemes were treated as repo-relative paths.
- Relative links were resolved against the repo root instead of the
file they sit in, calling two files missing that sat next to the
README linking them.
Same fix applied to the boilerplate check: a document ABOUT placeholder
detection was tripping the placeholder detector.
Also removed this repo's own static tests badge. There is no CI - the
forge has zero Actions runners registered - so it could never become
real, and it is the exact anti-pattern the gate now flags.
67 tests. Against llm-security every remaining finding is real and
matches the census's independent hand-measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
The H1 change added two tests; the badge, the README line and the
CHANGELOG still said 32. Exactly the drift this repo exists to catch,
in this repo - and check-versions.mjs would not have caught it, since
it only reads the version badge.
Also states in the README what actually fails the first-screen check
now that a differing H1 is a WARN.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Measured against the live repos: okr opens `# OKR for Public Sector`
and claude-design `# Claude Design Facilitator`. Neither breaks the
thread the contract exists to protect - description == catalog ==
opening line - because the H1 is none of those three. Failing them
would be the gate that stops a correct repo, which is what teaches
people to switch gates off.
A missing H1 stays an ERROR, and a differing one no longer short-
circuits the description check.
Validation against three cases the census measured by hand, all
reproduced independently: okr (neither install line), claude-design
(slash form, no marketplace add), repo-mailbox (install correct,
first screen wrong). 34 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h
Five checks a single repository can answer on its own: README first
screen, install block, files required by its class, open/<name>
references, description length. Pure classifiers with I/O resolved into
their input, mirroring check-versions.mjs; ERROR/WARN/SKIP/OK, exit 1
on ERROR. 32 tests.
The reference check has THREE outcomes: "matches no repo" (ERROR) is
separate from "matches a known non-repo" (WARN). Sharing an outcome
would let real dead links hide inside correct text. Only names in URL
position count, and .git is normalised first - without that a raw scan
turns 3 dead names into ~20.
enabledPlugins is treated as a legitimate second install form; what the
gate requires in addition is a CLI command. The JSON form is never
reported as the defect.
STATE.md is gitignored from this first commit - public remote.
No hook yet: a blocking gate must first be precise enough not to fail a
correct repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYJ3FHLtVgzFXMZ6UF598h