fix(engine): print engine version, so a stale plugin cache is visible
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
This commit is contained in:
parent
d1b6274924
commit
f85b9d3229
7 changed files with 88 additions and 11 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "repo-standard",
|
"name": "repo-standard",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"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.",
|
"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": {
|
"author": {
|
||||||
"name": "Kjell Tore Guttormsen"
|
"name": "Kjell Tore Guttormsen"
|
||||||
|
|
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -4,6 +4,20 @@ All notable changes to this project are documented here.
|
||||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
||||||
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
versioning is [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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-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, wrong engine, and
|
||||||
|
`0.1.1` has neither `BADGE-COUNT` nor `README-LANGUAGE`, so the run looked
|
||||||
|
clean instead of incomplete. The header line and `--json` output now both
|
||||||
|
carry `repo-standard v<version>`; `SKILL.md` tells 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
|
## [0.2.1] — 2026-08-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ checks that surface in one repository and reports what it finds.
|
||||||
|
|
||||||
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
*AI-generated: all code produced by Claude Code through dialog-driven development.*
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "repo-standard",
|
"name": "repo-standard",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
const REGISTER_PATH = join(HERE, '..', 'register', 'repos.json');
|
const REGISTER_PATH = join(HERE, '..', 'register', 'repos.json');
|
||||||
|
const PACKAGE_PATH = join(HERE, '..', 'package.json');
|
||||||
|
|
||||||
|
// This engine's own version, not the target repo's — distinct from
|
||||||
|
// readPackageVersion(dir) below, which reads the REPO BEING CHECKED.
|
||||||
|
export function readEngineVersion(path = PACKAGE_PATH) {
|
||||||
|
return JSON.parse(readFileSync(path, 'utf8')).version;
|
||||||
|
}
|
||||||
|
|
||||||
const LEVELS = ['OK', 'SKIP', 'WARN', 'ERROR'];
|
const LEVELS = ['OK', 'SKIP', 'WARN', 'ERROR'];
|
||||||
|
|
||||||
|
|
@ -980,11 +987,25 @@ const BUCKET_TITLE = {
|
||||||
weakening: 'WEAKENING — present, but it reads as amateur',
|
weakening: 'WEAKENING — present, but it reads as amateur',
|
||||||
};
|
};
|
||||||
|
|
||||||
function render(result) {
|
// A stale plugin cache once served 0.1.1 while 0.2.0 was installed and
|
||||||
const mark = { OK: '✓', WARN: '!', ERROR: '✗', SKIP: '·' };
|
// pinned, silently — the output looked like a clean pass, because nothing
|
||||||
|
// said which engine had run. This is the fix: name the version so a wrong
|
||||||
|
// engine is visible, not just correctable in hindsight.
|
||||||
|
const MARK = { OK: '✓', WARN: '!', ERROR: '✗', SKIP: '·' };
|
||||||
|
|
||||||
|
export function headerLine(result, engineVersion) {
|
||||||
const klass = result.klass ? ` [${result.klass}]` : '';
|
const klass = result.klass ? ` [${result.klass}]` : '';
|
||||||
const traits = result.traits?.length ? ` {${result.traits.join(', ')}}` : '';
|
const traits = result.traits?.length ? ` {${result.traits.join(', ')}}` : '';
|
||||||
console.log(`\n${mark[result.status]} ${result.name}${klass}${traits} — ${result.status}`);
|
return `${MARK[result.status]} ${result.name}${klass}${traits} — ${result.status} (repo-standard v${engineVersion})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withEngineVersion(result, engineVersion) {
|
||||||
|
return { ...result, engineVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(result, engineVersion) {
|
||||||
|
const mark = MARK;
|
||||||
|
console.log(`\n${headerLine(result, engineVersion)}`);
|
||||||
|
|
||||||
for (const bucket of BUCKETS) {
|
for (const bucket of BUCKETS) {
|
||||||
const inBucket = result.findings.filter((f) => f.bucket === bucket);
|
const inBucket = result.findings.filter((f) => f.bucket === bucket);
|
||||||
|
|
@ -1047,11 +1068,12 @@ async function main(argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = inspectRepo(dir, name, register, description, catalogNames);
|
const result = inspectRepo(dir, name, register, description, catalogNames);
|
||||||
|
const engineVersion = readEngineVersion();
|
||||||
|
|
||||||
if (argv.includes('--json')) {
|
if (argv.includes('--json')) {
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(withEngineVersion(result, engineVersion), null, 2));
|
||||||
} else {
|
} else {
|
||||||
render(result);
|
render(result, engineVersion);
|
||||||
}
|
}
|
||||||
process.exit(result.status === 'ERROR' ? 1 : 0);
|
process.exit(result.status === 'ERROR' ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import {
|
||||||
parseRepoNameFromRemote,
|
parseRepoNameFromRemote,
|
||||||
extractChangelogTop,
|
extractChangelogTop,
|
||||||
fetchWithRetry,
|
fetchWithRetry,
|
||||||
|
headerLine,
|
||||||
|
withEngineVersion,
|
||||||
} from './repo-standard-check.mjs';
|
} from './repo-standard-check.mjs';
|
||||||
|
|
||||||
const REGISTER = {
|
const REGISTER = {
|
||||||
|
|
@ -1117,3 +1119,31 @@ test('a non-429 response returns immediately — no retry, no sleep', async () =
|
||||||
assert.equal(fetchCalls, 1);
|
assert.equal(fetchCalls, 1);
|
||||||
assert.equal(sleepCalls, 0);
|
assert.equal(sleepCalls, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------- engine version stamp
|
||||||
|
// Measured 2026-08-04: the /repo-standard skill resolved to a cached 0.1.1
|
||||||
|
// plugin root while 0.2.0 was installed and the catalog pinned it. Two repos
|
||||||
|
// independently proved it by running both engines against the same checkout:
|
||||||
|
// 0.2.0 gave 0 WARN, 0.1.1 resurrected the three false positives fixed in
|
||||||
|
// v0.1.2/v0.1.3 — and nothing in the output said which engine had run. The
|
||||||
|
// header carrying no version is what let a stale engine look like a pass.
|
||||||
|
test('headerLine names the engine version, not just the repo status', () => {
|
||||||
|
const line = headerLine({ name: 'voyage', klass: 'plugin', traits: [], status: 'OK' }, '0.2.1');
|
||||||
|
assert.match(line, /voyage/);
|
||||||
|
assert.match(line, /OK/);
|
||||||
|
assert.match(line, /0\.2\.1/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('headerLine includes traits when present, same as the unversioned line did', () => {
|
||||||
|
const line = headerLine({ name: 'llm-security', klass: 'plugin', traits: ['security'], status: 'WARN' }, '0.2.1');
|
||||||
|
assert.match(line, /\{security\}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withEngineVersion adds the version without disturbing existing fields', () => {
|
||||||
|
const result = { name: 'x', klass: 'plugin', status: 'OK', findings: [] };
|
||||||
|
const stamped = withEngineVersion(result, '0.2.1');
|
||||||
|
assert.equal(stamped.engineVersion, '0.2.1');
|
||||||
|
assert.equal(stamped.name, 'x');
|
||||||
|
assert.equal(stamped.status, 'OK');
|
||||||
|
assert.deepEqual(result, { name: 'x', klass: 'plugin', status: 'OK', findings: [] });
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ description: >-
|
||||||
"fiks install-blokka", "finn døde repo-referanser", "gjør repoet presentabelt".
|
"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
|
Trigger when someone is about to release, publish, or hand over a repository
|
||||||
and wants its public surface to hold up.
|
and wants its public surface to hold up.
|
||||||
version: "0.2.1"
|
version: "0.2.2"
|
||||||
---
|
---
|
||||||
|
|
||||||
# repo-standard — the per-repo gate
|
# repo-standard — the per-repo gate
|
||||||
|
|
@ -26,14 +26,25 @@ the script cannot encode.
|
||||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
|
node "${CLAUDE_PLUGIN_ROOT}/scripts/repo-standard-check.mjs" --dir "$PWD"
|
||||||
|
|
||||||
Findings are `ERROR` (blocks), `WARN` (look, then decide), `SKIP` (the check
|
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
|
could not run), `OK`. Exit 1 on any ERROR. Add `--offline` to skip the two
|
||||||
network call, `--json` for machine output, `--refresh` to compare the bundled
|
network calls, `--json` for machine output, `--refresh` to compare the bundled
|
||||||
register against the live org listing.
|
register against the live org listing.
|
||||||
|
|
||||||
**Never report a `SKIP` as a pass.** A SKIP means the gate could not see enough
|
**Never report a `SKIP` as a pass.** A SKIP means the gate could not see enough
|
||||||
to judge — an unreachable forge, an untagged repo, a link leaving the
|
to judge — an unreachable forge, an untagged repo, a link leaving the
|
||||||
repository. Say which.
|
repository. Say which.
|
||||||
|
|
||||||
|
**Confirm the version before you trust a green run.** Measured 2026-08-04:
|
||||||
|
`${CLAUDE_PLUGIN_ROOT}` resolved to a cached `0.1.1` in a live session while
|
||||||
|
`0.2.0` was installed and the catalog pinned it — same instruction, same
|
||||||
|
variable, wrong engine, and nothing in the output said so at the time. Two
|
||||||
|
repos proved it by running both versions against the same checkout: `0.1.1`
|
||||||
|
resurrected three findings `0.1.2`/`0.1.3` had already fixed. The header line
|
||||||
|
and `--json` output now both carry `repo-standard v<version>` — read it, and
|
||||||
|
if it does not match what the catalog pins (`ktg-plugin-marketplace`'s
|
||||||
|
`marketplace.json`, or ask the operator), say so instead of reporting the run
|
||||||
|
as clean.
|
||||||
|
|
||||||
**Report in buckets, not in a flat list.** Every finding also carries `broken`
|
**Report in buckets, not in a flat list.** Every finding also carries `broken`
|
||||||
(a stranger is blocked or misled), `missing` (an expected artefact is absent) or
|
(a stranger is blocked or misled), `missing` (an expected artefact is absent) or
|
||||||
`weakening` (present, but reads as amateur). That is the order the work actually
|
`weakening` (present, but reads as amateur). That is the order the work actually
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue