bmad-method 6.3.1-next.12 → 6.3.1-next.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +51 -36
- package/src/bmm-skills/1-analysis/bmad-agent-analyst/customize.toml +90 -0
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +50 -33
- package/src/bmm-skills/1-analysis/bmad-agent-tech-writer/customize.toml +81 -0
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +44 -9
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +47 -0
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/contextual-discovery.md +8 -7
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/draft-and-review.md +6 -5
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/finalize.md +4 -1
- package/src/bmm-skills/1-analysis/bmad-product-brief/prompts/guided-elicitation.md +3 -2
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +50 -35
- package/src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml +85 -0
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +50 -31
- package/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/customize.toml +60 -0
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +50 -30
- package/src/bmm-skills/3-solutioning/bmad-agent-architect/customize.toml +65 -0
- package/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +48 -43
- package/src/bmm-skills/4-implementation/bmad-agent-dev/customize.toml +90 -0
- package/src/scripts/resolve_customization.py +230 -0
- package/tools/installer/core/install-paths.js +6 -3
- package/tools/installer/core/installer.js +57 -9
- package/tools/installer/core/manifest-generator.js +1 -4
- package/tools/installer/modules/official-modules.js +2 -2
|
@@ -3,67 +3,72 @@ name: bmad-agent-dev
|
|
|
3
3
|
description: Senior software engineer for story execution and code implementation. Use when the user asks to talk to Amelia or requests the developer agent.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Amelia
|
|
6
|
+
# Amelia — Senior Software Engineer
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
You are Amelia, the Senior Software Engineer. You execute approved stories with test-first discipline — red, green, refactor — shipping verified code that meets every acceptance criterion. File paths and AC IDs are your vocabulary.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Conventions
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- Bare paths (e.g. `references/guide.md`) resolve from the skill root.
|
|
15
|
+
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives).
|
|
16
|
+
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
17
|
+
- `{skill-name}` resolves to the skill directory's basename.
|
|
15
18
|
|
|
16
|
-
##
|
|
19
|
+
## On Activation
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
### Step 1: Resolve the Agent Block
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key agent`
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
- Every task/subtask must be covered by comprehensive unit tests before marking an item complete.
|
|
25
|
+
**If the script fails**, resolve the `agent` block yourself by reading these three files in base → team → user order and applying the same structural merge rules as the resolver:
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
1. `{skill-root}/customize.toml` — defaults
|
|
28
|
+
2. `{project-root}/_bmad/custom/{skill-name}.toml` — team overrides
|
|
29
|
+
3. `{project-root}/_bmad/custom/{skill-name}.user.toml` — personal overrides
|
|
26
30
|
|
|
27
|
-
-
|
|
28
|
-
- Execute tasks/subtasks IN ORDER as written in story file — no skipping, no reordering
|
|
29
|
-
- Mark task/subtask [x] ONLY when both implementation AND tests are complete and passing
|
|
30
|
-
- Run full test suite after each task — NEVER proceed with failing tests
|
|
31
|
-
- Execute continuously without pausing until all tasks/subtasks are complete
|
|
32
|
-
- Document in story file Dev Agent Record what was implemented, tests created, and any decisions made
|
|
33
|
-
- Update story file File List with ALL changed files after each task completion
|
|
34
|
-
- NEVER lie about tests being written or passing — tests must actually exist and pass 100%
|
|
31
|
+
Any missing file is skipped. Scalars override, tables deep-merge, arrays of tables keyed by `code` or `id` replace matching entries and append new entries, and all other arrays append.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
### Step 2: Execute Prepend Steps
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
Execute each entry in `{agent.activation_steps_prepend}` in order before proceeding.
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
### Step 3: Adopt Persona
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|------|-------------|-------|
|
|
44
|
-
| DS | Write the next or specified story's tests and code | bmad-dev-story |
|
|
45
|
-
| QD | Unified quick flow — clarify intent, plan, implement, review, present | bmad-quick-dev |
|
|
46
|
-
| QA | Generate API and E2E tests for existing features | bmad-qa-generate-e2e-tests |
|
|
47
|
-
| CR | Initiate a comprehensive code review across multiple quality facets | bmad-code-review |
|
|
48
|
-
| SP | Generate or update the sprint plan that sequences tasks for implementation | bmad-sprint-planning |
|
|
49
|
-
| CS | Prepare a story with all required context for implementation | bmad-create-story |
|
|
50
|
-
| ER | Party mode review of all work completed across an epic | bmad-retrospective |
|
|
39
|
+
Adopt the Amelia / Senior Software Engineer identity established in the Overview. Layer the customized persona on top: fill the additional role of `{agent.role}`, embody `{agent.identity}`, speak in the style of `{agent.communication_style}`, and follow `{agent.principles}`.
|
|
51
40
|
|
|
52
|
-
|
|
41
|
+
Fully embody this persona so the user gets the best experience. Do not break character until the user dismisses the persona. When the user calls a skill, this persona carries through and remains active.
|
|
42
|
+
|
|
43
|
+
### Step 4: Load Persistent Facts
|
|
44
|
+
|
|
45
|
+
Treat every entry in `{agent.persistent_facts}` as foundational context you carry for the rest of the session. Entries prefixed `file:` are paths or globs under `{project-root}` — load the referenced contents as facts. All other entries are facts verbatim.
|
|
46
|
+
|
|
47
|
+
### Step 5: Load Config
|
|
48
|
+
|
|
49
|
+
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
|
50
|
+
- Use `{user_name}` for greeting
|
|
51
|
+
- Use `{communication_language}` for all communications
|
|
52
|
+
- Use `{document_output_language}` for output documents
|
|
53
|
+
- Use `{planning_artifacts}` for output location and artifact scanning
|
|
54
|
+
- Use `{project_knowledge}` for additional context scanning
|
|
55
|
+
|
|
56
|
+
### Step 6: Greet the User
|
|
57
|
+
|
|
58
|
+
Greet `{user_name}` warmly by name as Amelia, speaking in `{communication_language}`. Lead the greeting with `{agent.icon}` so the user can see at a glance which agent is speaking. Remind the user they can invoke the `bmad-help` skill at any time for advice.
|
|
59
|
+
|
|
60
|
+
Continue to prefix your messages with `{agent.icon}` throughout the session so the active persona stays visually identifiable.
|
|
61
|
+
|
|
62
|
+
### Step 7: Execute Append Steps
|
|
63
|
+
|
|
64
|
+
Execute each entry in `{agent.activation_steps_append}` in order.
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
- Use `{user_name}` for greeting
|
|
56
|
-
- Use `{communication_language}` for all communications
|
|
57
|
-
- Use `{document_output_language}` for output documents
|
|
58
|
-
- Use `{planning_artifacts}` for output location and artifact scanning
|
|
59
|
-
- Use `{project_knowledge}` for additional context scanning
|
|
66
|
+
### Step 8: Dispatch or Present the Menu
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
- **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.
|
|
63
|
-
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
|
|
68
|
+
If the user's initial message already names an intent that clearly maps to a menu item (e.g. "hey Amelia, let's implement the next story"), skip the menu and dispatch that item directly after greeting.
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
Otherwise render `{agent.menu}` as a numbered table: `Code`, `Description`, `Action` (the item's `skill` name, or a short label derived from its `prompt` text). **Stop and wait for input.** Accept a number, menu `code`, or fuzzy description match.
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
Dispatch on a clear match by invoking the item's `skill` or executing its `prompt`. Only pause to clarify when two or more items are genuinely close — one short question, not a confirmation ritual. When nothing on the menu fits, just continue the conversation; chat, clarifying questions, and `bmad-help` are always fair game.
|
|
68
73
|
|
|
69
|
-
|
|
74
|
+
From here, Amelia stays active — persona, persistent facts, `{agent.icon}` prefix, and `{communication_language}` carry into every turn until the user dismisses her.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# DO NOT EDIT -- overwritten on every update.
|
|
2
|
+
#
|
|
3
|
+
# Amelia, the Senior Software Engineer, is the hardcoded identity of this agent.
|
|
4
|
+
# Customize the persona and menu below to shape behavior without
|
|
5
|
+
# changing who the agent is.
|
|
6
|
+
|
|
7
|
+
[agent]
|
|
8
|
+
# non-configurable skill frontmatter, create a custom agent if you need a new name/title
|
|
9
|
+
name = "Amelia"
|
|
10
|
+
title = "Senior Software Engineer"
|
|
11
|
+
|
|
12
|
+
# --- Configurable below. Overrides merge per BMad structural rules: ---
|
|
13
|
+
# scalars: override wins • arrays (persistent_facts, principles, activation_steps_*): append
|
|
14
|
+
# arrays-of-tables with `code`/`id`: replace matching items, append new ones.
|
|
15
|
+
|
|
16
|
+
icon = "💻"
|
|
17
|
+
|
|
18
|
+
# Steps to run before the standard activation (persona, config, greet).
|
|
19
|
+
# Overrides append. Use for pre-flight loads, compliance checks, etc.
|
|
20
|
+
|
|
21
|
+
activation_steps_prepend = []
|
|
22
|
+
|
|
23
|
+
# Steps to run after greet but before presenting the menu.
|
|
24
|
+
# Overrides append. Use for context-heavy setup that should happen
|
|
25
|
+
# once the user has been acknowledged.
|
|
26
|
+
|
|
27
|
+
activation_steps_append = []
|
|
28
|
+
|
|
29
|
+
# Persistent facts the agent keeps in mind for the whole session (org rules,
|
|
30
|
+
# domain constants, user preferences). Distinct from the runtime memory
|
|
31
|
+
# sidecar — these are static context loaded on activation. Overrides append.
|
|
32
|
+
#
|
|
33
|
+
# Each entry is either:
|
|
34
|
+
# - a literal sentence, e.g. "Our org is AWS-only -- do not propose GCP or Azure."
|
|
35
|
+
# - a file reference prefixed with `file:`, e.g. "file:{project-root}/docs/standards.md"
|
|
36
|
+
# (glob patterns are supported; the file's contents are loaded and treated as facts).
|
|
37
|
+
|
|
38
|
+
persistent_facts = [
|
|
39
|
+
"file:{project-root}/**/project-context.md",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
role = "Implement approved stories with test-first discipline and ship working, verified code during the BMad Method implementation phase."
|
|
43
|
+
identity = "Disciplined in Kent Beck's TDD and the Pragmatic Programmer's precision."
|
|
44
|
+
communication_style = "Ultra-succinct. Speaks in file paths and AC IDs — every statement citable. No fluff, all precision."
|
|
45
|
+
|
|
46
|
+
# The agent's value system. Overrides append to defaults.
|
|
47
|
+
principles = [
|
|
48
|
+
"No task complete without passing tests.",
|
|
49
|
+
"Red, green, refactor — in that order.",
|
|
50
|
+
"Tasks executed in the sequence written.",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Capabilities menu. Overrides merge by `code`: matching codes replace the item
|
|
54
|
+
# in place, new codes append. Each item has exactly one of `skill` (invokes a
|
|
55
|
+
# registered skill by name) or `prompt` (executes the prompt text directly).
|
|
56
|
+
|
|
57
|
+
[[agent.menu]]
|
|
58
|
+
code = "DS"
|
|
59
|
+
description = "Write the next or specified story's tests and code"
|
|
60
|
+
skill = "bmad-dev-story"
|
|
61
|
+
|
|
62
|
+
[[agent.menu]]
|
|
63
|
+
code = "QD"
|
|
64
|
+
description = "Unified quick flow — clarify intent, plan, implement, review, present"
|
|
65
|
+
skill = "bmad-quick-dev"
|
|
66
|
+
|
|
67
|
+
[[agent.menu]]
|
|
68
|
+
code = "QA"
|
|
69
|
+
description = "Generate API and E2E tests for existing features"
|
|
70
|
+
skill = "bmad-qa-generate-e2e-tests"
|
|
71
|
+
|
|
72
|
+
[[agent.menu]]
|
|
73
|
+
code = "CR"
|
|
74
|
+
description = "Initiate a comprehensive code review across multiple quality facets"
|
|
75
|
+
skill = "bmad-code-review"
|
|
76
|
+
|
|
77
|
+
[[agent.menu]]
|
|
78
|
+
code = "SP"
|
|
79
|
+
description = "Generate or update the sprint plan that sequences tasks for implementation"
|
|
80
|
+
skill = "bmad-sprint-planning"
|
|
81
|
+
|
|
82
|
+
[[agent.menu]]
|
|
83
|
+
code = "CS"
|
|
84
|
+
description = "Prepare a story with all required context for implementation"
|
|
85
|
+
skill = "bmad-create-story"
|
|
86
|
+
|
|
87
|
+
[[agent.menu]]
|
|
88
|
+
code = "ER"
|
|
89
|
+
description = "Party mode review of all work completed across an epic"
|
|
90
|
+
skill = "bmad-retrospective"
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Resolve customization for a BMad skill using three-layer TOML merge.
|
|
4
|
+
|
|
5
|
+
Reads customization from three layers (highest priority first):
|
|
6
|
+
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
|
|
7
|
+
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
|
|
8
|
+
3. {skill-root}/customize.toml (skill defaults)
|
|
9
|
+
|
|
10
|
+
Skill name is derived from the basename of the skill directory.
|
|
11
|
+
|
|
12
|
+
Outputs merged JSON to stdout. Errors go to stderr.
|
|
13
|
+
|
|
14
|
+
Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
|
|
15
|
+
no virtualenv — plain `python3` is sufficient.
|
|
16
|
+
|
|
17
|
+
python3 resolve_customization.py --skill /abs/path/to/skill-dir
|
|
18
|
+
python3 resolve_customization.py --skill ... --key agent
|
|
19
|
+
python3 resolve_customization.py --skill ... --key agent.menu
|
|
20
|
+
|
|
21
|
+
Merge rules (purely structural — no field-name special-casing):
|
|
22
|
+
- Scalars (string, int, bool, float): override wins
|
|
23
|
+
- Tables: deep merge (recursively apply these rules)
|
|
24
|
+
- Arrays of tables where every item shares the *same* identifier
|
|
25
|
+
field (every item has `code`, or every item has `id`):
|
|
26
|
+
merge by that key (matching keys replace, new keys append)
|
|
27
|
+
- All other arrays — including arrays where only some items have
|
|
28
|
+
`code` or `id`, or where items mix the two keys:
|
|
29
|
+
append (base items followed by override items)
|
|
30
|
+
|
|
31
|
+
No removal mechanism — overrides cannot delete base items. To suppress
|
|
32
|
+
a default, fork the skill or override the item by code with a no-op
|
|
33
|
+
description/prompt.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import tomllib
|
|
43
|
+
except ImportError:
|
|
44
|
+
sys.stderr.write(
|
|
45
|
+
"error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
|
|
46
|
+
"Install a newer Python or run the resolution manually per the\n"
|
|
47
|
+
"fallback instructions in the skill's SKILL.md.\n"
|
|
48
|
+
)
|
|
49
|
+
sys.exit(3)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_MISSING = object()
|
|
53
|
+
_KEYED_MERGE_FIELDS = ("code", "id")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def find_project_root(start: Path):
|
|
57
|
+
current = start.resolve()
|
|
58
|
+
while True:
|
|
59
|
+
if (current / "_bmad").exists() or (current / ".git").exists():
|
|
60
|
+
return current
|
|
61
|
+
parent = current.parent
|
|
62
|
+
if parent == current:
|
|
63
|
+
return None
|
|
64
|
+
current = parent
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_toml(file_path: Path, required: bool = False) -> dict:
|
|
68
|
+
if not file_path.exists():
|
|
69
|
+
if required:
|
|
70
|
+
sys.stderr.write(f"error: required customization file not found: {file_path}\n")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
return {}
|
|
73
|
+
try:
|
|
74
|
+
with file_path.open("rb") as f:
|
|
75
|
+
parsed = tomllib.load(f)
|
|
76
|
+
if not isinstance(parsed, dict):
|
|
77
|
+
if required:
|
|
78
|
+
sys.stderr.write(f"error: {file_path} did not parse to a table\n")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
return {}
|
|
81
|
+
return parsed
|
|
82
|
+
except tomllib.TOMLDecodeError as error:
|
|
83
|
+
level = "error" if required else "warning"
|
|
84
|
+
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
|
|
85
|
+
if required:
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
return {}
|
|
88
|
+
except OSError as error:
|
|
89
|
+
level = "error" if required else "warning"
|
|
90
|
+
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
|
|
91
|
+
if required:
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _detect_keyed_merge_field(items):
|
|
97
|
+
"""Return 'code' or 'id' if every table item carries that *same* field.
|
|
98
|
+
|
|
99
|
+
All items must share the same identifier (all `code`, or all `id`).
|
|
100
|
+
Mixed arrays — where some items use `code` and others use `id` —
|
|
101
|
+
return None and fall through to append semantics. This is intentional:
|
|
102
|
+
mixing identifier keys within one array is a schema smell, and
|
|
103
|
+
append-fallback is safer than guessing which key should merge.
|
|
104
|
+
"""
|
|
105
|
+
if not items or not all(isinstance(item, dict) for item in items):
|
|
106
|
+
return None
|
|
107
|
+
for candidate in _KEYED_MERGE_FIELDS:
|
|
108
|
+
if all(item.get(candidate) is not None for item in items):
|
|
109
|
+
return candidate
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _merge_by_key(base, override, key_name):
|
|
114
|
+
result = []
|
|
115
|
+
index_by_key = {}
|
|
116
|
+
|
|
117
|
+
for item in base:
|
|
118
|
+
if not isinstance(item, dict):
|
|
119
|
+
continue
|
|
120
|
+
if item.get(key_name) is not None:
|
|
121
|
+
index_by_key[item[key_name]] = len(result)
|
|
122
|
+
result.append(dict(item))
|
|
123
|
+
|
|
124
|
+
for item in override:
|
|
125
|
+
if not isinstance(item, dict):
|
|
126
|
+
result.append(item)
|
|
127
|
+
continue
|
|
128
|
+
key = item.get(key_name)
|
|
129
|
+
if key is not None and key in index_by_key:
|
|
130
|
+
result[index_by_key[key]] = dict(item)
|
|
131
|
+
else:
|
|
132
|
+
if key is not None:
|
|
133
|
+
index_by_key[key] = len(result)
|
|
134
|
+
result.append(dict(item))
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _merge_arrays(base, override):
|
|
140
|
+
"""Shape-aware array merge. Base + override combined tables may opt into
|
|
141
|
+
keyed merge if every item has `code` or `id`. Otherwise: append."""
|
|
142
|
+
base_arr = base if isinstance(base, list) else []
|
|
143
|
+
override_arr = override if isinstance(override, list) else []
|
|
144
|
+
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
|
145
|
+
if keyed_field:
|
|
146
|
+
return _merge_by_key(base_arr, override_arr, keyed_field)
|
|
147
|
+
return base_arr + override_arr
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def deep_merge(base, override):
|
|
151
|
+
"""Recursively merge override into base using structural rules.
|
|
152
|
+
- Table + table: deep merge
|
|
153
|
+
- Array + array: shape-aware (keyed merge if all items have code/id, else append)
|
|
154
|
+
- Anything else: override wins
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(base, dict) and isinstance(override, dict):
|
|
157
|
+
result = dict(base)
|
|
158
|
+
for key, over_val in override.items():
|
|
159
|
+
if key in result:
|
|
160
|
+
result[key] = deep_merge(result[key], over_val)
|
|
161
|
+
else:
|
|
162
|
+
result[key] = over_val
|
|
163
|
+
return result
|
|
164
|
+
if isinstance(base, list) and isinstance(override, list):
|
|
165
|
+
return _merge_arrays(base, override)
|
|
166
|
+
return override
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def extract_key(data, dotted_key: str):
|
|
170
|
+
parts = dotted_key.split(".")
|
|
171
|
+
current = data
|
|
172
|
+
for part in parts:
|
|
173
|
+
if isinstance(current, dict) and part in current:
|
|
174
|
+
current = current[part]
|
|
175
|
+
else:
|
|
176
|
+
return _MISSING
|
|
177
|
+
return current
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main():
|
|
181
|
+
parser = argparse.ArgumentParser(
|
|
182
|
+
description="Resolve customization for a BMad skill using three-layer TOML merge.",
|
|
183
|
+
add_help=True,
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--skill", "-s", required=True,
|
|
187
|
+
help="Absolute path to the skill directory (must contain customize.toml)",
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"--key", "-k", action="append", default=[],
|
|
191
|
+
help="Dotted field path to resolve (repeatable). Omit for full dump.",
|
|
192
|
+
)
|
|
193
|
+
args = parser.parse_args()
|
|
194
|
+
|
|
195
|
+
skill_dir = Path(args.skill).resolve()
|
|
196
|
+
skill_name = skill_dir.name
|
|
197
|
+
defaults_path = skill_dir / "customize.toml"
|
|
198
|
+
|
|
199
|
+
defaults = load_toml(defaults_path, required=True)
|
|
200
|
+
|
|
201
|
+
# Prefer the project that contains this skill. Only fall back to cwd if
|
|
202
|
+
# the skill isn't inside a recognizable project tree (unusual but possible
|
|
203
|
+
# for standalone skills invoked directly). Using cwd first is unsafe when
|
|
204
|
+
# an ancestor of cwd happens to have a stray _bmad/ from another project.
|
|
205
|
+
project_root = find_project_root(skill_dir) or find_project_root(Path.cwd())
|
|
206
|
+
|
|
207
|
+
team = {}
|
|
208
|
+
user = {}
|
|
209
|
+
if project_root:
|
|
210
|
+
custom_dir = project_root / "_bmad" / "custom"
|
|
211
|
+
team = load_toml(custom_dir / f"{skill_name}.toml")
|
|
212
|
+
user = load_toml(custom_dir / f"{skill_name}.user.toml")
|
|
213
|
+
|
|
214
|
+
merged = deep_merge(defaults, team)
|
|
215
|
+
merged = deep_merge(merged, user)
|
|
216
|
+
|
|
217
|
+
if args.key:
|
|
218
|
+
output = {}
|
|
219
|
+
for key in args.key:
|
|
220
|
+
value = extract_key(merged, key)
|
|
221
|
+
if value is not _MISSING:
|
|
222
|
+
output[key] = value
|
|
223
|
+
else:
|
|
224
|
+
output = merged
|
|
225
|
+
|
|
226
|
+
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|
|
@@ -19,14 +19,16 @@ class InstallPaths {
|
|
|
19
19
|
const isUpdate = await fs.pathExists(bmadDir);
|
|
20
20
|
|
|
21
21
|
const configDir = path.join(bmadDir, '_config');
|
|
22
|
-
const agentsDir = path.join(configDir, 'agents');
|
|
23
22
|
const coreDir = path.join(bmadDir, 'core');
|
|
23
|
+
const scriptsDir = path.join(bmadDir, 'scripts');
|
|
24
|
+
const customDir = path.join(bmadDir, 'custom');
|
|
24
25
|
|
|
25
26
|
for (const [dir, label] of [
|
|
26
27
|
[bmadDir, 'bmad directory'],
|
|
27
28
|
[configDir, 'config directory'],
|
|
28
|
-
[agentsDir, 'agents config directory'],
|
|
29
29
|
[coreDir, 'core module directory'],
|
|
30
|
+
[scriptsDir, 'shared scripts directory'],
|
|
31
|
+
[customDir, 'customizations directory'],
|
|
30
32
|
]) {
|
|
31
33
|
await ensureWritableDir(dir, label);
|
|
32
34
|
}
|
|
@@ -37,8 +39,9 @@ class InstallPaths {
|
|
|
37
39
|
projectRoot,
|
|
38
40
|
bmadDir,
|
|
39
41
|
configDir,
|
|
40
|
-
agentsDir,
|
|
41
42
|
coreDir,
|
|
43
|
+
scriptsDir,
|
|
44
|
+
customDir,
|
|
42
45
|
isUpdate,
|
|
43
46
|
});
|
|
44
47
|
}
|
|
@@ -244,6 +244,15 @@ class Installer {
|
|
|
244
244
|
|
|
245
245
|
const installTasks = [];
|
|
246
246
|
|
|
247
|
+
installTasks.push({
|
|
248
|
+
title: 'Installing shared scripts',
|
|
249
|
+
task: async () => {
|
|
250
|
+
await this._installSharedScripts(paths);
|
|
251
|
+
addResult('Shared scripts', 'ok');
|
|
252
|
+
return 'Shared scripts installed';
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
247
256
|
if (allModules.length > 0) {
|
|
248
257
|
installTasks.push({
|
|
249
258
|
title: isQuickUpdate ? `Updating ${allModules.length} module(s)` : `Installing ${allModules.length} module(s)`,
|
|
@@ -558,6 +567,44 @@ class Installer {
|
|
|
558
567
|
return { tempBackupDir, tempModifiedBackupDir };
|
|
559
568
|
}
|
|
560
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Sync src/scripts/* → _bmad/scripts/ so shared Python scripts
|
|
572
|
+
* (e.g. resolve_customization.py) are available at install time.
|
|
573
|
+
* Wipes the destination first so files removed or renamed in source
|
|
574
|
+
* don't linger and get recorded as installed. Also seeds
|
|
575
|
+
* _bmad/custom/.gitignore on fresh installs so *.user.toml overrides
|
|
576
|
+
* stay out of version control.
|
|
577
|
+
*/
|
|
578
|
+
async _installSharedScripts(paths) {
|
|
579
|
+
const srcScriptsDir = path.join(paths.srcDir, 'src', 'scripts');
|
|
580
|
+
if (!(await fs.pathExists(srcScriptsDir))) {
|
|
581
|
+
throw new Error(`Shared scripts source directory not found: ${srcScriptsDir}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await fs.remove(paths.scriptsDir);
|
|
585
|
+
await fs.ensureDir(paths.scriptsDir);
|
|
586
|
+
await fs.copy(srcScriptsDir, paths.scriptsDir, { overwrite: true });
|
|
587
|
+
await this._trackFilesRecursive(paths.scriptsDir);
|
|
588
|
+
|
|
589
|
+
const customGitignore = path.join(paths.customDir, '.gitignore');
|
|
590
|
+
if (!(await fs.pathExists(customGitignore))) {
|
|
591
|
+
await fs.writeFile(customGitignore, '*.user.toml\n', 'utf8');
|
|
592
|
+
this.installedFiles.add(customGitignore);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async _trackFilesRecursive(dir) {
|
|
597
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
598
|
+
for (const entry of entries) {
|
|
599
|
+
const full = path.join(dir, entry.name);
|
|
600
|
+
if (entry.isDirectory()) {
|
|
601
|
+
await this._trackFilesRecursive(full);
|
|
602
|
+
} else if (entry.isFile()) {
|
|
603
|
+
this.installedFiles.add(full);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
561
608
|
/**
|
|
562
609
|
* Install official (non-custom) modules.
|
|
563
610
|
* @param {Object} config - Installation configuration
|
|
@@ -671,8 +718,11 @@ class Installer {
|
|
|
671
718
|
const customFiles = [];
|
|
672
719
|
const modifiedFiles = [];
|
|
673
720
|
|
|
674
|
-
// Memory
|
|
675
|
-
|
|
721
|
+
// Memory subtrees (v6.1: _bmad/_memory, current: _bmad/memory) hold
|
|
722
|
+
// per-user runtime data generated by agents with sidecars. These files
|
|
723
|
+
// aren't installer-managed and must never be reported as "custom" or
|
|
724
|
+
// "modified" — they're user state, not user overrides.
|
|
725
|
+
const bmadMemoryPaths = ['_memory', 'memory'];
|
|
676
726
|
|
|
677
727
|
// Check if the manifest has hashes - if not, we can't detect modifications
|
|
678
728
|
let manifestHasHashes = false;
|
|
@@ -738,7 +788,7 @@ class Installer {
|
|
|
738
788
|
continue;
|
|
739
789
|
}
|
|
740
790
|
|
|
741
|
-
if (relativePath.startsWith(
|
|
791
|
+
if (bmadMemoryPaths.some((mp) => relativePath === mp || relativePath.startsWith(mp + '/'))) {
|
|
742
792
|
continue;
|
|
743
793
|
}
|
|
744
794
|
|
|
@@ -789,9 +839,8 @@ class Installer {
|
|
|
789
839
|
|
|
790
840
|
// Get all installed module directories
|
|
791
841
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
.map((entry) => entry.name);
|
|
842
|
+
const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
|
|
843
|
+
const installedModules = entries.filter((entry) => entry.isDirectory() && !nonModuleDirs.has(entry.name)).map((entry) => entry.name);
|
|
795
844
|
|
|
796
845
|
// Generate config.yaml for each installed module
|
|
797
846
|
for (const moduleName of installedModules) {
|
|
@@ -917,9 +966,8 @@ class Installer {
|
|
|
917
966
|
|
|
918
967
|
// Get all installed module directories
|
|
919
968
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
.map((entry) => entry.name);
|
|
969
|
+
const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
|
|
970
|
+
const installedModules = entries.filter((entry) => entry.isDirectory() && !nonModuleDirs.has(entry.name)).map((entry) => entry.name);
|
|
923
971
|
|
|
924
972
|
// Add core module to scan (it's installed at root level as _config, but we check src/core-skills)
|
|
925
973
|
const coreModulePath = getSourcePath('core-skills');
|
|
@@ -329,7 +329,6 @@ class ManifestGenerator {
|
|
|
329
329
|
displayName: m.displayName || m.name || entry.name,
|
|
330
330
|
title: m.title || '',
|
|
331
331
|
icon: m.icon || '',
|
|
332
|
-
capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '',
|
|
333
332
|
role: m.role ? this.cleanForCSV(m.role) : '',
|
|
334
333
|
identity: m.identity ? this.cleanForCSV(m.identity) : '',
|
|
335
334
|
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
|
|
@@ -499,7 +498,7 @@ class ManifestGenerator {
|
|
|
499
498
|
}
|
|
500
499
|
|
|
501
500
|
// Create CSV header with persona fields and canonicalId
|
|
502
|
-
let csvContent = 'name,displayName,title,icon,
|
|
501
|
+
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path,canonicalId\n';
|
|
503
502
|
|
|
504
503
|
// Combine existing and new agents, preferring new data for duplicates
|
|
505
504
|
const allAgents = new Map();
|
|
@@ -517,7 +516,6 @@ class ManifestGenerator {
|
|
|
517
516
|
displayName: agent.displayName,
|
|
518
517
|
title: agent.title,
|
|
519
518
|
icon: agent.icon,
|
|
520
|
-
capabilities: agent.capabilities,
|
|
521
519
|
role: agent.role,
|
|
522
520
|
identity: agent.identity,
|
|
523
521
|
communicationStyle: agent.communicationStyle,
|
|
@@ -535,7 +533,6 @@ class ManifestGenerator {
|
|
|
535
533
|
escapeCsv(record.displayName),
|
|
536
534
|
escapeCsv(record.title),
|
|
537
535
|
escapeCsv(record.icon),
|
|
538
|
-
escapeCsv(record.capabilities),
|
|
539
536
|
escapeCsv(record.role),
|
|
540
537
|
escapeCsv(record.identity),
|
|
541
538
|
escapeCsv(record.communicationStyle),
|
|
@@ -820,10 +820,10 @@ class OfficialModules {
|
|
|
820
820
|
let foundAny = false;
|
|
821
821
|
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
|
822
822
|
|
|
823
|
+
const nonModuleDirs = new Set(['_config', '_memory', 'memory', 'docs', 'scripts', 'custom']);
|
|
823
824
|
for (const entry of entries) {
|
|
824
825
|
if (entry.isDirectory()) {
|
|
825
|
-
|
|
826
|
-
if (entry.name === '_config' || entry.name === '_memory') {
|
|
826
|
+
if (nonModuleDirs.has(entry.name)) {
|
|
827
827
|
continue;
|
|
828
828
|
}
|
|
829
829
|
|