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

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