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:
5
.changeset/1098-generate-claude-md-guard-redirect.md
Normal file
5
.changeset/1098-generate-claude-md-guard-redirect.md
Normal 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)
|
||||
@@ -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) |
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"project_code": null,
|
||||
"agent_skills": {},
|
||||
"claude_md_path": "./CLAUDE.md"
|
||||
"claude_md_path": "./.claude/CLAUDE.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)
|
||||
|
||||
</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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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/'));
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user