diff --git a/.changeset/1098-generate-claude-md-guard-redirect.md b/.changeset/1098-generate-claude-md-guard-redirect.md new file mode 100644 index 000000000..8b1788896 --- /dev/null +++ b/.changeset/1098-generate-claude-md-guard-redirect.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 1118 +--- +**`gsd-tools generate-claude-md` no longer clobbers a hand-crafted `CLAUDE.md`, and defaults the Claude-runtime output to `./.claude/CLAUDE.md`** — `/gsd-new-project` wrote a repo-root `CLAUDE.md` full of broad project documentation, overwriting/diluting an existing hand-authored instruction file. Now: (1) an existing instruction file that contains no GSD section markers (a hand-crafted file) is left untouched and the command reports `action: "skipped"` — pass `--force` to overwrite intentionally (the flag was already parsed but ignored); (2) the default output for Claude-family runtimes is `./.claude/CLAUDE.md` (a valid project-scoped memory location) instead of repo-root `./CLAUDE.md`, so generated content does not pollute a repo-root file. The config default (`claude_md_path`), the project config template, and the new-project workflow are aligned to the new location. Codex projects still write `AGENTS.md`. (#1098) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c45a34450..b068fe717 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -129,7 +129,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new "intel": { "enabled": false }, - "claude_md_path": "./CLAUDE.md" + "claude_md_path": "./.claude/CLAUDE.md" } ``` @@ -161,7 +161,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new | `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32 | | `context_window` | number | any integer | `200000` | Context window size in tokens. Set `1000000` for 1M-context models (e.g., `claude-fable-5`). Values `>= 500000` enable adaptive context enrichment (full-body reads of prior SUMMARY.md, deeper anti-pattern reads). Configured via `/gsd-config --advanced`. | | `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 | -| `claude_md_path` | string | any file path | `./CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./CLAUDE.md` at the project root. Added in v1.36 | +| `claude_md_path` | string | any file path | `./.claude/CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./.claude/CLAUDE.md` — a valid project-scoped memory location that keeps GSD-generated content from polluting a hand-crafted repo-root `CLAUDE.md` ([#1098](https://github.com/open-gsd/gsd-core/issues/1098)). An existing file without GSD markers is never overwritten unless `--force` is passed. Default changed from `./CLAUDE.md` in v1.5. Added in v1.36 | | `claude_md_assembly.mode` | enum | `embed`, `link` | `embed` | Controls how managed sections are written into CLAUDE.md. `embed` (default) inlines content between GSD markers. `link` writes `@.planning/` instead — Claude Code expands the reference at runtime, reducing CLAUDE.md size by ~65% on typical projects. `link` only applies to sections that have a real source file; `workflow` and fallback sections always embed. Per-block overrides: `claude_md_assembly.blocks.
` (e.g. `claude_md_assembly.blocks.architecture: link`). Added in v1.38 | | `context` | string | any text | (none) | Custom context string injected into every agent prompt for the project. Use to provide persistent project-specific guidance (e.g., coding conventions, team practices) that every agent should be aware of | | `phase_naming` | string | any string | (none) | Custom prefix for phase directory names. When set, overrides the auto-generated phase slug (e.g., `"feature"` produces `feature-01-setup/` instead of the roadmap-derived slug) | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 5e8120e99..a66f02627 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -2507,9 +2507,10 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style **Purpose:** Allow projects to store their CLAUDE.md in a non-root location. The `claude_md_path` config key controls where `/gsd-profile-user` and related commands write the generated CLAUDE.md file. **Requirements:** -- REQ-CMDPATH-01: `claude_md_path` defaults to `./CLAUDE.md` +- REQ-CMDPATH-01: `claude_md_path` defaults to `./.claude/CLAUDE.md` (a valid project-scoped memory location; changed from `./CLAUDE.md` in v1.5 per [#1098](https://github.com/open-gsd/gsd-core/issues/1098) so generated content does not pollute a hand-crafted repo-root `CLAUDE.md`) - REQ-CMDPATH-02: Profile generation commands read the path from config and write to the specified location - REQ-CMDPATH-03: Relative paths are resolved from the project root +- REQ-CMDPATH-04: `generate-claude-md` never overwrites an existing instruction file that lacks GSD section markers (a hand-crafted file) unless `--force` is passed **Configuration:** `claude_md_path` diff --git a/gsd-core/bin/shared/config-defaults.manifest.json b/gsd-core/bin/shared/config-defaults.manifest.json index b843bae57..b1c53eb5f 100644 --- a/gsd-core/bin/shared/config-defaults.manifest.json +++ b/gsd-core/bin/shared/config-defaults.manifest.json @@ -13,7 +13,7 @@ "project_code": null, "phase_id_convention": null, "mode": "interactive", - "claude_md_path": "./CLAUDE.md", + "claude_md_path": "./.claude/CLAUDE.md", "git": { "branching_strategy": "none", "create_tag": true, diff --git a/gsd-core/templates/config.json b/gsd-core/templates/config.json index 4f4e0091b..efe4c0aa6 100644 --- a/gsd-core/templates/config.json +++ b/gsd-core/templates/config.json @@ -58,5 +58,5 @@ }, "project_code": null, "agent_skills": {}, - "claude_md_path": "./CLAUDE.md" + "claude_md_path": "./.claude/CLAUDE.md" } diff --git a/gsd-core/workflows/new-project.md b/gsd-core/workflows/new-project.md index 357f6853d..aa7ce6781 100644 --- a/gsd-core/workflows/new-project.md +++ b/gsd-core/workflows/new-project.md @@ -111,7 +111,7 @@ else RUNTIME="claude"; fi Set the instruction file variable: ```bash -if [ "$RUNTIME" = "codex" ]; then INSTRUCTION_FILE="AGENTS.md"; else INSTRUCTION_FILE="CLAUDE.md"; fi +if [ "$RUNTIME" = "codex" ]; then INSTRUCTION_FILE="AGENTS.md"; else INSTRUCTION_FILE=".claude/CLAUDE.md"; fi ``` All subsequent references to the project instruction file use `$INSTRUCTION_FILE`. @@ -1533,7 +1533,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" - `.planning/REQUIREMENTS.md` - `.planning/ROADMAP.md` - `.planning/STATE.md` -- `$INSTRUCTION_FILE` (`AGENTS.md` for Codex, `CLAUDE.md` for all other runtimes) +- `$INSTRUCTION_FILE` (`AGENTS.md` for Codex, `.claude/CLAUDE.md` for all other runtimes) @@ -1555,7 +1555,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" - [ ] ROADMAP.md created with phases, requirement mappings, success criteria - [ ] STATE.md initialized - [ ] REQUIREMENTS.md traceability updated -- [ ] `$INSTRUCTION_FILE` generated with GSD workflow guidance (AGENTS.md for Codex, CLAUDE.md otherwise) +- [ ] `$INSTRUCTION_FILE` generated with GSD workflow guidance (AGENTS.md for Codex, `.claude/CLAUDE.md` otherwise; an existing hand-crafted file without GSD markers is left untouched unless `--force`) - [ ] User knows next step is `/gsd:discuss-phase 1` **Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist. diff --git a/gsd-core/workflows/plan-phase.md b/gsd-core/workflows/plan-phase.md index 52937b19a..5713ac7d7 100644 --- a/gsd-core/workflows/plan-phase.md +++ b/gsd-core/workflows/plan-phase.md @@ -547,7 +547,7 @@ ${AGENT_SKILLS_RESEARCHER} **Phase description:** {phase_description} **Phase requirement IDs (MUST address):** {phase_req_ids} -**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines +**Project instructions:** Read ./CLAUDE.md or ./.claude/CLAUDE.md if either exists — follow project-specific guidelines **Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, research should account for project skill patterns @@ -977,7 +977,7 @@ Historical findings already incorporated, explicitly deferred/rejected in PLAN.m **Phase requirement IDs (every ID MUST appear in a plan's `requirements` field):** {phase_req_ids} -**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines +**Project instructions:** Read ./CLAUDE.md or ./.claude/CLAUDE.md if either exists — follow project-specific guidelines **Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules ${TDD_MODE === 'true' ? ` @@ -1316,7 +1316,7 @@ If an actionable finding remains only in REVIEWS.md and would be invisible to /g **Phase requirement IDs (MUST ALL be covered):** {phase_req_ids} -**Project instructions:** Read ./CLAUDE.md if exists — verify plans honor project guidelines +**Project instructions:** Read ./CLAUDE.md or ./.claude/CLAUDE.md if either exists — verify plans honor project guidelines **Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — verify plans account for project skill rules diff --git a/gsd-core/workflows/profile-user.md b/gsd-core/workflows/profile-user.md index 938ec1b81..3ef88d7eb 100644 --- a/gsd-core/workflows/profile-user.md +++ b/gsd-core/workflows/profile-user.md @@ -414,10 +414,12 @@ Then list paths for each generated artifact: ``` Artifacts: ✓ /gsd-dev-preferences $HOME/.claude/skills/gsd-dev-preferences/SKILL.md - ✓ CLAUDE.md section ./CLAUDE.md + ✓ CLAUDE.md section ✓ Global CLAUDE.md $HOME/.claude/CLAUDE.md ``` +(Show the `claude_md_path` actually returned by the command — it defaults to `./.claude/CLAUDE.md` but may be overridden by config or `--output`.) + (Only show artifacts that were actually generated.) **Clean up temp files:** diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index f98e2dd4b..eef7d2cfa 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -409,7 +409,7 @@ Agent( - .planning/STATE.md (Project state — what's already built) - .planning/PROJECT.md (Project context) -- ./CLAUDE.md (if exists — project-specific guidelines) +- ./CLAUDE.md or ./.claude/CLAUDE.md (if exists — project-specific guidelines) ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — research should align with these)' : ''} @@ -468,7 +468,7 @@ Agent( - .planning/STATE.md (Project State) -- ./CLAUDE.md (if exists — follow project-specific guidelines) +- ./CLAUDE.md or ./.claude/CLAUDE.md (if exists — follow project-specific guidelines) ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — locked, do not revisit)' : ''} ${RESEARCH_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md (Research findings — use to inform implementation choices)' : ''} @@ -684,7 +684,7 @@ ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispat - ${QUICK_DIR}/${quick_id}-PLAN.md (Plan) - .planning/STATE.md (Project state) -- ./CLAUDE.md (Project instructions, if exists) +- ./CLAUDE.md or ./.claude/CLAUDE.md (Project instructions, if exists) - .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation) diff --git a/src/config.cts b/src/config.cts index 4fa4d9efc..90810c297 100644 --- a/src/config.cts +++ b/src/config.cts @@ -263,7 +263,7 @@ function buildNewProjectConfig(userChoices: Record): Record = {}; - let configClaudeMdPath = './CLAUDE.md'; + // #1098: default the Claude-family instruction file to the project-scoped + // `.claude/CLAUDE.md` (a valid auto-loaded memory location) rather than a + // repo-root `CLAUDE.md`, so generated GSD content does not land next to — or + // pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path` + // config value or `--output` still wins. + let configClaudeMdPath = './.claude/CLAUDE.md'; try { const config = loadConfig(cwd); if (config['claude_md_path']) configClaudeMdPath = config['claude_md_path'] as string; @@ -1160,6 +1171,25 @@ function cmdGenerateClaudeMd(cwd: string, options: CmdGenerateClaudeMdOptions, r action = 'created'; platformEnsureDir(path.dirname(outputPath)); platformWriteSync(outputPath, existingContent); + } else if (!/')); 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,