From 827011b865021d4324e376ea0ee6b13feb475778 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 12 Jun 2026 16:03:51 -0400 Subject: [PATCH] fix(#1098): guard generate-claude-md against clobbering hand-crafted files; redirect default to .claude/CLAUDE.md (#1118) /gsd-new-project wrote a repo-root CLAUDE.md full of broad project docs, overwriting/diluting a hand-crafted instruction file. --force was parsed but silently dropped, and nothing guarded an existing non-GSD file. - Guard: an existing instruction file with no `')); assert.ok(content.includes('No project skills found. Add skills to any of')); @@ -138,7 +192,7 @@ describe('generate-claude-md skills section', () => { assert.ok(output.sections_generated.includes('skills')); assert.ok(!output.sections_fallback.includes('skills')); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('api-payments')); assert.ok(content.includes('Payment gateway integration')); assert.ok(content.includes('## Project Skills')); @@ -155,7 +209,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('data-sync')); assert.ok(content.includes('ERP synchronization flows')); }); @@ -182,7 +236,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('automation')); assert.ok(content.includes('Project Codex skill')); assert.ok(!content.includes('import-only')); @@ -209,7 +263,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(!content.includes('gsd-plan-phase')); assert.ok(content.includes('my-feature')); assert.ok(content.includes('Custom project skill')); @@ -226,7 +280,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('First line of description')); assert.ok(content.includes('Continued on second line')); assert.ok(content.includes('And a third line')); @@ -245,7 +299,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); const matches = content.match(/shared-skill/g); // Should appear exactly twice: once in name column, once in path column (single row) assert.strictEqual(matches.length, 2); @@ -254,7 +308,7 @@ describe('generate-claude-md skills section', () => { test('updates existing skills section on regeneration', () => { // First generation — no skills runGsdTools('generate-claude-md', tmpDir); - let content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + let content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('No project skills found')); // Add a skill and regenerate @@ -268,7 +322,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(!content.includes('No project skills found')); assert.ok(content.includes('new-skill')); assert.ok(content.includes('Just added')); @@ -285,7 +339,7 @@ describe('generate-claude-md skills section', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); const archIdx = content.indexOf('## Architecture'); const skillsIdx = content.indexOf('## Project Skills'); const workflowIdx = content.indexOf('## GSD Workflow Enforcement'); diff --git a/tests/profile-output.test.cjs b/tests/profile-output.test.cjs index f5f837943..4d592655f 100644 --- a/tests/profile-output.test.cjs +++ b/tests/profile-output.test.cjs @@ -219,21 +219,39 @@ describe('generate-claude-md command', () => { } }); - test('does not overwrite existing CLAUDE.md without --force', () => { + test('does not overwrite existing marker-less CLAUDE.md without --force (#1098)', () => { + const outputPath = path.join(tmpDir, 'CLAUDE.md'); + const original = '# Custom CLAUDE.md\n\nUser content.\n'; + fs.writeFileSync(outputPath, original); + + // No GSD markers in the file → the #1098 guard must leave it untouched. + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto'], tmpDir); + assert.ok(result.success, `command should exit 0 even when skipping: ${result.error}`); + assert.strictEqual(JSON.parse(result.output).action, 'skipped'); + + const content = fs.readFileSync(outputPath, 'utf-8'); + assert.strictEqual(content, original, 'hand-crafted file must be byte-identical (not overwritten)'); + }); + + test('overwrites existing marker-less CLAUDE.md with --force (#1098)', () => { const outputPath = path.join(tmpDir, 'CLAUDE.md'); fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n'); - runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir); - // Should merge, not overwrite + const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--force'], tmpDir); + assert.ok(result.success, `Failed: ${result.error}`); + assert.strictEqual(JSON.parse(result.output).action, 'updated'); + const content = fs.readFileSync(outputPath, 'utf-8'); - assert.ok(content.length > 0, 'should still have content'); + assert.ok(content.includes('User content.'), '--force preserves existing content while adding sections'); + assert.ok(content.includes('## GSD Workflow Enforcement'), '--force injects GSD sections'); }); test('skills fallback mentions the normalized project roots', () => { const result = runGsdTools('generate-claude-md', tmpDir); assert.ok(result.success, `Failed: ${result.error}`); - const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8'); + // #1098: default Claude output is now .claude/CLAUDE.md + const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8'); assert.ok(content.includes('.claude/skills/')); assert.ok(content.includes('.agents/skills/')); assert.ok(content.includes('.cursor/skills/')); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 38cec2690..c5e229977 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -44,20 +44,20 @@ "milestone-summary.md": 11774, "mvp-phase.md": 13582, "new-milestone.md": 32422, - "new-project.md": 61690, + "new-project.md": 61802, "new-workspace.md": 11254, "next.md": 17868, "node-repair.md": 4173, "note.md": 6563, "pause-work.md": 13654, "plan-milestone-gaps.md": 11765, - "plan-phase.md": 94253, + "plan-phase.md": 94343, "plan-review-convergence.md": 22949, "plant-seed.md": 11741, "pr-branch.md": 4994, - "profile-user.md": 20457, + "profile-user.md": 20650, "progress.md": 29387, - "quick.md": 46213, + "quick.md": 46282, "reapply-patches.md": 20393, "remove-phase.md": 8469, "remove-workspace.md": 7507,