feat(repo-standard): v0.1.0 - per-repo gate for the open/ standard

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
This commit is contained in:
Kjell Tore Guttormsen 2026-07-27 09:10:46 +02:00
commit 816ba97c63
11 changed files with 1319 additions and 0 deletions

View file

@ -0,0 +1,12 @@
{
"name": "repo-standard",
"version": "0.1.0",
"description": "Per-repo gate for the open/ presentation standard: README first screen, install block, files required by the repo's class, and dead repo references.",
"author": {
"name": "Kjell Tore Guttormsen"
},
"auto_discover": true,
"license": "MIT",
"repository": "https://git.fromaitochitta.com/open/repo-standard",
"keywords": ["repository-standard", "readme", "gate", "documentation", "multi-repo"]
}

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Operator-private continuity state. This repo has a PUBLIC remote, so STATE.md
# is LOCAL-ONLY here — it must never reach the mirror. (Private-remote repos
# track theirs; do not carry that habit across.)
STATE.md
*.local.md
node_modules/
.DS_Store

44
CHANGELOG.md Normal file
View file

@ -0,0 +1,44 @@
# Changelog
All notable changes to this project are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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 tagged `ERROR`/`WARN`/`SKIP`/`OK`,
exit 1 on `ERROR`. Five checks: README first screen, install block, files
required by the repo's class, `open/<name>` references, description length.
- **Taxonomy register** (`register/repos.json`) — one central file mapping each
repository to its class, plus the per-class file and install requirements.
`--refresh` compares 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 `.git`
suffix is normalised first.
- **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.
- 32 tests over the pure classifiers, using the 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.
- `enabledPlugins` in `settings.json` is treated as a legitimate second install
form. The gate requires a CLI command *as well*, and never reports the JSON
form as the defect.
- The org listing is read in **one** call, anonymously. Per-repo fetching trips
the forge's rate limiter.
- No hook ships in this release. A blocking gate has to be precise enough not to
fail a correct repository first.
[0.1.0]: https://git.fromaitochitta.com/open/repo-standard/src/tag/v0.1.0

63
CLAUDE.md Normal file
View file

@ -0,0 +1,63 @@
# repo-standard
Per-repo gate for the `open/` presentation standard, packaged as a marketplace
plugin.
## Context
Two components, one boundary:
- **Engine (`scripts/repo-standard-check.mjs`)** — pure classifiers with all I/O
resolved into their input, mirroring the marketplace's `check-versions.mjs`.
Findings are `ERROR`/`WARN`/`SKIP`/`OK`; exit 1 on `ERROR`. Pinned by
`scripts/repo-standard-check.test.mjs` (`npm test`).
- **Skill (`skills/repo-standard/`)** — the judgement the script cannot encode.
No checking logic lives here; it calls the engine.
`register/repos.json` is the single taxonomy register (name → class, per-class
requirements, and the known non-repo names). Central by design: per-repo copies
would recreate, in data, exactly the drift this plugin exists to remove.
## Invariants
- **This repo has a PUBLIC remote.** `STATE.md` is LOCAL-ONLY and gitignored
from the first commit. Repos on a private remote track theirs — do not carry
that habit across in either direction.
- **The gate sees ONE repo.** Anything needing a view across the whole org
(topic coverage, competing install forms, catalog-vs-forge divergence) does
not belong here. It is measured where the org is enumerated.
- **It records, it does not fix.** Findings first, remediation afterwards.
Patching while measuring is how the inconsistency it detects was produced.
- **`SKIP` is never a pass.** A check that could not run says so and names why.
- **Three outcomes on references.** "No match" and "match on a known non-repo"
must stay distinct findings. Collapsing them hides real loss inside correct
text — the exact defect class this gate exists to catch.
- **One API call, anonymous.** The org listing carries description and topics
already; per-repo fetching trips the rate limiter (HTTP 429). It reads without
a token, so the gate works for any reader — a public plugin whose documented
check only runs for its author is a broken plugin.
- **Codepoints, not bytes, not UTF-16 units.** Use `[...s].length`. An em-dash
exposes only the byte layer; astral characters expose the rest.
- **No hook until the rule is precise.** A blocking gate that fails a correct
repository is the mechanism that gets gates switched off.
## Commands
```bash
npm test # 32 tests
node scripts/repo-standard-check.mjs --dir "$PWD" # gate one repo
node scripts/repo-standard-check.mjs --offline # no network call
node scripts/repo-standard-check.mjs --json # machine output
node scripts/repo-standard-check.mjs --refresh # register vs. forge
```
## Release
Polyrepo rule: a version bump is not finished until the tag `vX.Y.Z` is pushed
**and** the catalog `ref` is bumped to it. Use `release-plugin.mjs`, never a
hand-edited `ref`.
`check-versions.mjs` reads the catalog README's per-plugin label, and a **missing**
entry is a silent `null` rather than an error — `release-plugin.mjs` rewrites an
existing heading but cannot create one. A new plugin's catalog README entry has
to be added by hand once.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Kjell Tore Guttormsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

121
README.md Normal file
View file

@ -0,0 +1,121 @@
# repo-standard
Per-repo gate for the open/ presentation standard: README first screen, install block, files required by the repo's class, and dead repo references.
A repository can be substantially good and still read as abandoned. The gap is
almost never the code — it is the first screen, an install path that stops
halfway, and references to a name that was retired two renames ago. This plugin
checks that surface in one repository and reports what it finds.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See the [marketplace governance](https://git.fromaitochitta.com/open/ktg-plugin-marketplace/src/branch/main/GOVERNANCE.md) for the full model.
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.1.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Skills](https://img.shields.io/badge/skills-1-orange)
![Tests](https://img.shields.io/badge/tests-32-green)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
## Install
Use the `https://` form. The forge UI's clone button hands out an `ssh://` URL,
and `marketplace add` answers it with `Invalid git URL` — a message that never
mentions the protocol.
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install repo-standard@ktg-plugin-marketplace
```
Or enable it directly in `~/.claude/settings.json` — a working second path, not
a replacement for the two commands above:
```json
{ "enabledPlugins": { "repo-standard@ktg-plugin-marketplace": true } }
```
## Requirements
Node 18 or newer. No dependencies. The one network call — the org listing, used
to read a repo's published description — reads anonymously, so it needs no
token; `--offline` skips it.
## What it does
Run it inside a repository:
```bash
node scripts/repo-standard-check.mjs --dir "$PWD"
```
It checks five things, and the repository's **class** decides what each one means:
| Check | What fails it |
| --- | --- |
| First screen | `# <name>` is not line 1, or the line under it is not the published description |
| Install block | the form for this class is missing, incomplete, or shown over `ssh://` |
| Required files | a file this class needs is absent |
| References | an `open/<name>` in URL position resolves to nothing |
| Description | empty, or past the length bound |
Findings are `ERROR`, `WARN`, `SKIP` or `OK`; the process exits 1 on any
`ERROR`. A `SKIP` means the check could not run — an unreachable forge, a repo
that is not in the register. It is not a pass, and the output says which.
### The class decides what is required
The class is read off the catalog and the remotes; it is structural, not a
judgement. It lives in `register/repos.json`.
| Class | Install form | Requires |
| --- | --- | --- |
| plugin | `marketplace add` **and** a CLI install command | README, LICENSE, CHANGELOG, plugin manifest |
| catalog | `marketplace add` only — it *is* the marketplace | + GOVERNANCE, CONVENTIONS |
| shared-asset | how to vendor it; never a plugin install line | README, LICENSE |
| standalone | pip/uv | README, LICENSE |
| org-profile | none | README |
A flat standard across every class would demand a CONTRIBUTING from a CSS
library that accepts no contributions, and a roadmap from a five-line profile.
That is how a gate teaches people to switch it off.
### Three outcomes on references, not two
"Matches no repository" and "matches something that is deliberately not a
repository" are different findings. The second class is real and it is common:
a retired name kept alive on purpose in prose, a reserved namespace occupying a
repo-shaped path, a published package name that was never a repo. If those share
an outcome with genuine dead links, the genuine ones hide inside a pile of
correct text.
Only names in **URL position** are treated as references, which excludes prose,
paths and directory names in one move. The `.git` suffix is normalised first —
without that, a raw scan turns three dead names into about twenty.
## Non-goals
- **Anything requiring a view across every repository at once.** This gate sees
one repo. A README can look correct from the inside and only turn out wrong
when a dozen are compared. Org-wide divergence is measured where the org is
enumerated, not here.
- **Fixing while measuring.** It records findings; you fix them afterwards.
Patching under measurement is how inconsistency accumulates unnoticed.
- **Deciding whether to rename a repo, or whether a roadmap is real.** Those are
operator calls. The gate brings evidence to them.
- **Blocking commits.** There is no hook. A gate that fails a correct repository
is the mechanism that gets gates switched off, so this one earns that role
before it takes it.
## Tests
```bash
npm test
```
32 tests over the pure classifiers. The reference fixtures are the measured
false positives that produced the three-outcome rule, each with its expected
verdict.
## Changelog
See [CHANGELOG.md](CHANGELOG.md).

12
package.json Normal file
View file

@ -0,0 +1,12 @@
{
"name": "repo-standard",
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=18"
},
"scripts": {
"test": "node --test scripts/*.test.mjs"
}
}

94
register/repos.json Normal file
View file

@ -0,0 +1,94 @@
{
"$comment": [
"Taxonomy register for the `open/` organisation (D4: central, one file).",
"The class is READ OFF the catalog and the remotes — it is structural, not a judgement.",
"Refresh the `repos` name set against ground truth with:",
" node scripts/repo-standard-check.mjs --refresh",
"which enumerates /api/v1/orgs/open/repos (ONE call — the listing carries",
"description and topics too; per-repo fetching trips the rate limiter).",
"Enumerate, never glob: 12 of these sit at depth 2 locally, one has a",
"basename that differs from its repo name, and `.profile` is hidden."
],
"org": "open",
"forge": "https://git.fromaitochitta.com",
"marketplace": {
"name": "ktg-plugin-marketplace",
"url": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git"
},
"repos": {
"llm-security": "plugin",
"config-audit": "plugin",
"voyage": "plugin",
"linkedin-studio": "plugin",
"graceful-handoff": "plugin",
"ai-psychosis": "plugin",
"ms-ai-architect": "plugin",
"okr": "plugin",
"human-friendly-style": "plugin",
"claude-design": "plugin",
"repo-mailbox": "plugin",
"repo-standard": "plugin",
"ktg-plugin-marketplace": "catalog",
"playground-design-system": "shared-asset",
".profile": "org-profile",
"portfolio-optimiser": "standalone",
"portfolio-optimiser-claude": "standalone",
"llm-ingestion-pipeline-security": "standalone",
"llm-ingestion-okf": "standalone"
},
"$comment_non_repos": [
"Names that LOOK like repo names and are not. These exist so that",
"'no match' and 'match on something that is not a repo' are DIFFERENT",
"outcomes — if they share an outcome, the loss goes silent, which is the",
"defect class this whole standard exists to catch.",
"Each entry is a measured false positive, not a guess."
],
"non_repos": {
"coord": "Retired repo name, deliberately still alive in prose: the CLI (coord-send), the mailbox root (~/.claude/coord/) and CLAUDE_COORD_DIR kept it — they are the transport protocol, not the product. The repo has been `repo-mailbox` since v0.3.0.",
"_broadcast": "Reserved engine namespace in the coord mailbox (`~/.claude/coord/_broadcast/`). Occupies a repo-shaped PATH position; `_` prefixed names are refused as repo identities.",
"llm-ingestion-guard": "Package name published by `llm-ingestion-pipeline-security`. A package, not a repo.",
"claude-code-llm-security": "Pre-split name of `llm-security`. This one IS dead — the org's only rename produced every dead reference we found. Listed so the finding names the successor instead of just failing."
},
"$comment_classes": [
"Per class: which files are required, and which install form the README must",
"carry. A flat standard across all classes would demand a CONTRIBUTING from a",
"CSS library that takes no contributions, and a ROADMAP from a 5-line profile.",
"ROADMAP is deliberately absent everywhere: it is 0/18 today and is step 8,",
"drafted from STATE by a human. A gate that fails every repo teaches people to",
"switch the gate off."
],
"classes": {
"plugin": {
"required_files": ["README.md", "LICENSE", "CHANGELOG.md", ".claude-plugin/plugin.json"],
"install": "plugin"
},
"catalog": {
"required_files": ["README.md", "LICENSE", "GOVERNANCE.md", "CONVENTIONS.md", ".claude-plugin/marketplace.json"],
"install": "catalog"
},
"shared-asset": {
"required_files": ["README.md", "LICENSE"],
"install": "vendor"
},
"org-profile": {
"required_files": ["README.md"],
"install": "none"
},
"standalone": {
"required_files": ["README.md", "LICENSE"],
"install": "package"
}
},
"description_max_codepoints": 180,
"$comment_length": [
"180 codepoints, not bytes and not UTF-16 units. The same string measures 248",
"/ 249 / 253 across the three yardsticks (graceful-handoff: `👉` is astral).",
"An em-dash costs 3 bytes but 1 codepoint AND 1 UTF-16 unit, so it exposes",
"only the outer layer and hides the inner one. JS-based tooling reads one",
"higher per astral character. Upper bound: 207 nearly filled the card's text",
"field; 220 is untested and may overflow."
]
}

View file

@ -0,0 +1,409 @@
#!/usr/bin/env node
// repo-standard — the per-repo gate.
//
// Checks ONE repository against the standard for its class:
// - README first screen: H1 is the repo name, next line IS the forge description
// - Install block complete, in the form its class actually uses
// - Required files present for its class
// - Every `open/<name>` reference in URL position resolves
// - Description within the length bound, measured in codepoints
//
// What it deliberately does NOT do: anything that needs to see all repos at once.
// Divergence across the org (0/18 topics, three competing install forms, README
// release notes duplicating a CHANGELOG that 16 of 18 repos have) is invisible
// from inside one repo. Those checks live in org-ops, not here.
//
// Structure mirrors the marketplace's check-versions.mjs on purpose: pure
// classifiers with all I/O resolved into their input, findings tagged
// ERROR/WARN/SKIP/OK, exit 1 on ERROR. This is a gate, not a checklist —
// the catalog's eleven descriptions are good because a gate runs on them; the
// forge's nine were empty. Same care, different outcome.
//
// Usage:
// node scripts/repo-standard-check.mjs [--dir <path>] [--name <repo>] [--offline] [--json]
// node scripts/repo-standard-check.mjs --refresh # register vs. live org listing
import { readFileSync, existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const REGISTER_PATH = join(HERE, '..', 'register', 'repos.json');
const LEVELS = ['OK', 'SKIP', 'WARN', 'ERROR'];
// ------------------------------------------------------------ pure helpers
// Codepoints. Not bytes (an em-dash costs 3) and not UTF-16 units (`👉` costs 2).
// The em-dash exposes only the outer layer, which is why "characters, not bytes"
// was not enough on its own.
export function countCodepoints(s) {
return [...String(s ?? '')].length;
}
// ~20 "dead" repo names collapsed to 3 real ones once this ran. A clone URL
// ending in .git is a legitimate reference, not a broken one.
export function normalizeRepoRef(raw) {
return String(raw ?? '')
.replace(/\/+$/, '')
.replace(/\.git$/, '');
}
// Only names in URL position are resolvable references. That single rule
// excludes all three of the measured "correct text that looks broken" cases at
// once: a path position (`~/.claude/coord/_broadcast/`), running prose (`coord`
// is still the transport protocol's name), and a bare directory name.
const URL_REF = /(?::\/\/[^\s)\]"'`]*\/|@[^\s:]+:)open\/([A-Za-z0-9._-]+)/g;
export function extractOpenRefs(text) {
const out = [];
const lines = String(text ?? '').split('\n');
lines.forEach((line, i) => {
for (const m of line.matchAll(URL_REF)) {
out.push({ name: normalizeRepoRef(m[1]), line: i + 1, raw: m[0] });
}
});
return out;
}
// Three outcomes, never two. "No match" and "match on something that is not a
// repo" must stay distinguishable — if they share an outcome, the loss goes
// silent, and silent loss is the defect class this standard exists to catch.
export function classifyRef(name, register) {
if (Object.prototype.hasOwnProperty.call(register.repos ?? {}, name)) return 'repo';
if (Object.prototype.hasOwnProperty.call(register.non_repos ?? {}, name)) return 'non-repo';
return 'unknown';
}
export function checkLinks({ files }, register) {
const findings = [];
for (const [path, text] of Object.entries(files ?? {})) {
for (const ref of extractOpenRefs(text)) {
const kind = classifyRef(ref.name, register);
if (kind === 'repo') continue;
if (kind === 'non-repo') {
findings.push({
level: 'WARN',
code: 'LINK-NON-REPO',
msg: `${path}:${ref.line}\`open/${ref.name}\` resolves to a known non-repo: ${register.non_repos[ref.name]}`,
});
} else {
findings.push({
level: 'ERROR',
code: 'LINK-DEAD',
msg: `${path}:${ref.line}\`open/${ref.name}\` matches no repo in the register (dead reference)`,
});
}
}
}
return findings;
}
export function checkDescription(description, register) {
if (description === null || description === undefined) {
return [{ level: 'SKIP', code: 'DESC-UNAVAILABLE', msg: 'forge description not available — check not run (offline, or the listing failed)' }];
}
const max = register.description_max_codepoints ?? 180;
const n = countCodepoints(description);
if (n === 0) return [{ level: 'ERROR', code: 'DESC-EMPTY', msg: 'forge description is empty' }];
if (n > max) {
return [{ level: 'ERROR', code: 'DESC-TOO-LONG', msg: `forge description is ${n} codepoints, bound is ${max}` }];
}
return [{ level: 'OK', code: 'DESC', msg: `description ${n}/${max} codepoints` }];
}
// The opening line makes description == catalog == README: the same thread on a
// third surface, and the only one of the three a machine can check from inside
// the repo.
export function checkFirstScreen({ readme, name, description }) {
const findings = [];
const lines = String(readme ?? '').split('\n');
const firstIdx = lines.findIndex((l) => l.trim() !== '');
if (firstIdx === -1 || lines[firstIdx].trim() !== `# ${name}`) {
findings.push({
level: 'ERROR',
code: 'README-H1',
msg: `README line 1 must be \`# ${name}\` (found: ${firstIdx === -1 ? '<empty file>' : `\`${lines[firstIdx].trim()}\``})`,
});
return findings;
}
findings.push({ level: 'OK', code: 'README-H1', msg: `H1 is \`# ${name}\`` });
if (description === null || description === undefined) {
findings.push({ level: 'SKIP', code: 'README-DESC', msg: 'forge description not available — opening-line match not checked' });
return findings;
}
const restIdx = lines.findIndex((l, i) => i > firstIdx && l.trim() !== '');
const opening = restIdx === -1 ? '' : lines[restIdx].trim();
if (opening !== String(description).trim()) {
findings.push({
level: 'ERROR',
code: 'README-DESC',
msg: `README opening line does not match the forge description\n README: ${opening}\n forge: ${description}`,
});
} else {
findings.push({ level: 'OK', code: 'README-DESC', msg: 'opening line matches the forge description' });
}
return findings;
}
// `claude plugin install x@mkt` or `/plugin install x@mkt` — the two CLI forms.
function hasCliInstall(readme, name, mkt) {
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`(?:claude\\s+plugin|/plugin)\\s+install\\s+${esc(name)}@${esc(mkt)}\\b`).test(readme);
}
function hasAnyPluginInstall(readme) {
return /(?:claude\s+plugin|\/plugin)\s+install\s+\S+@\S+/.test(readme);
}
export function checkInstallBlock({ readme, name, klass }, register) {
const form = register.classes?.[klass]?.install ?? 'none';
const text = String(readme ?? '');
const mkt = register.marketplace ?? {};
const findings = [];
if (form === 'none') return findings;
const addLines = text.split('\n').filter((l) => /plugin\s+marketplace\s+add/.test(l));
const hasAdd = addLines.length > 0;
// The forge UI's clone button hands out the ssh URL, and `marketplace add`
// answers it with "Invalid git URL" — a message that never mentions the
// protocol. Measured end-to-end 2026-07-25.
if (addLines.some((l) => /ssh:\/\//.test(l))) {
findings.push({
level: 'ERROR',
code: 'INSTALL-SSH',
msg: '`marketplace add` is shown with an ssh:// URL — it rejects those ("Invalid git URL"). Use the https form.',
});
}
if (form === 'plugin' || form === 'catalog') {
if (!hasAdd) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-MARKETPLACE',
msg: `no \`plugin marketplace add\` line — the reader is never told to add \`${mkt.name}\` (${mkt.url})`,
});
} else {
findings.push({ level: 'OK', code: 'INSTALL-MARKETPLACE', msg: '`marketplace add` present' });
}
}
if (form === 'plugin') {
// The corrected defect A. `enabledPlugins` in settings.json is a LEGITIMATE
// second form and it stands in 10 of 11 plugin READMEs — what is missing in
// 7 of them is a CLI command. So the contract requires the command and
// permits the JSON alongside it; it never accepts the JSON as a substitute.
// A reader who scrolls to the JSON block has a complete path; an agent told
// "install this" reaches for the CLI and finds `marketplace add` and nothing else.
if (!hasCliInstall(text, name, mkt.name)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-NO-CLI',
msg: `no CLI install command for this repo — expected \`claude plugin install ${name}@${mkt.name}\` (or the \`/plugin install\` form). An \`enabledPlugins\` block is a welcome addition, but it is not a CLI command.`,
});
} else {
findings.push({ level: 'OK', code: 'INSTALL-CLI', msg: `CLI install command names ${name}@${mkt.name}` });
}
}
if (form === 'vendor' && hasAnyPluginInstall(text)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
msg: 'shared asset shows a plugin install line — it is vendored into consumers, not installed. Document how to vendor it.',
});
}
if (form === 'package') {
if (hasAnyPluginInstall(text)) {
findings.push({
level: 'ERROR',
code: 'INSTALL-WRONG-FORM',
msg: 'standalone project shows a plugin install line — use the pip/uv form.',
});
} else if (!/\b(pip\s+install|uv\s+(?:pip\s+)?(?:add|sync|install|run)|uvx)\b/.test(text)) {
findings.push({
level: 'WARN',
code: 'INSTALL-NO-PACKAGE-FORM',
msg: 'no pip/uv install form found — expected for a standalone project',
});
}
}
return findings;
}
// Per class, never flat. A flat standard demands a CONTRIBUTING from a CSS
// library that takes no contributions and a ROADMAP from a five-line profile.
export function checkRequiredFiles({ present, klass }, register) {
const required = register.classes?.[klass]?.required_files ?? [];
const have = new Set(present ?? []);
const findings = [];
for (const f of required) {
if (!have.has(f)) {
findings.push({ level: 'ERROR', code: 'FILE-MISSING', msg: `missing required file for class \`${klass}\`: ${f}` });
}
}
if (findings.length === 0 && required.length > 0) {
findings.push({ level: 'OK', code: 'FILES', msg: `all ${required.length} required files present` });
}
return findings;
}
export function levelOf(findings) {
let worst = 'OK';
for (const f of findings ?? []) {
if (LEVELS.indexOf(f.level) > LEVELS.indexOf(worst)) worst = f.level;
}
return worst;
}
export function classifyRepo({ name, files, present, description }, register) {
const klass = register.repos?.[name];
if (!klass) {
return {
name,
klass: null,
status: 'SKIP',
findings: [{
level: 'SKIP',
code: 'REPO-UNREGISTERED',
msg: `\`${name}\` is not in the register — class unknown, so no class-specific rule can be applied. Add it to register/repos.json (or run --refresh).`,
}],
};
}
const readme = (files ?? {})['README.md'] ?? '';
const findings = [
...checkFirstScreen({ readme, name, description }),
...checkInstallBlock({ readme, name, klass }, register),
...checkRequiredFiles({ present, klass }, register),
...checkLinks({ files }, register),
...checkDescription(description, register),
];
return { name, klass, status: levelOf(findings), findings };
}
// ---------------------------------------------------------------- I/O shell
export function loadRegister(path = REGISTER_PATH) {
return JSON.parse(readFileSync(path, 'utf8'));
}
// ONE call. The org listing already carries description and topics; fetching
// per repo trips the rate limiter (HTTP 429). Reads anonymously — verified —
// so this works for any reader, not only for someone holding a token.
async function fetchOrgListing(register) {
const url = `${register.forge}/api/v1/orgs/${register.org}/repos?limit=50`;
const res = await fetch(url, { headers: { accept: 'application/json' } });
if (!res.ok) throw new Error(`org listing returned HTTP ${res.status}`);
return res.json();
}
function gitFiles(dir) {
try {
return execFileSync('git', ['-C', dir, 'ls-files'], { encoding: 'utf8' })
.split('\n')
.map((s) => s.trim())
.filter(Boolean);
} catch {
return null;
}
}
function repoNameFrom(dir) {
try {
return basename(execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim());
} catch {
return basename(dir);
}
}
export function inspectRepo(dir, name, register, description) {
const tracked = gitFiles(dir);
const present = (tracked ?? []).filter((f) => existsSync(join(dir, f)));
// Link scanning covers every tracked Markdown file — a dead reference in a
// doc is as broken as one in the README.
const files = {};
for (const f of (tracked ?? []).filter((p) => p.endsWith('.md'))) {
try { files[f] = readFileSync(join(dir, f), 'utf8'); } catch { /* unreadable — skip */ }
}
if (!files['README.md'] && existsSync(join(dir, 'README.md'))) {
files['README.md'] = readFileSync(join(dir, 'README.md'), 'utf8');
}
return classifyRepo({ name, files, present, description }, register);
}
function render(result) {
const mark = { OK: '✓', WARN: '!', ERROR: '✗', SKIP: '·' };
const klass = result.klass ? ` [${result.klass}]` : '';
console.log(`\n${mark[result.status]} ${result.name}${klass}${result.status}`);
for (const f of result.findings) {
if (f.level === 'OK') continue;
console.log(` ${mark[f.level]} ${f.level} ${f.code}: ${f.msg}`);
}
const okCount = result.findings.filter((f) => f.level === 'OK').length;
if (okCount) console.log(` ${mark.OK} ${okCount} check(s) passed`);
}
async function refresh(register) {
const live = await fetchOrgListing(register);
const liveNames = new Set(live.map((r) => r.name));
const known = new Set(Object.keys(register.repos ?? {}));
const added = [...liveNames].filter((n) => !known.has(n)).sort();
const gone = [...known].filter((n) => !liveNames.has(n)).sort();
console.log(`register: ${known.size} repos · forge: ${liveNames.size} repos`);
if (added.length) console.log(`\n on the forge, not in the register (add with a class):\n ${added.join('\n ')}`);
if (gone.length) console.log(`\n in the register, not on the forge:\n ${gone.join('\n ')}`);
if (!added.length && !gone.length) console.log('\n ✓ register matches the forge');
return added.length + gone.length === 0 ? 0 : 1;
}
async function main(argv) {
const arg = (flag, fallback = null) => {
const i = argv.indexOf(flag);
return i === -1 ? fallback : argv[i + 1];
};
const register = loadRegister();
if (argv.includes('--refresh')) {
process.exit(await refresh(register));
}
const dir = arg('--dir', process.cwd());
const name = arg('--name', repoNameFrom(dir));
let description = null;
if (!argv.includes('--offline')) {
try {
const listing = await fetchOrgListing(register);
const row = listing.find((r) => r.name === name);
description = row ? (row.description ?? '') : null;
} catch {
// Unreachable forge leaves description null, which reads as SKIP — never
// as a pass. A check that could not run says so.
}
}
const result = inspectRepo(dir, name, register, description);
if (argv.includes('--json')) {
console.log(JSON.stringify(result, null, 2));
} else {
render(result);
}
process.exit(result.status === 'ERROR' ? 1 : 0);
}
if (process.argv[1] && process.argv[1].endsWith('repo-standard-check.mjs')) {
main(process.argv.slice(2));
}

View file

@ -0,0 +1,370 @@
// Tests for the repo-standard gate.
//
// The pure classifiers are the unit under test — the I/O shell (inspectRepo/runGate)
// is exercised against a live checkout by the CLI, not here.
//
// The link-check fixtures are the six measured false positives from the census.
// They are the reason this gate has three outcomes instead of a boolean.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
countCodepoints,
normalizeRepoRef,
extractOpenRefs,
classifyRef,
checkLinks,
checkDescription,
checkFirstScreen,
checkInstallBlock,
checkRequiredFiles,
classifyRepo,
levelOf,
} from './repo-standard-check.mjs';
const REGISTER = {
org: 'open',
marketplace: {
name: 'ktg-plugin-marketplace',
url: 'https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git',
},
repos: {
'llm-security': 'plugin',
'repo-mailbox': 'plugin',
'repo-standard': 'plugin',
'ktg-plugin-marketplace': 'catalog',
'playground-design-system': 'shared-asset',
'.profile': 'org-profile',
'llm-ingestion-pipeline-security': 'standalone',
},
non_repos: {
coord: 'Retired repo name, deliberately still alive in prose: the CLI, the mailbox root and CLAUDE_COORD_DIR kept it — they are the transport protocol, not the product.',
_broadcast: 'reserved engine namespace',
'llm-ingestion-guard': 'package name, not a repo',
'claude-code-llm-security': 'pre-split name of llm-security',
},
classes: {
plugin: {
required_files: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
install: 'plugin',
},
catalog: {
required_files: ['README.md', 'LICENSE', 'GOVERNANCE.md', 'CONVENTIONS.md', '.claude-plugin/marketplace.json'],
install: 'catalog',
},
'shared-asset': { required_files: ['README.md', 'LICENSE'], install: 'vendor' },
'org-profile': { required_files: ['README.md'], install: 'none' },
standalone: { required_files: ['README.md', 'LICENSE'], install: 'package' },
},
description_max_codepoints: 180,
};
// ---------------------------------------------------------------- measurement
test('countCodepoints measures codepoints, not bytes and not UTF-16 units', () => {
// The em-dash exposes only the byte layer: 3 bytes, 1 codepoint, 1 UTF-16 unit.
assert.equal(countCodepoints('a—b'), 3);
assert.equal(Buffer.byteLength('a—b', 'utf8'), 5);
// 👉 is astral: 1 codepoint but 2 UTF-16 units. This is the layer the em-dash hides.
assert.equal(countCodepoints('👉'), 1);
assert.equal('👉'.length, 2);
});
// ------------------------------------------------------------- ref extraction
test('normalizeRepoRef strips a .git suffix and a trailing slash', () => {
// ~20 "dead" names collapsed to 3 real ones once .git was normalised.
assert.equal(normalizeRepoRef('llm-security.git'), 'llm-security');
assert.equal(normalizeRepoRef('llm-security/'), 'llm-security');
assert.equal(normalizeRepoRef('llm-security'), 'llm-security');
});
test('extractOpenRefs finds names in URL position only', () => {
const text = [
'clone https://git.fromaitochitta.com/open/llm-security.git today',
'see https://git.fromaitochitta.com/open/repo-mailbox/src/branch/main/README.md',
].join('\n');
const names = extractOpenRefs(text).map((r) => r.name);
assert.deepEqual(names, ['llm-security', 'repo-mailbox']);
});
test('extractOpenRefs ignores path position, prose and bare directory names', () => {
// False positives #4, #5, #6 — the text is correct and will STAY correct.
const text = [
'the mailbox root is ~/.claude/coord/_broadcast/inbox/',
'coord is the transport protocol, not the product',
'the catalog lives in ktg-plugin-marketplace/catalog',
'the package llm-ingestion-guard is published from that repo',
].join('\n');
assert.deepEqual(extractOpenRefs(text), []);
});
test('extractOpenRefs handles the ssh scp-style form', () => {
const refs = extractOpenRefs('git@git.fromaitochitta.com:open/llm-security.git');
assert.deepEqual(refs.map((r) => r.name), ['llm-security']);
});
test('extractOpenRefs reports the 1-indexed line of each hit', () => {
const text = 'line one\nline two\nhttps://git.fromaitochitta.com/open/llm-security';
assert.equal(extractOpenRefs(text)[0].line, 3);
});
// ---------------------------------------------- three outcomes, not a boolean
test('classifyRef separates repo, non-repo and unknown', () => {
assert.equal(classifyRef('llm-security', REGISTER), 'repo');
assert.equal(classifyRef('.profile', REGISTER), 'repo');
assert.equal(classifyRef('coord', REGISTER), 'non-repo');
assert.equal(classifyRef('nonesuch', REGISTER), 'unknown');
});
test('a URL-position ref to a known non-repo is a DISTINCT outcome from no match', () => {
// The specification requirement: "no match" and "match on something that is
// not a repo" must never share an outcome, or the loss goes silent.
const bad = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/nonesuch' } }, REGISTER);
const odd = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/coord' } }, REGISTER);
assert.equal(bad[0].level, 'ERROR');
assert.equal(bad[0].code, 'LINK-DEAD');
assert.equal(odd[0].level, 'WARN');
assert.equal(odd[0].code, 'LINK-NON-REPO');
assert.notEqual(bad[0].code, odd[0].code);
// The reason travels with the finding, so the reader is not sent measuring again.
assert.match(odd[0].msg, /transport protocol/);
});
test('a dead ref names its successor when the register knows one', () => {
const f = checkLinks(
{ files: { 'README.md': 'https://git.fromaitochitta.com/open/claude-code-llm-security' } },
REGISTER,
);
assert.equal(f[0].level, 'WARN');
assert.match(f[0].msg, /pre-split name/);
});
test('the .git suffix does not manufacture a dead reference', () => {
const f = checkLinks(
{ files: { 'README.md': 'https://git.fromaitochitta.com/open/llm-security.git' } },
REGISTER,
);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the hidden .profile resolves — the enumerator sees what a glob misses', () => {
const f = checkLinks({ files: { 'README.md': 'https://git.fromaitochitta.com/open/.profile' } }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ------------------------------------------------------------- description
test('description length is bounded, measured in codepoints', () => {
assert.equal(checkDescription('a fine description', REGISTER)[0].level, 'OK');
assert.equal(checkDescription('', REGISTER)[0].level, 'ERROR');
assert.equal(checkDescription('x'.repeat(181), REGISTER)[0].level, 'ERROR');
assert.equal(checkDescription('x'.repeat(180), REGISTER)[0].level, 'OK');
});
test('an unavailable description is SKIP, never a pass', () => {
// Offline is not compliance. A check that could not run says so.
const f = checkDescription(null, REGISTER);
assert.equal(f[0].level, 'SKIP');
});
// ------------------------------------------------------------- first screen
test('README line 1 must be the H1, and the description line must match the forge', () => {
const readme = '# repo-mailbox\nA local mailbox for coordination.\n';
assert.equal(
checkFirstScreen({ readme, name: 'repo-mailbox', description: 'A local mailbox for coordination.' })
.every((f) => f.level === 'OK'),
true,
);
});
test('a README whose opening line diverges from the description is an ERROR', () => {
const readme = '# repo-mailbox\nSomething else entirely.\n';
const f = checkFirstScreen({ readme, name: 'repo-mailbox', description: 'A local mailbox for coordination.' });
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'README-DESC'), true);
});
test('first-screen description match is SKIP when the forge text is unavailable', () => {
const f = checkFirstScreen({ readme: '# x\nbody\n', name: 'x', description: null });
assert.equal(f.some((x) => x.code === 'README-DESC' && x.level === 'SKIP'), true);
});
test('a wrong or missing H1 is an ERROR', () => {
const f = checkFirstScreen({ readme: 'no heading here\n', name: 'x', description: null });
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'README-H1'), true);
});
// ------------------------------------------------------------ install block
const MKT = REGISTER.marketplace;
test('plugin install needs BOTH lines: marketplace add and a CLI install command', () => {
const readme = [
'## Install',
'```',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install repo-mailbox@${MKT.name}`,
'```',
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-mailbox', klass: 'plugin' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the slash form counts as the CLI command', () => {
const readme = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`/plugin install claude-design@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'claude-design', klass: 'plugin' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('enabledPlugins JSON is an ALLOWED ADDITION, never a replacement for the CLI command', () => {
// This is the corrected defect A: 7 of 11 plugin READMEs stop after
// `marketplace add`. The JSON form works and stands in 10 of 11 — what is
// missing is a CLI command, so the contract requires the command and permits
// the JSON alongside it.
const jsonOnly = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
'Or enable directly in `~/.claude/settings.json`:',
`"enabledPlugins": { "llm-security@${MKT.name}": true }`,
].join('\n');
const f = checkInstallBlock({ readme: jsonOnly, name: 'llm-security', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-NO-CLI'), true);
const both = jsonOnly + `\nclaude plugin install llm-security@${MKT.name}\n`;
const g = checkInstallBlock({ readme: both, name: 'llm-security', klass: 'plugin' }, REGISTER);
assert.equal(g.filter((x) => x.level === 'ERROR').length, 0);
});
test('a missing marketplace add is its own finding — okr and claude-design are opposite halves', () => {
const noAdd = ['## Install', `/plugin install claude-design@${MKT.name}`].join('\n');
const f = checkInstallBlock({ readme: noAdd, name: 'claude-design', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-NO-MARKETPLACE'), true);
// okr: neither line. Both findings fire — a rule saying "both lines" must not
// hit this repo blind.
const neither = ['## Install', `"enabledPlugins": { "okr@${MKT.name}": true }`].join('\n');
const g = checkInstallBlock({ readme: neither, name: 'okr', klass: 'plugin' }, REGISTER);
assert.equal(g.some((x) => x.code === 'INSTALL-NO-MARKETPLACE'), true);
assert.equal(g.some((x) => x.code === 'INSTALL-NO-CLI'), true);
});
test('the install target must name THIS repo, not another plugin', () => {
const readme = [
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install some-other-plugin@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-standard', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.code === 'INSTALL-NO-CLI'), true);
});
test('ssh in the marketplace add line is an ERROR — marketplace add rejects it', () => {
// Measured end-to-end: `marketplace add ssh://...` → "Invalid git URL".
// The forge UI's clone button hands you exactly that URL.
const readme = [
'## Install',
'claude plugin marketplace add ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git',
`claude plugin install repo-standard@${MKT.name}`,
].join('\n');
const f = checkInstallBlock({ readme, name: 'repo-standard', klass: 'plugin' }, REGISTER);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-SSH'), true);
});
test('the install block is parametric — a different marketplace passes on its own values', () => {
// wiki-advise lives on the private ktg/ namespace and is distributed via
// `ktg-privat`. A skill that hardcodes the public marketplace produces an
// install line that does not work there — and being public itself, this skill
// cannot carry private marketplace names.
const priv = {
...REGISTER,
marketplace: { name: 'ktg-privat', url: 'https://git.fromaitochitta.com/ktg/ktg-privat.git' },
};
const readme = [
'## Install',
'claude plugin marketplace add https://git.fromaitochitta.com/ktg/ktg-privat.git',
'claude plugin install wiki-advise@ktg-privat',
].join('\n');
const f = checkInstallBlock({ readme, name: 'wiki-advise', klass: 'plugin' }, priv);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('the catalog needs only marketplace add — it IS the marketplace', () => {
const readme = `## Install\nclaude plugin marketplace add ${MKT.url}`;
const f = checkInstallBlock({ readme, name: 'ktg-plugin-marketplace', klass: 'catalog' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
test('a shared asset is vendored, not installed — a plugin install line is wrong there', () => {
const asPlugin = `## Install\nclaude plugin install playground-design-system@${MKT.name}`;
const f = checkInstallBlock(
{ readme: asPlugin, name: 'playground-design-system', klass: 'shared-asset' },
REGISTER,
);
assert.equal(f.some((x) => x.level === 'ERROR' && x.code === 'INSTALL-WRONG-FORM'), true);
});
test('.profile needs no install section at all', () => {
const f = checkInstallBlock({ readme: '# .profile\nOrg profile.\n', name: '.profile', klass: 'org-profile' }, REGISTER);
assert.equal(f.filter((x) => x.level === 'ERROR').length, 0);
});
// ----------------------------------------------------------- required files
test('required files are per class, not flat across the org', () => {
const ok = checkRequiredFiles({ present: ['README.md'], klass: 'org-profile' }, REGISTER);
assert.equal(ok.filter((f) => f.level === 'ERROR').length, 0);
const missing = checkRequiredFiles({ present: ['README.md'], klass: 'plugin' }, REGISTER);
assert.equal(missing.some((f) => f.code === 'FILE-MISSING' && f.msg.includes('LICENSE')), true);
});
test('no class requires a ROADMAP — it is 0/18 and belongs to a later step', () => {
for (const klass of Object.keys(REGISTER.classes)) {
assert.equal(REGISTER.classes[klass].required_files.includes('ROADMAP.md'), false);
}
});
// -------------------------------------------------------------- aggregation
test('levelOf ranks ERROR above WARN above SKIP above OK', () => {
assert.equal(levelOf([{ level: 'OK' }, { level: 'WARN' }, { level: 'ERROR' }]), 'ERROR');
assert.equal(levelOf([{ level: 'OK' }, { level: 'WARN' }]), 'WARN');
assert.equal(levelOf([{ level: 'OK' }, { level: 'SKIP' }]), 'SKIP');
assert.equal(levelOf([{ level: 'OK' }]), 'OK');
assert.equal(levelOf([]), 'OK');
});
test('an unregistered repo is SKIP, not a pass — the gate refuses to guess a class', () => {
const r = classifyRepo({ name: 'stranger', files: {}, present: [], description: null }, REGISTER);
assert.equal(r.status, 'SKIP');
});
test('a fully compliant plugin repo classifies OK', () => {
const readme = [
'# repo-mailbox',
'A local mailbox for coordination.',
'',
'Body text.',
'',
'## Install',
`claude plugin marketplace add ${MKT.url}`,
`claude plugin install repo-mailbox@${MKT.name}`,
].join('\n');
const r = classifyRepo(
{
name: 'repo-mailbox',
files: { 'README.md': readme },
present: ['README.md', 'LICENSE', 'CHANGELOG.md', '.claude-plugin/plugin.json'],
description: 'A local mailbox for coordination.',
},
REGISTER,
);
assert.equal(r.status, 'OK');
});

View file

@ -0,0 +1,165 @@
---
name: repo-standard
description: >-
Check and fix one repository against the org's presentation standard — README
first screen, install block, required files for its class, dead `open/<name>`
references, and description length. Use when the user asks to "check this repo
against the standard", "run repo-standard", "is this repo compliant", "fix the
README first screen", "the install instructions are incomplete", "check for
dead repo references", "why does this repo look abandoned", or before
publishing/releasing a repo to the org. Also triggers on Norwegian phrasings:
"sjekk repoet mot standarden", "er dette repoet i orden", "rydd opp i README",
"fiks install-blokka", "finn døde repo-referanser", "gjør repoet presentabelt".
Trigger when someone is about to release, publish, or hand over a repository
and wants its public surface to hold up.
version: "0.1.0"
---
# repo-standard — the per-repo gate
This skill checks **one repository** against the standard for its class, and
helps fix what it finds. The script is the engine; this file is the judgement
the script cannot encode.
## Run the gate first, always
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (the check
could not run), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the one
network call, `--json` for machine output, `--refresh` to compare the bundled
register against the live org listing.
**Never report a `SKIP` as a pass.** A SKIP means the gate could not see enough
to judge — an unreachable forge, a repo missing from the register. Say which.
## What it measures, and what it cannot
The gate sees one repo. Every finding in the census that mattered came from
measuring across eighteen. `llm-security`'s README looks correct from the
inside: it has a Quick Start, an Install heading, a code block. It is only wrong
once eleven repos are compared and one of them turns out to do install
completely.
So: **divergence across the org is not this skill's job.** Zero topics across
all repos, three competing install forms, README release notes duplicating a
CHANGELOG that most repos have — none of that is visible from in here. Those
checks live where the org is enumerated, not in a per-repo gate. If you suspect
a cross-repo problem, say so and stop; do not approximate it from one repo.
## The class decides what is required
The class is read off the catalog and the remotes — it is structural, not a
judgement call. `register/repos.json` holds it.
| Class | Install form |
| --- | --- |
| plugin | `marketplace add` **plus** a CLI install command |
| catalog | `marketplace add` only — it *is* the marketplace |
| shared-asset | how to vendor it; never a plugin install line |
| standalone | pip/uv |
| org-profile | no install section |
A flat standard across all classes would demand a CONTRIBUTING from a CSS
library that takes no contributions and a ROADMAP from a five-line profile.
That is how a gate teaches people to switch it off.
## Two things about the install block worth knowing before you edit one
**`enabledPlugins` is a legitimate second form, not a defect.** Most plugin
READMEs offer it ("Or enable directly in `~/.claude/settings.json`"), and a
reader who scrolls to it has a complete, working path. What the gate requires is
a **CLI command** as well, because an agent handed "install this" reaches for the
CLI and otherwise finds `marketplace add` and nothing after it. Require the
command; welcome the JSON beside it. Never treat the JSON as the failure.
**`marketplace add` rejects `ssh://`.** It answers `Invalid git URL`, and the
message never mentions the protocol. The forge UI's clone button hands out
exactly that URL, so this is a trap people walk into rather than a mistake they
make. Always write the `https://` form.
**Keep the block parametric.** The marketplace name and URL come from the
register, not from a hardcoded string. A repo distributed through a different
marketplace — including a private one — needs its own values, and a public skill
cannot carry private marketplace names in the first place.
## Fixing the first screen
Only lines 125. Nobody rewrites a 930-line body, and the gate does not ask you to.
# <name>
<one line — identical to the forge description>
<24 lines: which problem it solves, who it is for>
## Install
<the block for this class>
## Requirements
## What it does
## Non-goals
…rest unchanged…
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
The opening line is not decoration: it makes description == catalog == README,
which is the only place a machine can check that the three agree. `## Install`
on a fixed heading is what agents pattern-match on — position and predictability
beat brevity for that reader. And **Non-goals answers "is this for me?"** better
than any feature table; almost no README answers it at all.
**Release notes do not belong on the first screen.** A blockquote packing five
versions before the reader knows what the thing *is* both duplicates the
changelog and points at it. Move it under `## Changelog` as a link.
**What not to retrofit:** inline version annotations inside feature tables —
`(v4.1.0)`, "since v2.4.0" — stay. They are real work and they help a reader who
has already installed. The rule applies to new ones: do not add more.
## Dead references — three outcomes, never two
The link check separates "matches no repo" (ERROR) from "matches something that
is deliberately not a repo" (WARN). Keep them separate when you report, too. If
they collapse into one number, a real loss hides inside a pile of correct text.
Names in **URL position only** are references. A repo-shaped string in a path, in
running prose, or as a directory name is not a broken link — and in the measured
cases that text was correct and is *supposed* to stay correct. Do not "fix" it.
Before calling a reference dead, normalise the `.git` suffix. A raw grep once
turned three real dead names into about twenty.
## Descriptions
Written for someone who has never seen the repo, saying what it does and who
it is for. No sales language: "fully", "without exception", "everything from
the terminal" are red flags in a technical text.
Length is bounded and measured in **codepoints** — not bytes, not UTF-16 units.
The same string can measure 248 / 249 / 253 across those three yardsticks when
it contains an astral character like `👉`. An em-dash costs three bytes but one
codepoint, so it only ever exposed the outer layer.
**Never verify a description against the repo summary card.** It is server-side
cached with an undocumented TTL, measured unchanged for more than six hours
after a write. Check the API, or `og:description` in the HTML. A card that still
shows the old text is not a failed write, and re-writing it is how you turn a
cache into an outage of your own making.
## ROADMAP — not required, and not automatic
No class requires one. They are absent everywhere today, and a roadmap is a real
judgement about where a repo is going, not a template.
When one is written, it is the **published, sanitised** derivation of the repo's
own next-step block. **`STATE.md` must never reach a public surface.** Draft
from it, have a human approve, then publish. Never automatically.
## The boundary this skill will not cross
Whether to rename a repo, and whether a given roadmap is real or ceremonial, are
operator decisions. Bring the evidence; do not decide.
And **record findings before fixing them**. Patching while measuring is how the
inconsistency being cleaned up got there in the first place. Run the gate, report
what it says, then fix — in that order.