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:
parent
a371832688
commit
ba9f82f952
8 changed files with 124 additions and 6 deletions
|
|
@ -17,7 +17,7 @@ Analyzes and optimizes Claude Code configuration across three pillars:
|
|||
|---------|-------------|
|
||||
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
|
||||
| `/config-audit posture` | Quick health scorecard (A-F grades, 10 quality areas incl. Token Efficiency, Plugin Hygiene) |
|
||||
| `/config-audit tokens` | prompt-cache-aware token hotspots (6 patterns: cache-breaking, redundant perms, deep imports, oversized cascade, bloated SKILL.md desc, MCP tool-schema budget), each ranked hotspot tagged with its load pattern (always / on-demand / external) — optional `--accurate-tokens` API calibration, `--with-telemetry-recipe` cache-hit recipe pointer |
|
||||
| `/config-audit tokens` | prompt-cache-aware token hotspots (7 patterns: cache-breaking, redundant perms, deep imports, oversized cascade, bloated SKILL.md desc, MCP tool-schema budget, stale plugin-cache disk-cleanup), each ranked hotspot tagged with its load pattern (always / on-demand / external) — **cache-aware** (stale `~/.claude/plugins/cache` versions excluded by default; only each plugin's active version counts; `--no-exclude-cache` for the full walk), optional `--accurate-tokens` API calibration, `--with-telemetry-recipe` cache-hit recipe pointer |
|
||||
| `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens, each tagged with its load pattern (always-loaded / on-demand / external) + an always-loaded subtotal ("tokens that enter context every turn") |
|
||||
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact (incl. a conditional `disableBundledSkills` lever when the active skill listing is over budget — remediation companion to SKL `CA-SKL-002`) |
|
||||
| `/config-audit optimize` | Optimization lens (mechanism-fit) — config that works but fits a better mechanism: procedure→skill (CA-OPT-001, deterministic), lifecycle→hook / unscoped path→rule / "never"→permission (prose-judgment via opus `optimization-lens-agent`). Hybrid motor; every finding cites a best-practices-register rule. Agent-driven, **not byte-stable** |
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ Your team configuration changes over time. Track it:
|
|||
|---------|-------------|
|
||||
| `/config-audit` | Full audit with auto-scope detection (no setup needed) |
|
||||
| `/config-audit posture` | Quick health scorecard: A-F grades across 10 quality areas (incl. Token Efficiency, Plugin Hygiene) |
|
||||
| `/config-audit tokens` | prompt-cache-aware token hotspots — ranked by estimated waste, each tagged with its load pattern (always / on-demand / external); 6 patterns + optional `--accurate-tokens` API calibration |
|
||||
| `/config-audit tokens` | prompt-cache-aware token hotspots — ranked by estimated waste, each tagged with its load pattern (always / on-demand / external); 7 patterns + optional `--accurate-tokens` API calibration. **Cache-aware:** stale `~/.claude/plugins/cache` versions (superseded installs that load on zero turns) are excluded from the ranking by default — only each plugin's active version is counted; `--no-exclude-cache` restores the full walk. Stale versions surface as a separate **Dead config** disk-cleanup finding |
|
||||
| `/config-audit manifest` | Ranked table of every token source (CLAUDE.md, rules, agents, skills, output styles, MCP, hooks) sorted by estimated tokens — each tagged with its **load pattern** (always-loaded / on-demand / external) plus an **always-loaded subtotal** ("≈X tokens enter context every turn before you type"). Component-level: no coarse plugin roll-up (it would double-count) |
|
||||
| `/config-audit feature-gap` | Context-aware feature recommendations grouped by impact |
|
||||
| `/config-audit optimize` | Optimization lens (mechanism-fit): config that works but fits a better mechanism — procedure→skill, lifecycle→hook, unscoped path→rule, "never"→permission. Hybrid motor (deterministic pre-filter + opus precision gate), every finding cites a best-practices-register rule |
|
||||
|
|
@ -415,7 +415,7 @@ All tools work standalone — no Claude Code session needed:
|
|||
| **Posture** | `node scanners/posture.mjs <path> [--json] [--global] [--full-machine] [--output-file path]` |
|
||||
| **Fix** | `node scanners/fix-cli.mjs <path> [--apply] [--json] [--global]` |
|
||||
| **Drift** | `node scanners/drift-cli.mjs <path> [--save] [--baseline name] [--json]` |
|
||||
| **Tokens** | `node scanners/token-hotspots-cli.mjs <path> [--json] [--global] [--output-file path] [--accurate-tokens] [--with-telemetry-recipe]` |
|
||||
| **Tokens** | `node scanners/token-hotspots-cli.mjs <path> [--json] [--global] [--no-exclude-cache] [--output-file path] [--accurate-tokens] [--with-telemetry-recipe]` |
|
||||
| **Manifest** | `node scanners/manifest.mjs <path> [--json]` — ranked component-level source table with per-source load pattern + always-loaded subtotal |
|
||||
| **What's active** | `node scanners/whats-active.mjs <path> [--json] [--verbose] [--suggest-disables]` |
|
||||
| **Self-audit** | `node scanners/self-audit.mjs [--json] [--fix] [--check-readme]` |
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ Complementary to `/config-audit whats-active`:
|
|||
Split `$ARGUMENTS` into a path and flags. Path is the first non-flag argument. Default to `.` (current working directory). Recognized flags:
|
||||
|
||||
- `--global` — also include the user-level `~/.claude/` cascade
|
||||
- `--no-exclude-cache` — include stale `~/.claude/plugins/cache` versions in the ranking. **By default they are excluded** (cache-aware filtering, default ON): the cache holds superseded plugin versions that load on *zero* turns, and counting them used to crowd the top-10 with dead config. The active version of each plugin (per `installed_plugins.json`) is always kept — only stale versions are filtered. Use `--no-exclude-cache` to see the full on-disk walk.
|
||||
- `--json` — emit raw JSON instead of rendered tables (power-user mode; bypasses the humanizer for byte-stable v5.0.0 output)
|
||||
- `--raw` — pass-through to the scanner; produces v5.0.0 verbatim JSON (bypasses the humanizer). Use when piping into v5.0.0-baseline diff tooling.
|
||||
- `--with-telemetry-recipe` — include `telemetry_recipe_path` in the JSON output, pointing to `knowledge/cache-telemetry-recipe.md`. Use this when you want to verify a structural fix actually improved cache hit rate (manual jq recipe, opt-in)
|
||||
|
|
@ -42,7 +43,7 @@ Default mode (no `--json`, no `--raw`) emits a humanized JSON envelope: each fin
|
|||
TMPFILE="/tmp/config-audit-tokens-$$.json"
|
||||
RAW_FLAG=""
|
||||
if echo "$ARGUMENTS" | grep -q -- "--raw"; then RAW_FLAG="--raw"; fi
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/token-hotspots-cli.mjs <path> --output-file "$TMPFILE" [--global] $RAW_FLAG 2>/dev/null; echo $?
|
||||
node ${CLAUDE_PLUGIN_ROOT}/scanners/token-hotspots-cli.mjs <path> --output-file "$TMPFILE" [--global] [--no-exclude-cache] $RAW_FLAG 2>/dev/null; echo $?
|
||||
```
|
||||
|
||||
**Exit code handling:**
|
||||
|
|
@ -63,11 +64,13 @@ Use the Read tool on `$TMPFILE`. Extract:
|
|||
|
||||
- `total_estimated_tokens` — top-line number
|
||||
- `hotspots[]` — top 10 ranked sources; each carries a **load pattern** (`loadPattern` ∈ always / on-demand / external, plus `survivesCompaction` / `derivationConfidence`)
|
||||
- `findings[]` — prompt-cache pattern findings (CA-TOK-001..003); each finding in default mode carries humanizer fields (`userImpactCategory`, `userActionLanguage`, `relevanceContext`) alongside the v5.0.0 fields
|
||||
- `findings[]` — prompt-cache pattern findings; each finding in default mode carries humanizer fields (`userImpactCategory`, `userActionLanguage`, `relevanceContext`) alongside the v5.0.0 fields
|
||||
- `counts` — severity breakdown
|
||||
|
||||
A hotspot's **load pattern** matters as much as its size: an **always**-loaded source (CLAUDE.md, MCP tool schemas) is paid on *every* turn, an **on-demand** one (skill body, path-scoped rule) only when invoked/matched, and an **external** one (hooks, harness-config files like settings.json/.mcp.json) costs no per-turn context tokens at all. A big always-loaded hotspot is the most worth trimming.
|
||||
|
||||
**The stale plugin-cache finding is different from the rest.** All other TOK findings are about per-turn token cost. The *"Old plugin versions are sitting on disk"* finding (category `plugin-cache-hygiene`, impact **Dead config**, `--global` only) is a pure **disk-cleanup** item with **zero live-context impact** — the listed versions are never loaded. Render it as housekeeping, not a token problem: don't conflate its disk bytes with the per-turn token numbers above it.
|
||||
|
||||
Render as markdown. Group findings by `userImpactCategory` (e.g., "Wasted tokens" vs "Configuration mistake") rather than re-deriving severity prose; lead each line with `userActionLanguage` ("Fix this now", "Fix soon", "Optional cleanup", etc.) so the urgency phrasing stays consistent with the rest of the toolchain. The humanizer already replaced jargon-heavy `title`/`description`/`recommendation` strings with plain-language equivalents — render them verbatim.
|
||||
|
||||
```markdown
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,19 @@ test('humanizeFinding rewrites title for known static title', () => {
|
|||
`humanized title should mention instructions or claude, got: ${out.title}`);
|
||||
});
|
||||
|
||||
test('humanizeFinding: stale plugin-cache title humanizes to honest disk-cleanup prose, not the generic TOK default (B3)', () => {
|
||||
const input = makeFinding({
|
||||
scanner: 'TOK',
|
||||
title: 'Stale plugin-cache versions (disk cleanup, zero live-context impact)',
|
||||
category: 'plugin-cache-hygiene',
|
||||
});
|
||||
const out = humanizeFinding(input);
|
||||
assert.match(out.title, /disk/i, 'humanized title must name disk, not generic "space"');
|
||||
assert.match(out.description, /zero tokens per turn|never loaded/i,
|
||||
'humanized description must keep the zero-context-impact honesty');
|
||||
assert.equal(out.userImpactCategory, 'Dead config');
|
||||
});
|
||||
|
||||
test('humanizeFinding falls back to _default when title unknown', () => {
|
||||
const input = makeFinding({ scanner: 'CML', title: 'Unrecognized brand-new finding title' });
|
||||
const out = humanizeFinding(input);
|
||||
|
|
@ -183,6 +196,14 @@ test('humanizeFinding sets category Dead config for DIS', () => {
|
|||
assert.equal(out.userImpactCategory, 'Dead config');
|
||||
});
|
||||
|
||||
test('humanizeFinding: per-finding category plugin-cache-hygiene overrides scanner default to Dead config (B3)', () => {
|
||||
// TOK normally maps to "Wasted tokens", but a stale-cache finding loads on
|
||||
// zero turns — its honest impact is "Dead config", not wasted per-turn tokens.
|
||||
const out = humanizeFinding(makeFinding({ scanner: 'TOK', category: 'plugin-cache-hygiene' }));
|
||||
assert.equal(out.userImpactCategory, 'Dead config');
|
||||
assert.equal(out.category, 'plugin-cache-hygiene', 'raw category field preserved unchanged');
|
||||
});
|
||||
|
||||
test('humanizeFinding sets category Missed opportunity for GAP', () => {
|
||||
const out = humanizeFinding(makeFinding({ scanner: 'GAP', title: 'No CLAUDE.md file' }));
|
||||
assert.equal(out.userImpactCategory, 'Missed opportunity');
|
||||
|
|
|
|||
|
|
@ -354,3 +354,44 @@ describe('TOK scanner — F7 severity recalibration (v5)', () => {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── Pattern H: stale plugin-cache versions (v5.9 B3) ──────────────────────
|
||||
describe('TOK scanner — H stale plugin-cache versions (v5.9 B3)', () => {
|
||||
const staleDiscovery = {
|
||||
files: [],
|
||||
staleCacheVersions: [
|
||||
{ key: 'mkt/voyage/5.1.1', fileCount: 35, estimatedBytes: 416237 },
|
||||
{ key: 'mkt/config-audit/5.6.0', fileCount: 149, estimatedBytes: 301330 },
|
||||
],
|
||||
};
|
||||
|
||||
it('emits a low-severity stale-cache finding when stale versions exist', async () => {
|
||||
resetCounter();
|
||||
const result = await withHermeticHome(() => scan('/tmp/x', staleDiscovery));
|
||||
const f = result.findings.find(x => /stale plugin-cache/i.test(x.title || ''));
|
||||
assert.ok(f, 'expected a stale plugin-cache finding');
|
||||
assert.equal(f.severity, 'low');
|
||||
assert.equal(f.category, 'plugin-cache-hygiene');
|
||||
});
|
||||
|
||||
it('states zero live-context impact and lists the stale keys', async () => {
|
||||
resetCounter();
|
||||
const result = await withHermeticHome(() => scan('/tmp/x', staleDiscovery));
|
||||
const f = result.findings.find(x => /stale plugin-cache/i.test(x.title || ''));
|
||||
assert.match(f.evidence, /zero live-context impact/i);
|
||||
assert.match(f.evidence, /mkt\/voyage\/5\.1\.1/);
|
||||
assert.match(f.evidence, /mkt\/config-audit\/5\.6\.0/);
|
||||
});
|
||||
|
||||
it('does NOT fire when there are no stale versions', async () => {
|
||||
resetCounter();
|
||||
const result = await withHermeticHome(() => scan('/tmp/x', { files: [], staleCacheVersions: [] }));
|
||||
assert.ok(!result.findings.some(x => /stale plugin-cache/i.test(x.title || '')));
|
||||
});
|
||||
|
||||
it('does NOT fire when discovery omits staleCacheVersions entirely', async () => {
|
||||
resetCounter();
|
||||
const result = await withHermeticHome(() => scan('/tmp/x', { files: [] }));
|
||||
assert.ok(!result.findings.some(x => /stale plugin-cache/i.test(x.title || '')));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue