diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 212f504dc..287557a7a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -113,6 +113,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_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_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) | | `brave_search` | boolean | `true`/`false` | auto-detected | Override auto-detection of Brave Search API availability. When unset, GSD checks for `BRAVE_API_KEY` env var or `~/.gsd/brave_api_key` file | diff --git a/get-shit-done/bin/lib/config-schema.cjs b/get-shit-done/bin/lib/config-schema.cjs index bea2658f6..b474d5a20 100644 --- a/get-shit-done/bin/lib/config-schema.cjs +++ b/get-shit-done/bin/lib/config-schema.cjs @@ -54,6 +54,7 @@ const VALID_CONFIG_KEYS = new Set([ 'graphify.enabled', 'graphify.build_timeout', 'claude_md_path', + 'claude_md_assembly.mode', ]); /** @@ -61,9 +62,10 @@ const VALID_CONFIG_KEYS = new Set([ * Each entry has a `test` function and a human-readable `description`. */ const DYNAMIC_KEY_PATTERNS = [ - { test: (k) => /^agent_skills\.[a-zA-Z0-9_-]+$/.test(k), description: 'agent_skills.' }, - { test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k), description: 'review.models.' }, - { test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k), description: 'features.' }, + { test: (k) => /^agent_skills\.[a-zA-Z0-9_-]+$/.test(k), description: 'agent_skills.' }, + { test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k), description: 'review.models.' }, + { test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k), description: 'features.' }, + { test: (k) => /^claude_md_assembly\.blocks\.[a-zA-Z0-9_]+$/.test(k), description: 'claude_md_assembly.blocks.
' }, ]; /** diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index bfcdaad3f..a426525aa 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -394,6 +394,7 @@ function loadConfig(cwd) { manager: parsed.manager || {}, response_language: get('response_language') || null, claude_md_path: get('claude_md_path') || null, + claude_md_assembly: parsed.claude_md_assembly || null, }; } catch { // Fall back to ~/.gsd/defaults.json only for truly pre-project contexts (#1683) diff --git a/get-shit-done/bin/lib/profile-output.cjs b/get-shit-done/bin/lib/profile-output.cjs index 38156bac5..e899383b1 100644 --- a/get-shit-done/bin/lib/profile-output.cjs +++ b/get-shit-done/bin/lib/profile-output.cjs @@ -285,7 +285,7 @@ function generateProjectSection(cwd) { const projectPath = path.join(cwd, '.planning', 'PROJECT.md'); const content = safeReadFile(projectPath); if (!content) { - return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', hasFallback: true }; + return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', linkPath: null, hasFallback: true }; } const parts = []; const h1Match = content.match(/^# (.+)$/m); @@ -306,9 +306,9 @@ function generateProjectSection(cwd) { if (body) parts.push(`### Constraints\n\n${body}`); } if (parts.length === 0) { - return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', hasFallback: true }; + return { content: CLAUDE_MD_FALLBACKS.project, source: 'PROJECT.md', linkPath: null, hasFallback: true }; } - return { content: parts.join('\n\n'), source: 'PROJECT.md', hasFallback: false }; + return { content: parts.join('\n\n'), source: 'PROJECT.md', linkPath: '.planning/PROJECT.md', hasFallback: false }; } function generateStackSection(cwd) { @@ -316,12 +316,14 @@ function generateStackSection(cwd) { const researchPath = path.join(cwd, '.planning', 'research', 'STACK.md'); let content = safeReadFile(codebasePath); let source = 'codebase/STACK.md'; + let linkPath = '.planning/codebase/STACK.md'; if (!content) { content = safeReadFile(researchPath); source = 'research/STACK.md'; + linkPath = '.planning/research/STACK.md'; } if (!content) { - return { content: CLAUDE_MD_FALLBACKS.stack, source: 'STACK.md', hasFallback: true }; + return { content: CLAUDE_MD_FALLBACKS.stack, source: 'STACK.md', linkPath: null, hasFallback: true }; } const lines = content.split('\n'); const summaryLines = []; @@ -336,14 +338,14 @@ function generateStackSection(cwd) { if (line.startsWith('- ') || line.startsWith('* ')) summaryLines.push(line); } const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim(); - return { content: summary, source, hasFallback: false }; + return { content: summary, source, linkPath, hasFallback: false }; } function generateConventionsSection(cwd) { const conventionsPath = path.join(cwd, '.planning', 'codebase', 'CONVENTIONS.md'); const content = safeReadFile(conventionsPath); if (!content) { - return { content: CLAUDE_MD_FALLBACKS.conventions, source: 'CONVENTIONS.md', hasFallback: true }; + return { content: CLAUDE_MD_FALLBACKS.conventions, source: 'CONVENTIONS.md', linkPath: null, hasFallback: true }; } const lines = content.split('\n'); const summaryLines = []; @@ -352,14 +354,14 @@ function generateConventionsSection(cwd) { if (line.startsWith('- ') || line.startsWith('* ') || line.startsWith('|')) summaryLines.push(line); } const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim(); - return { content: summary, source: 'CONVENTIONS.md', hasFallback: false }; + return { content: summary, source: 'CONVENTIONS.md', linkPath: '.planning/codebase/CONVENTIONS.md', hasFallback: false }; } function generateArchitectureSection(cwd) { const architecturePath = path.join(cwd, '.planning', 'codebase', 'ARCHITECTURE.md'); const content = safeReadFile(architecturePath); if (!content) { - return { content: CLAUDE_MD_FALLBACKS.architecture, source: 'ARCHITECTURE.md', hasFallback: true }; + return { content: CLAUDE_MD_FALLBACKS.architecture, source: 'ARCHITECTURE.md', linkPath: null, hasFallback: true }; } const lines = content.split('\n'); const summaryLines = []; @@ -368,13 +370,14 @@ function generateArchitectureSection(cwd) { if (line.startsWith('- ') || line.startsWith('* ') || line.startsWith('|') || line.startsWith('```')) summaryLines.push(line); } const summary = summaryLines.length > 0 ? summaryLines.join('\n') : content.trim(); - return { content: summary, source: 'ARCHITECTURE.md', hasFallback: false }; + return { content: summary, source: 'ARCHITECTURE.md', linkPath: '.planning/codebase/ARCHITECTURE.md', hasFallback: false }; } function generateWorkflowSection() { return { content: CLAUDE_MD_WORKFLOW_ENFORCEMENT, source: 'GSD defaults', + linkPath: null, hasFallback: false, }; } @@ -948,19 +951,35 @@ function cmdGenerateClaudeMd(cwd, options, raw) { } } + let assemblyConfig = {}; + let configClaudeMdPath = './CLAUDE.md'; + try { + const config = loadConfig(cwd); + if (config.claude_md_path) configClaudeMdPath = config.claude_md_path; + if (config.claude_md_assembly) assemblyConfig = config.claude_md_assembly; + } catch { /* use default */ } + let outputPath = options.output; if (!outputPath) { - // Read claude_md_path from config, default to ./CLAUDE.md - let configClaudeMdPath = './CLAUDE.md'; - try { - const config = loadConfig(cwd); - if (config.claude_md_path) configClaudeMdPath = config.claude_md_path; - } catch { /* use default */ } outputPath = path.isAbsolute(configClaudeMdPath) ? configClaudeMdPath : path.join(cwd, configClaudeMdPath); } else if (!path.isAbsolute(outputPath)) { outputPath = path.join(cwd, outputPath); } + const globalAssemblyMode = assemblyConfig.mode || 'embed'; + const blockModes = assemblyConfig.blocks || {}; + + // Return the assembled content for a section, respecting link vs embed mode. + // "link" mode writes `@` when the generator has a real source file. + // Falls back to "embed" for sections without a linkable source (workflow, fallbacks). + function buildSectionContent(name, gen, heading) { + const effectiveMode = blockModes[name] || globalAssemblyMode; + if (effectiveMode === 'link' && gen.linkPath && !gen.hasFallback) { + return buildSection(name, gen.source, `${heading}\n\n@${gen.linkPath}`); + } + return buildSection(name, gen.source, `${heading}\n\n${gen.content}`); + } + let existingContent = safeReadFile(outputPath); let action; @@ -969,8 +988,7 @@ function cmdGenerateClaudeMd(cwd, options, raw) { for (const name of MANAGED_SECTIONS) { const gen = generated[name]; const heading = sectionHeadings[name]; - const body = `${heading}\n\n${gen.content}`; - sections.push(buildSection(name, gen.source, body)); + sections.push(buildSectionContent(name, gen, heading)); } sections.push(''); sections.push(CLAUDE_MD_PROFILE_PLACEHOLDER); @@ -985,13 +1003,15 @@ function cmdGenerateClaudeMd(cwd, options, raw) { for (const name of MANAGED_SECTIONS) { const gen = generated[name]; const heading = sectionHeadings[name]; - const body = `${heading}\n\n${gen.content}`; - const fullSection = buildSection(name, gen.source, body); + const fullSection = buildSectionContent(name, gen, heading); const hasMarkers = fileContent.indexOf(`