diff --git a/.gitignore b/.gitignore index bc8b930..110cddd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ Thumbs.db # Planning artifacts (internal, not shipped) docs/superpowers/ +.claude/ # Config / secrets .env diff --git a/src/optimize.ts b/src/optimize.ts index 433cb99..2e8913c 100644 --- a/src/optimize.ts +++ b/src/optimize.ts @@ -33,7 +33,6 @@ const TOKENS_PER_SKILL_DEF = 80 const TOKENS_PER_COMMAND_DEF = 60 const CLAUDEMD_TOKENS_PER_LINE = 13 const BASH_TOKENS_PER_CHAR = 0.25 -const ESTIMATED_READS_PER_MISSING_IGNORE = 10 // ============================================================================ // Detector thresholds @@ -53,7 +52,6 @@ const LOW_RATIO_HIGH_THRESHOLD = 2 const LOW_RATIO_MEDIUM_THRESHOLD = 3 const MIN_API_CALLS_FOR_CACHE = 10 const CACHE_EXCESS_HIGH_THRESHOLD = 15000 -const MISSING_IGNORE_HIGH_THRESHOLD = 3 const UNUSED_MCP_HIGH_THRESHOLD = 3 const GHOST_AGENTS_HIGH_THRESHOLD = 5 const GHOST_AGENTS_MEDIUM_THRESHOLD = 2 @@ -98,8 +96,6 @@ const JUNK_PATTERN = new RegExp(`/(?:${JUNK_DIRS.join('|')})/`) const SHELL_PROFILES = ['.zshrc', '.bashrc', '.bash_profile', '.profile'] const TOP_ITEMS_PREVIEW = 3 -const MISSING_IGNORE_PATHS_PREVIEW = 2 -const JUNK_DIRS_IGNORE_PREVIEW = 8 const GHOST_NAMES_PREVIEW = 5 const GHOST_CLEANUP_COMMANDS_LIMIT = 10 @@ -409,18 +405,17 @@ export function detectJunkReads(calls: ToolCall[], dateRange?: DateRange): Waste const detected = sorted.map(([d]) => d) const commonDefaults = ['node_modules', '.git', 'dist', '__pycache__'] const extras = commonDefaults.filter(d => !dirCounts.has(d)).slice(0, Math.max(0, 6 - detected.length)) - const ignoreContent = [...detected, ...extras].join('\n') + const dirsToAvoid = [...detected, ...extras].join(', ') return { title: 'Claude is reading build/dependency folders', - explanation: `Claude read into ${dirList} (${totalJunkReads} reads). These are generated or dependency directories, not your code. A .claudeignore tells Claude to skip them.`, + explanation: `Claude read into ${dirList} (${totalJunkReads} reads). These are generated or dependency directories, not your code. Tell Claude in CLAUDE.md to avoid them.`, impact: totalJunkReads > JUNK_READS_HIGH_THRESHOLD ? 'high' : totalJunkReads > JUNK_READS_MEDIUM_THRESHOLD ? 'medium' : 'low', tokensSaved, fix: { - type: 'file-content', - label: 'Create .claudeignore in your project root:', - path: '.claudeignore', - content: ignoreContent, + type: 'paste', + label: 'Append to your project CLAUDE.md:', + text: `Do not read or search files under these directories unless I explicitly ask: ${dirsToAvoid}.`, }, trend, } @@ -532,43 +527,6 @@ export function detectUnusedMcp( } } -export function detectMissingClaudeignore(projectCwds: Set): WasteFinding | null { - const missing: string[] = [] - - for (const cwd of projectCwds) { - if (!existsSync(cwd)) continue - if (existsSync(join(cwd, '.claudeignore'))) continue - for (const dir of JUNK_DIRS) { - if (existsSync(join(cwd, dir))) { - missing.push(cwd) - break - } - } - } - - if (missing.length === 0) return null - - const shortPaths = missing.map(shortHomePath) - const display = shortPaths.length <= MISSING_IGNORE_PATHS_PREVIEW + 1 - ? shortPaths.join(', ') - : `${shortPaths.slice(0, MISSING_IGNORE_PATHS_PREVIEW).join(', ')} + ${shortPaths.length - MISSING_IGNORE_PATHS_PREVIEW} more` - - const tokensSaved = missing.length * ESTIMATED_READS_PER_MISSING_IGNORE * AVG_TOKENS_PER_READ - - return { - title: `Add .claudeignore to ${missing.length} project${missing.length > 1 ? 's' : ''}`, - explanation: `${missing.length} project${missing.length > 1 ? 's have' : ' has'} build/dependency folders (node_modules, .git, etc.) but no .claudeignore: ${display}. Without it, Claude can wander into them.`, - impact: missing.length >= MISSING_IGNORE_HIGH_THRESHOLD ? 'high' : 'medium', - tokensSaved, - fix: { - type: 'file-content', - label: 'Create .claudeignore in each project root:', - path: '.claudeignore', - content: JUNK_DIRS.slice(0, JUNK_DIRS_IGNORE_PREVIEW).join('\n'), - }, - } -} - function expandImports(filePath: string, seen: Set, depth: number): { totalLines: number; importedFiles: number } { if (depth > MAX_IMPORT_DEPTH || seen.has(filePath)) return { totalLines: 0, importedFiles: 0 } seen.add(filePath) @@ -1018,7 +976,6 @@ export async function scanAndDetect( () => detectJunkReads(toolCalls, dateRange), () => detectDuplicateReads(toolCalls, dateRange), () => detectUnusedMcp(toolCalls, projects, projectCwds), - () => detectMissingClaudeignore(projectCwds), () => detectBloatedClaudeMd(projectCwds), () => detectBashBloat(), ] diff --git a/tests/optimize-fs.test.ts b/tests/optimize-fs.test.ts index a476042..e43f66b 100644 --- a/tests/optimize-fs.test.ts +++ b/tests/optimize-fs.test.ts @@ -15,7 +15,6 @@ vi.mock('os', async () => { const FAKE_HOME_FOR_MOCK = process.env['CODEBURN_TEST_FAKE_HOME']! import { - detectMissingClaudeignore, detectBloatedClaudeMd, detectUnusedMcp, detectBashBloat, @@ -60,48 +59,6 @@ afterAll(() => { } }) -// ============================================================================ -// detectMissingClaudeignore -// ============================================================================ - -describe('detectMissingClaudeignore', () => { - it('flags a project with node_modules but no .claudeignore', () => { - const root = makeFixtureRoot() - const projectDir = join(root, 'myapp') - mkdirSync(join(projectDir, 'node_modules'), { recursive: true }) - const finding = detectMissingClaudeignore(new Set([projectDir])) - expect(finding).not.toBeNull() - expect(finding!.impact).toBe('medium') - }) - - it('does not flag when .claudeignore exists', () => { - const root = makeFixtureRoot() - const projectDir = join(root, 'myapp') - mkdirSync(join(projectDir, 'node_modules'), { recursive: true }) - writeFile(join(projectDir, '.claudeignore'), 'node_modules\n') - expect(detectMissingClaudeignore(new Set([projectDir]))).toBeNull() - }) - - it('does not flag project without junk dirs', () => { - const root = makeFixtureRoot() - const projectDir = join(root, 'myapp') - mkdirSync(join(projectDir, 'src'), { recursive: true }) - expect(detectMissingClaudeignore(new Set([projectDir]))).toBeNull() - }) - - it('escalates to high when three or more projects need it', () => { - const root = makeFixtureRoot() - const cwds = new Set() - for (let i = 0; i < 3; i++) { - const p = join(root, `proj-${i}`) - mkdirSync(join(p, 'node_modules'), { recursive: true }) - cwds.add(p) - } - const finding = detectMissingClaudeignore(cwds) - expect(finding!.impact).toBe('high') - }) -}) - // ============================================================================ // detectBloatedClaudeMd (including @-import expansion) // ============================================================================ diff --git a/tests/optimize.test.ts b/tests/optimize.test.ts index 5ebcab8..698a18f 100644 --- a/tests/optimize.test.ts +++ b/tests/optimize.test.ts @@ -6,7 +6,6 @@ import { detectLowReadEditRatio, detectCacheBloat, detectBloatedClaudeMd, - detectMissingClaudeignore, computeHealth, computeTrend, type ToolCall, @@ -77,13 +76,14 @@ describe('detectJunkReads', () => { expect(detectJunkReads(calls)).toBeNull() }) - it('builds .claudeignore content from detected + common extras', () => { + it('suggests CLAUDE.md advice listing detected and common junk dirs', () => { const calls = Array.from({ length: 5 }, () => call('Read', { file_path: '/x/node_modules/a.js' })) const finding = detectJunkReads(calls)! - expect(finding.fix.type).toBe('file-content') - if (finding.fix.type === 'file-content') { - expect(finding.fix.content).toContain('node_modules') + expect(finding.fix.type).toBe('paste') + if (finding.fix.type === 'paste') { + expect(finding.fix.text).toContain('node_modules') } + expect(finding.fix.label).toContain('CLAUDE.md') }) }) @@ -207,16 +207,6 @@ describe('detectBloatedClaudeMd', () => { }) }) -describe('detectMissingClaudeignore', () => { - it('returns null for empty set', () => { - expect(detectMissingClaudeignore(new Set())).toBeNull() - }) - - it('returns null for non-existent cwds', () => { - expect(detectMissingClaudeignore(new Set(['/does/not/exist']))).toBeNull() - }) -}) - describe('computeHealth', () => { it('returns A with 100 for no findings', () => { const { score, grade } = computeHealth([])