feat(feature-gap): recommend disableBundledSkills under skill-listing pressure
Chunk 2 of the disableBundledSkills GAP feature. Adds a conditional GAP check
that prescribes the `disableBundledSkills` lever — but only when the active
skill listing is measurably over budget (SKL's CA-SKL-002 overflow signal) and
the lever is un-pulled. It stays an opportunity, not noise.
Bundled skills (/code-review, /batch, /debug, /loop, /claude-api, …) live in the
CC binary, not on disk, so their exact cost is unmeasurable here — the finding
says so plainly, and frames the lever as zero-cost budget reclaim that leaves
the user's own skills untouched. CC 2.1.169+.
- Pure, exported bundledSkillsLeverFinding({leverPulled, aggregate}) → finding|null
(severity low, category token-efficiency, CA-GAP-NNN), wired into scan() via the
shared measureActiveSkillListing().
- Lever resolved via new isBundledSkillsDisabled(): env var + settings cascade
read directly, because discovery does NOT tag ~/.claude/settings.json (its
relPath lacks ".claude" when walked from the .claude root) — the dominant
user-scope location for this global preference would otherwise be missed.
- GAP scan() now reads HOME → existing GAP tests retrofitted to withHermeticHome
per the hermetic rule. Snapshots unchanged, contamination grep clean.
- 16 new tests (9 GAP, 7 lib). Suite 887 -> 903. README/CLAUDE.md document the
cross-scanner remediation; test counts synced. self-audit: PASS, configGrade
A 96, pluginGrade A 100, readme gate passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
0a631e3061
commit
dfe9049b55
6 changed files with 338 additions and 12 deletions
|
|
@ -1,7 +1,12 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'node:path';
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import {
|
||||
assessSkillListingBudget,
|
||||
envFlag,
|
||||
isBundledSkillsDisabled,
|
||||
DESCRIPTION_CAP,
|
||||
AGGREGATE_BUDGET_TOKENS,
|
||||
CONTEXT_WINDOW_ANCHOR,
|
||||
|
|
@ -98,3 +103,79 @@ describe('assessSkillListingBudget — aggregate math', () => {
|
|||
assert.equal(r.scanned, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('envFlag', () => {
|
||||
it('treats 1/true/yes/on as set', () => {
|
||||
for (const v of ['1', 'true', 'TRUE', 'yes', 'on', ' 1 ']) {
|
||||
assert.equal(envFlag(v), true, `expected ${JSON.stringify(v)} → true`);
|
||||
}
|
||||
});
|
||||
|
||||
it('treats null/empty/0/false/no/off as un-set', () => {
|
||||
for (const v of [undefined, null, '', '0', 'false', 'no', 'off', ' ']) {
|
||||
assert.equal(envFlag(v), false, `expected ${JSON.stringify(v)} → false`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBundledSkillsDisabled — lever cascade', () => {
|
||||
async function withHome(fn) {
|
||||
const home = await mkdtemp(join(tmpdir(), 'config-audit-lever-home-'));
|
||||
const originalHome = process.env.HOME;
|
||||
const originalEnv = process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
|
||||
process.env.HOME = home;
|
||||
delete process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
|
||||
try {
|
||||
return await fn(home);
|
||||
} finally {
|
||||
process.env.HOME = originalHome;
|
||||
if (originalEnv === undefined) delete process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS;
|
||||
else process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS = originalEnv;
|
||||
await rm(home, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSettings(dir, obj) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(join(dir, 'settings.json'), JSON.stringify(obj));
|
||||
}
|
||||
|
||||
it('is false on a clean HOME with no env and no settings', async () => {
|
||||
await withHome(async () => {
|
||||
assert.equal(await isBundledSkillsDisabled(), false);
|
||||
});
|
||||
});
|
||||
|
||||
it('is true when the env var is set', async () => {
|
||||
await withHome(async () => {
|
||||
process.env.CLAUDE_CODE_DISABLE_BUNDLED_SKILLS = '1';
|
||||
assert.equal(await isBundledSkillsDisabled(), true);
|
||||
});
|
||||
});
|
||||
|
||||
it('is true when user ~/.claude/settings.json sets it (the location discovery misses)', async () => {
|
||||
await withHome(async (home) => {
|
||||
await writeSettings(join(home, '.claude'), { disableBundledSkills: true });
|
||||
assert.equal(await isBundledSkillsDisabled(), true);
|
||||
});
|
||||
});
|
||||
|
||||
it('is true when project .claude/settings.json sets it', async () => {
|
||||
await withHome(async () => {
|
||||
const project = await mkdtemp(join(tmpdir(), 'config-audit-lever-proj-'));
|
||||
try {
|
||||
await writeSettings(join(project, '.claude'), { disableBundledSkills: true });
|
||||
assert.equal(await isBundledSkillsDisabled(project), true);
|
||||
} finally {
|
||||
await rm(project, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('is false when the setting is present but not strictly true', async () => {
|
||||
await withHome(async (home) => {
|
||||
await writeSettings(join(home, '.claude'), { disableBundledSkills: false });
|
||||
assert.equal(await isBundledSkillsDisabled(), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue