feat(skl): flag oversized skill bodies on demand (v5.11 B7) [skip-docs]

New CA-SKL-003 (low): a SKILL.md body over ~5,000 tokens (~500 lines)
should split reference content into supporting files / use context: fork.

- measureActiveSkillListing() now returns body metrics (chars/lines/tokens);
  the body was already read in full, only the frontmatter was parsed before.
- Honest framing: BODY_CALIBRATION_NOTE marks this as ON-DEMAND cost (loads
  only when the skill is invoked, NOT every turn like the always-loaded
  listing) and an estimate — hence low severity.
- 5 new tests; full suite 1262 green; snapshots byte-stable (default branch
  untouched; new finding fires only on bodies >5k tok, none in fixtures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 21:36:20 +02:00
commit 27988801be
3 changed files with 149 additions and 1 deletions

View file

@ -40,6 +40,8 @@ import {
DESCRIPTION_CAP,
AGGREGATE_BUDGET_TOKENS,
BUDGET_CALIBRATION_NOTE,
BODY_TOKEN_THRESHOLD,
BODY_CALIBRATION_NOTE,
measureActiveSkillListing,
} from './lib/skill-listing-budget.mjs';
@ -116,5 +118,38 @@ export async function scan(_targetPath, _discovery) {
}));
}
// CA-SKL-003 (oversized body). Emitted last so the common single-issue cases
// read cleanly. Unlike the listing budget, this is an ON-DEMAND cost — the body
// loads only when the skill is invoked, not every turn — hence low severity and
// an explicit on-demand calibration note.
for (const skill of skills) {
if (skill.bodyTokens <= BODY_TOKEN_THRESHOLD) continue;
const sourceLabel = skill.source === 'plugin'
? `plugin:${skill.pluginName}`
: 'user';
findings.push(finding({
scanner: SCANNER,
severity: SEVERITY.low,
title: 'Skill body is large (loads on demand when the skill runs)',
description:
`Skill "${skill.name}" (${sourceLabel}) has a body of about ${skill.bodyTokens} tokens ` +
`(${skill.bodyLines} lines), over the ~${BODY_TOKEN_THRESHOLD}-token guidance for a skill body. ` +
'The body is not in the always-loaded listing — it loads only when the skill is invoked — but ' +
'once loaded a large body consumes context for the rest of that session. Claude Code skill ' +
'guidance is to keep the body lean and move heavy reference material into supporting files.',
file: skill.path,
evidence:
`body_tokens~${skill.bodyTokens}; body_lines=${skill.bodyLines}; body_chars=${skill.bodyChars}; ` +
`threshold=${BODY_TOKEN_THRESHOLD} tok; skill="${skill.name}"; source=${sourceLabel} - ${BODY_CALIBRATION_NOTE}`,
recommendation:
'Move reference content into supporting files the skill loads only when needed, and consider ' +
'`context: fork` in the skill frontmatter for heavy skills so the body runs in a forked context ' +
'instead of consuming the main thread.',
category: 'token-efficiency',
}));
}
return scannerResult(SCANNER, 'ok', findings, aggregate.scanned, Date.now() - start);
}