bmad-method 6.11.1-next.8 → 6.12.0
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/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +12 -0
- package/CLAUDE.md +1 -0
- package/README.md +13 -19
- package/README_CN.md +0 -10
- package/README_KR.md +90 -0
- package/README_VN.md +0 -10
- package/greptile.json +52 -0
- package/package.json +6 -5
- package/src/bmm-skills/agents/bmad-agent-analyst/SKILL.md +1 -1
- package/src/bmm-skills/agents/bmad-agent-analyst/customize.toml +1 -3
- package/src/bmm-skills/agents/bmad-agent-architect/SKILL.md +1 -1
- package/src/bmm-skills/agents/bmad-agent-architect/customize.toml +1 -3
- package/src/bmm-skills/agents/bmad-agent-dev/SKILL.md +1 -1
- package/src/bmm-skills/agents/bmad-agent-dev/customize.toml +1 -3
- package/src/bmm-skills/agents/bmad-agent-pm/SKILL.md +1 -1
- package/src/bmm-skills/agents/bmad-agent-pm/customize.toml +1 -3
- package/src/bmm-skills/agents/bmad-agent-ux-designer/SKILL.md +1 -1
- package/src/bmm-skills/agents/bmad-agent-ux-designer/customize.toml +1 -3
- package/src/bmm-skills/module-help.csv +2 -2
- package/src/bmm-skills/plan/bmad-architecture/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-architecture/customize.toml +5 -6
- package/src/bmm-skills/plan/bmad-create-epics-and-stories/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-create-epics-and-stories/customize.toml +1 -3
- package/src/bmm-skills/plan/bmad-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
- package/src/bmm-skills/plan/bmad-prd/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-prd/customize.toml +5 -6
- package/src/bmm-skills/plan/bmad-prfaq/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-prfaq/customize.toml +1 -3
- package/src/bmm-skills/plan/bmad-prfaq/references/verdict.md +1 -1
- package/src/bmm-skills/plan/bmad-product-brief/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-product-brief/customize.toml +5 -6
- package/src/bmm-skills/plan/bmad-project-context/SKILL.md +7 -6
- package/src/bmm-skills/plan/bmad-project-context/references/best-practices.md +2 -2
- package/src/bmm-skills/plan/bmad-spec/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-spec/customize.toml +5 -5
- package/src/bmm-skills/plan/bmad-sprint-planning/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-sprint-planning/customize.toml +1 -3
- package/src/bmm-skills/plan/bmad-sprint-planning/scripts/__pycache__/sprint_plan.cpython-311.pyc +0 -0
- package/src/bmm-skills/plan/bmad-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-311-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/plan/bmad-ux/SKILL.md +1 -1
- package/src/bmm-skills/plan/bmad-ux/customize.toml +1 -3
- package/src/bmm-skills/ship/bmad-build/SKILL.md +1 -1
- package/src/bmm-skills/ship/bmad-build/customize.toml +21 -33
- package/src/bmm-skills/ship/bmad-build/references/claims-check.md +14 -0
- package/src/bmm-skills/ship/bmad-build/review-prompts/edge-case-hunter.md +31 -7
- package/src/bmm-skills/ship/bmad-build/review-prompts/verification-gap.md +4 -4
- package/src/bmm-skills/ship/bmad-build/spec-template.md +26 -4
- package/src/bmm-skills/ship/bmad-build/step-01-clarify-and-route.md +21 -26
- package/src/bmm-skills/ship/bmad-build/step-02-plan.md +31 -16
- package/src/bmm-skills/ship/bmad-build/step-03-implement.md +5 -3
- package/src/bmm-skills/ship/bmad-build/step-04-review.md +49 -19
- package/src/bmm-skills/ship/bmad-build/step-05-present.md +9 -41
- package/src/bmm-skills/ship/bmad-build/step-oneshot.md +60 -29
- package/src/bmm-skills/ship/bmad-build/sync-sprint-status.md +5 -19
- package/src/bmm-skills/ship/bmad-build-auto/customize.toml +17 -19
- package/src/bmm-skills/ship/bmad-build-auto/references/claims-check.md +14 -0
- package/src/bmm-skills/ship/bmad-build-auto/review-prompts/edge-case-hunter.md +31 -7
- package/src/bmm-skills/ship/bmad-build-auto/review-prompts/verification-gap.md +4 -4
- package/src/bmm-skills/ship/bmad-build-auto/spec-template.md +4 -7
- package/src/bmm-skills/ship/bmad-build-auto/step-01-clarify-and-route.md +3 -2
- package/src/bmm-skills/ship/bmad-build-auto/step-03-implement.md +5 -1
- package/src/bmm-skills/ship/bmad-build-auto/step-04-review.md +58 -38
- package/src/bmm-skills/ship/bmad-build-auto/workflow.md +1 -1
- package/src/bmm-skills/ship/bmad-code-review/SKILL.md +1 -1
- package/src/bmm-skills/ship/bmad-code-review/customize.toml +15 -18
- package/src/bmm-skills/ship/bmad-code-review/references/claims-check.md +14 -0
- package/src/bmm-skills/ship/bmad-code-review/review-prompts/edge-case-hunter.md +31 -7
- package/src/bmm-skills/ship/bmad-code-review/review-prompts/verification-gap.md +4 -4
- package/src/bmm-skills/ship/bmad-code-review/steps/step-01-gather-context.md +30 -23
- package/src/bmm-skills/ship/bmad-code-review/steps/step-02-review.md +3 -3
- package/src/bmm-skills/ship/bmad-code-review/steps/step-03-triage.md +25 -21
- package/src/bmm-skills/ship/bmad-code-review/steps/step-04-present.md +20 -18
- package/src/bmm-skills/ship/bmad-correct-course/SKILL.md +13 -5
- package/src/bmm-skills/ship/bmad-correct-course/customize.toml +1 -3
- package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/SKILL.md +2 -2
- package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/customize.toml +1 -3
- package/src/bmm-skills/ship/bmad-retrospective/SKILL.md +1 -1
- package/src/bmm-skills/ship/bmad-retrospective/customize.toml +1 -3
- package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
- package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/SKILL.md +4 -4
- package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/customize.toml +2 -4
- package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-05-wrapup.md +1 -1
- package/src/bmm-skills/v6-shims/README.md +1 -0
- package/src/bmm-skills/v6-shims/bmad-checkpoint-preview/SKILL.md +21 -0
- package/src/bmm-skills/v6-shims/bmad-create-architecture/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-create-architecture/customize.toml +1 -3
- package/src/bmm-skills/v6-shims/bmad-create-prd/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-create-prd/customize.toml +1 -3
- package/src/bmm-skills/v6-shims/bmad-create-story/SKILL.md +3 -3
- package/src/bmm-skills/v6-shims/bmad-create-story/customize.toml +1 -3
- package/src/bmm-skills/v6-shims/bmad-dev-story/SKILL.md +2 -2
- package/src/bmm-skills/v6-shims/bmad-dev-story/customize.toml +1 -6
- package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-edit-prd/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-edit-prd/customize.toml +1 -3
- package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-sprint-status/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-sprint-status/customize.toml +1 -3
- package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-validate-prd/SKILL.md +1 -1
- package/src/bmm-skills/v6-shims/bmad-validate-prd/customize.toml +1 -3
- package/src/core-skills/bmad-advanced-elicitation/SKILL.md +22 -23
- package/src/core-skills/bmad-brainstorming/SKILL.md +1 -1
- package/src/core-skills/bmad-brainstorming/customize.toml +5 -6
- package/src/core-skills/bmad-brainstorming/scripts/brain.py +19 -0
- package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +50 -0
- package/src/core-skills/bmad-customize/SKILL.md +1 -1
- package/src/core-skills/bmad-deep-recon/SKILL.md +1 -1
- package/src/core-skills/bmad-forge-idea/SKILL.md +1 -1
- package/src/core-skills/bmad-forge-idea/customize.toml +5 -6
- package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +3 -1
- package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +22 -0
- package/src/core-skills/bmad-help/SKILL.md +1 -1
- package/src/core-skills/bmad-party-mode/SKILL.md +1 -1
- package/src/core-skills/bmad-party-mode/customize.toml +4 -4
- package/src/core-skills/bmad-party-mode/references/create-party.md +1 -1
- package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +3 -1
- package/src/core-skills/bmad-party-mode/scripts/tests/test_resolve_party.py +22 -0
- package/src/core-skills/bmad-review/SKILL.md +5 -4
- package/src/core-skills/bmad-review/customize.toml +1 -1
- package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +18 -0
- package/src/core-skills/module-help.csv +1 -1
- package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
- package/src/scripts/resolve_config.py +9 -1
- package/src/scripts/resolve_customization.py +72 -7
- package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
- package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
- package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
- package/src/scripts/tests/test_resolve_config.py +28 -0
- package/src/scripts/tests/test_resolve_customization.py +102 -0
- package/tools/installer/core/installer.js +33 -1
- package/tools/installer/core/shim-policy.js +75 -7
- package/tools/installer/ide/platform-codes.yaml +13 -0
- package/tools/installer/prompts.js +15 -2
- package/tools/installer/ui.js +28 -8
- package/tools/skill-validator.md +85 -151
- package/tools/tests/__pycache__/test_validate_skills.cpython-311.pyc +0 -0
- package/tools/tests/fixtures/validate-skills/bmad/SKILL.md +8 -0
- package/tools/tests/fixtures/validate-skills/deprecated-shim/SKILL.md +9 -0
- package/tools/tests/fixtures/validate-skills/missing-trigger/SKILL.md +9 -0
- package/tools/tests/fixtures/validate-skills/with-trigger/SKILL.md +8 -0
- package/tools/tests/test_validate_skills.py +476 -0
- package/tools/validate-published-implementation-model.mjs +0 -9
- package/tools/validate_skills.py +698 -0
- package/tools/validate-skills.js +0 -735
- /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/generate-trail.md +0 -0
- /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-01-orientation.md +0 -0
- /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-02-walkthrough.md +0 -0
- /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-03-detail-pass.md +0 -0
- /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-04-testing.md +0 -0
|
@@ -22,10 +22,10 @@ activation_steps_append = []
|
|
|
22
22
|
# Persistent facts the orchestrator keeps in mind for the whole session
|
|
23
23
|
# (house rules, running gags, topics to avoid). Each entry is a literal
|
|
24
24
|
# sentence, a `skill:`-prefixed reference, or a `file:`-prefixed path/glob whose
|
|
25
|
-
# contents load as facts.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
]
|
|
25
|
+
# contents load as facts. Empty by default — repo-wide context belongs in AGENTS.md
|
|
26
|
+
# (see bmad-project-context), which every skill already sees. Use this for context only
|
|
27
|
+
# the party needs, loaded on demand rather than carried as constant memory.
|
|
28
|
+
persistent_facts = []
|
|
29
29
|
|
|
30
30
|
# Which party loads when the user just says "party mode" with no override.
|
|
31
31
|
# Empty = the installed BMAD agents — exactly the default behavior of a plain
|
|
@@ -30,7 +30,7 @@ Ask which they're after if it isn't obvious, then proceed.
|
|
|
30
30
|
|
|
31
31
|
## Editing an existing party
|
|
32
32
|
|
|
33
|
-
When the user wants to change a party that already exists (retune a member's persona, add someone to a group, swap the default), read the current state first so you change rather than clobber: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow` returns the merged `party_members`, `party_groups`, and `default_party`. Show the member or group being touched, capture only the delta with the user, and hand that sparse change to `bmad-customize` — it replaces a `party_members`/`party_groups` entry whose `code`/`id` matches and appends the rest, so an edit is just the changed entry, never a full rewrite.
|
|
33
|
+
When the user wants to change a party that already exists (retune a member's persona, add someone to a group, swap the default), read the current state first so you change rather than clobber: `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow` returns the merged `party_members`, `party_groups`, and `default_party`. Show the member or group being touched, capture only the delta with the user, and hand that sparse change to `bmad-customize` — it replaces a `party_members`/`party_groups` entry whose `code`/`id` matches and appends the rest, so an edit is just the changed entry, never a full rewrite.
|
|
34
34
|
|
|
35
35
|
## Keeping new faces from a session
|
|
36
36
|
|
|
@@ -71,7 +71,9 @@ def load_agents(project_root: Path):
|
|
|
71
71
|
def load_workflow(project_root: Path, skill_root: Path):
|
|
72
72
|
"""Merged [workflow] table. Falls back to the skill's base customize.toml."""
|
|
73
73
|
script = project_root / "_bmad" / "scripts" / "resolve_customization.py"
|
|
74
|
-
data = _run_json(
|
|
74
|
+
data = _run_json(
|
|
75
|
+
[sys.executable, str(script), "--skill", str(skill_root), "--project-root", str(project_root), "--key", "workflow"]
|
|
76
|
+
)
|
|
75
77
|
if data is not None and "workflow" in data:
|
|
76
78
|
return data["workflow"]
|
|
77
79
|
# Fallback: read the skill's base customize.toml directly (no override merge).
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"""Unit tests for resolve_party.py — merge, alias, override, group resolution."""
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
import unittest
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -142,5 +143,26 @@ class TestInstalledCodesIsDefaultRoom(unittest.TestCase):
|
|
|
142
143
|
self.assertEqual(col["bmad-agent-analyst"]["name"], "Mary-Custom")
|
|
143
144
|
|
|
144
145
|
|
|
146
|
+
class TestResolverInvocation(unittest.TestCase):
|
|
147
|
+
"""The wrapper knows the project root, so it must not let the resolver
|
|
148
|
+
infer one from the working directory (#2796)."""
|
|
149
|
+
|
|
150
|
+
def _captured_command(self, tmp):
|
|
151
|
+
captured = []
|
|
152
|
+
original = rp._run_json
|
|
153
|
+
rp._run_json = lambda cmd: captured.append(cmd) or {"workflow": {}}
|
|
154
|
+
try:
|
|
155
|
+
rp.load_workflow(Path(tmp) / "project", Path(tmp) / "skill")
|
|
156
|
+
finally:
|
|
157
|
+
rp._run_json = original
|
|
158
|
+
return captured[0]
|
|
159
|
+
|
|
160
|
+
def test_passes_project_root_to_the_customization_resolver(self):
|
|
161
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
162
|
+
cmd = self._captured_command(tmp)
|
|
163
|
+
self.assertIn("--project-root", cmd)
|
|
164
|
+
self.assertEqual(cmd[cmd.index("--project-root") + 1], str(Path(tmp) / "project"))
|
|
165
|
+
|
|
166
|
+
|
|
145
167
|
if __name__ == "__main__":
|
|
146
168
|
unittest.main()
|
|
@@ -14,6 +14,7 @@ The lens set is whatever `{workflow.lenses}` resolves to, not a fixed list — o
|
|
|
14
14
|
- **content** — what to review: a diff, branch, uncommitted changes, file, spec, story, or any document. Args: `[path]`.
|
|
15
15
|
- **lenses** (optional) — one or more lens codes or names, however the caller expresses them: a spoken request, or a directive of the form `skill:bmad-review lenses=<code>[,<code>...]` (the form bmm's `doc_standards` uses). Default: every applicable lens (a full review).
|
|
16
16
|
- **also_consider** (optional) — areas to keep in mind alongside each lens's normal analysis.
|
|
17
|
+
- **claims** (optional) — the change's own narrative: the commit messages it covers, or whatever description of it the caller supplied. Goes to the edge-case lens alone.
|
|
17
18
|
- **pre-resolved customization** (optional) — `[workflow]` field values supplied by a forwarding caller. See Execution step 1.
|
|
18
19
|
|
|
19
20
|
## Conventions
|
|
@@ -24,12 +25,12 @@ The lens set is whatever `{workflow.lenses}` resolves to, not a fixed list — o
|
|
|
24
25
|
|
|
25
26
|
## Execution
|
|
26
27
|
|
|
27
|
-
1. **Resolve customization:** `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults. **Forwarded activation:** if a caller invoked you with pre-resolved customization fields (e.g. the `bmad-editorial-review` shim), honor them verbatim for those named fields — they already carry the user's overrides — and resolve only the remaining fields from your own `customize.toml`. Then execute each `{workflow.activation_steps_prepend}` entry in order, hold `{workflow.persistent_facts}` as standing context for the session, and treat `{workflow.review_guidance}` entries as standing review directives for every lens.
|
|
28
|
-
2. **Load the content.**
|
|
28
|
+
1. **Resolve customization:** `uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow`. On failure, read `{skill-root}/customize.toml` directly and use defaults. **Forwarded activation:** if a caller invoked you with pre-resolved customization fields (e.g. the `bmad-editorial-review` shim), honor them verbatim for those named fields — they already carry the user's overrides — and resolve only the remaining fields from your own `customize.toml`. Then execute each `{workflow.activation_steps_prepend}` entry in order, hold `{workflow.persistent_facts}` as standing context for the session, and treat `{workflow.review_guidance}` entries as standing review directives for every lens.
|
|
29
|
+
2. **Load the content.** Stage it once as a file: when the content is a branch, uncommitted work, or a commit range, use the repository's version-control tooling to write the unified diff to a uniquely-named file in the system temp directory and take that file's absolute path as the content. A branch means its diff against the merge base with its base branch; uncommitted work includes untracked files. Stage `claims` to its own file the same way — it is input for one lens, staged separately precisely so the other lenses never see it. If the content is empty or cannot be decoded as text: when the caller expects the raw findings JSON array (e.g. the legacy edge-case forwarder), return `[{"location":"N/A","trigger_condition":"Input empty or undecodable","guard_snippet":"Provide valid content to review","potential_consequence":"Review skipped — no analysis performed"}]` (no `lens` field) and stop; otherwise say what's wrong and ask for reviewable content. Classify the content — diff, source file, function, or document — and whether it is **code** or **docs**; scope rules and lens applicability both depend on it. A document that defines behavior (spec, requirements, plan, story) is `docs` that a behavioral lens may still apply to; judge by `when`.
|
|
29
30
|
3. **Select lenses** from `{workflow.lenses}`. A lens with an empty `instruction` is disabled. If the user or caller named lenses, run exactly those only — `applies_to` and `when` do not filter an explicit request. Otherwise run every enabled lens whose `applies_to` covers the content class (`any` always covers) and whose `when` applies.
|
|
30
31
|
4. **Announce the plan** in one line before running anything: the content class, the lenses about to run, and — when any lens has `after` set — that it runs on top of the named lens's findings. Skip the announcement entirely when the caller pinned an exact output contract (the legacy forwarders that demand raw JSON or one exact line) — their contract covers everything you emit, not just the findings block. Then execute each `{workflow.activation_steps_append}` entry in order.
|
|
31
|
-
5. **Run the independent lenses** — every selected lens without `after`. Each sees the content and `also_consider`, never another lens's findings. Follow each lens's `instruction`; the shipped lenses load their reference file just-in-time, so load only what runs. When subagents are available, spawn one per lens
|
|
32
|
-
6. **Run the dependent lenses** — every selected lens with `after`, once the lens it names has completed, passing that lens's findings in. A lens whose `after` target was not selected or produced nothing still runs, with no prior findings. Dependent lenses that name different targets are independent of each other and
|
|
32
|
+
5. **Run the independent lenses** — every selected lens without `after`. Each sees the content and `also_consider`, never another lens's findings. Follow each lens's `instruction`; the shipped lenses load their reference file just-in-time, so load only what runs. When subagents are available, launch every independent lens before handling any lens's result. Try running them simultaneously: spawn one per lens; give it the lens `instruction` with `{skill-root}` and paths resolved absolute, the absolute path of the staged content file (a lens prompt carries the path and the lens reads the file, never the content bytes; inline the content only when it was never staged as a file), any `also_consider` areas, the standing review directives, the `claims` path to the edge-case lens alone (marked to leave unread until its instructions call for it), and the constraint "Return ONLY your findings — no other output. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer." Otherwise run the lenses sequentially yourself, completing one before starting the next.
|
|
33
|
+
6. **Run the dependent lenses** — every selected lens with `after`, once the lens it names has completed, passing that lens's findings in. A lens whose `after` target was not selected or produced nothing still runs, with no prior findings. Dependent lenses that name different targets are independent of each other: launch every ready one before handling any of their results. Try running them simultaneously. When subagents are available, spawn them with the same constraint as independent lenses: "Return ONLY your findings — no other output. Do not invoke any skill, and do not spawn subagents of your own — you are the reviewer. Return your findings as text in your final message; do not route them through any findings-reporting tool the host may offer."
|
|
33
34
|
7. **Assemble and present** per Output below. Keep every lens's findings — overlap between lenses is signal, not duplication; note it in the markdown report rather than deduping. Execute `{workflow.on_complete}` if set.
|
|
34
35
|
|
|
35
36
|
## Output
|
|
@@ -23,7 +23,7 @@ activation_steps_append = []
|
|
|
23
23
|
# prefixed `file:` are paths or globs whose contents load as facts; all others
|
|
24
24
|
# are literal facts. The shipped entry is a project-wide glob — set it to []
|
|
25
25
|
# if you don't want every review scanning for it.
|
|
26
|
-
persistent_facts = [
|
|
26
|
+
persistent_facts = []
|
|
27
27
|
|
|
28
28
|
# Standing review directives applied on every run alongside each lens's own
|
|
29
29
|
# method. Each entry is a literal sentence or a `file:`-prefixed path/glob
|
|
@@ -9,6 +9,7 @@ You are a pure path tracer. Never comment on whether the content is good or bad;
|
|
|
9
9
|
- When the content is a diff, scan only the diff hunks and list boundaries that are directly reachable from the changed lines and lack an explicit guard in the diff.
|
|
10
10
|
- When it is not a diff (full file, function, or document), the entire provided content is the scope.
|
|
11
11
|
- Ignore the rest of the codebase unless the provided content explicitly references external functions.
|
|
12
|
+
- When the launch message names a claims file, do NOT read it before Step 4: the path tracing in Steps 1–2 must finish before the narrative is seen.
|
|
12
13
|
|
|
13
14
|
## Step 1: Exhaustive path analysis
|
|
14
15
|
|
|
@@ -17,6 +18,8 @@ Walk every branching path and boundary condition within scope — report only un
|
|
|
17
18
|
- If `also_consider` areas were provided, incorporate them into the analysis
|
|
18
19
|
- Walk all branching paths: control flow (conditionals, loops, error handlers, early returns) and domain boundaries (where values, states, or conditions transition). Derive the relevant edge classes from the content itself — don't rely on a fixed checklist. Examples: missing else/default, unguarded inputs, off-by-one loops, arithmetic overflow, implicit type coercion, race conditions, timeout gaps
|
|
19
20
|
- Consider implicit branches: the diff special-cases or changes the handling of one or more members of a fixed set of values — enums, status codes, sentinels, type tags, flags, value ranges. The rest of the set is implicit branches (e.g. the diff changes the `RED` and `YELLOW` cases of a `RED`/`YELLOW`/`GREEN` enum; `GREEN` is the implicit branch)
|
|
21
|
+
- Consider handle lifetime: when the changed code re-checks, re-fetches, or re-validates something it already held — a handle, index, id, pointer — the re-check exists because an intervening call can invalidate it. Identify that call, what it does to the thing held, and what the changed code silently skips when the re-check fails
|
|
22
|
+
- For each call site the diff adds or changes — in test files as well as production code — read the callee's declaration and check the call against it: argument count, order, types, and defaults. Report any mismatch
|
|
20
23
|
- For each path: determine whether the content handles it
|
|
21
24
|
- Collect only the unhandled paths as findings — discard handled ones silently
|
|
22
25
|
|
|
@@ -38,6 +41,21 @@ Deletion findings go in the same array with the four standard fields plus:
|
|
|
38
41
|
|
|
39
42
|
For a deletion finding the standard fields read as: `location` = the removed item; `trigger_condition` = the behavior or contract it enforced; `guard_snippet` = where or how to re-establish it; `potential_consequence` = the regression or orphan.
|
|
40
43
|
|
|
44
|
+
## Step 4: Claims check
|
|
45
|
+
|
|
46
|
+
Runs only when the message that launched you named a claims file. Read that file now, for the first time; the path tracing is finished and the claims cannot steer it retroactively.
|
|
47
|
+
|
|
48
|
+
The file holds the change's own narrative — commit messages and any stated description. The narrative is the author's testimony, not evidence: a claim repeated in a code comment is still the same claim, not confirmation. Extract each checkable claim — what the change does, what it preserves, ordering, arithmetic, and parity with existing code ("exactly as X does") — then try to falsify each one against the code you have already traced. Where your trace is not enough to decide, read the code that decides it: the compared-to function, the actual callee, the state the claim assumes.
|
|
49
|
+
|
|
50
|
+
Claim findings go in the same array with the four standard fields plus:
|
|
51
|
+
|
|
52
|
+
- `kind`: `"claim"`
|
|
53
|
+
- `confidence`: `"high"`, `"medium"`, or `"low"`
|
|
54
|
+
|
|
55
|
+
For a claim finding the standard fields read as: `location` = where the code contradicts the claim; `trigger_condition` = the claim, quoted or tightly paraphrased; `guard_snippet` = what the code actually does; `potential_consequence` = what goes wrong for someone who believed the claim.
|
|
56
|
+
|
|
57
|
+
Verified claims produce nothing. Add nothing if nothing is falsified.
|
|
58
|
+
|
|
41
59
|
## Findings shape
|
|
42
60
|
|
|
43
61
|
Each edge-case finding contains exactly these four fields:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
|
2
|
-
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org
|
|
2
|
+
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/,
|
|
3
3
|
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
|
|
4
4
|
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,
|
|
5
5
|
Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
|
|
Binary file
|
|
@@ -34,6 +34,14 @@ def extract_key(data, dotted_key: str):
|
|
|
34
34
|
return current
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def write_json_stdout(output) -> None:
|
|
38
|
+
"""Pin stdout to UTF-8 — a Windows cp1252 default cannot encode emoji icons."""
|
|
39
|
+
reconfigure = getattr(sys.stdout, "reconfigure", None)
|
|
40
|
+
if reconfigure is not None:
|
|
41
|
+
reconfigure(encoding="utf-8")
|
|
42
|
+
sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
|
|
43
|
+
|
|
44
|
+
|
|
37
45
|
def main() -> int:
|
|
38
46
|
parser = argparse.ArgumentParser(
|
|
39
47
|
description="Resolve BMad central config using four-layer TOML merge."
|
|
@@ -66,7 +74,7 @@ def main() -> int:
|
|
|
66
74
|
value = extract_key(merged, key)
|
|
67
75
|
if value is not _MISSING:
|
|
68
76
|
output[key] = value
|
|
69
|
-
|
|
77
|
+
write_json_stdout(output)
|
|
70
78
|
return 0
|
|
71
79
|
|
|
72
80
|
|
|
@@ -25,15 +25,77 @@ _MISSING = object()
|
|
|
25
25
|
|
|
26
26
|
|
|
27
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
|
|
28
35
|
current = start.resolve()
|
|
29
36
|
while True:
|
|
30
|
-
if (current / "_bmad").
|
|
37
|
+
if (current / "_bmad").is_dir():
|
|
31
38
|
return current
|
|
39
|
+
if git_root is None and (current / ".git").exists():
|
|
40
|
+
git_root = current
|
|
32
41
|
if current.parent == current:
|
|
33
|
-
return
|
|
42
|
+
return git_root
|
|
34
43
|
current = current.parent
|
|
35
44
|
|
|
36
45
|
|
|
46
|
+
def script_project_root() -> Path | None:
|
|
47
|
+
"""Project root implied by this script's own install path.
|
|
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
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def candidate_project_roots(skill_dir: Path) -> list[Path]:
|
|
60
|
+
"""Plausible project roots, most trustworthy first.
|
|
61
|
+
|
|
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.
|
|
65
|
+
"""
|
|
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
|
|
97
|
+
|
|
98
|
+
|
|
37
99
|
def extract_key(data, dotted_key: str):
|
|
38
100
|
current = data
|
|
39
101
|
for part in dotted_key.split("."):
|
|
@@ -73,11 +135,14 @@ def main() -> int:
|
|
|
73
135
|
args = parser.parse_args()
|
|
74
136
|
|
|
75
137
|
skill_dir = Path(args.skill).resolve()
|
|
76
|
-
project_root
|
|
77
|
-
Path(args.project_root).resolve()
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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)
|
|
145
|
+
|
|
81
146
|
try:
|
|
82
147
|
merged = load_customization(project_root, skill_dir)
|
|
83
148
|
except ConfigError as error:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
@@ -74,6 +75,33 @@ class ResolveConfigCliTests(unittest.TestCase):
|
|
|
74
75
|
self.assertNotEqual(result.returncode, 0)
|
|
75
76
|
self.assertIn("failed to parse", result.stderr)
|
|
76
77
|
|
|
78
|
+
def test_writes_emoji_json_when_stdout_encoding_is_cp1252(self):
|
|
79
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
80
|
+
root = Path(temp_dir)
|
|
81
|
+
(root / "_bmad").mkdir(parents=True)
|
|
82
|
+
(root / "_bmad" / "config.toml").write_text(
|
|
83
|
+
'[agents]\nname = "Analyst"\nicon = "📊"\n',
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
env = os.environ.copy()
|
|
88
|
+
env["PYTHONIOENCODING"] = "cp1252"
|
|
89
|
+
result = subprocess.run(
|
|
90
|
+
[sys.executable, str(SCRIPT), "--project-root", str(root)],
|
|
91
|
+
stdout=subprocess.PIPE,
|
|
92
|
+
stderr=subprocess.PIPE,
|
|
93
|
+
env=env,
|
|
94
|
+
check=False,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
stderr = result.stderr.decode("utf-8", errors="replace")
|
|
98
|
+
self.assertEqual(result.returncode, 0, msg=stderr)
|
|
99
|
+
|
|
100
|
+
output = result.stdout.decode("utf-8")
|
|
101
|
+
self.assertIn("📊", output)
|
|
102
|
+
resolved = json.loads(output)
|
|
103
|
+
self.assertEqual(resolved["agents"]["icon"], "📊")
|
|
104
|
+
|
|
77
105
|
@staticmethod
|
|
78
106
|
def _run(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
79
107
|
return subprocess.run(
|
|
@@ -73,5 +73,107 @@ class ResolveCustomizationStdoutTests(unittest.TestCase):
|
|
|
73
73
|
self.assertEqual(resolved["agent"]["icon"], "🧭")
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def write(path: Path, body: str) -> None:
|
|
77
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
path.write_text(body, encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def facts(*entries: str) -> str:
|
|
82
|
+
listed = ", ".join(f'"{entry}"' for entry in entries)
|
|
83
|
+
return f"[workflow]\npersistent_facts = [{listed}]\n"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def resolve(skill_dir: Path, cwd: Path, *extra: str):
|
|
87
|
+
return subprocess.run(
|
|
88
|
+
[sys.executable, str(SCRIPT), "--skill", str(skill_dir), "--key", "workflow", *extra],
|
|
89
|
+
text=True,
|
|
90
|
+
stdout=subprocess.PIPE,
|
|
91
|
+
stderr=subprocess.PIPE,
|
|
92
|
+
cwd=str(cwd),
|
|
93
|
+
check=False,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ProjectRootResolutionTests(unittest.TestCase):
|
|
98
|
+
"""Regression cover for #2796 — the project is where the user works, not
|
|
99
|
+
where the skill is installed."""
|
|
100
|
+
|
|
101
|
+
def test_home_installed_skill_reads_project_override_not_home_bmad(self):
|
|
102
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
103
|
+
home = Path(temp_dir) / "home"
|
|
104
|
+
project = Path(temp_dir) / "project"
|
|
105
|
+
skill = home / ".claude" / "skills" / "demo-skill"
|
|
106
|
+
write(skill / "customize.toml", facts("shipped default"))
|
|
107
|
+
(home / "_bmad" / "custom").mkdir(parents=True)
|
|
108
|
+
write(project / "_bmad" / "custom" / "demo-skill.toml", facts("team override"))
|
|
109
|
+
|
|
110
|
+
result = resolve(skill, project)
|
|
111
|
+
|
|
112
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
113
|
+
resolved = json.loads(result.stdout)["workflow"]["persistent_facts"]
|
|
114
|
+
self.assertEqual(resolved, ["shipped default", "team override"])
|
|
115
|
+
|
|
116
|
+
def test_project_installed_skill_still_resolves(self):
|
|
117
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
118
|
+
project = Path(temp_dir) / "project"
|
|
119
|
+
skill = project / ".claude" / "skills" / "demo-skill"
|
|
120
|
+
write(skill / "customize.toml", facts("shipped default"))
|
|
121
|
+
write(project / "_bmad" / "custom" / "demo-skill.toml", facts("team override"))
|
|
122
|
+
|
|
123
|
+
result = resolve(skill, project)
|
|
124
|
+
|
|
125
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
126
|
+
resolved = json.loads(result.stdout)["workflow"]["persistent_facts"]
|
|
127
|
+
self.assertEqual(resolved, ["shipped default", "team override"])
|
|
128
|
+
|
|
129
|
+
def test_walk_prefers_bmad_over_a_nearer_git_directory(self):
|
|
130
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
131
|
+
project = Path(temp_dir) / "project"
|
|
132
|
+
submodule = project / "vendor" / "sub"
|
|
133
|
+
skill = Path(temp_dir) / "skills" / "demo-skill"
|
|
134
|
+
write(skill / "customize.toml", facts("shipped default"))
|
|
135
|
+
write(project / "_bmad" / "custom" / "demo-skill.toml", facts("team override"))
|
|
136
|
+
(submodule / ".git").mkdir(parents=True)
|
|
137
|
+
|
|
138
|
+
result = resolve(skill, submodule)
|
|
139
|
+
|
|
140
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
141
|
+
resolved = json.loads(result.stdout)["workflow"]["persistent_facts"]
|
|
142
|
+
self.assertEqual(resolved, ["shipped default", "team override"])
|
|
143
|
+
|
|
144
|
+
def test_notes_when_a_rejected_root_holds_the_only_override(self):
|
|
145
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
146
|
+
home = Path(temp_dir) / "home"
|
|
147
|
+
project = Path(temp_dir) / "project"
|
|
148
|
+
skill = home / ".claude" / "skills" / "demo-skill"
|
|
149
|
+
write(skill / "customize.toml", facts("shipped default"))
|
|
150
|
+
write(home / "_bmad" / "custom" / "demo-skill.toml", facts("home override"))
|
|
151
|
+
(project / "_bmad" / "custom").mkdir(parents=True)
|
|
152
|
+
|
|
153
|
+
result = resolve(skill, project)
|
|
154
|
+
|
|
155
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
156
|
+
resolved = json.loads(result.stdout)["workflow"]["persistent_facts"]
|
|
157
|
+
self.assertEqual(resolved, ["shipped default"])
|
|
158
|
+
self.assertIn("demo-skill", result.stderr)
|
|
159
|
+
self.assertIn("--project-root", result.stderr)
|
|
160
|
+
|
|
161
|
+
def test_explicit_project_root_wins_and_stays_quiet(self):
|
|
162
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
163
|
+
home = Path(temp_dir) / "home"
|
|
164
|
+
project = Path(temp_dir) / "project"
|
|
165
|
+
skill = home / ".claude" / "skills" / "demo-skill"
|
|
166
|
+
write(skill / "customize.toml", facts("shipped default"))
|
|
167
|
+
write(home / "_bmad" / "custom" / "demo-skill.toml", facts("home override"))
|
|
168
|
+
(project / "_bmad" / "custom").mkdir(parents=True)
|
|
169
|
+
|
|
170
|
+
result = resolve(skill, project, "--project-root", str(home))
|
|
171
|
+
|
|
172
|
+
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
173
|
+
resolved = json.loads(result.stdout)["workflow"]["persistent_facts"]
|
|
174
|
+
self.assertEqual(resolved, ["shipped default", "home override"])
|
|
175
|
+
self.assertEqual(result.stderr, "")
|
|
176
|
+
|
|
177
|
+
|
|
76
178
|
if __name__ == "__main__":
|
|
77
179
|
unittest.main()
|
|
@@ -13,7 +13,14 @@ const { InstallPaths } = require('./install-paths');
|
|
|
13
13
|
const { ExternalModuleManager } = require('../modules/external-manager');
|
|
14
14
|
const { resolveModuleVersion } = require('../modules/version-resolver');
|
|
15
15
|
const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
formatRemovedShimNotice,
|
|
18
|
+
formatRetainedShimNotice,
|
|
19
|
+
inferShimPreference,
|
|
20
|
+
readInstalledShims,
|
|
21
|
+
readInstalledSkillIds,
|
|
22
|
+
selectShimOutcome,
|
|
23
|
+
} = require('./shim-policy');
|
|
17
24
|
|
|
18
25
|
const { ExistingInstall } = require('./existing-install');
|
|
19
26
|
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
|
|
@@ -59,6 +66,24 @@ class Installer {
|
|
|
59
66
|
}),
|
|
60
67
|
};
|
|
61
68
|
|
|
69
|
+
const installedShims = existingInstall.installed ? await readInstalledShims(paths.bmadDir) : [];
|
|
70
|
+
const { retained: retainedShims, removed: removedShims } = selectShimOutcome({
|
|
71
|
+
installedShims,
|
|
72
|
+
availableShims,
|
|
73
|
+
install: shimPolicy.install,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Reported here, not at the prompt, so --yes/--shims/scripted runs get it too.
|
|
77
|
+
if (retainedShims.length > 0) {
|
|
78
|
+
await prompts.note(formatRetainedShimNotice(retainedShims), 'Deprecated shim skills retained');
|
|
79
|
+
}
|
|
80
|
+
if (removedShims.length > 0) {
|
|
81
|
+
await prompts.note(formatRemovedShimNotice(removedShims, { canReinstall: shimPolicy.available }), 'Deprecated shim skills removed');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The notices above scroll away on a long install; repeat them in the summary.
|
|
85
|
+
const shimStatus = { retained: retainedShims.length, removed: removedShims.length };
|
|
86
|
+
|
|
62
87
|
try {
|
|
63
88
|
await warnPreNativeSkillsLegacy({
|
|
64
89
|
projectRoot: paths.projectRoot,
|
|
@@ -132,6 +157,7 @@ class Installer {
|
|
|
132
157
|
customFiles: restoreResult.customFiles.length > 0 ? restoreResult.customFiles : undefined,
|
|
133
158
|
modifiedFiles: restoreResult.modifiedFiles.length > 0 ? restoreResult.modifiedFiles : undefined,
|
|
134
159
|
preInstallVersions,
|
|
160
|
+
shimStatus,
|
|
135
161
|
});
|
|
136
162
|
|
|
137
163
|
return {
|
|
@@ -1261,6 +1287,12 @@ class Installer {
|
|
|
1261
1287
|
if (context.modifiedFiles && context.modifiedFiles.length > 0) {
|
|
1262
1288
|
lines.push(` ${color.yellow(`Modified files backed up (.bak): ${context.modifiedFiles.length}`)}`);
|
|
1263
1289
|
}
|
|
1290
|
+
if (context.shimStatus?.retained > 0) {
|
|
1291
|
+
lines.push(` ${color.yellow(`Deprecated shim skills retained: ${context.shimStatus.retained}`)} (re-run to remove them)`);
|
|
1292
|
+
}
|
|
1293
|
+
if (context.shimStatus?.removed > 0) {
|
|
1294
|
+
lines.push(` ${color.green(`Deprecated shim skills removed: ${context.shimStatus.removed}`)}`);
|
|
1295
|
+
}
|
|
1264
1296
|
|
|
1265
1297
|
// Next steps
|
|
1266
1298
|
lines.push(
|
|
@@ -37,6 +37,7 @@ async function discoverShims(modulePath) {
|
|
|
37
37
|
if (isShimSkill(metadata)) {
|
|
38
38
|
shims.push({
|
|
39
39
|
id: metadata.name || path.basename(dir),
|
|
40
|
+
description: typeof metadata.description === 'string' ? metadata.description : '',
|
|
40
41
|
directory: dir,
|
|
41
42
|
relativeDirectory: path.relative(modulePath, dir),
|
|
42
43
|
});
|
|
@@ -54,25 +55,40 @@ async function discoverShims(modulePath) {
|
|
|
54
55
|
return shims;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
async function
|
|
58
|
-
const ids = new Set();
|
|
58
|
+
async function readSkillManifest(bmadDir) {
|
|
59
59
|
const manifestPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
|
60
|
-
if (!(await fs.pathExists(manifestPath))) return
|
|
60
|
+
if (!(await fs.pathExists(manifestPath))) return [];
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
63
|
const content = await fs.readFile(manifestPath, 'utf8');
|
|
64
|
-
|
|
65
|
-
for (const record of records) {
|
|
66
|
-
if (record.canonicalId) ids.add(record.canonicalId);
|
|
67
|
-
}
|
|
64
|
+
return csv.parse(content, { columns: true, skip_empty_lines: true });
|
|
68
65
|
} catch {
|
|
69
66
|
// A missing or unreadable legacy manifest means there is no reliable
|
|
70
67
|
// evidence that compatibility shims were installed.
|
|
68
|
+
return [];
|
|
71
69
|
}
|
|
70
|
+
}
|
|
72
71
|
|
|
72
|
+
async function readInstalledSkillIds(bmadDir) {
|
|
73
|
+
const ids = new Set();
|
|
74
|
+
for (const record of await readSkillManifest(bmadDir)) {
|
|
75
|
+
if (record.canonicalId) ids.add(record.canonicalId);
|
|
76
|
+
}
|
|
73
77
|
return ids;
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// The installed manifest carries no lifecycle column, so the description
|
|
81
|
+
// prefix every shim ships with is the only record of what was a shim. This
|
|
82
|
+
// is the same signal validate_skills.py uses to exempt them.
|
|
83
|
+
async function readInstalledShims(bmadDir) {
|
|
84
|
+
const shims = [];
|
|
85
|
+
for (const record of await readSkillManifest(bmadDir)) {
|
|
86
|
+
if (!record.canonicalId || !/^\s*deprecated\b/i.test(record.description || '')) continue;
|
|
87
|
+
shims.push({ id: record.canonicalId, description: record.description || '', module: record.module || '' });
|
|
88
|
+
}
|
|
89
|
+
return shims;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
function inferShimPreference({ requested, persisted, availableShims = [], installedSkillIds = new Set(), existing = false }) {
|
|
77
93
|
if (availableShims.length === 0) return false;
|
|
78
94
|
if (typeof requested === 'boolean') return requested;
|
|
@@ -82,10 +98,62 @@ function inferShimPreference({ requested, persisted, availableShims = [], instal
|
|
|
82
98
|
return availableShims.some((shim) => installedSkillIds.has(shim.id));
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
// Removal is driven by what is installed, not by what this release ships: a
|
|
102
|
+
// shim retired from source is still deleted by the update cleanup.
|
|
103
|
+
function selectShimOutcome({ installedShims = [], availableShims = [], install = false }) {
|
|
104
|
+
const availableShimIds = new Set(availableShims.map((shim) => shim.id));
|
|
105
|
+
return {
|
|
106
|
+
retained: install ? availableShims : [],
|
|
107
|
+
removed: installedShims.filter((shim) => !(install && availableShimIds.has(shim.id))),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Shim descriptions all open with "Deprecated — "; the notice heading says it once.
|
|
112
|
+
function describeShim(shim) {
|
|
113
|
+
const cleaned = (shim.description || '').replace(/^\s*deprecated\s*[-–—:]*\s*/i, '').trim();
|
|
114
|
+
const source = shim.module ? ` (${shim.module})` : '';
|
|
115
|
+
return cleaned ? ` ${shim.id}${source}: ${cleaned}` : ` ${shim.id}${source}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatRetainedShimNotice(availableShims = []) {
|
|
119
|
+
const lines = availableShims.map((shim) => describeShim(shim)).sort();
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
`${availableShims.length} deprecated shim skill(s) are still installed. Each one only forwards to the skill that replaced it:`,
|
|
123
|
+
'',
|
|
124
|
+
...lines,
|
|
125
|
+
'',
|
|
126
|
+
'Shims will be removed with v7, and anything still calling the old name stops working then.',
|
|
127
|
+
'Only keep a shim if you customized it and still need to move that customization to the replacement.',
|
|
128
|
+
'Once you have, re-run Quick Update and answer No to this question so the shims come off.',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatRemovedShimNotice(removedShims = [], { canReinstall = true } = {}) {
|
|
133
|
+
const lines = removedShims.map((shim) => describeShim(shim)).sort();
|
|
134
|
+
const recovery = canReinstall
|
|
135
|
+
? 'these shims, move it to the replacement, or re-run with --shims to put the shims back.'
|
|
136
|
+
: 'these shims, move it to the replacement. This release no longer ships them, so --shims cannot bring them back.';
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
`${removedShims.length} deprecated shim skill(s) are being removed. Invoking these names will no longer work:`,
|
|
140
|
+
'',
|
|
141
|
+
...lines,
|
|
142
|
+
'',
|
|
143
|
+
'Each replacement named above is installed and ready. If you still had a customization on one of',
|
|
144
|
+
recovery,
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
85
148
|
module.exports = {
|
|
149
|
+
describeShim,
|
|
150
|
+
formatRemovedShimNotice,
|
|
86
151
|
discoverShims,
|
|
152
|
+
formatRetainedShimNotice,
|
|
87
153
|
inferShimPreference,
|
|
88
154
|
isShimSkill,
|
|
89
155
|
parseSkillMetadata,
|
|
156
|
+
readInstalledShims,
|
|
90
157
|
readInstalledSkillIds,
|
|
158
|
+
selectShimOutcome,
|
|
91
159
|
};
|