diff --git a/docs/how-to/monorepo-setup.md b/docs/how-to/monorepo-setup.md new file mode 100644 index 000000000..dd89edb94 --- /dev/null +++ b/docs/how-to/monorepo-setup.md @@ -0,0 +1,48 @@ +--- +title: "Monorepo / Multi-Project Setup" +description: Run multiple independent projects within a single BMAD installation +sidebar: + order: 8 +--- + +# Monorepo / Multi-Project Setup + +BMAD supports running multiple independent projects within a single installation. This is useful for monorepos, multi-app repositories, or any workspace where you want isolated artifact directories per project. + +## How It Works + +A `.current_project` file in your `_bmad/` directory stores the active project name. When set, all artifact output paths are redirected to project-specific subdirectories. + +## Quick Start + +1. List existing projects by invoking `skill:bmad-project-list` +2. Create a new project by invoking `skill:bmad-project-new` with a project name +3. Switch to an existing project by invoking `skill:bmad-project-switch` with a project name +4. Clear project context by invoking `skill:bmad-project-switch` and entering `CLEAR` + +## Directory Structure + +When a project context is active, for example `my-app`: + +- `_bmad-output/my-app/planning-artifacts/` for PRDs, briefs, UX designs +- `_bmad-output/my-app/implementation-artifacts/` for sprint plans and stories +- `_bmad-output/my-app/knowledge/` for long-term project knowledge + +## Inline Overrides + +Temporarily target a different project without changing the global context: + +- `#project:my-app` +- `#p:my-app` + +**Precedence:** + +1. Inline override (`#p:NAME`) — highest priority +2. Global context file (`.current_project`) +3. Default config paths + +## Security + +- Path traversal (`..`) is rejected +- Absolute paths are rejected +- Only alphanumeric characters, dots, dashes, underscores, and forward slashes are allowed diff --git a/src/bmm/agents/architect.agent.yaml b/src/bmm/agents/architect.agent.yaml index ce76a3b49..764f3804a 100644 --- a/src/bmm/agents/architect.agent.yaml +++ b/src/bmm/agents/architect.agent.yaml @@ -27,3 +27,15 @@ agent: - trigger: IR or fuzzy match on implementation-readiness exec: "skill:bmad-check-implementation-readiness" description: "[IR] Implementation Readiness: Ensure the PRD, UX, and Architecture and Epics and Stories List are all aligned" + + - trigger: PN or fuzzy match on project-new + exec: "skill:bmad-project-new" + description: "[PN] Project New: Create a new project and make it the active project context" + + - trigger: PS or fuzzy match on project-switch + exec: "skill:bmad-project-switch" + description: "[PS] Project Switch: Switch the active project context for monorepo support" + + - trigger: PL or fuzzy match on project-list + exec: "skill:bmad-project-list" + description: "[PL] Project List: Show available project contexts" diff --git a/src/bmm/agents/dev.agent.yaml b/src/bmm/agents/dev.agent.yaml index cdcf9ea5f..95bdb6a49 100644 --- a/src/bmm/agents/dev.agent.yaml +++ b/src/bmm/agents/dev.agent.yaml @@ -36,3 +36,15 @@ agent: - trigger: CR or fuzzy match on code-review exec: "skill:bmad-code-review" description: "[CR] Code Review: Initiate a comprehensive code review across multiple quality facets. For best results, use a fresh context and a different quality LLM if available" + + - trigger: PN or fuzzy match on project-new + exec: "skill:bmad-project-new" + description: "[PN] Project New: Create a new project and make it the active project context" + + - trigger: PS or fuzzy match on project-switch + exec: "skill:bmad-project-switch" + description: "[PS] Project Switch: Switch the active project context for monorepo support" + + - trigger: PL or fuzzy match on project-list + exec: "skill:bmad-project-list" + description: "[PL] Project List: Show available project contexts" diff --git a/src/bmm/agents/pm.agent.yaml b/src/bmm/agents/pm.agent.yaml index b9e5c4ed3..3168b9cf6 100644 --- a/src/bmm/agents/pm.agent.yaml +++ b/src/bmm/agents/pm.agent.yaml @@ -42,3 +42,15 @@ agent: - trigger: CC or fuzzy match on correct-course exec: "skill:bmad-correct-course" description: "[CC] Course Correction: Use this so we can determine how to proceed if major need for change is discovered mid implementation" + + - trigger: PN or fuzzy match on project-new + exec: "skill:bmad-project-new" + description: "[PN] Project New: Create a new project and make it the active project context" + + - trigger: PS or fuzzy match on project-switch + exec: "skill:bmad-project-switch" + description: "[PS] Project Switch: Switch the active project context for monorepo support" + + - trigger: PL or fuzzy match on project-list + exec: "skill:bmad-project-list" + description: "[PL] Project List: Show available project contexts" diff --git a/src/bmm/agents/quick-flow-solo-dev.agent.yaml b/src/bmm/agents/quick-flow-solo-dev.agent.yaml index 6cebb3cf1..283094bf7 100644 --- a/src/bmm/agents/quick-flow-solo-dev.agent.yaml +++ b/src/bmm/agents/quick-flow-solo-dev.agent.yaml @@ -34,3 +34,15 @@ agent: - trigger: CR or fuzzy match on code-review exec: "skill:bmad-code-review" description: "[CR] Code Review: Initiate a comprehensive code review across multiple quality facets. For best results, use a fresh context and a different quality LLM if available" + + - trigger: PN or fuzzy match on project-new + exec: "skill:bmad-project-new" + description: "[PN] Project New: Create a new project and make it the active project context" + + - trigger: PS or fuzzy match on project-switch + exec: "skill:bmad-project-switch" + description: "[PS] Project Switch: Switch the active project context for monorepo support" + + - trigger: PL or fuzzy match on project-list + exec: "skill:bmad-project-list" + description: "[PL] Project List: Show available project contexts" diff --git a/src/bmm/agents/sm.agent.yaml b/src/bmm/agents/sm.agent.yaml index 614465553..5e5c70ca9 100644 --- a/src/bmm/agents/sm.agent.yaml +++ b/src/bmm/agents/sm.agent.yaml @@ -35,3 +35,15 @@ agent: - trigger: CC or fuzzy match on correct-course exec: "skill:bmad-correct-course" description: "[CC] Course Correction: Use this so we can determine how to proceed if major need for change is discovered mid implementation" + + - trigger: PN or fuzzy match on project-new + exec: "skill:bmad-project-new" + description: "[PN] Project New: Create a new project and make it the active project context" + + - trigger: PS or fuzzy match on project-switch + exec: "skill:bmad-project-switch" + description: "[PS] Project Switch: Switch the active project context for monorepo support" + + - trigger: PL or fuzzy match on project-list + exec: "skill:bmad-project-list" + description: "[PL] Project List: Show available project contexts" diff --git a/src/bmm/module.yaml b/src/bmm/module.yaml index 76f6b7433..2ab00e71c 100644 --- a/src/bmm/module.yaml +++ b/src/bmm/module.yaml @@ -48,3 +48,35 @@ directories: - "{planning_artifacts}" - "{implementation_artifacts}" - "{project_knowledge}" + +# --- Monorepo / Multi-Project Context Support --- +# IMPORTANT: When loading this config, apply monorepo context resolution BEFORE using any path variables. +# +# Resolution order: +# 1. INLINE OVERRIDE: Scan user's invocation for #project:NAME or #p:NAME (case-insensitive). +# If found, set project_suffix = NAME. +# 2. FILE FALLBACK: If no inline override, check if {project-root}/_bmad/.current_project exists. +# If so, read its single-line content as project_suffix (trim whitespace/newlines). +# 3. VALIDATE (if project_suffix is set): +# - REJECT if empty, contains '..', starts with '/' or drive letter (e.g. C:) +# - REJECT if fails whitelist regex: ^[a-zA-Z0-9._\-/]+$ +# - On rejection: output "Security Error: Invalid project context" and HALT +# 4. OVERRIDE PATHS (if valid project_suffix): +# - output_folder = {project-root}/_bmad-output/{project_suffix} +# - planning_artifacts = {output_folder}/planning-artifacts +# - implementation_artifacts = {output_folder}/implementation-artifacts +# - project_knowledge = {output_folder}/knowledge +# - sprint_status_file = {output_folder}/sprint-status.yaml +# - project_name = {project_suffix} +# 5. If no project_suffix is set, use the default paths from config as-is. + +monorepo_context: + enabled: true + context_file: ".current_project" + inline_override_patterns: + - "#project:" + - "#p:" + validation: + whitelist_regex: "^[a-zA-Z0-9._\\-/]+$" + reject_traversal: true + reject_absolute: true diff --git a/src/bmm/workflows/0-context/bmad-project-list/SKILL.md b/src/bmm/workflows/0-context/bmad-project-list/SKILL.md new file mode 100644 index 000000000..22809a459 --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-list/SKILL.md @@ -0,0 +1,6 @@ +--- +name: bmad-project-list +description: 'List available projects for monorepo support. Use when the user says "project list", "list projects", or "show projects"' +--- + +Follow the instructions in [workflow.md](workflow.md). diff --git a/src/bmm/workflows/0-context/bmad-project-list/bmad-skill-manifest.yaml b/src/bmm/workflows/0-context/bmad-project-list/bmad-skill-manifest.yaml new file mode 100644 index 000000000..d0f08abdb --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-list/bmad-skill-manifest.yaml @@ -0,0 +1 @@ +type: skill diff --git a/src/bmm/workflows/0-context/bmad-project-list/workflow.md b/src/bmm/workflows/0-context/bmad-project-list/workflow.md new file mode 100644 index 000000000..b49bffce5 --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-list/workflow.md @@ -0,0 +1,22 @@ +# Project List + +**Goal:** List the available project contexts for BMAD artifacts. + +**Your Role:** Configuration Assistant. + +## WORKFLOW ARCHITECTURE + +### 1. Identify Projects + +- Use your file listing or system capabilities to examine the `{project-root}/_bmad-output/` directory. +- Identify all subdirectories within this path. Each subdirectory represents an available project context, except: + - Ignore hidden directories starting with `.` + - Ignore standard BMAD artifact directories: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets` +- The root `_bmad-output/` directory itself represents the `default (root)` unset project context. + +### 2. Output Results + +1. Display a formatted numbered list of the existing projects you found. +2. Clearly indicate the `default (root)` project context, usually as `[0]`. +3. Check whether an active project is currently set by reading the active project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`). If it exists, indicate which project from the list is active, for example by adding `(ACTIVE)` next to the entry. +4. Inform the user that they can switch projects with PS (Project Switch) or create a new one with PN (Project New). diff --git a/src/bmm/workflows/0-context/bmad-project-new/SKILL.md b/src/bmm/workflows/0-context/bmad-project-new/SKILL.md new file mode 100644 index 000000000..54a36adac --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-new/SKILL.md @@ -0,0 +1,6 @@ +--- +name: bmad-project-new +description: 'Create a new project for monorepo support. Use when the user says "project new", "new project", or "create project"' +--- + +Follow the instructions in [workflow.md](workflow.md). diff --git a/src/bmm/workflows/0-context/bmad-project-new/bmad-skill-manifest.yaml b/src/bmm/workflows/0-context/bmad-project-new/bmad-skill-manifest.yaml new file mode 100644 index 000000000..d0f08abdb --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-new/bmad-skill-manifest.yaml @@ -0,0 +1 @@ +type: skill diff --git a/src/bmm/workflows/0-context/bmad-project-new/workflow.md b/src/bmm/workflows/0-context/bmad-project-new/workflow.md new file mode 100644 index 000000000..210c343d4 --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-new/workflow.md @@ -0,0 +1,54 @@ +# Project New + +**Goal:** Create a new project context with the standard BMAD artifact structure and make it active. + +**Your Role:** Configuration Assistant. + +## WORKFLOW ARCHITECTURE + +This is a single-step workflow that creates directories and updates the active project context file. + +### 1. Configuration Loading + +Load and read full config from `{project-root}/_bmad/bmm/config.yaml` and resolve variables and artifact paths. +The context file is the default hidden project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`). + +### 2. Project Creation + +1. **Analyze Request**: Determine the requested new project name from the user's initial invocation, such as "new project my-app". +2. **Wait for Input (If Missing)**: If the user did not provide a project name: + - Ask: `What should the new project be called?` + - Wait for input. +3. **Validate Project Name** + - **Cleanup**: Remove leading and trailing slashes and any occurrences of `_bmad-output/`. + - **Validate - No Traversal**: Reject if path contains `..`. + - **Validate - No Absolute**: Reject if path starts with `/` or a drive letter such as `C:`. + - **Validate - Empty/Whitespace**: Reject if empty or only whitespace. + - **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._/]+$`. + - If invalid: + - Output: `Error: Invalid project name — must be a relative path and contain only alphanumeric characters, dots, dashes, underscores, or slashes. Traversal (..) is strictly forbidden.` + - Halt. +4. **Check for Existing Project** + - Check if `{project-root}/_bmad-output/` already exists. + - If it already exists: + - Ask: `The project already exists. Do you want to switch to it instead? (Yes/No)` + - Wait for input. + - If `Yes`: + - Write the active project selector file in `{project-root}/_bmad/` with content `` + - Output: `Project context set to: . Artifacts will go to _bmad-output//.` + - Halt. + - If `No`: + - Inform the user to choose another name or use PS (Project Switch) to switch to an existing project. + - Halt. +5. **Create Project Structure** + - Create directory: `{project-root}/_bmad-output//` + - Create directory: `{project-root}/_bmad-output//planning-artifacts/` + - Create directory: `{project-root}/_bmad-output//implementation-artifacts/` + - Create directory: `{project-root}/_bmad-output//knowledge/` + - Write the active project selector file in `{project-root}/_bmad/` with content `` + - Output: `Created project and set it as active. Artifacts will go to _bmad-output//.` + +### 3. Verification + +- Display the full resolved output path for confirmation. +- Mention that the user can use PS (Project Switch) to switch projects later and PL (Project List) to list available projects. diff --git a/src/bmm/workflows/0-context/bmad-project-switch/SKILL.md b/src/bmm/workflows/0-context/bmad-project-switch/SKILL.md new file mode 100644 index 000000000..93e809e0d --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-switch/SKILL.md @@ -0,0 +1,6 @@ +--- +name: bmad-project-switch +description: 'Switch the active project for monorepo support. Use when the user says "project switch", "switch project", or "choose project"' +--- + +Follow the instructions in [workflow.md](workflow.md). diff --git a/src/bmm/workflows/0-context/bmad-project-switch/bmad-skill-manifest.yaml b/src/bmm/workflows/0-context/bmad-project-switch/bmad-skill-manifest.yaml new file mode 100644 index 000000000..d0f08abdb --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-switch/bmad-skill-manifest.yaml @@ -0,0 +1 @@ +type: skill diff --git a/src/bmm/workflows/0-context/bmad-project-switch/workflow.md b/src/bmm/workflows/0-context/bmad-project-switch/workflow.md new file mode 100644 index 000000000..f144d8ec7 --- /dev/null +++ b/src/bmm/workflows/0-context/bmad-project-switch/workflow.md @@ -0,0 +1,80 @@ +# Project Switch + +**Goal:** Switch the active project path for BMAD artifacts. + +**Your Role:** Configuration Assistant. + +## WORKFLOW ARCHITECTURE + +This is a single-step workflow that updates a local state file. + +### 1. Configuration Loading + +Load and read full config from `{project-root}/_bmad/bmm/config.yaml` and resolve variables and artifact paths. +The context file is the default hidden project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`). + +### 2. Context Management + +1. **Analyze Request**: Determine the requested project name from the user's initial invocation, such as "switch project my-app". +2. **Wait for Input (If Missing)**: If the user did not provide a project name: + - Use your file listing capabilities to examine the `{project-root}/_bmad-output/` directory. + - Output a formatted list of the existing projects you found, explicitly noting the `default (root)` project context. + Exclude hidden directories and standard BMAD artifact folders: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets`. + - Present options in a numbered list format: + +```text +Select a project: +[0] default (root) - Current location for standard artifacts +[1] project-name-1 +[2] project-name-2 +... + +Enter a number to select, type an existing project name, or enter 'CLEAR' to reset to root: +``` + + - Wait for input. +3. **Process Input**: Once a project name is established: + - **Case: CLEAR** + - Delete the active project selector file in `{project-root}/_bmad/` + - Output: `Project context cleared. Artifacts will go to root _bmad-output/.` + - Halt. + - **Case: Numeric Selection** + - Map the number to the corresponding project from your earlier listing. + - If number is `0`, treat it as the `CLEAR` case. + - If the number is out of range, show an error and ask again. + - If valid, use that project name as the path. + - **Case: Path Provided** (text input) + - **Cleanup**: Remove leading and trailing slashes and any occurrences of `_bmad-output/`. + - **Validate - No Traversal**: Reject if path contains `..`. + - **Validate - No Absolute**: Reject if path starts with `/` or a drive letter such as `C:`. + - **Validate - Empty/Whitespace**: Reject if empty or only whitespace. + - **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._/]+$`. + - **Check Results** + - If invalid: + - Output: `Error: Invalid project name — must be a relative path and contain only alphanumeric characters, dots, dashes, underscores, or slashes. Traversal (..) is strictly forbidden.` + - Halt. + - **Validate Existence**: Check if `{project-root}/_bmad-output/` exists on disk. + - If it does not exist: + - Output: `Error: Project does not exist. Use PN (Project New) to create it first, or PL (Project List) to see available projects.` + - Halt. + - Write the active project selector file in `{project-root}/_bmad/` with content `` + - Output: `Project context set to: . Artifacts will go to _bmad-output//.` + +### 3. Verification + +- Display the full resolved output path for confirmation. + +## Inline Project Overrides + +You can also temporarily run a command against a different project without changing the global context file. Use the `#project:NAME` or `#p:NAME` syntax in your command invocation. + +**Examples:** + +- `create-prd #project:my-app` +- `sprint-planning #p:admin-portal` + +**Precedence:** + +1. **Inline Override** (`#p:NAME`) — highest priority +2. **Global Context File** (the active project selector file in `{project-root}/_bmad/`) +3. **Default Config** (if neither is present) diff --git a/test/test-installation-components.js b/test/test-installation-components.js index e86541593..d927160f5 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -15,6 +15,7 @@ const path = require('node:path'); const os = require('node:os'); const fs = require('fs-extra'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); +const { Installer } = require('../tools/cli/installers/lib/core/installer'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { IdeManager } = require('../tools/cli/installers/lib/ide/manager'); const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes'); @@ -1868,6 +1869,118 @@ async function runTests() { console.log(''); + // ============================================================ + // Test 32: Monorepo config block emission + // ============================================================ + console.log(`${colors.yellow}Test Suite 32: Monorepo Config Generation${colors.reset}\n`); + + let tempFixture32; + try { + tempFixture32 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-monorepo-config-')); + await fs.ensureDir(path.join(tempFixture32, 'bmm')); + + const installer32 = new Installer(); + const sourceModule32 = await fs.readFile(path.join(projectRoot, 'src', 'bmm', 'module.yaml'), 'utf8'); + const extractedBlock32 = installer32.extractMonorepoContextBlock(sourceModule32); + + assert( + extractedBlock32 && extractedBlock32.includes('monorepo_context:'), + 'Installer extracts monorepo context block from module schema', + ); + assert(extractedBlock32 && extractedBlock32.includes('INLINE OVERRIDE'), 'Extracted monorepo block preserves instructional comments'); + + await installer32.generateModuleConfigs(tempFixture32, { + core: { + user_name: 'TestUser', + communication_language: 'English', + document_output_language: 'English', + output_folder: '_bmad-output', + }, + bmm: { + project_name: 'demo-project', + user_skill_level: 'intermediate', + planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', + implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', + project_knowledge: '{project-root}/docs', + }, + }); + + const generatedConfig32 = await fs.readFile(path.join(tempFixture32, 'bmm', 'config.yaml'), 'utf8'); + assert( + generatedConfig32.includes('# --- Monorepo / Multi-Project Context Support ---'), + 'Generated bmm config preserves monorepo instruction header', + ); + assert(generatedConfig32.includes('monorepo_context:'), 'Generated bmm config includes monorepo_context YAML'); + assert( + generatedConfig32.includes('inline_override_patterns:') && generatedConfig32.includes('- "#p:"'), + 'Generated bmm config preserves monorepo override patterns', + ); + } catch (error) { + assert(false, 'Monorepo config generation test succeeds', error.message); + } finally { + if (tempFixture32) await fs.remove(tempFixture32).catch(() => {}); + } + + console.log(''); + + // ============================================================ + // Test 33: Context skills are discoverable as skills + // ============================================================ + console.log(`${colors.yellow}Test Suite 33: Context Skill Discovery${colors.reset}\n`); + + let tempFixture33; + try { + tempFixture33 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-context-skills-')); + await fs.ensureDir(path.join(tempFixture33, '_config')); + await fs.ensureDir(path.join(tempFixture33, 'bmm', 'agents')); + await fs.writeFile(path.join(tempFixture33, 'bmm', 'agents', 'test.md'), 'p'); + + const sourceContextDir33 = path.join(projectRoot, 'src', 'bmm', 'workflows', '0-context'); + const targetContextDir33 = path.join(tempFixture33, 'bmm', 'workflows', '0-context'); + await fs.copy(sourceContextDir33, targetContextDir33); + + const generator33 = new ManifestGenerator(); + await generator33.generateManifests(tempFixture33, ['bmm'], [], { ides: [] }); + + const newProjectSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-new'); + const setProjectSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-switch'); + const listProjectsSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-list'); + + assert(newProjectSkill33 !== undefined, 'New project skill appears in skills[]'); + assert(setProjectSkill33 !== undefined, 'Set project context skill appears in skills[]'); + assert(listProjectsSkill33 !== undefined, 'List projects skill appears in skills[]'); + assert( + newProjectSkill33 && newProjectSkill33.path.includes('workflows/0-context/bmad-project-new/SKILL.md'), + 'New project skill keeps its workflow directory path', + ); + assert( + setProjectSkill33 && setProjectSkill33.path.includes('workflows/0-context/bmad-project-switch/SKILL.md'), + 'Set project skill keeps its workflow directory path', + ); + assert( + listProjectsSkill33 && listProjectsSkill33.path.includes('workflows/0-context/bmad-project-list/SKILL.md'), + 'List projects skill keeps its workflow directory path', + ); + assert( + !generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-new')), + 'New project skill does not appear in workflows[]', + ); + assert( + !generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-switch')), + 'Set project context skill does not appear in workflows[]', + ); + assert( + !generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-list')), + 'List projects skill does not appear in workflows[]', + ); + } catch (error) { + assert(false, 'Context skill discovery test succeeds', error.message); + } finally { + if (tempFixture33) await fs.remove(tempFixture33).catch(() => {}); + } + + console.log(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index 85864145f..6426f74a3 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -2067,6 +2067,12 @@ class Installer { } } + const staticConfigBlocks = await this.getStaticConfigBlocks(moduleName); + if (staticConfigBlocks.length > 0) { + yamlContent = yamlContent.trimEnd(); + yamlContent += `\n\n${staticConfigBlocks.join('\n\n')}\n`; + } + // Write the clean config file with POSIX-compliant final newline const content = header + yamlContent; await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8'); @@ -2077,6 +2083,65 @@ class Installer { } } + /** + * Return static config blocks that should be preserved from a module schema + * even though they are not collected interactively. + * @param {string} moduleName - Module name + * @returns {Promise} Raw YAML/comment blocks to append to config.yaml + */ + async getStaticConfigBlocks(moduleName) { + const moduleSchemaPath = await this.resolveModuleSchemaPath(moduleName); + if (!moduleSchemaPath || !(await fs.pathExists(moduleSchemaPath))) { + return []; + } + + const content = await fs.readFile(moduleSchemaPath, 'utf8'); + const monorepoBlock = this.extractMonorepoContextBlock(content); + return monorepoBlock ? [monorepoBlock] : []; + } + + /** + * Resolve the source module.yaml path for a module. + * @param {string} moduleName - Module name + * @returns {Promise} Path to module.yaml or null if not found + */ + async resolveModuleSchemaPath(moduleName) { + if (this.configCollector.customModulePaths?.has(moduleName)) { + return path.join(this.configCollector.customModulePaths.get(moduleName), 'module.yaml'); + } + + const standardPath = path.join(getModulePath(moduleName), 'module.yaml'); + if (await fs.pathExists(standardPath)) { + return standardPath; + } + + const moduleSourcePath = await this.moduleManager.findModuleSource(moduleName, { silent: true }); + return moduleSourcePath ? path.join(moduleSourcePath, 'module.yaml') : null; + } + + /** + * Extract the monorepo context instructions and YAML block from module.yaml. + * @param {string} content - Raw module.yaml content + * @returns {string|null} Preserved block or null when absent + */ + extractMonorepoContextBlock(content) { + // Capture from the header comment through to the end of the monorepo_context YAML block. + // The block is expected at the end of the file, after any other config sections. + const headerIndex = content.indexOf('# --- Monorepo / Multi-Project Context Support ---'); + if (headerIndex === -1) { + return null; + } + + const block = content.slice(headerIndex).trimEnd(); + + // Sanity check: the block must contain the structured YAML key + if (!block.includes('monorepo_context:')) { + return null; + } + + return block; + } + /** * Install core with resolved dependencies * @param {string} bmadDir - BMAD installation directory