feat(tokens): stale plugin-cache disk-cleanup finding, honestly labelled Dead config (v5.9 B3b)

The B3a cache filter already exposes discovery.staleCacheVersions; surface them
as a finding so the user knows the superseded plugin versions are safe to delete.

Honesty (Verifiseringsplikt): the finding loads on ZERO turns, so it must NOT
read as a per-turn token cost. TOK normally humanizes to "Wasted tokens"; a new
per-finding category override ('plugin-cache-hygiene' -> "Dead config") plus a
dedicated humanizer translation ("Old plugin versions are sitting on disk (safe
to delete) ... cost zero tokens per turn ... housekeeping, not a performance
problem") keep the prose accurate instead of the generic "using more space"
default. evidence carries the explicit "zero live-context impact" note.

- token-hotspots: Pattern H emits CA-TOK (low) from discovery.staleCacheVersions
  when stale versions exist (`--global`); silent otherwise.
- humanizer: CATEGORY_TO_IMPACT lets a finding's category override the
  scanner-default impact label (raw `category` field unchanged -> --json/--raw
  byte-stable). humanizer-data: honest static translation for the finding.
- Tests: finding fires/severity/category, lists stale keys + zero-impact note,
  silent when none; humanizer override -> Dead config; honest translation locked.
- Docs: tokens command render note (disk-cleanup, not a token problem) +
  --no-exclude-cache flag; README + CLAUDE.md rows (7 patterns, cache-aware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 18:17:55 +02:00
commit ba9f82f952
8 changed files with 124 additions and 6 deletions

View file

@ -569,6 +569,11 @@ export const TRANSLATIONS = {
description: 'Skill descriptions load on every turn whether you use the skill or not. Long descriptions add up.',
recommendation: 'Trim the description to one short sentence and move details into the skill body.',
},
'Stale plugin-cache versions (disk cleanup, zero live-context impact)': {
title: 'Old plugin versions are sitting on disk (safe to delete)',
description: 'Your plugin cache holds older versions that newer installs have replaced. They take up disk space but are never loaded into a conversation — so they cost zero tokens per turn. This is housekeeping, not a performance problem.',
recommendation: 'Delete the old version folders to reclaim disk. The details list exactly which ones; the active version of each plugin stays untouched.',
},
},
patterns: [
{

View file

@ -45,6 +45,17 @@ const SCANNER_TO_CATEGORY = {
OPT: 'Missed opportunity',
};
/**
* Per-finding `category` values that override the scanner-default impact label.
* Needed when one finding inside a scanner means something different from the
* scanner's usual bucket e.g. stale plugin-cache versions are emitted by TOK
* (normally "Wasted tokens") but load on ZERO turns, so their honest impact is
* "Dead config" (present on disk, never loaded), not wasted per-turn tokens.
*/
const CATEGORY_TO_IMPACT = {
'plugin-cache-hygiene': 'Dead config',
};
/**
* Map severity to one-line action-language phrase (research/02 line 134).
*/
@ -125,7 +136,8 @@ export function humanizeFinding(finding) {
}
const translation = lookupTranslation(finding.scanner, finding.title);
const category = SCANNER_TO_CATEGORY[finding.scanner] || 'Other';
const category =
CATEGORY_TO_IMPACT[finding.category] || SCANNER_TO_CATEGORY[finding.scanner] || 'Other';
const action = SEVERITY_TO_ACTION[finding.severity] || 'FYI';
const relevance = computeRelevanceContext(finding.file);

View file

@ -544,6 +544,42 @@ export async function scan(targetPath, discovery) {
}));
}
// ── Pattern H: stale plugin-cache versions (v5.9 B3 — disk hygiene) ──
// file-discovery populates discovery.staleCacheVersions when walking
// ~/.claude/plugins/cache: version dirs on disk NOT referenced by
// installed_plugins.json (superseded installs). They occupy disk but load on
// ZERO turns — already excluded from the hotspot ranking + CNF by
// --exclude-cache. Honest framing (category 'plugin-cache-hygiene' →
// "Dead config", not "Wasted tokens"): a disk-cleanup opportunity, NOT a
// per-turn token cost.
const stale = Array.isArray(discovery.staleCacheVersions) ? discovery.staleCacheVersions : [];
if (stale.length > 0) {
const totalFiles = stale.reduce((s, v) => s + (v.fileCount || 0), 0);
const totalBytes = stale.reduce((s, v) => s + (v.estimatedBytes || 0), 0);
const keys = stale.map(v => v.key);
findings.push(finding({
scanner: SCANNER,
severity: SEVERITY.low,
title: 'Stale plugin-cache versions (disk cleanup, zero live-context impact)',
description:
`${stale.length} plugin version${stale.length === 1 ? '' : 's'} under ` +
`~/.claude/plugins/cache ${stale.length === 1 ? 'is' : 'are'} not referenced by ` +
'installed_plugins.json — superseded by newer installs. They occupy disk but load on ' +
'ZERO turns: excluded from the hotspot ranking and conflict detection by default ' +
'(--exclude-cache). This is disk hygiene, not a per-turn token cost.',
file: null,
evidence:
`stale_versions=${keys.join(', ')}; config_files=${totalFiles}; ` +
`approx_disk_bytes=${totalBytes}; note=zero live-context impact ` +
'(installed_plugins.json points at newer versions)',
recommendation:
'Delete the listed stale version directories under ~/.claude/plugins/cache to reclaim ' +
'disk (reinstall/prune via the plugin manager, or remove the dirs directly). Re-run ' +
'with --no-exclude-cache to include cached versions in the token/conflict scan.',
category: 'plugin-cache-hygiene',
}));
}
// ── Hotspots ranking ──
const hotspots = await buildHotspots(discovery, targetPath, activeConfig);