Compare commits
6 Commits
8f4af91646
...
dfffabd2ad
| Author | SHA1 | Date |
|---|---|---|
|
|
dfffabd2ad | |
|
|
1040c3c306 | |
|
|
ed9dea9058 | |
|
|
3d8a89c7e1 | |
|
|
3d74db8023 | |
|
|
d23a9fa366 |
|
|
@ -39,6 +39,7 @@ file_paths_to_ignore:
|
||||||
- ".agent/**"
|
- ".agent/**"
|
||||||
- ".agentvibes/**"
|
- ".agentvibes/**"
|
||||||
- ".kiro/**"
|
- ".kiro/**"
|
||||||
|
- ".junie/**"
|
||||||
- ".roo/**"
|
- ".roo/**"
|
||||||
- ".github/chatmodes/**"
|
- ".github/chatmodes/**"
|
||||||
# --- Shared baseline: build temp ---
|
# --- Shared baseline: build temp ---
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ ignores:
|
||||||
- .roo/**
|
- .roo/**
|
||||||
- .codex/**
|
- .codex/**
|
||||||
- .kiro/**
|
- .kiro/**
|
||||||
|
- .junie/**
|
||||||
- sample-project/**
|
- sample-project/**
|
||||||
- test-project-install/**
|
- test-project-install/**
|
||||||
- z*/**
|
- z*/**
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `--user-name <name>` | Name for agents to use | System username |
|
||||||
| `--communication-language <lang>` | Agent communication language | English |
|
| `--communication-language <lang>` | Agent communication language | English |
|
||||||
| `--document-output-language <lang>` | Document output 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
|
### Other Options
|
||||||
|
|
||||||
|
|
@ -141,6 +153,7 @@ Invalid values will either:
|
||||||
|
|
||||||
:::tip[Best Practices]
|
:::tip[Best Practices]
|
||||||
- Use absolute paths for `--directory` to avoid ambiguity
|
- 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
|
- Test flags locally before using in CI/CD pipelines
|
||||||
- Combine with `-y` for truly unattended installations
|
- Combine with `-y` for truly unattended installations
|
||||||
- Use `--debug` if you encounter issues during installation
|
- Use `--debug` if you encounter issues during installation
|
||||||
|
|
|
||||||
|
|
@ -166,10 +166,28 @@ def resolve_project_root_placeholder(value, project_root):
|
||||||
"""Replace {project-root} placeholder with actual path."""
|
"""Replace {project-root} placeholder with actual path."""
|
||||||
if not value or not isinstance(value, str):
|
if not value or not isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
if '{project-root}' in value:
|
if '{project-root}' not in value:
|
||||||
return value.replace('{project-root}', str(project_root))
|
|
||||||
return 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):
|
def parse_var_specs(vars_string):
|
||||||
"""
|
"""
|
||||||
|
|
@ -222,9 +240,22 @@ def apply_result_template(var_def, raw_value, context):
|
||||||
if not result_template:
|
if not result_template:
|
||||||
return raw_value
|
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 = dict(context)
|
||||||
ctx['value'] = raw_value
|
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
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,37 @@ class TestResolveProjectRootPlaceholder(unittest.TestCase):
|
||||||
def test_non_string(self):
|
def test_non_string(self):
|
||||||
self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
|
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):
|
class TestExpandTemplate(unittest.TestCase):
|
||||||
|
|
||||||
|
|
@ -147,6 +178,39 @@ class TestApplyResultTemplate(unittest.TestCase):
|
||||||
result = apply_result_template(var_def, 'English', {})
|
result = apply_result_template(var_def, 'English', {})
|
||||||
self.assertEqual(result, '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):
|
class TestLoadModuleYaml(unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2028,6 +2028,67 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 34: Junie Native Skills Install
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 34: Junie Native Skills${colors.reset}\n`);
|
||||||
|
|
||||||
|
let tempProjectDir34;
|
||||||
|
let installedBmadDir34;
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearCache();
|
||||||
|
const platformCodes34 = await loadPlatformCodes();
|
||||||
|
const junieInstaller = platformCodes34.platforms.junie?.installer;
|
||||||
|
|
||||||
|
assert(junieInstaller?.target_dir === '.junie/skills', 'Junie target_dir uses native skills path');
|
||||||
|
assert(junieInstaller?.skill_format === true, 'Junie installer enables native skill output');
|
||||||
|
assert(
|
||||||
|
Array.isArray(junieInstaller?.legacy_targets) && junieInstaller.legacy_targets.includes('.junie/commands'),
|
||||||
|
'Junie installer cleans legacy command output',
|
||||||
|
);
|
||||||
|
|
||||||
|
tempProjectDir34 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-junie-test-'));
|
||||||
|
installedBmadDir34 = await createTestBmadFixture();
|
||||||
|
const legacyDir34 = path.join(tempProjectDir34, '.junie', 'commands', 'bmad-legacy-dir');
|
||||||
|
await fs.ensureDir(legacyDir34);
|
||||||
|
await fs.writeFile(path.join(tempProjectDir34, '.junie', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
||||||
|
await fs.writeFile(path.join(legacyDir34, 'SKILL.md'), 'legacy\n');
|
||||||
|
|
||||||
|
const ideManager34 = new IdeManager();
|
||||||
|
await ideManager34.ensureInitialized();
|
||||||
|
const result34 = await ideManager34.setup('junie', tempProjectDir34, installedBmadDir34, {
|
||||||
|
silent: true,
|
||||||
|
selectedModules: ['bmm'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(result34.success === true, 'Junie setup succeeds against temp project');
|
||||||
|
|
||||||
|
const skillFile34 = path.join(tempProjectDir34, '.junie', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
|
assert(await fs.pathExists(skillFile34), 'Junie install writes SKILL.md directory output');
|
||||||
|
|
||||||
|
const skillContent34 = await fs.readFile(skillFile34, 'utf8');
|
||||||
|
const nameMatch34 = skillContent34.match(/^name:\s*(.+)$/m);
|
||||||
|
assert(nameMatch34 && nameMatch34[1].trim() === 'bmad-master', 'Junie skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
|
assert(!(await fs.pathExists(path.join(tempProjectDir34, '.junie', 'commands'))), 'Junie setup removes legacy commands dir');
|
||||||
|
|
||||||
|
const result34b = await ideManager34.setup('junie', tempProjectDir34, installedBmadDir34, {
|
||||||
|
silent: true,
|
||||||
|
selectedModules: ['bmm'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(result34b.success === true, 'Junie reinstall/upgrade succeeds over existing skills');
|
||||||
|
assert(await fs.pathExists(skillFile34), 'Junie reinstall preserves SKILL.md output');
|
||||||
|
} catch (error) {
|
||||||
|
assert(false, 'Junie native skills test succeeds', error.message);
|
||||||
|
} finally {
|
||||||
|
if (tempProjectDir34) await fs.remove(tempProjectDir34).catch(() => {});
|
||||||
|
if (installedBmadDir34) await fs.remove(path.dirname(installedBmadDir34)).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,18 @@ platforms:
|
||||||
template_type: default
|
template_type: default
|
||||||
skill_format: true
|
skill_format: true
|
||||||
|
|
||||||
|
junie:
|
||||||
|
name: "Junie"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "AI coding agent by JetBrains"
|
||||||
|
installer:
|
||||||
|
legacy_targets:
|
||||||
|
- .junie/commands
|
||||||
|
target_dir: .junie/skills
|
||||||
|
template_type: default
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
kilo:
|
kilo:
|
||||||
name: "KiloCoder"
|
name: "KiloCoder"
|
||||||
preferred: false
|
preferred: false
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ platforms:
|
||||||
category: cli
|
category: cli
|
||||||
description: "Anthropic's official CLI for Claude"
|
description: "Anthropic's official CLI for Claude"
|
||||||
|
|
||||||
|
junie:
|
||||||
|
name: "Junie"
|
||||||
|
preferred: false
|
||||||
|
category: cli
|
||||||
|
description: "AI coding agent by JetBrains"
|
||||||
|
|
||||||
cursor:
|
cursor:
|
||||||
name: "Cursor"
|
name: "Cursor"
|
||||||
preferred: true
|
preferred: true
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue