chore: address Copilot review round 4
- 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>
This commit is contained in:
parent
7e0bb8b58d
commit
d05e76d470
2 changed files with 330 additions and 51 deletions
|
|
@ -13,7 +13,11 @@
|
|||
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
/** Combined per-entry cap Claude Code applies to a skill listing entry. */
|
||||
/**
|
||||
* Per-entry cap Claude Code applies to a skill listing entry. It covers
|
||||
* `description` **and** `when_to_use` together, so checking `description` alone
|
||||
* would pass an over-cap entry (Copilot).
|
||||
*/
|
||||
export const DESCRIPTION_CAP = 1536;
|
||||
|
||||
/**
|
||||
|
|
@ -101,9 +105,18 @@ export function parseSkill(dirName, text) {
|
|||
const description = meta.description;
|
||||
if (typeof description !== "string" || description.trim().length === 0) {
|
||||
errors.push("`description` is required and must be a non-empty string");
|
||||
} else if (description.length > DESCRIPTION_CAP) {
|
||||
}
|
||||
|
||||
const whenToUse = meta["when_to_use"];
|
||||
if ("when_to_use" in meta && typeof whenToUse !== "string") {
|
||||
errors.push("`when_to_use` must be a string");
|
||||
}
|
||||
const entryLength =
|
||||
(typeof description === "string" ? description.length : 0) +
|
||||
(typeof whenToUse === "string" ? whenToUse.length : 0);
|
||||
if (entryLength > DESCRIPTION_CAP) {
|
||||
errors.push(
|
||||
`\`description\` is ${description.length} chars; Claude Code caps a listing entry at ${DESCRIPTION_CAP}`,
|
||||
`\`description\` + \`when_to_use\` is ${entryLength} chars; Claude Code caps a listing entry at ${DESCRIPTION_CAP}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +154,7 @@ export function parseSkill(dirName, text) {
|
|||
return {
|
||||
name: typeof name === "string" ? name : undefined,
|
||||
description: typeof description === "string" ? description : undefined,
|
||||
whenToUse: typeof whenToUse === "string" ? whenToUse : undefined,
|
||||
modelInvoked,
|
||||
userInvocable: meta["user-invocable"] !== false,
|
||||
paths: Array.isArray(meta.paths) ? meta.paths : undefined,
|
||||
|
|
@ -152,13 +166,22 @@ export function parseSkill(dirName, text) {
|
|||
* Cost of the skill listing Claude Code loads into context, counting only the
|
||||
* skills that appear in it.
|
||||
*
|
||||
* @param {Array<{ name?: string, description?: string, modelInvoked?: boolean }>} skills
|
||||
* The number is this repo's CONTRIBUTION to the listing, not the whole listing:
|
||||
* bundled skills and a contributor's own `~/.claude/skills` share the same
|
||||
* budget, and none of those is visible from here (Copilot). Keeping the repo's
|
||||
* share well under the cap is what leaves room for them.
|
||||
*
|
||||
* @param {Array<{ name?: string, description?: string, whenToUse?: string, modelInvoked?: boolean }>} skills
|
||||
*/
|
||||
export function listingCost(skills) {
|
||||
return skills
|
||||
.filter((s) => s.modelInvoked)
|
||||
.reduce(
|
||||
(n, s) => n + (s.name?.length ?? 0) + (s.description?.length ?? 0),
|
||||
(n, s) =>
|
||||
n +
|
||||
(s.name?.length ?? 0) +
|
||||
(s.description?.length ?? 0) +
|
||||
(s.whenToUse?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
|
@ -207,6 +230,13 @@ export function validateEvalCases(skillName, cases) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Code version the authoritative validator is pinned to when it has to
|
||||
* be fetched. Pinned rather than @latest: a validator that moves on its own can
|
||||
* start failing a PR that changed nothing.
|
||||
*/
|
||||
export const PINNED_CLI_VERSION = "2.1.250";
|
||||
|
||||
/** First Claude Code release with `claude plugin validate`. */
|
||||
export const PLUGIN_VALIDATE_MIN_VERSION = [2, 1, 233];
|
||||
|
||||
|
|
|
|||
|
|
@ -219,3 +219,36 @@ test("compareVersions orders the plugin-validate floor correctly", () => {
|
|||
assert.ok(compareVersions([3], PLUGIN_VALIDATE_MIN_VERSION) > 0);
|
||||
assert.ok(compareVersions([1, 9, 999], PLUGIN_VALIDATE_MIN_VERSION) < 0);
|
||||
});
|
||||
|
||||
test("the listing-entry cap covers description AND when_to_use together", () => {
|
||||
// The cap is over the combined entry, so checking `description` alone would
|
||||
// pass an over-cap listing entry.
|
||||
const under = fm(
|
||||
`name: x\ndescription: ${"a".repeat(1000)}\nwhen_to_use: ${"b".repeat(536)}\ndisable-model-invocation: true`,
|
||||
);
|
||||
assert.deepEqual(parseSkill("x", under).errors, []);
|
||||
|
||||
const over = fm(
|
||||
`name: x\ndescription: ${"a".repeat(1000)}\nwhen_to_use: ${"b".repeat(537)}\ndisable-model-invocation: true`,
|
||||
);
|
||||
assert.match(parseSkill("x", over).errors.join(), /caps a listing entry/);
|
||||
|
||||
assert.match(
|
||||
parseSkill(
|
||||
"x",
|
||||
fm(
|
||||
"name: x\ndescription: d\nwhen_to_use: 7\ndisable-model-invocation: true",
|
||||
),
|
||||
).errors.join(),
|
||||
/when_to_use. must be a string/,
|
||||
);
|
||||
});
|
||||
|
||||
test("listingCost counts when_to_use too", () => {
|
||||
assert.equal(
|
||||
listingCost([
|
||||
{ name: "aa", description: "bbb", whenToUse: "cc", modelInvoked: true },
|
||||
]),
|
||||
7,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue