diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1a57e84..6283c17 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "name": "bmad", "source": "./plugins/bmad", "description": "BMAD Method - Breakthrough Method for Agile AI-Driven Development", - "version": "6.2.0.0" + "version": "6.2.0.1" } ] } diff --git a/.husky/pre-commit b/.husky/pre-commit index a92e711..f9dee0d 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ +set -e bun run lint:staged bun run typecheck diff --git a/.husky/pre-push b/.husky/pre-push index 91d0910..008e0da 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,4 @@ +set -e bun run typecheck bun run lint bun run validate diff --git a/.plugin-version b/.plugin-version index eb9f566..04deb72 100644 --- a/.plugin-version +++ b/.plugin-version @@ -1 +1 @@ -v6.2.0.0 +v6.2.0.1 diff --git a/README.md b/README.md index 18f48fe..f408364 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ -**Plugin version:** v6.2.0.0 +**Plugin version:** v6.2.0.1 | Module | Version | Released | Last Synced | |---|---|---|---| diff --git a/docs/script-pipeline.md b/docs/script-pipeline.md new file mode 100644 index 0000000..ef731bf --- /dev/null +++ b/docs/script-pipeline.md @@ -0,0 +1,247 @@ +# Script Pipeline + +How the sync, generation, validation, and release scripts work together. + +## Data Flow + +```text +.upstream// (pinned to version tag) + │ + ▼ +[sync-upstream-content.ts] ── copies files + rewrites paths + │ + ├──▶ plugins/bmad/skills//{instructions, steps, data} + ├──▶ plugins/bmad/_shared/{tasks/, tea-index.csv} + └──▶ .upstream-versions/.json (updated) + │ + ▼ +[generate-agents.ts] ── converts .agent.yaml → markdown + │ + └──▶ plugins/bmad/agents/.md + │ + ▼ +[generate-skills.ts] ── generates SKILL.md from workflow metadata + │ + └──▶ plugins/bmad/skills//SKILL.md + │ + ▼ +[generate-agent-manifest.ts] ── builds agent registry + │ + └──▶ plugins/bmad/_shared/agent-manifest.csv + │ + ▼ +[validate-upstream-coverage.ts] ── checks everything matches + │ + └──▶ exit 0 (pass) or exit 1 (fail) +``` + +## Upstream Sources + +Five repositories contribute content, each pinned to a version tag in +`.upstream-versions/.json`: + +```csv +ID,Repository,Layout,Description +core,bmadcode/BMAD-METHOD,categorized,Main framework — agents + skills + tasks +tea,bmad-code-org/bmad-method-test-architecture-enterprise,flat,Testing and architecture skills +bmb,bmad-code-org/bmad-builder,flat,Builder skills +cis,bmad-code-org/bmad-module-creative-intelligence-suite,flat,Creative intelligence skills +gds,bmad-code-org/bmad-module-game-dev-studio,categorized,Game development skills +``` + +**Layout** determines directory structure: + +- **Categorized** (core, gds): `contentRoot///` +- **Flat** (tea, bmb, cis): `contentRoot//` + +The `workflow-iterator.ts` library handles both transparently. + +## Scripts + +### sync-upstream-content.ts + +Copies supporting files from upstream into the plugin, applying path rewrites. + +- **Reads:** upstream repos (checked out to pinned version), `.upstream-versions/` +- **Writes:** `plugins/bmad/skills/`, `plugins/bmad/_shared/`, `.upstream-versions/` +- **Skips:** `workflow.md`, `workflow.yaml` (plugin generates its own SKILL.md) +- **Path rewrites:** transforms `{project-root}/_bmad/...` references to + `${CLAUDE_PLUGIN_ROOT}/skills/...` via `path-rewriter.ts` +- **CLI:** `bun run sync [--source ] [--dry-run]` + +Without `--source`, runs all sources and bumps plugin version to `.0`. + +### generate-agents.ts + +Converts upstream agent definitions to plugin agent markdown files. + +- **Reads:** `.agent.yaml` files (or `SKILL.md` for newer format) from upstream +- **Writes:** `plugins/bmad/agents/.md` +- **Process:** parses YAML metadata (title, role, identity, principles, menu) → + generates markdown with activation section. SKILL.md agents are copied directly. +- **CLI:** `bun run generate:agents [--source ] [--dry-run]` + +### generate-skills.ts + +Generates `SKILL.md` files from workflow metadata. + +- **Reads:** `workflow.yaml`/`workflow.md` frontmatter from upstream skill dirs +- **Writes:** `plugins/bmad/skills//SKILL.md` +- **Skips:** generation when upstream already provides `SKILL.md` +- **Process:** extracts name/description from workflow metadata, detects + sub-workflows, looks up corresponding agent, generates SKILL.md +- **CLI:** `bun run generate:skills [--source ] [--dry-run]` + +### generate-agent-manifest.ts + +Builds the agent registry CSV used by party-mode and advanced-elicitation. + +- **Reads:** all `plugins/bmad/agents/*.md` +- **Writes:** `plugins/bmad/_shared/agent-manifest.csv` +- **CLI:** `bun run generate:manifest [--dry-run]` + +No `--source` filter — the manifest is always a complete registry. + +### validate-upstream-coverage.ts + +Checks that all upstream content is accounted for in the plugin. + +Seven validation checks: + +```csv +Check,What it validates +checkSync,Upstream repos at tracked versions +checkAgents,Upstream agents ↔ plugin agent files +checkWorkflows,Upstream workflows ↔ plugin skill dirs ↔ plugin.json +checkContent,Supporting files synced correctly +checkVersion,.upstream-versions ↔ upstream package.json +checkAgentSkills,Agent menu workflows ↔ plugin skill dirs +checkPaths,Path rewrites complete and valid +``` + +- **Output:** pass (exit 0) or fail with gaps (exit 1), plus workaround warnings +- **CLI:** `bun run validate [--verbose]` + +### sync-all.ts + +Orchestrator that runs the full pipeline sequentially: + +```text +sync → generate:agents → generate:skills +``` + +Passes through `--source ` and `--dry-run`. Exits on first failure. + +- **CLI:** `bun run sync-all [--source ] [--dry-run]` + +### clean-orphaned-skills.ts + +Detects and removes skill directories that no longer match any upstream source. + +- **Reads:** all upstream sources, plugin-only lists, workaround mappings +- **Removes:** orphaned dirs in `plugins/bmad/skills/` +- **CLI:** `bun run clean:orphaned [--dry-run]` + +### bump-core.ts + +Bumps plugin version to track a new core upstream release. + +- Fetches latest tag from `.upstream/BMAD-METHOD` +- Updates `.upstream-versions/core.json` +- Derives plugin version: `.0` (resets patch) +- Updates all 4 version files + README badge +- **CLI:** `bun run bump-core [--tag v6.0.2] [--dry-run] [--yes]` + +### bump-module.ts + +Bumps plugin version for a module update (non-core). + +- Fetches latest tag from the module's upstream repo +- Updates `.upstream-versions/.json` +- Increments plugin patch: `.X` → `.` +- Updates all 4 version files + README badge +- **CLI:** `bun run bump-module -- --source [--tag v1.7.1] [--dry-run] [--yes]` + +### update-readme-version.ts + +Updates the README version table and badge JSON files. + +- **Reads:** `.upstream-versions/`, `.plugin-version`, upstream git tags +- **Writes:** `README.md` (version table + badges), `.github/badges/*.json` +- **CLI:** `bun run update-readme` + +### release.sh + +Two-phase release workflow. See `./docs/releasing.md` for full details. + +- **Phase 1 (prepare):** beads sync → release branch → PR to main → wait for CI +- **Phase 2 (finish):** merge PR → tag main → GitHub release → return to dev +- **CLI:** `./scripts/release.sh [version]` or `./scripts/release.sh --after-ci` + +## Shared Libraries + +```csv +Library,Key exports,Used by +upstream-sources.ts,"UPSTREAM_SOURCES, getSource(), readVersion(), workflowWorkarounds","sync, generate-*, validate, clean, bump-*" +config.ts,"ROOT, PLUGIN, PLUGIN_JSON_PATH",all scripts +path-rewriter.ts,"rewriteFileContent(), buildRewriteMap()",sync +workflow-iterator.ts,"getWorkflowEntries()","generate-skills, validate, clean" +fs-utils.ts,"listFilesRecursive(), normalize()","sync, validate" +git-utils.ts,gitInUpstream(),"sync, bump-*" +output.ts,"pass(), fail(), warn(), section()",validate +bump-utils.ts,"VERSION_FILES, fetchLatestTag(), updateJsonVersionFiles()","bump-core, bump-module" +``` + +## Git Hooks + +```csv +Hook,Commands,Scope +pre-commit,"lint:staged (biome --staged), typecheck",Staged files only +pre-push,"typecheck, lint (biome full repo), validate",Full project +``` + +**Gap:** The pre-push hook runs all three commands sequentially without `set -e`. +If lint fails, validate still runs and the hook exits with validate's exit code +(which may be 0). This means lint failures don't block pushes. + +## Version Files + +Four files must stay in sync: + +```csv +File,Format,Prefix +.plugin-version,plain text,v (e.g. v6.2.0.0) +package.json,JSON version field,no prefix (6.2.0.0) +plugins/bmad/.claude-plugin/plugin.json,JSON version field,no prefix +.claude-plugin/marketplace.json,JSON version field,no prefix +``` + +**Versioning scheme:** `.X` where X is a patch counter. +Core bump resets X to 0. Module bumps increment X. + +## Common Command Sequences + +Full sync after core bump: + +```sh +bun run bump-core +bun run sync-all # sync → generate:agents → generate:skills +bun run generate:manifest +bun run validate +``` + +Module-only update: + +```sh +bun run bump-module -- --source tea +bun run sync-all -- --source tea +bun run validate +``` + +Release: + +```sh +./scripts/release.sh # current version +./scripts/release.sh 6.2.1.0 # bump + release +./scripts/release.sh --after-ci # resume after CI fix +``` diff --git a/package.json b/package.json index 1827220..d68fbfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bmad-plugin", - "version": "6.2.0.0", + "version": "6.2.0.1", "type": "module", "scripts": { "prepare": "husky", diff --git a/plugins/bmad/.claude-plugin/plugin.json b/plugins/bmad/.claude-plugin/plugin.json index 96adde1..7db7bbc 100644 --- a/plugins/bmad/.claude-plugin/plugin.json +++ b/plugins/bmad/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "bmad", - "version": "6.2.0.0", + "version": "6.2.0.1", "description": "BMAD Method - Breakthrough Method for Agile AI-Driven Development", "author": { "name": "PabloLION", diff --git a/scripts/clean-orphaned-skills.ts b/scripts/clean-orphaned-skills.ts index d0b8ad0..cb00b92 100644 --- a/scripts/clean-orphaned-skills.ts +++ b/scripts/clean-orphaned-skills.ts @@ -22,7 +22,7 @@ const SKILLS_DIR = join(PLUGIN, 'skills'); /** * Build a set of all valid skill names by iterating upstream sources. - * Includes plugin-only skills and workaround-mapped names. + * Includes plugin-only skills and agent ref mapped names. */ async function getValidSkillNames(): Promise> { const valid = new Set(); diff --git a/scripts/generate-agents.ts b/scripts/generate-agents.ts index 913ba7f..0d4147b 100644 --- a/scripts/generate-agents.ts +++ b/scripts/generate-agents.ts @@ -68,7 +68,7 @@ function extractSkillName( if (!match) return undefined; const workflowName = match[1]!; - const workarounds = source.workflowWorkarounds ?? {}; + const workarounds = source.agentRefMappings ?? {}; return workarounds[workflowName] ?? workflowName; } @@ -266,7 +266,9 @@ async function processSource(source: UpstreamSource): Promise { if (format === 'skill') { // SKILL.md-based agent — copy directly if (DRY_RUN) { - console.log(` [dry-run] would copy: agents/${slug}.md (from SKILL.md)`); + console.log( + ` [dry-run] would copy: agents/${slug}.md (from SKILL.md)`, + ); } else { const content = await Bun.file(path).text(); await Bun.write(outPath, content); diff --git a/scripts/generate-skills.ts b/scripts/generate-skills.ts index 92cb8af..4ef1326 100644 --- a/scripts/generate-skills.ts +++ b/scripts/generate-skills.ts @@ -262,9 +262,7 @@ async function processSource(source: UpstreamSource): Promise { // Skip generation if upstream provides SKILL.md (sync already copied it) const upstreamSkillMd = join(entry.upstreamDir, 'SKILL.md'); if (await exists(upstreamSkillMd)) { - console.log( - ` skip: ${entry.skillName} (upstream provides SKILL.md)`, - ); + console.log(` skip: ${entry.skillName} (upstream provides SKILL.md)`); continue; } diff --git a/scripts/lib/checks/agent-skills.ts b/scripts/lib/checks/agent-skills.ts index 93adf52..78660e6 100644 --- a/scripts/lib/checks/agent-skills.ts +++ b/scripts/lib/checks/agent-skills.ts @@ -54,19 +54,14 @@ function checkAgentWorkflows( agentName: string, workflows: string[], skillDirs: Set, - workarounds: Record, + mappings: Record, planned: Set, ): void { for (const wf of workflows) { - const skillName = workarounds[wf] ?? wf; + const skillName = mappings[wf] ?? wf; if (skillDirs.has(skillName)) { - if (workarounds[wf]) { - warn( - `[${sourceId}] ${agentName} → ${skillName} (workaround — upstream "${wf}" ≠ plugin "${skillName}")`, - ); - } else { - pass(`[${sourceId}] ${agentName} → ${skillName}`); - } + // Mapped refs are expected — pass silently like direct matches + pass(`[${sourceId}] ${agentName} → ${skillName}`); } else if (planned.has(wf)) { warn( `[${sourceId}] ${agentName} references planned workflow "${wf}" (not yet implemented upstream)`, @@ -96,7 +91,7 @@ export async function checkAgentSkills(): Promise { if (!(await exists(agentsDir))) continue; const entries = await readdir(agentsDir, { withFileTypes: true }); - const workarounds = source.workflowWorkarounds ?? {}; + const workarounds = source.agentRefMappings ?? {}; const planned = source.plannedWorkflows ?? new Set(); for (const entry of entries) { diff --git a/scripts/lib/checks/workflows.ts b/scripts/lib/checks/workflows.ts index 2692893..9840c52 100644 --- a/scripts/lib/checks/workflows.ts +++ b/scripts/lib/checks/workflows.ts @@ -44,19 +44,6 @@ function getAllPluginOnlySkills(): Set { return all; } -/** Build inverse workaround map: mapped name → original name (for display only). */ -function getInverseWorkarounds(): Record { - const inverse: Record = {}; - for (const source of getEnabledSources()) { - for (const [original, mapped] of Object.entries( - source.workflowWorkarounds ?? {}, - )) { - inverse[mapped] = original; - } - } - return inverse; -} - /** Collect plugin skill directory names. */ async function getPluginDirectories(): Promise> { const entries = await readdir(join(PLUGIN, 'skills'), { @@ -97,21 +84,15 @@ function sorted(set: Set): string[] { } /** Check upstream workflows have corresponding plugin directories. - * Names in `upstream` are already mapped via per-source workarounds. - * Use inverse map for display only. */ + * Names in `upstream` are already mapped via per-source agent ref mappings. */ function checkUpstreamToDirs( upstream: Set, directories: Set, - inverse: Record, ): void { section('Skills: Upstream → Plugin Directories'); for (const name of sorted(upstream)) { if (directories.has(name)) { - if (inverse[name]) { - warn(`${inverse[name]} → ${name} (workaround)`); - } else { - pass(name); - } + pass(name); } else { fail(`Missing directory: skills/${name}`); } @@ -176,10 +157,9 @@ export async function checkWorkflows(): Promise { const upstream = await getUpstreamWorkflows(); const directories = await getPluginDirectories(); const manifest = await getManifestCommands(); - const inverse = getInverseWorkarounds(); const pluginOnly = getAllPluginOnlySkills(); - checkUpstreamToDirs(upstream, directories, inverse); + checkUpstreamToDirs(upstream, directories); checkUpstreamToManifest(upstream, manifest); checkDirsManifestAlignment(directories, manifest); checkPluginOnlySkills(upstream, directories, pluginOnly); diff --git a/scripts/lib/upstream-sources.ts b/scripts/lib/upstream-sources.ts index 5d12215..fa11edd 100644 --- a/scripts/lib/upstream-sources.ts +++ b/scripts/lib/upstream-sources.ts @@ -42,7 +42,12 @@ export interface UpstreamSource { skipContentPatterns?: RegExp[]; /** Workflows referenced by agents but not yet implemented upstream (warn, not fail) */ plannedWorkflows?: Set; - workflowWorkarounds?: Record; + /** + * Maps stale agent YAML workflow refs to current plugin skill names. + * Only needed when upstream agent YAML hasn't been updated to match + * renamed workflow directories. Remove entries when upstream catches up. + */ + agentRefMappings?: Record; pluginOnlySkills?: Set; pluginOnlyAgents?: Set; sharedFileTargets?: Record; @@ -62,7 +67,7 @@ export const UPSTREAM_SOURCES: UpstreamSource[] = [ skipWorkflows: new Set(['automate']), skipDirs: new Set(['_shared', 'templates', 'workflows']), skipContentFiles: new Set(['workflow.md', 'workflow.yaml']), - workflowWorkarounds: {}, + agentRefMappings: {}, pluginOnlySkills: new Set(['help', 'init', 'status', 'brainstorming']), pluginOnlyAgents: new Set(['bmad-master', 'tech-writer']), sharedFileTargets: {}, @@ -82,8 +87,9 @@ export const UPSTREAM_SOURCES: UpstreamSource[] = [ /^validation-report-.*\.md$/, /^workflow-plan.*\.md$/, ], - workflowWorkarounds: { - // TEA agent YAML still references old un-prefixed names + agentRefMappings: { + // TEA agent YAML still references old un-prefixed names. + // Remove when upstream updates refs to match actual dir names. 'teach-me-testing': 'bmad-teach-me-testing', framework: 'bmad-testarch-framework', atdd: 'bmad-testarch-atdd', @@ -109,7 +115,7 @@ export const UPSTREAM_SOURCES: UpstreamSource[] = [ flatWorkflows: true, skipDirs: new Set(['_shared', 'templates']), skipContentFiles: new Set(['workflow.md', 'workflow.yaml']), - workflowWorkarounds: {}, + agentRefMappings: {}, pluginOnlySkills: new Set(), pluginOnlyAgents: new Set(), sharedFileTargets: {}, @@ -125,7 +131,7 @@ export const UPSTREAM_SOURCES: UpstreamSource[] = [ flatWorkflows: true, skipDirs: new Set(['_shared', 'templates']), skipContentFiles: new Set(['workflow.md', 'workflow.yaml']), - workflowWorkarounds: {}, + agentRefMappings: {}, pluginOnlySkills: new Set(), pluginOnlyAgents: new Set(), sharedFileTargets: {}, @@ -141,24 +147,9 @@ export const UPSTREAM_SOURCES: UpstreamSource[] = [ flatWorkflows: false, skipDirs: new Set(['_shared', 'templates']), skipContentFiles: new Set(['workflow.md', 'workflow.yaml']), - workflowWorkarounds: { - 'document-project': 'gds-document-project', - 'generate-project-context': 'gds-generate-project-context', - // Collisions with core (4-implementation + bmad-quick-flow) - 'code-review': 'gds-code-review', - 'correct-course': 'gds-correct-course', - 'create-story': 'gds-create-story', - 'dev-story': 'gds-dev-story', - 'quick-dev': 'gds-quick-dev', - 'quick-spec': 'gds-quick-spec', - retrospective: 'gds-retrospective', - 'sprint-planning': 'gds-sprint-planning', - 'sprint-status': 'gds-sprint-status', - // Collisions with TEA (gametest workflows) - automate: 'gds-automate', - 'test-design': 'gds-test-design', - 'test-review': 'gds-test-review', - }, + // GDS v0.2.2 dirs already use gds- prefix; agents are SKILL.md (no YAML refs). + // No agent ref mappings needed. + agentRefMappings: {}, plannedWorkflows: new Set(['quick-prototype']), pluginOnlySkills: new Set(), pluginOnlyAgents: new Set(['tech-writer']), diff --git a/scripts/lib/workflow-iterator.ts b/scripts/lib/workflow-iterator.ts index 84ba4ce..8b82c3e 100644 --- a/scripts/lib/workflow-iterator.ts +++ b/scripts/lib/workflow-iterator.ts @@ -40,7 +40,7 @@ async function getFlatEntries( const dirEntries = await readdir(workflowsRoot, { withFileTypes: true }); const skipDirs = source.skipDirs ?? new Set(); const skipWorkflows = source.skipWorkflows ?? new Set(); - const workarounds = source.workflowWorkarounds ?? {}; + const workarounds = source.agentRefMappings ?? {}; for (const entry of dirEntries) { if (!entry.isDirectory() || skipDirs.has(entry.name)) continue; @@ -69,7 +69,7 @@ async function getCategorizedEntries( const categories = await readdir(workflowsRoot, { withFileTypes: true }); const skipDirs = source.skipDirs ?? new Set(); const skipWorkflows = source.skipWorkflows ?? new Set(); - const workarounds = source.workflowWorkarounds ?? {}; + const workarounds = source.agentRefMappings ?? {}; for (const cat of categories) { if (!cat.isDirectory() || skipDirs.has(cat.name)) continue; diff --git a/scripts/validate-upstream-coverage.ts b/scripts/validate-upstream-coverage.ts index 6dd819b..d59c2c2 100644 --- a/scripts/validate-upstream-coverage.ts +++ b/scripts/validate-upstream-coverage.ts @@ -10,7 +10,6 @@ * 5. Naming consistency — SKILL.md frontmatter name ↔ directory name * 6. Agent–skill cross-reference — agent menu workflows ↔ plugin skill dirs * - * Known workarounds are documented in config.ts and printed with ⚠ markers. * Exit 0 = pass, Exit 1 = gaps found. */ @@ -23,14 +22,7 @@ import { checkVersion, checkWorkflows, } from './lib/checks/index.ts'; -import { - GREEN, - hasFailed, - RED, - RESET, - setVerbose, - YELLOW, -} from './lib/output.ts'; +import { GREEN, hasFailed, RED, RESET, setVerbose } from './lib/output.ts'; import { getEnabledSources } from './lib/upstream-sources.ts'; setVerbose(process.argv.includes('--verbose')); @@ -48,20 +40,11 @@ await checkVersion(); await checkAgentSkills(); await checkPaths(); -const workaroundCount = getEnabledSources().reduce( - (sum, s) => sum + Object.keys(s.workflowWorkarounds ?? {}).length, - 0, -); - console.log(''); if (hasFailed()) { console.log(`${RED}✗ Validation failed — gaps found above.${RESET}`); process.exit(1); -} else if (workaroundCount > 0) { - console.log( - `${GREEN}✓ All upstream content covered.${RESET} ${YELLOW}(${workaroundCount} workarounds — see ⚠ above)${RESET}`, - ); } else { console.log( `${GREEN}✓ All upstream content covered — agents, skills, and files in sync.${RESET}`,