- verify:skills:cli — the authoritative validator is now a GUARANTEED step in `local:gate` and CI, not a best-effort hand-off. It resolves the CLI itself (an installed one at 2.1.233+, else a pinned `npx -y`), so both tiers run the same script and cannot drift. `verify:skills` keeps skipping when the CLI is absent, which is right for `validate`: fast, offline, no Claude Code required. - skill-manifest: the 1,536-char listing-entry cap covers `description` + `when_to_use` together; checking the description alone passed an over-cap entry. `when_to_use` is now parsed, validated and counted, with boundary tests at 1,536/1,537. - verify-skills: report the budget as *this repo's contribution*. The real listing budget is shared with bundled skills and the contributor's own ~/.claude/skills, none of which is visible from here, so the number was claiming more than it measured. - skill-eval: a negative case now means "no skill OF THIS REPO'S fired". It failed on any Skill invocation, so a contributor with an unrelated global skill got a false failure about their environment rather than these skills. - docs/architecture + local-dev: the Node backend runner gets its `@inspector/core` alias from `clients/web/tsup.runner.config.ts`, not from Vite. The old text sent runner work to the wrong config. - local-dev: the tarball does not ship "only each client's build/" — the allowlist also carries `clients/web/dist`, `clients/web/static` and the install helper. States the invariant that actually matters instead. - issue-triage: the test for needing triage is "no board card". Adding "and no milestone" would skip the approved-but-unboarded case the same section then handles. Also corrected the `$M` note: the lookup is `--state all`, so closed issues DO have entries — `isopen` is about the invariants, not the lookup. - issue-create: replaced "all five" with a per-version table. Steps 3 and 6 (milestone, Priority) do not exist for v1, so the old list opened with a rule the same skill later tells you to violate. Signed-off-by: cliffhall <cliff@futurescale.com>
101 lines
3.2 KiB
JavaScript
101 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Runs `claude plugin validate` — the AUTHORITATIVE skill schema — as a
|
|
// guaranteed step, resolving the CLI rather than hoping it is installed (#2163).
|
|
//
|
|
// `verify:skills` (inside `validate`) reimplements the parse and treats a
|
|
// missing CLI as a skip, deliberately: `npm run validate` is the fast inner
|
|
// loop, it must work offline, and a Node contributor should not need Claude Code
|
|
// installed to run it. But "skips when absent" plus "usually absent" adds up to
|
|
// "never runs", which made the acceptance criterion aspirational (Copilot).
|
|
//
|
|
// So the authoritative check gets its own step, in `local:gate` and in CI, and
|
|
// it does NOT skip: an already-installed CLI new enough to have the subcommand
|
|
// is used as-is, and otherwise a PINNED one is fetched with `npx -y`. Pinned
|
|
// rather than @latest because a validator that moves on its own can start
|
|
// failing a PR that changed nothing, which is how a gate loses its credibility.
|
|
//
|
|
// It needs no authentication — verified against a scrubbed environment and a
|
|
// clean HOME.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
compareVersions,
|
|
parseClaudeVersion,
|
|
PINNED_CLI_VERSION,
|
|
PLUGIN_VALIDATE_MIN_VERSION,
|
|
} from "./lib/skill-manifest.mjs";
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const SKILLS_DIR = ".claude/skills";
|
|
|
|
/**
|
|
* The argv that runs a usable validator: the local CLI when it is new enough,
|
|
* otherwise the pinned one through `npx`.
|
|
*
|
|
* @param {{ version: number[] | null }} local
|
|
* @returns {{ command: string, args: string[], via: string }}
|
|
*/
|
|
export function validatorCommand({ version }) {
|
|
if (
|
|
version !== null &&
|
|
compareVersions(version, PLUGIN_VALIDATE_MIN_VERSION) >= 0
|
|
) {
|
|
return {
|
|
command: "claude",
|
|
args: ["plugin", "validate", SKILLS_DIR],
|
|
via: `local claude ${version.join(".")}`,
|
|
};
|
|
}
|
|
return {
|
|
command: "npx",
|
|
args: [
|
|
"-y",
|
|
`@anthropic-ai/claude-code@${PINNED_CLI_VERSION}`,
|
|
"plugin",
|
|
"validate",
|
|
SKILLS_DIR,
|
|
],
|
|
via:
|
|
version === null
|
|
? `npx @anthropic-ai/claude-code@${PINNED_CLI_VERSION} (no local CLI)`
|
|
: `npx @anthropic-ai/claude-code@${PINNED_CLI_VERSION} (local ${version.join(".")} predates the subcommand)`,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
const probe = spawnSync("claude", ["--version"], { encoding: "utf8" });
|
|
const version =
|
|
probe.error || probe.status !== 0
|
|
? null
|
|
: parseClaudeVersion(probe.stdout ?? "");
|
|
|
|
const { command, args, via } = validatorCommand({ version });
|
|
console.log(`verify:skills:cli — validating ${SKILLS_DIR} via ${via}…`);
|
|
|
|
const res = spawnSync(command, args, {
|
|
cwd: ROOT,
|
|
stdio: "inherit",
|
|
shell: process.platform === "win32",
|
|
});
|
|
if (res.error) {
|
|
console.error(
|
|
`\nverify:skills:cli — could not run \`${command}\`: ${res.error.message}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (res.status !== 0) {
|
|
console.error(
|
|
"\nverify:skills:cli — `claude plugin validate` rejected the skills.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
main();
|
|
}
|