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 `<!-- GSD:<section>-start` markers
  (hand-crafted) is left untouched; report action:"skipped". --force (now wired
  through CmdGenerateClaudeMdOptions) overwrites intentionally. The marker check
  uses /<!-- GSD:[a-z]+-start/ so a file merely documenting GSD syntax is safe.
- Redirect: the Claude-family default output is now ./.claude/CLAUDE.md (a valid
  auto-loaded project-memory location) instead of repo-root ./CLAUDE.md. Aligned
  across the handler default, config-defaults.manifest.json, buildNewProjectConfig,
  the config template, new-project.md, and cmdGenerateClaudeProfile; advisory
  read-CLAUDE.md hints in plan-phase/quick/profile-user updated. Codex still
  writes AGENTS.md.

Closes #1098

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-12 16:03:51 -04:00
committed by GitHub
parent fc37ae0c4f
commit 827011b865
15 changed files with 157 additions and 47 deletions

View File

@@ -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)

View File

@@ -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/<source-path>` 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.<section>` (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) |

View File

@@ -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`

View File

@@ -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,

View File

@@ -58,5 +58,5 @@
},
"project_code": null,
"agent_skills": {},
"claude_md_path": "./CLAUDE.md"
"claude_md_path": "./.claude/CLAUDE.md"
}

View File

@@ -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)
</output>
@@ -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.

View File

@@ -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
</additional_context>
@@ -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
</verification_context>

View File

@@ -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 <resolved claude_md_path, default ./.claude/CLAUDE.md>
✓ 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:**

View File

@@ -409,7 +409,7 @@ Agent(
<files_to_read>
- .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)' : ''}
</files_to_read>
@@ -468,7 +468,7 @@ Agent(
<files_to_read>
- .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)' : ''}
</files_to_read>
@@ -684,7 +684,7 @@ ORCHESTRATOR build-time embed (NOT a sub-agent runtime step): before this dispat
<files_to_read>
- ${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)
</files_to_read>

View File

@@ -263,7 +263,7 @@ function buildNewProjectConfig(userChoices: Record<string, unknown>): Record<str
project_code: null,
phase_naming: 'sequential',
agent_skills: {},
claude_md_path: './CLAUDE.md',
claude_md_path: './.claude/CLAUDE.md',
plan_review: {
source_grounding: true,
source_grounding_authority: 'grep',

View File

@@ -99,6 +99,10 @@ interface CmdGenerateClaudeProfileOptions {
interface CmdGenerateClaudeMdOptions {
output?: string;
auto?: boolean;
// #1098: overwrite an existing instruction file that has no GSD section
// markers (a hand-crafted CLAUDE.md/AGENTS.md). Without it, such a file is
// left untouched rather than clobbered.
force?: boolean;
}
// ─── Constants ────────────────────────────────────────────────────────────────
@@ -1027,8 +1031,10 @@ function cmdGenerateClaudeProfile(cwd: string, options: CmdGenerateClaudeProfile
} else if (options.output) {
targetPath = path.isAbsolute(options.output) ? options.output : path.join(cwd, options.output);
} else {
// Read claude_md_path from config, default to ./CLAUDE.md
let configClaudeMdPath = './CLAUDE.md';
// Read claude_md_path from config; #1098 default is ./.claude/CLAUDE.md
// (kept consistent with cmdGenerateClaudeMd so the profile section and the
// managed sections land in the same file on a config-less project).
let configClaudeMdPath = './.claude/CLAUDE.md';
try {
const config = loadConfig(cwd);
if (config['claude_md_path']) configClaudeMdPath = config['claude_md_path'] as string;
@@ -1106,7 +1112,12 @@ function cmdGenerateClaudeMd(cwd: string, options: CmdGenerateClaudeMdOptions, r
}
let assemblyConfig: Record<string, unknown> = {};
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 (!/<!-- GSD:[a-z]+-start/.test(existingContent) && !options.force) {
// #1098: the target instruction file already exists and contains NO GSD
// section markers — it is a hand-crafted CLAUDE.md/AGENTS.md, not a
// GSD-managed one. Do NOT clobber it with generated project documentation
// (broad project detail belongs in PROJECT.md / REQUIREMENTS.md, which GSD
// already owns). Leave the file untouched and report a skip; `--force`
// overwrites intentionally.
output({
claude_md_path: outputPath,
action: 'skipped',
reason: 'existing instruction file has no GSD markers (hand-crafted); not overwriting. Pass --force to overwrite.',
sections_generated: [],
sections_fallback: [],
sections_skipped: MANAGED_SECTIONS,
sections_total: MANAGED_SECTIONS.length,
profile_status: 'skipped',
message: `Left existing ${path.basename(outputPath)} untouched (no GSD markers found). Broad project context lives in PROJECT.md / REQUIREMENTS.md; pass --force to overwrite this file with GSD-managed sections.`,
}, raw, undefined);
return;
} else {
action = 'updated';
let fileContent = existingContent;

View File

@@ -27,7 +27,7 @@ describe('claude_md_path config key', () => {
test('config template includes claude_md_path', () => {
const templatePath = path.join(__dirname, '..', 'gsd-core', 'templates', 'config.json');
const template = JSON.parse(fs.readFileSync(templatePath, 'utf-8'));
assert.strictEqual(template.claude_md_path, './CLAUDE.md');
assert.strictEqual(template.claude_md_path, './.claude/CLAUDE.md');
});
test('config-get claude_md_path returns default value when not set', () => {
@@ -59,7 +59,7 @@ describe('claude_md_path config key', () => {
const configPath = path.join(tmpDir, '.planning', 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.strictEqual(config.claude_md_path, './CLAUDE.md');
assert.strictEqual(config.claude_md_path, './.claude/CLAUDE.md');
});
});
@@ -186,7 +186,7 @@ describe('cmdGenerateClaudeMd reads claude_md_path from config', () => {
assert.strictEqual(parsed.claude_md_path, path.join(realTmpDir, outputFile));
});
test('defaults to ./CLAUDE.md when config has no claude_md_path', () => {
test('defaults to .claude/CLAUDE.md when config has no claude_md_path (#1098)', () => {
// Set up config without claude_md_path
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(configPath, JSON.stringify({ mode: 'interactive' }), 'utf-8');
@@ -196,6 +196,6 @@ describe('cmdGenerateClaudeMd reads claude_md_path from config', () => {
const parsed = JSON.parse(result.output);
const realTmpDir = fs.realpathSync(tmpDir);
assert.strictEqual(parsed.claude_md_path, path.join(realTmpDir, 'CLAUDE.md'));
assert.strictEqual(parsed.claude_md_path, path.join(realTmpDir, '.claude', 'CLAUDE.md'));
});
});

View File

@@ -37,7 +37,7 @@ describe('generate-claude-md', () => {
assert.strictEqual(output.sections_total, 6);
assert.ok(output.sections_generated.includes('workflow'));
const claudePath = path.join(tmpDir, 'CLAUDE.md');
const claudePath = path.join(tmpDir, '.claude', 'CLAUDE.md');
const content = fs.readFileSync(claudePath, 'utf-8');
assert.ok(content.includes('## GSD Workflow Enforcement'));
// #3584: generated CLAUDE.md must emit the runtime-routable hyphen-form
@@ -51,23 +51,77 @@ describe('generate-claude-md', () => {
assert.ok(content.includes('Do not make direct repo edits outside a GSD workflow'));
});
test('adds workflow enforcement section when updating an existing CLAUDE.md', () => {
test('adds workflow enforcement section when force-updating an existing marker-less CLAUDE.md', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
fs.writeFileSync(path.join(tmpDir, 'CLAUDE.md'), '## Local Notes\n\nKeep this intro.\n');
// #1098: a hand-crafted file (no GSD markers) is only overwritten with --force.
fs.mkdirSync(path.join(tmpDir, '.claude'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), '## Local Notes\n\nKeep this intro.\n');
const result = runGsdTools('generate-claude-md', tmpDir);
const result = runGsdTools('generate-claude-md --force', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.action, 'updated');
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('## Local Notes'));
assert.ok(content.includes('## GSD Workflow Enforcement'));
});
test('#1098: does NOT clobber an existing marker-less CLAUDE.md without --force', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
const handCrafted = '## My Hand-Crafted Instructions\n\nDo exactly what I say.\n';
fs.mkdirSync(path.join(tmpDir, '.claude'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), handCrafted);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.action, 'skipped', 'must skip a hand-crafted file without --force');
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
assert.strictEqual(content, handCrafted, 'hand-crafted file must be left byte-identical');
assert.ok(!content.includes('## GSD Workflow Enforcement'), 'GSD sections must NOT be injected');
});
test('#1098: updates an existing GSD-managed CLAUDE.md (has markers) without --force', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
// First generation creates a GSD-managed file (with markers).
const first = runGsdTools('generate-claude-md', tmpDir);
assert.ok(first.success, `Command failed: ${first.error}`);
assert.strictEqual(JSON.parse(first.output).action, 'created');
// Re-running updates it in place — markers present, so no --force needed.
const second = runGsdTools('generate-claude-md', tmpDir);
assert.ok(second.success, `Command failed: ${second.error}`);
assert.strictEqual(JSON.parse(second.output).action, 'updated');
});
test('#1098: defaults the Claude-runtime output to .claude/CLAUDE.md (not repo root)', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA small test project.\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.claude_md_path.replace(/\\/g, '/').endsWith('/.claude/CLAUDE.md'),
`default output must be .claude/CLAUDE.md; got ${output.claude_md_path}`
);
assert.ok(!fs.existsSync(path.join(tmpDir, 'CLAUDE.md')), 'must NOT create a repo-root CLAUDE.md');
assert.ok(fs.existsSync(path.join(tmpDir, '.claude', 'CLAUDE.md')), 'must create .claude/CLAUDE.md');
});
});
describe('new-project workflow includes CLAUDE.md generation', () => {
@@ -117,7 +171,7 @@ describe('generate-claude-md skills section', () => {
const output = JSON.parse(result.output);
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('<!-- GSD:skills-start'));
assert.ok(content.includes('<!-- GSD:skills-end -->'));
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');

View File

@@ -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/'));

View File

@@ -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,