Compare commits

...

8 Commits

Author SHA1 Message Date
Nikolas Hor 7af9f5ca82
Merge b696e9a246 into 1040c3c306 2026-03-27 03:05:58 -03:00
Akhilesh Tyagi 1040c3c306
fix: correctly resolve output_folder paths outside project root (#2132)
* fix(bmad-init): correctly resolve output_folder paths outside project root

  When output_folder was set to an absolute path (e.g. /Users/me/outputs),
  the {project-root}/{value} result template stored it as
  {project-root}//absolute/path. resolve_project_root_placeholder then did
  a naive string replace, producing /project//absolute/path — a broken path
  that workflows could not resolve.

  For relative paths outside the root (e.g. ../../sibling), the same naive
  replace left un-normalized paths like /project/../../sibling in the
  resolved config, which some tools mishandled.

  Fix resolve_project_root_placeholder to strip the {project-root} token,
  detect whether the remainder is absolute (returning it directly) or
  relative (joining with project root and normalizing via os.path.normpath).

  Fix apply_result_template to skip the template entirely when raw_value is
  already an absolute path, and to normalize the result for relative-but-
  outside paths. This covers the bmad-init SKILL write path, which bakes
  the resolved path directly into config.yaml.

  Add 7 tests covering all three path cases (absolute, relative-with-
  traversal, normal in-project) for both functions.

* Address review comments

---------

Co-authored-by: Akhilesh Tyagi <akhilesh.t@nextiva.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-03-26 21:46:14 -05:00
Brian ed9dea9058
refactor: consolidate plugin.json metadata into marketplace.json (#2137)
Merge license, homepage, repository, keywords, and author from
plugin.json into marketplace.json and remove the redundant file.
The npx skills installer only reads marketplace.json for skill
discovery — plugin.json contributed no functional value.
2026-03-26 19:48:04 -05:00
Brian 3d8a89c7e1
feat: add .claude-plugin marketplace and plugin metadata (#2136) 2026-03-26 19:12:32 -05:00
github-actions[bot] 819d373e2e chore(release): v6.2.2 [skip ci] 2026-03-26 02:44:35 +00:00
Brian a5640c890d
chore: add v6.2.2 changelog and use CHANGELOG.md for GH release notes (#2127)
Update publish workflow to extract release notes from CHANGELOG.md
instead of using --generate-notes, with fallback if no entry found.
2026-03-25 21:43:25 -05:00
Brian 6dd0a97c1f
fix: update bmb module-definition path for skills/ restructure (#2126)
bmad-builder moved skills from src/skills/ to skills/ at repo root
for Claude Code plugin and Vercel Skills CLI compatibility.
2026-03-25 21:25:33 -05:00
Nikolas de Hor b696e9a246 fix: use double quotes in skill templates to prevent YAML parse errors
Default skill templates used single-quoted YAML values for name
and description fields. When descriptions contain apostrophes
(e.g., "agent's workflow"), the generated YAML frontmatter would
be invalid, causing parse failures in IDEs like Antigravity.

Switches templates to double quotes and escapes any double quotes
in the description during template rendering.

Fixes #1744
2026-03-11 19:40:24 -03:00
10 changed files with 228 additions and 15 deletions

View File

@ -0,0 +1,78 @@
{
"name": "bmad-method",
"owner": {
"name": "Brian (BMad) Madison"
},
"description": "Breakthrough Method of Agile AI-driven Development — a full-lifecycle framework with agents and workflows for analysis, planning, architecture, and implementation.",
"license": "MIT",
"homepage": "https://github.com/bmad-code-org/BMAD-METHOD",
"repository": "https://github.com/bmad-code-org/BMAD-METHOD",
"keywords": ["bmad", "agile", "ai", "orchestrator", "development", "methodology", "agents"],
"plugins": [
{
"name": "bmad-pro-skills",
"source": "./",
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
"version": "6.3.0",
"author": {
"name": "Brian (BMad) Madison"
},
"skills": [
"./src/core-skills/bmad-help",
"./src/core-skills/bmad-init",
"./src/core-skills/bmad-brainstorming",
"./src/core-skills/bmad-distillator",
"./src/core-skills/bmad-party-mode",
"./src/core-skills/bmad-shard-doc",
"./src/core-skills/bmad-advanced-elicitation",
"./src/core-skills/bmad-editorial-review-prose",
"./src/core-skills/bmad-editorial-review-structure",
"./src/core-skills/bmad-index-docs",
"./src/core-skills/bmad-review-adversarial-general",
"./src/core-skills/bmad-review-edge-case-hunter"
]
},
{
"name": "bmad-method-lifecycle",
"source": "./",
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
"version": "6.3.0",
"author": {
"name": "Brian (BMad) Madison"
},
"skills": [
"./src/bmm-skills/1-analysis/bmad-product-brief",
"./src/bmm-skills/1-analysis/bmad-agent-analyst",
"./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
"./src/bmm-skills/1-analysis/bmad-document-project",
"./src/bmm-skills/1-analysis/research/bmad-domain-research",
"./src/bmm-skills/1-analysis/research/bmad-market-research",
"./src/bmm-skills/1-analysis/research/bmad-technical-research",
"./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
"./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
"./src/bmm-skills/2-plan-workflows/bmad-create-prd",
"./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
"./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
"./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
"./src/bmm-skills/3-solutioning/bmad-agent-architect",
"./src/bmm-skills/3-solutioning/bmad-create-architecture",
"./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
"./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
"./src/bmm-skills/3-solutioning/bmad-generate-project-context",
"./src/bmm-skills/4-implementation/bmad-agent-dev",
"./src/bmm-skills/4-implementation/bmad-agent-sm",
"./src/bmm-skills/4-implementation/bmad-agent-qa",
"./src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev",
"./src/bmm-skills/4-implementation/bmad-dev-story",
"./src/bmm-skills/4-implementation/bmad-quick-dev",
"./src/bmm-skills/4-implementation/bmad-sprint-planning",
"./src/bmm-skills/4-implementation/bmad-sprint-status",
"./src/bmm-skills/4-implementation/bmad-code-review",
"./src/bmm-skills/4-implementation/bmad-create-story",
"./src/bmm-skills/4-implementation/bmad-correct-course",
"./src/bmm-skills/4-implementation/bmad-retrospective",
"./src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests"
]
}
]
}

View File

@ -120,7 +120,18 @@ jobs:
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest'
run: |
TAG="v$(node -p 'require("./package.json").version')"
gh release create "$TAG" --generate-notes
VERSION="${TAG#v}"
# Extract the current version's section from CHANGELOG.md
BODY=$(awk -v ver="$VERSION" '
/^## v/ { if (found) exit; if (index($0, "## v" ver)) found=1; next }
found { print }
' CHANGELOG.md)
if [ -z "$BODY" ]; then
echo "::warning::No CHANGELOG.md entry for $TAG — falling back to auto-generated notes"
gh release create "$TAG" --generate-notes
else
gh release create "$TAG" --notes "$BODY"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,5 +1,21 @@
# Changelog
## v6.2.2 - 2026-03-25
### ♻️ Refactoring
* Modernize module-help CSV to 13-column format with `after`/`before` dependency graph replacing sequence numbers (#2120)
* Rewrite bmad-help from procedural 8-step execution to outcome-based skill design (~50% shorter) (#2120)
### 🐛 Bug Fixes
* Update bmad-builder module-definition path from `src/module.yaml` to `skills/module.yaml` for bmad-builder v1.2.0 compatibility (#2126)
* Fix eslint config to ignore gitignored lock files (#2120)
### 📚 Documentation
* Close Epic 4.5 explanation gaps in Chinese (zh-CN): normalize command naming to current `bmad-*` convention and add cross-links across 9 explanation pages (#2102)
## v6.2.1 - 2026-03-24
### 🎁 Highlights

View File

@ -37,7 +37,19 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
| `--output-folder <path>` | Output folder path (see resolution rules below) | `_bmad-output` |
#### Output Folder Path Resolution
The value passed to `--output-folder` (or entered interactively) is resolved according to these rules:
| Input type | Example | Resolved as |
|------------|---------|-------------|
| Relative path (default) | `_bmad-output` | `<project-root>/_bmad-output` |
| Relative path with traversal | `../../shared-outputs` | Normalized absolute path — e.g. `/Users/me/shared-outputs` |
| Absolute path | `/Users/me/shared-outputs` | Used as-is — project root is **not** prepended |
The resolved path is what agents and workflows use at runtime when writing output files. Using an absolute path or a traversal-based relative path lets you direct all generated artifacts to a directory outside your project tree — useful for shared or monorepo setups.
### Other Options
@ -141,6 +153,7 @@ Invalid values will either:
:::tip[Best Practices]
- Use absolute paths for `--directory` to avoid ambiguity
- Use an absolute path for `--output-folder` when you want artifacts written outside the project tree (e.g. a shared monorepo outputs directory)
- Test flags locally before using in CI/CD pipelines
- Combine with `-y` for truly unattended installations
- Use `--debug` if you encounter issues during installation

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "bmad-method",
"version": "6.2.1",
"version": "6.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bmad-method",
"version": "6.2.1",
"version": "6.2.2",
"license": "MIT",
"dependencies": {
"@clack/core": "^1.0.0",

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method",
"version": "6.2.1",
"version": "6.2.2",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",

View File

@ -166,9 +166,27 @@ 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
if '{project-root}' not in value:
return value
# Strip the {project-root} token to inspect what remains, so we can
# correctly handle absolute paths stored as "{project-root}//absolute/path"
# (produced by the "{project-root}/{value}" template applied to an absolute value).
suffix = value.replace('{project-root}', '', 1)
# Strip the one path separator that follows the token (if any)
if suffix.startswith('/') or suffix.startswith('\\'):
remainder = suffix[1:]
else:
remainder = suffix
if os.path.isabs(remainder):
# The original value was an absolute path stored with a {project-root}/ prefix.
# Return the absolute path directly — no joining needed.
return remainder
# Relative path: join with project root and normalize to resolve any .. segments.
return os.path.normpath(os.path.join(str(project_root), remainder))
def parse_var_specs(vars_string):
@ -222,9 +240,22 @@ def apply_result_template(var_def, raw_value, context):
if not result_template:
return raw_value
# If the user supplied an absolute path and the template would prefix it with
# "{project-root}/", skip the template entirely to avoid producing a broken path
# like "/my/project//absolute/path".
if isinstance(raw_value, str) and os.path.isabs(raw_value):
return raw_value
ctx = dict(context)
ctx['value'] = raw_value
return expand_template(result_template, ctx)
result = expand_template(result_template, ctx)
# Normalize the resulting path to resolve any ".." segments (e.g. when the user
# entered a relative path such as "../../outside-dir").
if isinstance(result, str) and '{' not in result and os.path.isabs(result):
result = os.path.normpath(result)
return result
# =============================================================================

View File

@ -110,6 +110,37 @@ class TestResolveProjectRootPlaceholder(unittest.TestCase):
def test_non_string(self):
self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
def test_absolute_path_stored_with_prefix(self):
"""Absolute output_folder entered by user is stored as '{project-root}//abs/path'
by the '{project-root}/{value}' template. It must resolve to '/abs/path', not
'/project//abs/path'."""
result = resolve_project_root_placeholder(
'{project-root}//Users/me/outside', Path('/Users/me/myproject')
)
self.assertEqual(result, '/Users/me/outside')
def test_relative_path_with_traversal_is_normalized(self):
"""A relative path like '../../sibling' produces '{project-root}/../../sibling'
after the template. It must resolve to the normalized absolute path, not the
un-normalized string '/project/../../sibling'."""
result = resolve_project_root_placeholder(
'{project-root}/../../sibling', Path('/Users/me/myproject')
)
self.assertEqual(result, '/Users/sibling')
def test_relative_path_one_level_up(self):
result = resolve_project_root_placeholder(
'{project-root}/../outside-outputs', Path('/project/root')
)
self.assertEqual(result, '/project/outside-outputs')
def test_standard_relative_path_unchanged(self):
"""Normal in-project relative paths continue to work correctly."""
result = resolve_project_root_placeholder(
'{project-root}/_bmad-output', Path('/project/root')
)
self.assertEqual(result, '/project/root/_bmad-output')
class TestExpandTemplate(unittest.TestCase):
@ -147,6 +178,39 @@ class TestApplyResultTemplate(unittest.TestCase):
result = apply_result_template(var_def, 'English', {})
self.assertEqual(result, 'English')
def test_absolute_value_skips_project_root_template(self):
"""When the user enters an absolute path, the '{project-root}/{value}' template
must not be applied doing so would produce '/project//absolute/path'."""
var_def = {'result': '{project-root}/{value}'}
result = apply_result_template(
var_def, '/Users/me/shared-outputs', {'project-root': '/Users/me/myproject'}
)
self.assertEqual(result, '/Users/me/shared-outputs')
def test_relative_traversal_value_is_normalized(self):
"""A relative path like '../../outside' combined with the project-root template
must produce a clean normalized absolute path, not '/project/../../outside'."""
var_def = {'result': '{project-root}/{value}'}
result = apply_result_template(
var_def, '../../outside-dir', {'project-root': '/Users/me/myproject'}
)
self.assertEqual(result, '/Users/outside-dir')
def test_relative_one_level_up_is_normalized(self):
var_def = {'result': '{project-root}/{value}'}
result = apply_result_template(
var_def, '../sibling-outputs', {'project-root': '/project/root'}
)
self.assertEqual(result, '/project/sibling-outputs')
def test_normal_relative_value_unchanged(self):
"""Standard in-project relative paths still produce the expected joined path."""
var_def = {'result': '{project-root}/{value}'}
result = apply_result_template(
var_def, '_bmad-output', {'project-root': '/project/root'}
)
self.assertEqual(result, '/project/root/_bmad-output')
class TestLoadModuleYaml(unittest.TestCase):

View File

@ -4,7 +4,7 @@
modules:
bmad-builder:
url: https://github.com/bmad-code-org/bmad-builder
module-definition: src/module.yaml
module-definition: skills/module.yaml
code: bmb
name: "BMad Builder"
description: "Agent and Builder"

View File

@ -254,8 +254,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
getDefaultTemplate(artifactType) {
if (artifactType === 'agent') {
return `---
name: '{{name}}'
description: '{{description}}'
name: "{{name}}"
description: "{{description}}"
disable-model-invocation: true
---
@ -269,8 +269,8 @@ You must fully embody this agent's persona and follow all activation instruction
`;
}
return `---
name: '{{name}}'
description: '{{description}}'
name: "{{name}}"
description: "{{description}}"
---
# {{name}}
@ -319,7 +319,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
.replaceAll('{{name}}', artifact.name || '')
.replaceAll('{{module}}', artifact.module || 'core')
.replaceAll('{{path}}', pathToUse)
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
.replaceAll('{{description}}', (artifact.description || `${artifact.name} ${artifact.type || ''}`).replaceAll('"', String.raw`\"`))
.replaceAll('{{workflow_path}}', pathToUse);
return rendered;