@skyf0xx/hedgehog 6.4.1 → 6.4.2
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/hosts/gemini/gemini-extension.json +1 -1
- package/vendor-skills/BMAD/ATTRIBUTION.md +46 -23
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prd/SKILL.md +2 -2
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prd/assets/prd-template.md +1 -1
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prd/references/validate.md +3 -3
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prfaq/SKILL.md +2 -2
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prfaq/bmad-manifest.json +2 -2
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-prfaq/references/verdict.md +1 -1
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-product-brief/SKILL.md +2 -2
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-ux/SKILL.md +2 -2
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-ux/assets/color-themes.md +1 -1
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-ux/references/creative-tools.md +1 -1
- package/vendor-skills/BMAD/bmm-skills/plan/bmad-ux/references/validate.md +1 -1
- package/vendor-skills/BMAD/core-skills/bmad-advanced-elicitation/SKILL.md +23 -24
- package/vendor-skills/BMAD/core-skills/bmad-brainstorming/SKILL.md +2 -2
- package/vendor-skills/BMAD/core-skills/bmad-brainstorming/scripts/brain.py +19 -0
- package/vendor-skills/BMAD/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +50 -0
- package/vendor-skills/BMAD/core-skills/bmad-deep-recon/SKILL.md +2 -2
- package/vendor-skills/BMAD/core-skills/bmad-forge-idea/SKILL.md +3 -3
- package/vendor-skills/BMAD/core-skills/bmad-forge-idea/scripts/resolve_personas.py +3 -1
- package/vendor-skills/BMAD/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +22 -0
- package/vendor-skills/BMAD/scripts/resolve_customization.py +102 -178
package/package.json
CHANGED
|
@@ -5,8 +5,8 @@ This directory vendors a subset of [BMAD-METHOD](https://github.com/bmad-code-or
|
|
|
5
5
|
for the full license text.
|
|
6
6
|
|
|
7
7
|
- **Source repo:** https://github.com/bmad-code-org/BMAD-METHOD
|
|
8
|
-
- **Vendored from:** tag `v6.
|
|
9
|
-
- **Vendored on:** 2026-
|
|
8
|
+
- **Vendored from:** tag `v6.12.1`, commit `790dae9c8e2a1d73575cb2d40b14dd4963391f29`
|
|
9
|
+
- **Vendored on:** 2026-10-10
|
|
10
10
|
- **Module:** `bmm` (BMAD Method Module)
|
|
11
11
|
|
|
12
12
|
## What's vendored
|
|
@@ -26,37 +26,48 @@ they depend on:
|
|
|
26
26
|
every vendored skill calls, originally at `src/scripts/` in the source
|
|
27
27
|
repo)
|
|
28
28
|
|
|
29
|
-
Upstream
|
|
30
|
-
`
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
is its documented normal path, not a degraded one.
|
|
29
|
+
Upstream keeps the four `bmm-skills` above under a single flat
|
|
30
|
+
`bmm-skills/plan/` directory, collapsed out of the numbered
|
|
31
|
+
`1-analysis/` and `2-plan-workflows/` directories an earlier pass found
|
|
32
|
+
them in. `bmad-deep-recon` is in the pinned tag, so this pass pins to a
|
|
33
|
+
release tag rather than a `main` commit SHA.
|
|
34
|
+
|
|
35
|
+
`bmad-forge-idea` is a persona-driven interrogation skill that
|
|
36
|
+
pressure-tests an idea before any artifact gets written, run ahead of
|
|
37
|
+
`bmad-brainstorming` as the shelf's first step. It carries its own
|
|
38
|
+
script, `scripts/resolve_personas.py`, vendored alongside it (not shared
|
|
39
|
+
— only this skill calls it). Its upstream counterpart `bmad-party-mode`
|
|
40
|
+
(the multi-agent roster it can optionally draw on) is not vendored, since
|
|
41
|
+
a real roster needs BMAD's own `bmm-skills/agents/bmad-agent-*` persona
|
|
42
|
+
skills too — a parallel system to Hedgehog's own `src/agents/`, out of
|
|
43
|
+
scope here. `bmad-forge-idea` degrades gracefully without it:
|
|
44
|
+
`resolve_personas.py` returns an empty roster and the skill falls back to
|
|
45
|
+
generating personas on the fly, which is its documented normal path, not
|
|
46
|
+
a degraded one.
|
|
48
47
|
|
|
49
48
|
Each skill directory carries its own templates, reference files, and
|
|
50
49
|
scripts as vendored, unmodified except where noted below.
|
|
51
50
|
|
|
51
|
+
`scripts/resolve_customization.py` resolves the project root by checking,
|
|
52
|
+
in order, the working directory, the script's own install path, and the
|
|
53
|
+
invoking skill's directory — preferring any ancestor holding `_bmad/`
|
|
54
|
+
over one merely holding `.git` at every depth — and warns on stderr when
|
|
55
|
+
a candidate root it didn't pick has an override the chosen root lacks.
|
|
56
|
+
This keeps a skill installed outside the target project (e.g. one that
|
|
57
|
+
would otherwise walk up to `~`) from landing on the wrong root and
|
|
58
|
+
silently missing the project's own override.
|
|
59
|
+
|
|
52
60
|
## What's stripped
|
|
53
61
|
|
|
54
62
|
BMAD-METHOD's own orchestration layer is not vendored and is removed from
|
|
55
63
|
every skill file that referenced it:
|
|
56
64
|
|
|
57
65
|
- Central config resolution (`_bmad/scripts/resolve_config.py`,
|
|
58
|
-
`_bmad/
|
|
59
|
-
defaults inline in
|
|
66
|
+
`_bmad/scripts/config_utils.py`, `_bmad/config.toml`,
|
|
67
|
+
`_bmad/bmm/config.yaml`) — replaced with trivial defaults inline in
|
|
68
|
+
each skill. Upstream's `plan/` skills call `resolve_config.py` for
|
|
69
|
+
this; re-apply the same strip to whatever call shape upstream uses on
|
|
70
|
+
each re-vendor pass.
|
|
60
71
|
- `bmad-party-mode` (multi-agent roster) mentions and invocations.
|
|
61
72
|
- Chain-forward "common next skill" suggestions and `bmad-help` routing.
|
|
62
73
|
- Misroute-detection logic pointing at non-vendored BMAD skills.
|
|
@@ -77,6 +88,18 @@ this vendored tree.
|
|
|
77
88
|
deliverables are markdown/prose only (skyf0xx/hedgehog#386). A
|
|
78
89
|
re-vendor pass must re-apply this key to the freshly-fetched files
|
|
79
90
|
rather than letting it silently disappear.
|
|
91
|
+
- `persistent_facts` in every vendored `customize.toml` that carries it
|
|
92
|
+
(`bmad-forge-idea`, `bmad-brainstorming`, `bmad-product-brief`,
|
|
93
|
+
`bmad-prfaq`, `bmad-prd`, `bmad-ux`) defaults to
|
|
94
|
+
`["file:{project-root}/**/project-context.md"]`, not upstream's empty
|
|
95
|
+
default. Upstream moved repo-wide context to a hand-maintained
|
|
96
|
+
`AGENTS.md` loaded via `bmad-project-context`, a skill not vendored
|
|
97
|
+
here; Hedgehog's planning intake instead relies on project context
|
|
98
|
+
flowing into these skills automatically, so requiring a consuming
|
|
99
|
+
project to discover and set an opt-in override just to keep that
|
|
100
|
+
behavior would be a regression. A re-vendor pass must re-apply this
|
|
101
|
+
non-empty default to the freshly-fetched `customize.toml` files rather
|
|
102
|
+
than letting it silently reset to empty.
|
|
80
103
|
|
|
81
104
|
## Re-vendoring
|
|
82
105
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-prd
|
|
3
|
-
description: Create, update, or validate a PRD. Use when the user wants help producing, editing, or validating a PRD
|
|
3
|
+
description: Create, update, or validate a PRD. Use when the user wants help producing, editing, or validating a PRD
|
|
4
4
|
---
|
|
5
5
|
# BMad PRD
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ You are a master facilitator and coach helping the user create, edit, or validat
|
|
|
17
17
|
|
|
18
18
|
**Forwarded activation:** if a caller invoked you with a stated intent and pre-resolved customization fields (e.g. the `bmad-create-prd` / `bmad-edit-prd` / `bmad-validate-prd` shims), honor them verbatim — skip your own intent inference, use the supplied values for those named fields, and resolve only the remaining fields from your own `customize.toml`.
|
|
19
19
|
|
|
20
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
20
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
21
21
|
2. Run `{workflow.activation_steps_prepend}`. Treat `{workflow.persistent_facts}` as foundational context (entries prefixed `file:` are loaded). `{workflow.external_sources}` is an org-configured registry of internal tools (knowledge bases, MCP tools); consult them alongside generic web research on the same triggers, org tools preferred when their directive matches. Research itself fires during Discovery — see **Research subagents**.
|
|
22
22
|
3. Resolve `{user_name}` (ask the user or omit), `{communication_language}` (English), `{document_output_language}` (English), `{planning_artifacts}`, `{project_name}` (infer from the Hedgehog project), `{date}` (today's date) using sensible defaults. Missing values → neutral defaults; never block.
|
|
23
23
|
4. If headless, follow `references/headless.md` for the whole run. Otherwise greet the user **by name** using `{user_name}` and **in their language** using `{communication_language}` — and stay in `{communication_language}` for every turn for the entire run, not just the greeting. In the greeting, let the user know that at any point they can invoke `bmad-advanced-elicitation` for deeper exploration on a specific section.
|
|
@@ -161,5 +161,5 @@ updated: {YYYY-MM-DD}
|
|
|
161
161
|
- **Environmental and Reliability Requirements**.
|
|
162
162
|
|
|
163
163
|
### Small-scope all-inclusive *(use when scope is 1-2 stories' worth and the user wants a single captured artifact — chosen during the Right-skill check in Discovery)*
|
|
164
|
-
- **Stories** — story-level specs listed inline at the end of the doc. Each story: *"As a [persona], I can [action] [under conditions]. Acceptance: [testable criteria]."* Numbered Story-1, Story-2, ... for reference. Pair with very lean §1 Vision, §2 Target User (often just JTBD + one UJ), §3 Glossary (handful of terms), §4 Features (often a single feature), §6 MVP Scope (in/out very tight). The whole doc fits on a page or two and captures intent + implementable stories in one place. If the user doesn't want the captured artifact at all, `bmad-
|
|
164
|
+
- **Stories** — story-level specs listed inline at the end of the doc. Each story: *"As a [persona], I can [action] [under conditions]. Acceptance: [testable criteria]."* Numbered Story-1, Story-2, ... for reference. Pair with very lean §1 Vision, §2 Target User (often just JTBD + one UJ), §3 Glossary (handful of terms), §4 Features (often a single feature), §6 MVP Scope (in/out very tight). The whole doc fits on a page or two and captures intent + implementable stories in one place. If the user doesn't want the captured artifact at all, `bmad-build` is the better path — this cluster is only for "I want a doc *and* the stories."
|
|
165
165
|
|
|
@@ -41,11 +41,11 @@ Once every selected reviewer has returned, the parent synthesizes one consolidat
|
|
|
41
41
|
- **Footer.** Rubric path, ISO timestamp.
|
|
42
42
|
3. Write the filled HTML to `{doc_workspace}/validation-report.html`.
|
|
43
43
|
4. Write the markdown twin to `{doc_workspace}/validation-report.md` (same content, grouped by severity rather than by dimension — see format below; this is the canonical form for downstream re-reading).
|
|
44
|
-
5. Open the HTML in the default browser:
|
|
44
|
+
5. Open the HTML in the default browser with the platform opener — `open` on macOS, `xdg-open` on Linux, `start ""` on Windows — double-quoting the path:
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
open "{doc_workspace}/validation-report.html"
|
|
47
47
|
```
|
|
48
|
-
Skip the open step in headless mode (see `references/headless.md`).
|
|
48
|
+
If the command fails, don't retry with another opener: tell the user the file path and move on. Skip the open step in headless mode (see `references/headless.md`).
|
|
49
49
|
|
|
50
50
|
### Markdown twin format
|
|
51
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-prfaq
|
|
3
|
-
description: Working Backwards
|
|
3
|
+
description: "Test a product concept with Amazon's Working Backwards method: write the press release for the finished product first, then answer hard customer and stakeholder questions, ending in a complete PRFAQ document. Use when the user requests to 'create a PRFAQ', 'work backwards', or 'run the PRFAQ challenge'"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Working Backwards: The PRFAQ Challenge
|
|
@@ -31,7 +31,7 @@ The PRFAQ forces customer-first clarity: write the press release announcing the
|
|
|
31
31
|
|
|
32
32
|
### Step 1: Resolve the Workflow Block
|
|
33
33
|
|
|
34
|
-
Run: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`
|
|
34
|
+
Run: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`
|
|
35
35
|
|
|
36
36
|
**If the script fails**, read `{skill-root}/customize.toml` directly and use its defaults.
|
|
37
37
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
{
|
|
5
5
|
"name": "working-backwards",
|
|
6
6
|
"menu-code": "WB",
|
|
7
|
-
"description": "Produces
|
|
7
|
+
"description": "Produces a PRFAQ document and an optional condensed version for use as PRD input.",
|
|
8
8
|
"supports-headless": true,
|
|
9
|
-
"phase-name": "
|
|
9
|
+
"phase-name": "plan",
|
|
10
10
|
"preceded-by": ["brainstorming", "perform-research"],
|
|
11
11
|
"followed-by": ["create-prd"],
|
|
12
12
|
"is-required": false,
|
|
@@ -78,6 +78,6 @@ purpose: "Token-efficient context for downstream PRD creation"
|
|
|
78
78
|
|
|
79
79
|
This is the terminal stage. If the user wants to revise, loop back to the relevant stage. Otherwise, the workflow is done.
|
|
80
80
|
|
|
81
|
-
Run: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
|
|
81
|
+
Run: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow.on_complete`
|
|
82
82
|
|
|
83
83
|
If the resolved `workflow.on_complete` is non-empty, follow it as the final terminal instruction before exiting.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-product-brief
|
|
3
|
-
description: Create, update, or validate a product brief. Use when the user wants help producing, editing, or validating a brief
|
|
3
|
+
description: Create, update, or validate a product brief. Use when the user wants help producing, editing, or validating a brief
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Overview
|
|
@@ -19,7 +19,7 @@ At the opening greeting, let the user know they can invoke `bmad-advanced-elicit
|
|
|
19
19
|
|
|
20
20
|
## On Activation
|
|
21
21
|
|
|
22
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
22
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
23
23
|
2. Execute each entry in `{workflow.activation_steps_prepend}` in order.
|
|
24
24
|
3. Treat every entry in `{workflow.persistent_facts}` as foundational context for the rest of the run. Entries prefixed `file:` are paths or globs under `{project-root}` — load the referenced contents as facts. All other entries are facts verbatim.
|
|
25
25
|
4. `{workflow.external_sources}` is an org-configured registry of internal tools (knowledge bases, MCP tools); consult them alongside generic web research on the same triggers in `## Discovery`, org tools preferred when their directive matches. If a named tool is unavailable at runtime, fall back to standard behavior and note the gap when relevant.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-ux
|
|
3
|
-
description:
|
|
3
|
+
description: 'Capture the user''s UX vision in two documents: DESIGN.md for how the product looks and EXPERIENCE.md for how it behaves. Use when the user says "lets create UX design" or "create UX specifications" or "help me plan the UX"'
|
|
4
4
|
---
|
|
5
5
|
# BMad UX
|
|
6
6
|
|
|
@@ -34,7 +34,7 @@ UX may lead, follow, or stand alone. Inherit `sources:` by reference; the spines
|
|
|
34
34
|
|
|
35
35
|
## On Activation
|
|
36
36
|
|
|
37
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
37
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
38
38
|
2. Run `{workflow.activation_steps_prepend}`. Treat `{workflow.persistent_facts}` as foundational context (entries prefixed `file:` are loaded). `{workflow.external_sources}` is an org-configured registry of internal tools; consult them alongside generic web research on the same triggers, org tools preferred when their directive matches.
|
|
39
39
|
3. Resolve `{user_name}` (ask the user or omit), `{communication_language}` (English), `{document_output_language}` (English), `{planning_artifacts}`, `{project_name}` (infer from the Hedgehog project), `{date}` (today's date) using sensible defaults. Missing values → neutral defaults; never block.
|
|
40
40
|
4. If headless, follow `references/headless.md` for the whole run. Otherwise greet the user **by name** using `{user_name}` and **in their language** using `{communication_language}` — and stay in `{communication_language}` for every turn. In the greeting, let the user know `bmad-advanced-elicitation` is always available.
|
|
@@ -6,4 +6,4 @@ Each variation: header (name + one-line emotional register), token chips for eve
|
|
|
6
6
|
|
|
7
7
|
Inline CSS only, system font stack, no JS, no network. Document concrete hex values in `<style>` comments per variation so the user can lift them if they pick that theme. The spine itself stays semantic.
|
|
8
8
|
|
|
9
|
-
Return to the parent: file path, one-line per variation, mode coverage. Do not dump HTML into the parent context. If interactive, open the file with `
|
|
9
|
+
Return to the parent: file path, one-line per variation, mode coverage. Do not dump HTML into the parent context. If interactive, open the file with the platform opener — `open "PATH"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead.
|
|
@@ -16,4 +16,4 @@ Every renderer writes to `{doc_workspace}/.working/` with a descriptive filename
|
|
|
16
16
|
|
|
17
17
|
The parent passes the subagent: current `.memlog.md`, relevant prior `.working/` captures, the user's stated intent for this pass, the output path. The subagent writes its artifact under `.working/` and returns ONLY a compact summary (file path, one line per variant, mode coverage). Parent never holds the full payload.
|
|
18
18
|
|
|
19
|
-
For HTML, open in browser when interactive
|
|
19
|
+
For HTML, open in the browser when interactive with the platform opener — `open "PATH"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead. Skip in headless.
|
|
@@ -69,7 +69,7 @@ Under Validate intent, after every reviewer returns, render one consolidated rep
|
|
|
69
69
|
2. Fill `{workflow.validation_report_template}`. No overall grade — the per-category verdicts and severity counts already say what's true. Synthesis paragraph lifts the rubric's overall verdict; add a second if extra reviewers shift the picture. One section per rubric category (open if thin / broken), one per extra reviewer (closed, adversarial voice preserved).
|
|
70
70
|
3. Write `{doc_workspace}/validation-report.html`.
|
|
71
71
|
4. Write the Markdown twin `{doc_workspace}/validation-report.md` — same content grouped by severity.
|
|
72
|
-
5. Open HTML
|
|
72
|
+
5. Open HTML with the platform opener — `open "{doc_workspace}/validation-report.html"` on macOS, `xdg-open` on Linux, `start ""` on Windows, path always double-quoted. On failure, give the user the path instead. Skip headless.
|
|
73
73
|
|
|
74
74
|
Re-running overwrites the consolidated report; individual `review-*.md` files persist.
|
|
75
75
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-advanced-elicitation
|
|
3
|
-
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
|
|
3
|
+
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'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Advanced Elicitation
|
|
@@ -15,12 +15,12 @@ You are BMad's shared refinement checkpoint: other skills invoke you at natural
|
|
|
15
15
|
|
|
16
16
|
## On Activation
|
|
17
17
|
|
|
18
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
18
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults.
|
|
19
19
|
2. Hold every `{workflow.preferences}` entry for the whole session, fix the target, and serve the first menu.
|
|
20
20
|
|
|
21
21
|
## Serving the Catalog
|
|
22
22
|
|
|
23
|
-
`scripts/pick_methods.py` serves the method catalog (num, category, method_name, description, output_pattern) so it never enters context whole — the one exception is
|
|
23
|
+
`scripts/pick_methods.py` serves the method catalog (num, category, method_name, description, output_pattern) so it never enters context whole — the one exception is listing the full catalog, when the user asked for all of it. Invoke as:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
uv run {skill-root}/scripts/pick_methods.py --file {workflow.methods_file} <command>
|
|
@@ -29,7 +29,7 @@ uv run {skill-root}/scripts/pick_methods.py --file {workflow.methods_file} <comm
|
|
|
29
29
|
If `{workflow.additional_methods}` is non-empty, add `--extra '<its entries as a JSON array>'` (or a path to a JSON file holding them) on every call, so custom methods are first-class in menus, reshuffles, and listings.
|
|
30
30
|
|
|
31
31
|
- `categories` — category names + counts, the cheap map.
|
|
32
|
-
- `list --category <cat> [--category <cat>]` — the index for chosen categories; `--all` dumps the whole catalog, only
|
|
32
|
+
- `list --category <cat> [--category <cat>]` — the index for chosen categories; `--all` dumps the whole catalog, only when listing all.
|
|
33
33
|
- `show <name-or-num> [...]` — full rows by name or num.
|
|
34
34
|
- `random -n 5 --spread [--exclude <name>]...` — a category-diverse random draw.
|
|
35
35
|
|
|
@@ -37,30 +37,29 @@ If `{workflow.additional_methods}` is non-empty, add `--extra '<its entries as a
|
|
|
37
37
|
|
|
38
38
|
## The Menu
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
3. [Method Name]
|
|
47
|
-
4. [Method Name]
|
|
48
|
-
5. [Method Name]
|
|
49
|
-
r. Reshuffle the list with 5 new options
|
|
50
|
-
a. List all methods with descriptions
|
|
51
|
-
x. Proceed / No Further Actions
|
|
52
|
-
```
|
|
40
|
+
HALT and give the user a choice:
|
|
41
|
+
|
|
42
|
+
- The five offered methods, listed by name. The user may pick one or several.
|
|
43
|
+
- **Reshuffle** — replace the list with five new options.
|
|
44
|
+
- **List all** — show the full catalog with descriptions.
|
|
45
|
+
- **Proceed** — no further elicitation.
|
|
53
46
|
|
|
54
|
-
This menu is the interface other skills and their users rely on — keep its options and behavior stable.
|
|
47
|
+
This menu is the interface other skills and their users rely on — keep its options and behavior stable.
|
|
55
48
|
|
|
56
|
-
-
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
-
|
|
49
|
+
- If the user picks methods: run them (several: in sequence), then offer the menu again.
|
|
50
|
+
- If the user chooses **Reshuffle**: reshuffle as above and offer the menu again.
|
|
51
|
+
- If the user chooses **List all**: show the full catalog (`list --all`) as a compact table; a pick by name or number runs like a method choice.
|
|
52
|
+
- If the user chooses **Proceed**: done. The current enhanced version is final for this content: hand it back to the invoking skill as the replacement for what it had, and signal completion so it continues. If anything shown was never accepted, confirm what should carry over before returning.
|
|
53
|
+
- Any other reply is direction: apply it to the target and offer the menu again.
|
|
61
54
|
|
|
62
55
|
## Running a Method
|
|
63
56
|
|
|
64
|
-
Use the method's description as its intent and its output_pattern as a flexible flow guide; scale depth to the target — a paragraph gets a light pass, an architecture decision gets the full treatment. Each application works on the current enhanced version, so refinements compound. Show what the method revealed and the changes it proposes, then
|
|
57
|
+
Use the method's description as its intent and its output_pattern as a flexible flow guide; scale depth to the target — a paragraph gets a light pass, an architecture decision gets the full treatment. Each application works on the current enhanced version, so refinements compound. Show what the method revealed and the changes it proposes, then HALT and give the user a choice:
|
|
58
|
+
|
|
59
|
+
- **Apply** — accept the proposed changes.
|
|
60
|
+
- **Reject** — drop the proposal entirely.
|
|
61
|
+
- Or give different direction.
|
|
62
|
+
|
|
63
|
+
Never change the work unless the user accepts the proposal. If they reject it, drop the proposal entirely. Any other reply is instruction to follow.
|
|
65
64
|
|
|
66
65
|
When a method casts personas (round tables, panels, debates), invent named viewpoints suited to the content.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-brainstorming
|
|
3
|
-
description: Facilitate a brainstorming session using diverse creative techniques. Use when the user says 'help me brainstorm' or 'help me ideate'
|
|
3
|
+
description: Facilitate a brainstorming session using diverse creative techniques. Use when the user says 'help me brainstorm' or 'help me ideate'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BMad Brainstorming
|
|
@@ -19,7 +19,7 @@ The session runs in one of three stances, chosen by the user — set explicitly
|
|
|
19
19
|
|
|
20
20
|
## On Activation
|
|
21
21
|
|
|
22
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, use a subagent to read `{skill-root}/customize.toml` directly with defaults.
|
|
22
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, use a subagent to read `{skill-root}/customize.toml` directly with defaults.
|
|
23
23
|
2. Run each `{workflow.activation_steps_prepend}` entry. Treat each `{workflow.persistent_facts}` entry as foundational context (`file:`-prefixed entries are paths/globs under `{project-root}` — load their contents; others are facts verbatim).
|
|
24
24
|
3. Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{output_folder}`, `{project_name}` using sensible defaults (ask the user or omit `{user_name}`; English for language fields; infer `{project_name}` from the Hedgehog project); `{date}` is today. Missing values → neutral defaults; never block.
|
|
25
25
|
4. **If launched headless** (a machine signal, not a human asking for output — `references/headless.md` lists them): load `references/headless.md` and follow it for the whole run; never load it otherwise. Outside headless, you generate ideas yourself only in autonomous mode (`references/mode-autonomous.md`) — never in facilitator or partner mode.
|
|
@@ -692,7 +692,26 @@ def html_doc(rows: list[dict]) -> str:
|
|
|
692
692
|
)
|
|
693
693
|
|
|
694
694
|
|
|
695
|
+
def pin_utf8(stream):
|
|
696
|
+
"""Pin a console stream to UTF-8, keeping its own error handler.
|
|
697
|
+
|
|
698
|
+
`--extra` technique text and the technique names echoed to stderr are
|
|
699
|
+
arbitrary user input, so either stream can carry a character the platform
|
|
700
|
+
default cannot encode (cp1252 on Windows) and print() then raises.
|
|
701
|
+
|
|
702
|
+
errors= is passed through deliberately: reconfigure(encoding=...) alone
|
|
703
|
+
resets the handler to "strict", which would silently downgrade stderr's
|
|
704
|
+
POSIX default of "backslashreplace" and turn a diagnostic about an
|
|
705
|
+
undecodable path into a traceback.
|
|
706
|
+
"""
|
|
707
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
708
|
+
if reconfigure is not None:
|
|
709
|
+
reconfigure(encoding="utf-8", errors=getattr(stream, "errors", None) or "strict")
|
|
710
|
+
|
|
711
|
+
|
|
695
712
|
def main(argv: list[str] | None = None) -> int:
|
|
713
|
+
pin_utf8(sys.stdout)
|
|
714
|
+
pin_utf8(sys.stderr)
|
|
696
715
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
697
716
|
p.add_argument("--file", type=Path, default=DEFAULT_FILE, help="technique CSV (default: sibling assets/brain-methods.csv)")
|
|
698
717
|
p.add_argument("--extra", type=Path, help="JSON overlay of additional techniques (customize.toml additional_techniques), merged into every command")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# dependencies = ["pytest>=8.0"]
|
|
4
4
|
# ///
|
|
5
5
|
"""Tests for brain.py. Run: uv run -m pytest scripts/tests/test_brain.py"""
|
|
6
|
+
import io
|
|
6
7
|
import json
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
@@ -227,6 +228,55 @@ def test_unknown_category_style_uses_fallback_glyph():
|
|
|
227
228
|
assert glyph == brain._FALLBACK_GLYPH
|
|
228
229
|
|
|
229
230
|
|
|
231
|
+
# --- console encoding (Windows cp1252) ----------------------------------
|
|
232
|
+
|
|
233
|
+
def _cp1252_stream():
|
|
234
|
+
"""A text stream that behaves like a Windows console: cp1252, strict."""
|
|
235
|
+
return io.TextIOWrapper(io.BytesIO(), encoding="cp1252", errors="strict", write_through=True)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_extra_technique_prints_when_stdout_encoding_is_cp1252(lib, tmp_path, monkeypatch):
|
|
239
|
+
# --extra text is arbitrary user input; the shipped catalog happens to be
|
|
240
|
+
# cp1252-safe, an overlay is not. Unpinned stdout raises UnicodeEncodeError
|
|
241
|
+
# and the command exits having printed nothing.
|
|
242
|
+
overlay = tmp_path / "extra.json"
|
|
243
|
+
overlay.write_text(
|
|
244
|
+
json.dumps([{"category": "wild", "technique_name": "Fikir Fırtınası 🌪", "description": "Beyin fırtınası — 日本語"}]),
|
|
245
|
+
encoding="utf-8",
|
|
246
|
+
)
|
|
247
|
+
fake = _cp1252_stream()
|
|
248
|
+
monkeypatch.setattr(sys, "stdout", fake)
|
|
249
|
+
assert brain.main(["--file", str(lib), "--extra", str(overlay), "list", "--all"]) == 0
|
|
250
|
+
out = fake.buffer.getvalue().decode("utf-8")
|
|
251
|
+
assert "Fikir Fırtınası 🌪" in out
|
|
252
|
+
assert "日本語" in out
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_missing_technique_name_reports_when_stderr_encoding_is_cp1252(lib, monkeypatch):
|
|
256
|
+
# `show` echoes the name it could not find; that name came from argv.
|
|
257
|
+
fake = _cp1252_stream()
|
|
258
|
+
monkeypatch.setattr(sys, "stderr", fake)
|
|
259
|
+
assert brain.main(["--file", str(lib), "show", "日本語"]) == 1
|
|
260
|
+
assert "日本語" in fake.buffer.getvalue().decode("utf-8")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_pin_utf8_preserves_the_streams_error_handler():
|
|
264
|
+
# reconfigure(encoding=...) on its own resets errors to "strict"; stderr on
|
|
265
|
+
# POSIX defaults to "backslashreplace" and must keep it, or a diagnostic
|
|
266
|
+
# carrying a surrogate-escaped path becomes a traceback.
|
|
267
|
+
stream = io.TextIOWrapper(io.BytesIO(), encoding="ascii", errors="backslashreplace")
|
|
268
|
+
brain.pin_utf8(stream)
|
|
269
|
+
assert stream.encoding == "utf-8"
|
|
270
|
+
assert stream.errors == "backslashreplace"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_pin_utf8_ignores_a_stream_without_reconfigure():
|
|
274
|
+
class Captured: # e.g. pytest's capture object, or a StringIO stand-in
|
|
275
|
+
errors = None
|
|
276
|
+
|
|
277
|
+
brain.pin_utf8(Captured()) # must not raise
|
|
278
|
+
|
|
279
|
+
|
|
230
280
|
def test_shipped_selector_is_in_sync_with_catalog():
|
|
231
281
|
# foolproofing: if someone edits brain-methods.csv they must regenerate the page.
|
|
232
282
|
# Regenerate with: uv run brain.py html --out assets/brain-selector.html
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-deep-recon
|
|
3
|
-
description: '
|
|
3
|
+
description: 'Research a topic to support a decision, three ways: draft a research prompt for the user to run in their own tool (ChatGPT, Gemini, Grok, Perplexity, …), turn a finished research report into a short summary with cited sources that other skills can use directly, or run the research here with parallel web searches. Built-in research types: market, domain, technical, competitive, user-voice, academic-lit; also supports choosing between candidates, and custom types via overrides. Use when the user says "deep recon", "research this", "draft a research prompt", "process this research report", "market research", "domain research", "technical research", "competitor research", "literature review", or "help me choose between"'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BMad Deep Recon
|
|
@@ -37,7 +37,7 @@ Three services, freely combined — each detailed in its reference: **Draft** a
|
|
|
37
37
|
|
|
38
38
|
**Forwarded activation:** if a caller invoked you with a stated intent, research type, or pre-resolved customization fields, honor them verbatim — skip your own inference for those values and resolve only the rest.
|
|
39
39
|
|
|
40
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow` (on failure read `{skill-root}/customize.toml`, use defaults). Run `{workflow.activation_steps_prepend}`, then `{workflow.activation_steps_append}`.
|
|
40
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow` (on failure read `{skill-root}/customize.toml`, use defaults). Run `{workflow.activation_steps_prepend}`, then `{workflow.activation_steps_append}`.
|
|
41
41
|
2. Resolve `{user_name}` (ask the user or omit), `{communication_language}` (English), `{document_output_language}` (English), `{project_name}` (infer from the Hedgehog project), `{output_folder}`, `{planning_artifacts}` (defaults to `{output_folder}` if unset), and `{date}` (today's date) using sensible defaults; missing values take neutral defaults, never block.
|
|
42
42
|
3. Headless (no interactive user) → see `## Headless Mode`. Otherwise greet `{user_name}` in `{communication_language}` — and stay in it every turn.
|
|
43
43
|
4. Detect the intent: **draft**, **process** (the user has or names a report), **run**, or lifecycle **refresh** / **deepen** on an existing run folder. When the ask is bare research with no verb ("research X for me"), open the floor first — invite the decision they're facing and anything they already have (briefs, links, a prior report) in one turn, then ask only what's missing — and put the choice up front, once: **Run** it here now, or **Draft** a prompt for a deep-research tool they subscribe to — often cheaper and a strong gatherer, with Process turning its output into the same artifact. State the trade honestly (tokens and minutes here vs. one manual round-trip there); their call, remembered for the session.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bmad-forge-idea
|
|
3
|
-
description:
|
|
3
|
+
description: Test a half-formed idea in a questioning conversation, with different personas probing its weak points, until the user can act on it or drop it with confidence. Optionally writes a short brief for planning skills to build on. Use when the user says 'forge an idea', 'pressure-test this idea', 'stress-test my thinking', or 'harden this idea'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BMad Forge Idea
|
|
@@ -23,7 +23,7 @@ Lead by questioning, not lecturing. Ask one question at a time, press on weak po
|
|
|
23
23
|
|
|
24
24
|
## On Activation
|
|
25
25
|
|
|
26
|
-
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly with defaults. Apply the resolved `{workflow.*}` values throughout.
|
|
26
|
+
1. Resolve customization: `uv run {bmad-root}/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly with defaults. Apply the resolved `{workflow.*}` values throughout.
|
|
27
27
|
2. Run each `{workflow.activation_steps_prepend}` entry; treat each `{workflow.persistent_facts}` entry as foundational context (`file:` entries load their contents, `skill:` names a skill to consult, others are facts verbatim).
|
|
28
28
|
3. Resolve `{user_name}`, `{communication_language}`, `{output_folder}` using sensible defaults (ask the user or omit `{user_name}`; English for language fields); `{date}` is today. Missing values → neutral defaults; never block. Greet `{user_name}` in `{communication_language}` and stay in it.
|
|
29
29
|
4. Resume: glob `{workflow.forge_output_path}/**/.memlog.md` (recursive, so it still finds sessions when `run_folder_pattern` is overridden to nest paths) and read only each match's frontmatter to find any whose `status` is not `complete`. Offer to resume one — then read its full memlog once to rebuild state and continue append-only — or to start fresh.
|
|
@@ -48,7 +48,7 @@ Otherwise ask for what's missing, in order:
|
|
|
48
48
|
|
|
49
49
|
### Steering the conversation
|
|
50
50
|
|
|
51
|
-
Tell the user they can say **"attack this"**, **"defend this"**, or **"switch roles"** at any time to change how the current idea is argued. In attack mode, do not agree with the idea; look for contradictions, weak assumptions, and failure cases. In defend mode, argue for the strongest version of the idea. Tell the user they can also name a persona at any time to change who participates in the session.
|
|
51
|
+
Tell the user they can say **"attack this"**, **"defend this"**, or **"switch roles"** at any time to change how the current idea is argued. In attack mode, do not agree with the idea; look for contradictions, weak assumptions, and failure cases. In defend mode, argue for the strongest version of the idea. Tell the user they can also name a persona or party at any time to change who participates in the session.
|
|
52
52
|
|
|
53
53
|
### Set up the session
|
|
54
54
|
|
|
@@ -110,7 +110,9 @@ def find_party_skill(project_root: Path, skill_root: Path):
|
|
|
110
110
|
def load_party_workflow(project_root: Path, party_skill: Path):
|
|
111
111
|
"""Merged [workflow] table for bmad-party-mode (base + user overrides)."""
|
|
112
112
|
resolver = project_root / "_bmad" / "scripts" / "resolve_customization.py"
|
|
113
|
-
data = _run_json(
|
|
113
|
+
data = _run_json(
|
|
114
|
+
[sys.executable, str(resolver), "--skill", str(party_skill), "--project-root", str(project_root), "--key", "workflow"]
|
|
115
|
+
)
|
|
114
116
|
if data is not None and isinstance(data.get("workflow"), dict):
|
|
115
117
|
return data["workflow"]
|
|
116
118
|
# Fallback: base customize.toml directly, no override merge.
|
package/vendor-skills/BMAD/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"""Unit tests for resolve_personas.py — pool merge, alias, party resolution."""
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
import unittest
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -134,5 +135,26 @@ class TestOverrideMergeFallback(unittest.TestCase):
|
|
|
134
135
|
self.assertEqual([m["code"] for m in wf["party_members"]], ["x", "y"]) # appended
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
class TestResolverInvocation(unittest.TestCase):
|
|
139
|
+
"""The wrapper knows the project root, so it must not let the resolver
|
|
140
|
+
infer one from the working directory (#2796)."""
|
|
141
|
+
|
|
142
|
+
def _captured_command(self, tmp):
|
|
143
|
+
captured = []
|
|
144
|
+
original = rp._run_json
|
|
145
|
+
rp._run_json = lambda cmd: captured.append(cmd) or {"workflow": {}}
|
|
146
|
+
try:
|
|
147
|
+
rp.load_party_workflow(Path(tmp) / "project", Path(tmp) / "skill")
|
|
148
|
+
finally:
|
|
149
|
+
rp._run_json = original
|
|
150
|
+
return captured[0]
|
|
151
|
+
|
|
152
|
+
def test_passes_project_root_to_the_customization_resolver(self):
|
|
153
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
154
|
+
cmd = self._captured_command(tmp)
|
|
155
|
+
self.assertIn("--project-root", cmd)
|
|
156
|
+
self.assertEqual(cmd[cmd.index("--project-root") + 1], str(Path(tmp) / "project"))
|
|
157
|
+
|
|
158
|
+
|
|
137
159
|
if __name__ == "__main__":
|
|
138
160
|
unittest.main()
|
|
@@ -1,177 +1,104 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
Uses only the Python stdlib (`tomllib`) — no third-party dependencies.
|
|
15
|
-
BMad is standardizing on `uv run` to invoke scripts (uv provisions a suitable
|
|
16
|
-
interpreter for you); a plain `python3` on PATH still works during the
|
|
17
|
-
transition. Either runner needs Python 3.11+ for `tomllib`.
|
|
18
|
-
|
|
19
|
-
uv run resolve_customization.py --skill /abs/path/to/skill-dir
|
|
20
|
-
uv run resolve_customization.py --skill ... --key agent
|
|
21
|
-
uv run resolve_customization.py --skill ... --key agent.menu
|
|
22
|
-
|
|
23
|
-
Merge rules (purely structural — no field-name special-casing):
|
|
24
|
-
- Scalars (string, int, bool, float): override wins
|
|
25
|
-
- Tables: deep merge (recursively apply these rules)
|
|
26
|
-
- Arrays of tables where every item shares the *same* identifier
|
|
27
|
-
field (every item has `code`, or every item has `id`):
|
|
28
|
-
merge by that key (matching keys replace, new keys append)
|
|
29
|
-
- All other arrays — including arrays where only some items have
|
|
30
|
-
`code` or `id`, or where items mix the two keys:
|
|
31
|
-
append (base items followed by override items)
|
|
32
|
-
|
|
33
|
-
No removal mechanism — overrides cannot delete base items. To suppress
|
|
34
|
-
a default, fork the skill or override the item by code with a no-op
|
|
35
|
-
description/prompt.
|
|
36
|
-
"""
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# ///
|
|
5
|
+
"""Resolve a skill's default, team, and user TOML customization layers."""
|
|
37
6
|
|
|
38
7
|
import argparse
|
|
39
8
|
import json
|
|
40
9
|
import sys
|
|
41
10
|
from pathlib import Path
|
|
42
11
|
|
|
12
|
+
# Installed scripts are consumer files, not a location for interpreter caches.
|
|
13
|
+
sys.dont_write_bytecode = True
|
|
14
|
+
|
|
43
15
|
try:
|
|
44
|
-
import
|
|
45
|
-
except
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
sys.exit(3)
|
|
16
|
+
from config_utils import ConfigError, load_customization
|
|
17
|
+
except ModuleNotFoundError as error:
|
|
18
|
+
if error.name != "tomllib":
|
|
19
|
+
raise
|
|
20
|
+
sys.stderr.write("error: Python 3.11+ is required (stdlib `tomllib` not found).\n")
|
|
21
|
+
raise SystemExit(3) from None
|
|
52
22
|
|
|
53
23
|
|
|
54
24
|
_MISSING = object()
|
|
55
|
-
_KEYED_MERGE_FIELDS = ("code", "id")
|
|
56
25
|
|
|
57
26
|
|
|
58
|
-
def find_project_root(start: Path):
|
|
27
|
+
def find_project_root(start: Path) -> Path | None:
|
|
28
|
+
"""Nearest ancestor holding `_bmad/`, falling back to the nearest holding `.git`.
|
|
29
|
+
|
|
30
|
+
`_bmad/` outranks `.git` at every depth: a submodule or nested repo carries
|
|
31
|
+
`.git` without being the BMad project, so treating the two as equal stops the
|
|
32
|
+
walk short of the root that owns `_bmad/custom/`.
|
|
33
|
+
"""
|
|
34
|
+
git_root: Path | None = None
|
|
59
35
|
current = start.resolve()
|
|
60
36
|
while True:
|
|
61
|
-
if (current / "_bmad").
|
|
37
|
+
if (current / "_bmad").is_dir():
|
|
62
38
|
return current
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def load_toml(file_path: Path, required: bool = False) -> dict:
|
|
70
|
-
if not file_path.exists():
|
|
71
|
-
if required:
|
|
72
|
-
sys.stderr.write(f"error: required customization file not found: {file_path}\n")
|
|
73
|
-
sys.exit(1)
|
|
74
|
-
return {}
|
|
75
|
-
try:
|
|
76
|
-
with file_path.open("rb") as f:
|
|
77
|
-
parsed = tomllib.load(f)
|
|
78
|
-
if not isinstance(parsed, dict):
|
|
79
|
-
if required:
|
|
80
|
-
sys.stderr.write(f"error: {file_path} did not parse to a table\n")
|
|
81
|
-
sys.exit(1)
|
|
82
|
-
return {}
|
|
83
|
-
return parsed
|
|
84
|
-
except tomllib.TOMLDecodeError as error:
|
|
85
|
-
level = "error" if required else "warning"
|
|
86
|
-
sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
|
|
87
|
-
if required:
|
|
88
|
-
sys.exit(1)
|
|
89
|
-
return {}
|
|
90
|
-
except OSError as error:
|
|
91
|
-
level = "error" if required else "warning"
|
|
92
|
-
sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
|
|
93
|
-
if required:
|
|
94
|
-
sys.exit(1)
|
|
95
|
-
return {}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def _detect_keyed_merge_field(items):
|
|
99
|
-
"""Return 'code' or 'id' if every table item carries that *same* field.
|
|
100
|
-
|
|
101
|
-
All items must share the same identifier (all `code`, or all `id`).
|
|
102
|
-
Mixed arrays — where some items use `code` and others use `id` —
|
|
103
|
-
return None and fall through to append semantics. This is intentional:
|
|
104
|
-
mixing identifier keys within one array is a schema smell, and
|
|
105
|
-
append-fallback is safer than guessing which key should merge.
|
|
106
|
-
"""
|
|
107
|
-
if not items or not all(isinstance(item, dict) for item in items):
|
|
108
|
-
return None
|
|
109
|
-
for candidate in _KEYED_MERGE_FIELDS:
|
|
110
|
-
if all(item.get(candidate) is not None for item in items):
|
|
111
|
-
return candidate
|
|
112
|
-
return None
|
|
113
|
-
|
|
39
|
+
if git_root is None and (current / ".git").exists():
|
|
40
|
+
git_root = current
|
|
41
|
+
if current.parent == current:
|
|
42
|
+
return git_root
|
|
43
|
+
current = current.parent
|
|
114
44
|
|
|
115
|
-
def _merge_by_key(base, override, key_name):
|
|
116
|
-
result = []
|
|
117
|
-
index_by_key = {}
|
|
118
|
-
|
|
119
|
-
for item in base:
|
|
120
|
-
if not isinstance(item, dict):
|
|
121
|
-
continue
|
|
122
|
-
if item.get(key_name) is not None:
|
|
123
|
-
index_by_key[item[key_name]] = len(result)
|
|
124
|
-
result.append(dict(item))
|
|
125
|
-
|
|
126
|
-
for item in override:
|
|
127
|
-
if not isinstance(item, dict):
|
|
128
|
-
result.append(item)
|
|
129
|
-
continue
|
|
130
|
-
key = item.get(key_name)
|
|
131
|
-
if key is not None and key in index_by_key:
|
|
132
|
-
result[index_by_key[key]] = dict(item)
|
|
133
|
-
else:
|
|
134
|
-
if key is not None:
|
|
135
|
-
index_by_key[key] = len(result)
|
|
136
|
-
result.append(dict(item))
|
|
137
45
|
|
|
138
|
-
|
|
46
|
+
def script_project_root() -> Path | None:
|
|
47
|
+
"""Project root implied by this script's own install path.
|
|
139
48
|
|
|
49
|
+
Skills invoke `{project-root}/_bmad/scripts/resolve_customization.py`, so when
|
|
50
|
+
this file sits at that path its grandparent is a project root the caller already
|
|
51
|
+
resolved.
|
|
52
|
+
"""
|
|
53
|
+
parents = Path(__file__).resolve().parents
|
|
54
|
+
if len(parents) >= 3 and parents[0].name == "scripts" and parents[1].name == "_bmad":
|
|
55
|
+
return parents[2]
|
|
56
|
+
return None
|
|
140
57
|
|
|
141
|
-
def _merge_arrays(base, override):
|
|
142
|
-
"""Shape-aware array merge. Base + override combined tables may opt into
|
|
143
|
-
keyed merge if every item has `code` or `id`. Otherwise: append."""
|
|
144
|
-
base_arr = base if isinstance(base, list) else []
|
|
145
|
-
override_arr = override if isinstance(override, list) else []
|
|
146
|
-
keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
|
|
147
|
-
if keyed_field:
|
|
148
|
-
return _merge_by_key(base_arr, override_arr, keyed_field)
|
|
149
|
-
return base_arr + override_arr
|
|
150
58
|
|
|
59
|
+
def candidate_project_roots(skill_dir: Path) -> list[Path]:
|
|
60
|
+
"""Plausible project roots, most trustworthy first.
|
|
151
61
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
- Array + array: shape-aware (keyed merge if all items have code/id, else append)
|
|
156
|
-
- Anything else: override wins
|
|
62
|
+
The working directory leads because the project is where the user is working,
|
|
63
|
+
not where the skill happens to be installed — a home-installed skill walks up to
|
|
64
|
+
`~`, and any `~/_bmad` there would otherwise mask the real project's overrides.
|
|
157
65
|
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
66
|
+
ordered: list[Path] = []
|
|
67
|
+
for root in (
|
|
68
|
+
find_project_root(Path.cwd()),
|
|
69
|
+
script_project_root(),
|
|
70
|
+
find_project_root(skill_dir),
|
|
71
|
+
):
|
|
72
|
+
if root is not None and root not in ordered:
|
|
73
|
+
ordered.append(root)
|
|
74
|
+
return ordered
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def has_override(root: Path, skill_name: str) -> bool:
|
|
78
|
+
custom_dir = root / "_bmad" / "custom"
|
|
79
|
+
return any(
|
|
80
|
+
(custom_dir / name).is_file()
|
|
81
|
+
for name in (f"{skill_name}.toml", f"{skill_name}.user.toml")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def warn_on_masked_override(chosen: Path, rejected: list[Path], skill_name: str) -> None:
|
|
86
|
+
"""Break the silence when a real override exists under a root we did not pick."""
|
|
87
|
+
if has_override(chosen, skill_name):
|
|
88
|
+
return
|
|
89
|
+
for root in rejected:
|
|
90
|
+
if has_override(root, skill_name):
|
|
91
|
+
sys.stderr.write(
|
|
92
|
+
f"note: resolved project root {chosen} has no customization for "
|
|
93
|
+
f"`{skill_name}`, but {root} does. Using {chosen}; pass "
|
|
94
|
+
f"--project-root to select the other explicitly.\n"
|
|
95
|
+
)
|
|
96
|
+
return
|
|
169
97
|
|
|
170
98
|
|
|
171
99
|
def extract_key(data, dotted_key: str):
|
|
172
|
-
parts = dotted_key.split(".")
|
|
173
100
|
current = data
|
|
174
|
-
for part in
|
|
101
|
+
for part in dotted_key.split("."):
|
|
175
102
|
if isinstance(current, dict) and part in current:
|
|
176
103
|
current = current[part]
|
|
177
104
|
else:
|
|
@@ -179,62 +106,59 @@ def extract_key(data, dotted_key: str):
|
|
|
179
106
|
return current
|
|
180
107
|
|
|
181
108
|
|
|
182
|
-
def write_json_stdout(output):
|
|
183
|
-
"""Write JSON as UTF-8 so Windows cp1252 stdout can carry emoji icons."""
|
|
109
|
+
def write_json_stdout(output) -> None:
|
|
184
110
|
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
|
185
111
|
if reconfigure is not None:
|
|
186
112
|
reconfigure(encoding="utf-8")
|
|
187
113
|
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
|
|
188
114
|
|
|
189
115
|
|
|
190
|
-
def main():
|
|
116
|
+
def main() -> int:
|
|
191
117
|
parser = argparse.ArgumentParser(
|
|
192
|
-
description="Resolve customization
|
|
193
|
-
|
|
118
|
+
description="Resolve skill customization using three-layer TOML merge."
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--skill", "-s", required=True, help="Absolute path to the skill directory"
|
|
194
122
|
)
|
|
195
123
|
parser.add_argument(
|
|
196
|
-
"--
|
|
197
|
-
|
|
124
|
+
"--project-root",
|
|
125
|
+
"-p",
|
|
126
|
+
help="Explicit project root containing _bmad/ (recommended)",
|
|
198
127
|
)
|
|
199
128
|
parser.add_argument(
|
|
200
|
-
"--key",
|
|
129
|
+
"--key",
|
|
130
|
+
"-k",
|
|
131
|
+
action="append",
|
|
132
|
+
default=[],
|
|
201
133
|
help="Dotted field path to resolve (repeatable). Omit for full dump.",
|
|
202
134
|
)
|
|
203
135
|
args = parser.parse_args()
|
|
204
136
|
|
|
205
137
|
skill_dir = Path(args.skill).resolve()
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# for standalone skills invoked directly). Using cwd first is unsafe when
|
|
214
|
-
# an ancestor of cwd happens to have a stray _bmad/ from another project.
|
|
215
|
-
project_root = find_project_root(skill_dir) or find_project_root(Path.cwd())
|
|
216
|
-
|
|
217
|
-
team = {}
|
|
218
|
-
user = {}
|
|
219
|
-
if project_root:
|
|
220
|
-
custom_dir = project_root / "_bmad" / "custom"
|
|
221
|
-
team = load_toml(custom_dir / f"{skill_name}.toml")
|
|
222
|
-
user = load_toml(custom_dir / f"{skill_name}.user.toml")
|
|
138
|
+
if args.project_root:
|
|
139
|
+
project_root = Path(args.project_root).resolve()
|
|
140
|
+
else:
|
|
141
|
+
candidates = candidate_project_roots(skill_dir)
|
|
142
|
+
project_root = candidates[0] if candidates else None
|
|
143
|
+
if project_root is not None:
|
|
144
|
+
warn_on_masked_override(project_root, candidates[1:], skill_dir.name)
|
|
223
145
|
|
|
224
|
-
|
|
225
|
-
|
|
146
|
+
try:
|
|
147
|
+
merged = load_customization(project_root, skill_dir)
|
|
148
|
+
except ConfigError as error:
|
|
149
|
+
sys.stderr.write(f"error: {error}\n")
|
|
150
|
+
return 1
|
|
226
151
|
|
|
152
|
+
output = merged
|
|
227
153
|
if args.key:
|
|
228
154
|
output = {}
|
|
229
155
|
for key in args.key:
|
|
230
156
|
value = extract_key(merged, key)
|
|
231
157
|
if value is not _MISSING:
|
|
232
158
|
output[key] = value
|
|
233
|
-
else:
|
|
234
|
-
output = merged
|
|
235
|
-
|
|
236
159
|
write_json_stdout(output)
|
|
160
|
+
return 0
|
|
237
161
|
|
|
238
162
|
|
|
239
163
|
if __name__ == "__main__":
|
|
240
|
-
main()
|
|
164
|
+
raise SystemExit(main())
|