Compare commits

..

2 Commits

Author SHA1 Message Date
Alex Verkhovsky 9ca0316674
refactor(quick-dev): eliminate spec-wip.md singleton (#2214)
* refactor(quick-dev): eliminate spec-wip.md singleton

Write directly to spec-{slug}.md with status: draft instead of using
a shared spec-wip.md file. Use draft status for resume detection in
step-01. Removes wipFile variable from all step frontmatter and
workflow initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(quick-dev): address PR review findings

- step-02: preserve Intent block on draft resume instead of regenerating from template (F1)
- step-01: resume existing draft on slug collision rather than creating -2 duplicate (F3)
- step-01: recognize `done` status and ingest as context instead of silently re-implementing (F4)
- step-oneshot: remove unused spec_file frontmatter declaration (F6)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:14:24 -07:00
Alex Verkhovsky 6cecab2626
chore(install): stop copying skill prompts to _bmad by default (#2182)
* chore(install): stop copying skill prompts to _bmad by default

Flip install_to_bmad default from true to false so skill directories
are cleaned from _bmad/ after IDE install. Skills are self-contained
in their IDE directories (.claude/skills/, etc.) and no longer need
duplicate copies in _bmad/.

Two skills (bmad-create-prd, bmad-validate-prd) opt back in via
explicit manifests because bmad-edit-prd cross-references their data
files. Also fixes broken bmm-skills/ path references and corrects
the file-ref validator module-to-source mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(install): make edit-prd self-contained and remove install_to_bmad

Give bmad-edit-prd its own copy of prd-purpose.md and replace the
cross-skill validation workflow reference with a skill invocation, so
all three PRD skills are fully self-contained. With no remaining
consumers, remove the install_to_bmad flag from manifests, CSV output,
the post-install cleanup loop, and the dedicated test file.

* feat(install): clean up skill directories from _bmad after IDE install

Skills are self-contained in IDE directories, so _bmad/ only needs
module-level files (config.yaml, _config/). After all IDE setups
complete, remove skill directories from _bmad/ via skill-manifest.csv.
Also cleans up skill dirs left by older installer versions.

* test(install): drop stale install_to_bmad column from suite 27 CSV row

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:02:59 -07:00
17 changed files with 263 additions and 225 deletions

View File

@ -0,0 +1,197 @@
# BMAD PRD Purpose
**The PRD is the top of the required funnel that feeds all subsequent product development work in rhw BMad Method.**
---
## What is a BMAD PRD?
A dual-audience document serving:
1. **Human Product Managers and builders** - Vision, strategy, stakeholder communication
2. **LLM Downstream Consumption** - UX Design → Architecture → Epics → Development AI Agents
Each successive document becomes more AI-tailored and granular.
---
## Core Philosophy: Information Density
**High Signal-to-Noise Ratio**
Every sentence must carry information weight. LLMs consume precise, dense content efficiently.
**Anti-Patterns (Eliminate These):**
- ❌ "The system will allow users to..." → ✅ "Users can..."
- ❌ "It is important to note that..." → ✅ State the fact directly
- ❌ "In order to..." → ✅ "To..."
- ❌ Conversational filler and padding → ✅ Direct, concise statements
**Goal:** Maximum information per word. Zero fluff.
---
## The Traceability Chain
**PRD starts the chain:**
```
Vision → Success Criteria → User Journeys → Functional Requirements → (future: User Stories)
```
**In the PRD, establish:**
- Vision → Success Criteria alignment
- Success Criteria → User Journey coverage
- User Journey → Functional Requirement mapping
- All requirements traceable to user needs
**Why:** Each downstream artifact (UX, Architecture, Epics, Stories) must trace back to documented user needs and business objectives. This chain ensures we build the right thing.
---
## What Makes Great Functional Requirements?
### FRs are Capabilities, Not Implementation
**Good FR:** "Users can reset their password via email link"
**Bad FR:** "System sends JWT via email and validates with database" (implementation leakage)
**Good FR:** "Dashboard loads in under 2 seconds for 95th percentile"
**Bad FR:** "Fast loading time" (subjective, unmeasurable)
### SMART Quality Criteria
**Specific:** Clear, precisely defined capability
**Measurable:** Quantifiable with test criteria
**Attainable:** Realistic within constraints
**Relevant:** Aligns with business objectives
**Traceable:** Links to source (executive summary or user journey)
### FR Anti-Patterns
**Subjective Adjectives:**
- ❌ "easy to use", "intuitive", "user-friendly", "fast", "responsive"
- ✅ Use metrics: "completes task in under 3 clicks", "loads in under 2 seconds"
**Implementation Leakage:**
- ❌ Technology names, specific libraries, implementation details
- ✅ Focus on capability and measurable outcomes
**Vague Quantifiers:**
- ❌ "multiple users", "several options", "various formats"
- ✅ "up to 100 concurrent users", "3-5 options", "PDF, DOCX, TXT formats"
**Missing Test Criteria:**
- ❌ "The system shall provide notifications"
- ✅ "The system shall send email notifications within 30 seconds of trigger event"
---
## What Makes Great Non-Functional Requirements?
### NFRs Must Be Measurable
**Template:**
```
"The system shall [metric] [condition] [measurement method]"
```
**Examples:**
- ✅ "The system shall respond to API requests in under 200ms for 95th percentile as measured by APM monitoring"
- ✅ "The system shall maintain 99.9% uptime during business hours as measured by cloud provider SLA"
- ✅ "The system shall support 10,000 concurrent users as measured by load testing"
### NFR Anti-Patterns
**Unmeasurable Claims:**
- ❌ "The system shall be scalable" → ✅ "The system shall handle 10x load growth through horizontal scaling"
- ❌ "High availability required" → ✅ "99.9% uptime as measured by cloud provider SLA"
**Missing Context:**
- ❌ "Response time under 1 second" → ✅ "API response time under 1 second for 95th percentile under normal load"
---
## Domain-Specific Requirements
**Auto-Detect and Enforce Based on Project Context**
Certain industries have mandatory requirements that must be present:
- **Healthcare:** HIPAA Privacy & Security Rules, PHI encryption, audit logging, MFA
- **Fintech:** PCI-DSS Level 1, AML/KYC compliance, SOX controls, financial audit trails
- **GovTech:** NIST framework, Section 508 accessibility (WCAG 2.1 AA), FedRAMP, data residency
- **E-Commerce:** PCI-DSS for payments, inventory accuracy, tax calculation by jurisdiction
**Why:** Missing these requirements in the PRD means they'll be missed in architecture and implementation, creating expensive rework. During PRD creation there is a step to cover this - during validation we want to make sure it was covered. For this purpose steps will utilize a domain-complexity.csv and project-types.csv.
---
## Document Structure (Markdown, Human-Readable)
### Required Sections
1. **Executive Summary** - Vision, differentiator, target users
2. **Success Criteria** - Measurable outcomes (SMART)
3. **Product Scope** - MVP, Growth, Vision phases
4. **User Journeys** - Comprehensive coverage
5. **Domain Requirements** - Industry-specific compliance (if applicable)
6. **Innovation Analysis** - Competitive differentiation (if applicable)
7. **Project-Type Requirements** - Platform-specific needs
8. **Functional Requirements** - Capability contract (FRs)
9. **Non-Functional Requirements** - Quality attributes (NFRs)
### Formatting for Dual Consumption
**For Humans:**
- Clear, professional language
- Logical flow from vision to requirements
- Easy for stakeholders to review and approve
**For LLMs:**
- ## Level 2 headers for all main sections (enables extraction)
- Consistent structure and patterns
- Precise, testable language
- High information density
---
## Downstream Impact
**How the PRD Feeds Next Artifacts:**
**UX Design:**
- User journeys → interaction flows
- FRs → design requirements
- Success criteria → UX metrics
**Architecture:**
- FRs → system capabilities
- NFRs → architecture decisions
- Domain requirements → compliance architecture
- Project-type requirements → platform choices
**Epics & Stories (created after architecture):**
- FRs → user stories (1 FR could map to 1-3 stories potentially)
- Acceptance criteria → story acceptance tests
- Priority → sprint sequencing
- Traceability → stories map back to vision
**Development AI Agents:**
- Precise requirements → implementation clarity
- Test criteria → automated test generation
- Domain requirements → compliance enforcement
- Measurable NFRs → performance targets
---
## Summary: What Makes a Great BMAD PRD?
**High Information Density** - Every sentence carries weight, zero fluff
**Measurable Requirements** - All FRs and NFRs are testable with specific criteria
**Clear Traceability** - Each requirement links to user need and business objective
**Domain Awareness** - Industry-specific requirements auto-detected and included
**Zero Anti-Patterns** - No subjective adjectives, implementation leakage, or vague quantifiers
**Dual Audience Optimized** - Human-readable AND LLM-consumable
**Markdown Format** - Professional, clean, accessible to all stakeholders
---
**Remember:** The PRD is the foundation. Quality here ripples through every subsequent phase. A dense, precise, well-traced PRD makes UX design, architecture, epic breakdown, and AI development dramatically more effective.

View File

@ -1,6 +1,6 @@
--- ---
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md' prdPurpose: '../data/prd-purpose.md'
--- ---
# Step E-1: Discovery & Understanding # Step E-1: Discovery & Understanding

View File

@ -1,7 +1,7 @@
--- ---
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdFile: '{prd_file_path}' prdFile: '{prd_file_path}'
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md' prdPurpose: '../data/prd-purpose.md'
--- ---
# Step E-1B: Legacy PRD Conversion Assessment # Step E-1B: Legacy PRD Conversion Assessment

View File

@ -2,7 +2,7 @@
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdFile: '{prd_file_path}' prdFile: '{prd_file_path}'
validationReport: '{validation_report_path}' # If provided validationReport: '{validation_report_path}' # If provided
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md' prdPurpose: '../data/prd-purpose.md'
--- ---
# Step E-2: Deep Review & Analysis # Step E-2: Deep Review & Analysis

View File

@ -1,7 +1,7 @@
--- ---
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdFile: '{prd_file_path}' prdFile: '{prd_file_path}'
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md' prdPurpose: '../data/prd-purpose.md'
--- ---
# Step E-3: Edit & Update # Step E-3: Edit & Update

View File

@ -1,7 +1,6 @@
--- ---
# File references (ONLY variables used in this step) # File references (ONLY variables used in this step)
prdFile: '{prd_file_path}' prdFile: '{prd_file_path}'
validationWorkflow: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-01-discovery.md'
--- ---
# Step E-4: Complete & Validate # Step E-4: Complete & Validate
@ -117,8 +116,7 @@ Display:
- Display: "This will run all 13 validation checks on the updated PRD." - Display: "This will run all 13 validation checks on the updated PRD."
- Display: "Preparing to validate: {prd_file_path}" - Display: "Preparing to validate: {prd_file_path}"
- Display: "**Proceeding to validation...**" - Display: "**Proceeding to validation...**"
- Read fully and follow: {validationWorkflow} (steps-v/step-v-01-discovery.md) - Invoke the `bmad-validate-prd` skill to run the complete validation workflow
- Note: This hands off to the validation workflow which will run its complete 13-step process
- **IF E (Edit More):** - **IF E (Edit More):**
- Display: "**Additional Edits**" - Display: "**Additional Edits**"

View File

@ -1,5 +1,4 @@
--- ---
wipFile: '{implementation_artifacts}/spec-wip.md'
deferred_work_file: '{implementation_artifacts}/deferred-work.md' deferred_work_file: '{implementation_artifacts}/deferred-work.md'
spec_file: '' # set at runtime for both routes before leaving this step spec_file: '' # set at runtime for both routes before leaving this step
--- ---
@ -21,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
1. Explicit argument 1. Explicit argument
Did the user pass a specific file path, spec name, or clear instruction this message? Did the user pass a specific file path, spec name, or clear instruction this message?
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: ready-for-dev, in-progress, or in-review) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-03 for ready/in-progress, step-04 for review). - If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it. - Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
2. Recent conversation 2. Recent conversation
@ -29,8 +28,8 @@ Before listing artifacts or prompting the user, check whether you already know t
Use the same routing as above. Use the same routing as above.
3. Otherwise — scan artifacts and ask 3. Otherwise — scan artifacts and ask
- `{wipFile}` exists? → Offer resume or archive. - Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
- Active specs (`ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new). - If `draft` selected: Set `spec_file`. **EARLY EXIT**`./step-02-plan.md` (resume planning from the draft)
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT**`./step-03-implement.md` - If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT**`./step-03-implement.md`
- If `in-review` selected: Set `spec_file`. **EARLY EXIT**`./step-04-review.md` - If `in-review` selected: Set `spec_file`. **EARLY EXIT**`./step-04-review.md`
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it. - Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
@ -65,7 +64,7 @@ Never ask extra questions if you already understand what the user intends.
- On **K**: Proceed as-is. - On **K**: Proceed as-is.
5. Route — choose exactly one: 5. Route — choose exactly one:
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists, append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`. Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT**`./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions. **a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.

View File

@ -1,5 +1,4 @@
--- ---
wipFile: '{implementation_artifacts}/spec-wip.md'
deferred_work_file: '{implementation_artifacts}/deferred-work.md' deferred_work_file: '{implementation_artifacts}/deferred-work.md'
--- ---
@ -12,11 +11,12 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
## INSTRUCTIONS ## INSTRUCTIONS
1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._ 1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{wipFile}`. 2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
3. Self-review against READY FOR DEVELOPMENT standard. 3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent}` is non-empty, substitute it for the `<frozen-after-approval>` block in your filled spec before writing. Write the result to `{spec_file}`.
4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human. 4. Self-review against READY FOR DEVELOPMENT standard.
5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens: 5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
- Show user the token count. - Show user the token count.
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks` - HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint. - On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
@ -26,7 +26,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit` Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit`
- **A**: Rename `{wipFile}` to `{spec_file}`, set status `ready-for-dev`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3. - **A**: Set status `ready-for-dev` in `{spec_file}`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
- **E**: Apply changes, then return to CHECKPOINT 1. - **E**: Apply changes, then return to CHECKPOINT 1.

View File

@ -1,6 +1,5 @@
--- ---
deferred_work_file: '{implementation_artifacts}/deferred-work.md' deferred_work_file: '{implementation_artifacts}/deferred-work.md'
spec_file: '' # set by step-01 before entering this step
--- ---
# Step One-Shot: Implement, Review, Present # Step One-Shot: Implement, Review, Present

View File

@ -70,10 +70,6 @@ Load and read full config from `{main_config}` and resolve:
YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`. YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`.
### 2. Paths ### 2. First Step Execution
- `wipFile` = `{implementation_artifacts}/spec-wip.md`
### 3. First Step Execution
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow. Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.

View File

@ -1,154 +0,0 @@
/**
* install_to_bmad Flag Design Contract Tests
*
* Unit tests against the functions that implement the install_to_bmad flag.
* These nail down the 4 core design decisions:
*
* 1. true/omitted skill stays in _bmad/ (default behavior)
* 2. false skill removed from _bmad/ after IDE install
* 3. No platform no cleanup runs (cleanup lives in installVerbatimSkills)
* 4. Mixed flags each skill evaluated independently
*
* Usage: node test/test-install-to-bmad.js
*/
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const { loadSkillManifest, getInstallToBmad } = require('../tools/installer/ide/shared/skill-manifest');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
async function runTests() {
console.log(`${colors.cyan}========================================`);
console.log('install_to_bmad — Design Contract Tests');
console.log(`========================================${colors.reset}\n`);
// ============================================================
// 1. true/omitted → getInstallToBmad returns true (keep in _bmad/)
// ============================================================
console.log(`${colors.yellow}Design decision 1: true or omitted → skill stays in _bmad/${colors.reset}\n`);
// Null manifest (no bmad-skill-manifest.yaml) → true
assert(getInstallToBmad(null, 'workflow.md') === true, 'null manifest defaults to true');
// Single-entry, flag omitted → true
assert(
getInstallToBmad({ __single: { type: 'skill' } }, 'workflow.md') === true,
'single-entry manifest with flag omitted defaults to true',
);
// Single-entry, explicit true → true
assert(
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: true } }, 'workflow.md') === true,
'single-entry manifest with explicit true returns true',
);
console.log('');
// ============================================================
// 2. false → getInstallToBmad returns false (remove from _bmad/)
// ============================================================
console.log(`${colors.yellow}Design decision 2: false → skill removed from _bmad/${colors.reset}\n`);
// Single-entry, explicit false → false
assert(
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: false } }, 'workflow.md') === false,
'single-entry manifest with explicit false returns false',
);
// loadSkillManifest round-trip: YAML with false is preserved through load
{
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-itb-'));
await fs.writeFile(path.join(tmpDir, 'bmad-skill-manifest.yaml'), 'type: skill\ninstall_to_bmad: false\n');
const loaded = await loadSkillManifest(tmpDir);
assert(getInstallToBmad(loaded, 'workflow.md') === false, 'loadSkillManifest preserves install_to_bmad: false through round-trip');
await fs.remove(tmpDir);
}
console.log('');
// ============================================================
// 3. No platform → cleanup only runs inside installVerbatimSkills
// (This is a design invariant: getInstallToBmad is only consulted
// during IDE install. Without a platform, the flag has no effect.)
// ============================================================
console.log(`${colors.yellow}Design decision 3: flag is a per-skill property, not a pipeline gate${colors.reset}\n`);
// The flag value is stored but doesn't trigger any side effects by itself.
// Cleanup is driven by reading the CSV column inside installVerbatimSkills.
// We verify the flag is just data — getInstallToBmad doesn't touch the filesystem.
{
const manifest = { __single: { type: 'skill', install_to_bmad: false } };
const result = getInstallToBmad(manifest, 'workflow.md');
assert(typeof result === 'boolean', 'getInstallToBmad returns a boolean (pure data, no side effects)');
assert(result === false, 'false value is faithfully returned for consumer to act on');
}
console.log('');
// ============================================================
// 4. Mixed flags → each skill evaluated independently
// ============================================================
console.log(`${colors.yellow}Design decision 4: mixed flags — each skill independent${colors.reset}\n`);
// Multi-entry manifest: different files can have different flags
{
const manifest = {
'workflow.md': { type: 'skill', install_to_bmad: false },
'other.md': { type: 'skill', install_to_bmad: true },
};
assert(getInstallToBmad(manifest, 'workflow.md') === false, 'multi-entry: workflow.md with false returns false');
assert(getInstallToBmad(manifest, 'other.md') === true, 'multi-entry: other.md with true returns true');
assert(getInstallToBmad(manifest, 'unknown.md') === true, 'multi-entry: unknown file defaults to true');
}
console.log('');
// ============================================================
// Summary
// ============================================================
console.log(`${colors.cyan}========================================`);
console.log('Results:');
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
console.log(`========================================${colors.reset}\n`);
if (failed === 0) {
console.log(`${colors.green}All install_to_bmad contract tests passed!${colors.reset}\n`);
process.exit(0);
} else {
console.log(`${colors.red}Some install_to_bmad contract tests failed${colors.reset}\n`);
process.exit(1);
}
}
runTests().catch((error) => {
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
console.error(error.stack);
process.exit(1);
});

View File

@ -59,8 +59,8 @@ async function createTestBmadFixture() {
await fs.writeFile( await fs.writeFile(
path.join(fixtureDir, '_config', 'skill-manifest.csv'), path.join(fixtureDir, '_config', 'skill-manifest.csv'),
[ [
'canonicalId,name,description,module,path,install_to_bmad', 'canonicalId,name,description,module,path',
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"', '"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md"',
'', '',
].join('\n'), ].join('\n'),
); );
@ -103,8 +103,8 @@ async function createSkillCollisionFixture() {
await fs.writeFile( await fs.writeFile(
path.join(configDir, 'skill-manifest.csv'), path.join(configDir, 'skill-manifest.csv'),
[ [
'canonicalId,name,description,module,path,install_to_bmad', 'canonicalId,name,description,module,path',
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md","true"', '"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md"',
'', '',
].join('\n'), ].join('\n'),
); );
@ -1306,7 +1306,7 @@ async function runTests() {
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8'); const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
await fs.writeFile( await fs.writeFile(
path.join(configDir27, 'skill-manifest.csv'), path.join(configDir27, 'skill-manifest.csv'),
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md","true"\n', existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md"\n',
); );
// Run Claude Code setup (which triggers cleanup then install) // Run Claude Code setup (which triggers cleanup then install)

View File

@ -132,6 +132,10 @@ class Installer {
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds); await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
// Skills are now in IDE directories — remove redundant copies from _bmad/.
// Also cleans up skill dirs left by older installer versions.
await this._cleanupSkillDirs(paths.bmadDir);
const restoreResult = await this._restoreUserFiles(paths, updateState); const restoreResult = await this._restoreUserFiles(paths, updateState);
// Render consolidated summary // Render consolidated summary
@ -413,6 +417,33 @@ class Installer {
} }
} }
/**
* Remove skill directories from _bmad/ after IDE installation.
* Skills are self-contained in IDE directories, so _bmad/ only needs
* module-level files (config.yaml, _config/, etc.).
* Also cleans up skill dirs left by older installer versions.
* @param {string} bmadDir - BMAD installation directory
*/
async _cleanupSkillDirs(bmadDir) {
const csv = require('csv-parse/sync');
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return;
const csvContent = await fs.readFile(csvPath, 'utf8');
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
const bmadFolderName = path.basename(bmadDir);
const bmadPrefix = bmadFolderName + '/';
for (const record of records) {
if (!record.path) continue;
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
if (await fs.pathExists(sourceDir)) {
await fs.remove(sourceDir);
}
}
}
/** /**
* Restore custom and modified files that were backed up before the update. * Restore custom and modified files that were backed up before the update.
* No-op for fresh installs (updateState is null). * No-op for fresh installs (updateState is null).

View File

@ -9,7 +9,6 @@ const {
loadSkillManifest: loadSkillManifestShared, loadSkillManifest: loadSkillManifestShared,
getCanonicalId: getCanonicalIdShared, getCanonicalId: getCanonicalIdShared,
getArtifactType: getArtifactTypeShared, getArtifactType: getArtifactTypeShared,
getInstallToBmad: getInstallToBmadShared,
} = require('../ide/shared/skill-manifest'); } = require('../ide/shared/skill-manifest');
// Load package.json for version info // Load package.json for version info
@ -42,11 +41,6 @@ class ManifestGenerator {
return getArtifactTypeShared(manifest, filename); return getArtifactTypeShared(manifest, filename);
} }
/** Delegate to shared skill-manifest module */
getInstallToBmad(manifest, filename) {
return getInstallToBmadShared(manifest, filename);
}
/** /**
* Clean text for CSV output by normalizing whitespace. * Clean text for CSV output by normalizing whitespace.
* Note: Quote escaping is handled by escapeCsv() at write time. * Note: Quote escaping is handled by escapeCsv() at write time.
@ -127,7 +121,7 @@ class ManifestGenerator {
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints. * Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
* A directory is discovered as a skill when it contains a SKILL.md file with * A directory is discovered as a skill when it contains a SKILL.md file with
* valid name/description frontmatter (name must match directory name). * valid name/description frontmatter (name must match directory name).
* Manifest YAML is loaded only when present for install_to_bmad and agent metadata. * Manifest YAML is loaded only when present for agent metadata.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths). * Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/ */
async collectSkills() { async collectSkills() {
@ -156,7 +150,7 @@ class ManifestGenerator {
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug); const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
if (skillMeta) { if (skillMeta) {
// Load manifest when present (for install_to_bmad and agent metadata) // Load manifest when present (for agent metadata)
const manifest = await this.loadSkillManifest(dir); const manifest = await this.loadSkillManifest(dir);
const artifactType = this.getArtifactType(manifest, skillFile); const artifactType = this.getArtifactType(manifest, skillFile);
@ -182,7 +176,6 @@ class ManifestGenerator {
module: moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId, canonicalId,
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
}); });
// Add to files list // Add to files list
@ -472,7 +465,7 @@ class ManifestGenerator {
const csvPath = path.join(cfgDir, 'skill-manifest.csv'); const csvPath = path.join(cfgDir, 'skill-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
let csvContent = 'canonicalId,name,description,module,path,install_to_bmad\n'; let csvContent = 'canonicalId,name,description,module,path\n';
for (const skill of this.skills) { for (const skill of this.skills) {
const row = [ const row = [
@ -481,7 +474,6 @@ class ManifestGenerator {
escapeCsv(skill.description), escapeCsv(skill.description),
escapeCsv(skill.module), escapeCsv(skill.module),
escapeCsv(skill.path), escapeCsv(skill.path),
escapeCsv(skill.install_to_bmad),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }

View File

@ -183,18 +183,6 @@ class ConfigDrivenIdeSetup {
count++; count++;
} }
// Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false"
for (const record of records) {
if (record.install_to_bmad === 'false') {
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
const sourceFile = path.join(bmadDir, relativePath);
const sourceDir = path.dirname(sourceFile);
if (await fs.pathExists(sourceDir)) {
await fs.remove(sourceDir);
}
}
}
return count; return count;
} }

View File

@ -54,19 +54,4 @@ function getArtifactType(manifest, filename) {
return null; return null;
} }
/** module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };
* Get the install_to_bmad flag for a specific file from a loaded skill manifest.
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
* @param {string} filename - Source filename to look up
* @returns {boolean} install_to_bmad value (defaults to true)
*/
function getInstallToBmad(manifest, filename) {
if (!manifest) return true;
// Single-entry manifest applies to all files in the directory
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
// Multi-entry: look up by filename directly
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
return true;
}
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad };

View File

@ -156,8 +156,15 @@ function mapInstalledToSource(refPath) {
// Skip install-only paths (generated at install time, not in source) // Skip install-only paths (generated at install time, not in source)
if (isInstallOnly(cleaned)) return null; if (isInstallOnly(cleaned)) return null;
// core/, bmm/, and utility/ are directly under src/ // Map installed module names to their source directory names
if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) { // _bmad/core/ → src/core-skills/, _bmad/bmm/ → src/bmm-skills/
if (cleaned.startsWith('core/')) {
return path.join(SRC_DIR, 'core-skills', cleaned.slice('core/'.length));
}
if (cleaned.startsWith('bmm/')) {
return path.join(SRC_DIR, 'bmm-skills', cleaned.slice('bmm/'.length));
}
if (cleaned.startsWith('utility/')) {
return path.join(SRC_DIR, cleaned); return path.join(SRC_DIR, cleaned);
} }