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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/AGENTS.md +12 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +13 -19
  5. package/README_CN.md +0 -10
  6. package/README_KR.md +90 -0
  7. package/README_VN.md +0 -10
  8. package/greptile.json +52 -0
  9. package/package.json +6 -5
  10. package/src/bmm-skills/agents/bmad-agent-analyst/SKILL.md +1 -1
  11. package/src/bmm-skills/agents/bmad-agent-analyst/customize.toml +1 -3
  12. package/src/bmm-skills/agents/bmad-agent-architect/SKILL.md +1 -1
  13. package/src/bmm-skills/agents/bmad-agent-architect/customize.toml +1 -3
  14. package/src/bmm-skills/agents/bmad-agent-dev/SKILL.md +1 -1
  15. package/src/bmm-skills/agents/bmad-agent-dev/customize.toml +1 -3
  16. package/src/bmm-skills/agents/bmad-agent-pm/SKILL.md +1 -1
  17. package/src/bmm-skills/agents/bmad-agent-pm/customize.toml +1 -3
  18. package/src/bmm-skills/agents/bmad-agent-ux-designer/SKILL.md +1 -1
  19. package/src/bmm-skills/agents/bmad-agent-ux-designer/customize.toml +1 -3
  20. package/src/bmm-skills/module-help.csv +2 -2
  21. package/src/bmm-skills/plan/bmad-architecture/SKILL.md +1 -1
  22. package/src/bmm-skills/plan/bmad-architecture/customize.toml +5 -6
  23. package/src/bmm-skills/plan/bmad-create-epics-and-stories/SKILL.md +1 -1
  24. package/src/bmm-skills/plan/bmad-create-epics-and-stories/customize.toml +1 -3
  25. package/src/bmm-skills/plan/bmad-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
  26. package/src/bmm-skills/plan/bmad-prd/SKILL.md +1 -1
  27. package/src/bmm-skills/plan/bmad-prd/customize.toml +5 -6
  28. package/src/bmm-skills/plan/bmad-prfaq/SKILL.md +1 -1
  29. package/src/bmm-skills/plan/bmad-prfaq/customize.toml +1 -3
  30. package/src/bmm-skills/plan/bmad-prfaq/references/verdict.md +1 -1
  31. package/src/bmm-skills/plan/bmad-product-brief/SKILL.md +1 -1
  32. package/src/bmm-skills/plan/bmad-product-brief/customize.toml +5 -6
  33. package/src/bmm-skills/plan/bmad-project-context/SKILL.md +7 -6
  34. package/src/bmm-skills/plan/bmad-project-context/references/best-practices.md +2 -2
  35. package/src/bmm-skills/plan/bmad-spec/SKILL.md +1 -1
  36. package/src/bmm-skills/plan/bmad-spec/customize.toml +5 -5
  37. package/src/bmm-skills/plan/bmad-sprint-planning/SKILL.md +1 -1
  38. package/src/bmm-skills/plan/bmad-sprint-planning/customize.toml +1 -3
  39. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/__pycache__/sprint_plan.cpython-311.pyc +0 -0
  40. package/src/bmm-skills/plan/bmad-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-311-pytest-9.1.1.pyc +0 -0
  41. package/src/bmm-skills/plan/bmad-ux/SKILL.md +1 -1
  42. package/src/bmm-skills/plan/bmad-ux/customize.toml +1 -3
  43. package/src/bmm-skills/ship/bmad-build/SKILL.md +1 -1
  44. package/src/bmm-skills/ship/bmad-build/customize.toml +21 -33
  45. package/src/bmm-skills/ship/bmad-build/references/claims-check.md +14 -0
  46. package/src/bmm-skills/ship/bmad-build/review-prompts/edge-case-hunter.md +31 -7
  47. package/src/bmm-skills/ship/bmad-build/review-prompts/verification-gap.md +4 -4
  48. package/src/bmm-skills/ship/bmad-build/spec-template.md +26 -4
  49. package/src/bmm-skills/ship/bmad-build/step-01-clarify-and-route.md +21 -26
  50. package/src/bmm-skills/ship/bmad-build/step-02-plan.md +31 -16
  51. package/src/bmm-skills/ship/bmad-build/step-03-implement.md +5 -3
  52. package/src/bmm-skills/ship/bmad-build/step-04-review.md +49 -19
  53. package/src/bmm-skills/ship/bmad-build/step-05-present.md +9 -41
  54. package/src/bmm-skills/ship/bmad-build/step-oneshot.md +60 -29
  55. package/src/bmm-skills/ship/bmad-build/sync-sprint-status.md +5 -19
  56. package/src/bmm-skills/ship/bmad-build-auto/customize.toml +17 -19
  57. package/src/bmm-skills/ship/bmad-build-auto/references/claims-check.md +14 -0
  58. package/src/bmm-skills/ship/bmad-build-auto/review-prompts/edge-case-hunter.md +31 -7
  59. package/src/bmm-skills/ship/bmad-build-auto/review-prompts/verification-gap.md +4 -4
  60. package/src/bmm-skills/ship/bmad-build-auto/spec-template.md +4 -7
  61. package/src/bmm-skills/ship/bmad-build-auto/step-01-clarify-and-route.md +3 -2
  62. package/src/bmm-skills/ship/bmad-build-auto/step-03-implement.md +5 -1
  63. package/src/bmm-skills/ship/bmad-build-auto/step-04-review.md +58 -38
  64. package/src/bmm-skills/ship/bmad-build-auto/workflow.md +1 -1
  65. package/src/bmm-skills/ship/bmad-code-review/SKILL.md +1 -1
  66. package/src/bmm-skills/ship/bmad-code-review/customize.toml +15 -18
  67. package/src/bmm-skills/ship/bmad-code-review/references/claims-check.md +14 -0
  68. package/src/bmm-skills/ship/bmad-code-review/review-prompts/edge-case-hunter.md +31 -7
  69. package/src/bmm-skills/ship/bmad-code-review/review-prompts/verification-gap.md +4 -4
  70. package/src/bmm-skills/ship/bmad-code-review/steps/step-01-gather-context.md +30 -23
  71. package/src/bmm-skills/ship/bmad-code-review/steps/step-02-review.md +3 -3
  72. package/src/bmm-skills/ship/bmad-code-review/steps/step-03-triage.md +25 -21
  73. package/src/bmm-skills/ship/bmad-code-review/steps/step-04-present.md +20 -18
  74. package/src/bmm-skills/ship/bmad-correct-course/SKILL.md +13 -5
  75. package/src/bmm-skills/ship/bmad-correct-course/customize.toml +1 -3
  76. package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/SKILL.md +2 -2
  77. package/src/bmm-skills/ship/bmad-qa-generate-e2e-tests/customize.toml +1 -3
  78. package/src/bmm-skills/ship/bmad-retrospective/SKILL.md +1 -1
  79. package/src/bmm-skills/ship/bmad-retrospective/customize.toml +1 -3
  80. package/src/bmm-skills/ship/bmad-retrospective/scripts/__pycache__/sprint_status.cpython-311.pyc +0 -0
  81. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-311-pytest-9.1.1.pyc +0 -0
  82. package/src/bmm-skills/ship/bmad-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-311-pytest-9.1.1.pyc +0 -0
  83. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/SKILL.md +4 -4
  84. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/customize.toml +2 -4
  85. package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-05-wrapup.md +1 -1
  86. package/src/bmm-skills/v6-shims/README.md +1 -0
  87. package/src/bmm-skills/v6-shims/bmad-checkpoint-preview/SKILL.md +21 -0
  88. package/src/bmm-skills/v6-shims/bmad-create-architecture/SKILL.md +1 -1
  89. package/src/bmm-skills/v6-shims/bmad-create-architecture/customize.toml +1 -3
  90. package/src/bmm-skills/v6-shims/bmad-create-prd/SKILL.md +1 -1
  91. package/src/bmm-skills/v6-shims/bmad-create-prd/customize.toml +1 -3
  92. package/src/bmm-skills/v6-shims/bmad-create-story/SKILL.md +3 -3
  93. package/src/bmm-skills/v6-shims/bmad-create-story/customize.toml +1 -3
  94. package/src/bmm-skills/v6-shims/bmad-dev-story/SKILL.md +2 -2
  95. package/src/bmm-skills/v6-shims/bmad-dev-story/customize.toml +1 -6
  96. package/src/bmm-skills/v6-shims/bmad-domain-research/SKILL.md +1 -1
  97. package/src/bmm-skills/v6-shims/bmad-edit-prd/SKILL.md +1 -1
  98. package/src/bmm-skills/v6-shims/bmad-edit-prd/customize.toml +1 -3
  99. package/src/bmm-skills/v6-shims/bmad-market-research/SKILL.md +1 -1
  100. package/src/bmm-skills/v6-shims/bmad-sprint-status/SKILL.md +1 -1
  101. package/src/bmm-skills/v6-shims/bmad-sprint-status/customize.toml +1 -3
  102. package/src/bmm-skills/v6-shims/bmad-technical-research/SKILL.md +1 -1
  103. package/src/bmm-skills/v6-shims/bmad-validate-prd/SKILL.md +1 -1
  104. package/src/bmm-skills/v6-shims/bmad-validate-prd/customize.toml +1 -3
  105. package/src/core-skills/bmad-advanced-elicitation/SKILL.md +22 -23
  106. package/src/core-skills/bmad-brainstorming/SKILL.md +1 -1
  107. package/src/core-skills/bmad-brainstorming/customize.toml +5 -6
  108. package/src/core-skills/bmad-brainstorming/scripts/brain.py +19 -0
  109. package/src/core-skills/bmad-brainstorming/scripts/tests/test_brain.py +50 -0
  110. package/src/core-skills/bmad-customize/SKILL.md +1 -1
  111. package/src/core-skills/bmad-deep-recon/SKILL.md +1 -1
  112. package/src/core-skills/bmad-forge-idea/SKILL.md +1 -1
  113. package/src/core-skills/bmad-forge-idea/customize.toml +5 -6
  114. package/src/core-skills/bmad-forge-idea/scripts/resolve_personas.py +3 -1
  115. package/src/core-skills/bmad-forge-idea/scripts/tests/test_resolve_personas.py +22 -0
  116. package/src/core-skills/bmad-help/SKILL.md +1 -1
  117. package/src/core-skills/bmad-party-mode/SKILL.md +1 -1
  118. package/src/core-skills/bmad-party-mode/customize.toml +4 -4
  119. package/src/core-skills/bmad-party-mode/references/create-party.md +1 -1
  120. package/src/core-skills/bmad-party-mode/scripts/resolve_party.py +3 -1
  121. package/src/core-skills/bmad-party-mode/scripts/tests/test_resolve_party.py +22 -0
  122. package/src/core-skills/bmad-review/SKILL.md +5 -4
  123. package/src/core-skills/bmad-review/customize.toml +1 -1
  124. package/src/core-skills/bmad-review/references/lens-edge-case-hunter.md +18 -0
  125. package/src/core-skills/module-help.csv +1 -1
  126. package/src/scripts/__pycache__/config_utils.cpython-311.pyc +0 -0
  127. package/src/scripts/resolve_config.py +9 -1
  128. package/src/scripts/resolve_customization.py +72 -7
  129. package/src/scripts/tests/__pycache__/test_config_utils.cpython-311.pyc +0 -0
  130. package/src/scripts/tests/__pycache__/test_resolve_config.cpython-311.pyc +0 -0
  131. package/src/scripts/tests/__pycache__/test_resolve_customization.cpython-311.pyc +0 -0
  132. package/src/scripts/tests/test_resolve_config.py +28 -0
  133. package/src/scripts/tests/test_resolve_customization.py +102 -0
  134. package/tools/installer/core/installer.js +33 -1
  135. package/tools/installer/core/shim-policy.js +75 -7
  136. package/tools/installer/ide/platform-codes.yaml +13 -0
  137. package/tools/installer/prompts.js +15 -2
  138. package/tools/installer/ui.js +28 -8
  139. package/tools/skill-validator.md +85 -151
  140. package/tools/tests/__pycache__/test_validate_skills.cpython-311.pyc +0 -0
  141. package/tools/tests/fixtures/validate-skills/bmad/SKILL.md +8 -0
  142. package/tools/tests/fixtures/validate-skills/deprecated-shim/SKILL.md +9 -0
  143. package/tools/tests/fixtures/validate-skills/missing-trigger/SKILL.md +9 -0
  144. package/tools/tests/fixtures/validate-skills/with-trigger/SKILL.md +8 -0
  145. package/tools/tests/test_validate_skills.py +476 -0
  146. package/tools/validate-published-implementation-model.mjs +0 -9
  147. package/tools/validate_skills.py +698 -0
  148. package/tools/validate-skills.js +0 -735
  149. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/generate-trail.md +0 -0
  150. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-01-orientation.md +0 -0
  151. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-02-walkthrough.md +0 -0
  152. /package/src/bmm-skills/ship/{bmad-checkpoint-preview → bmad-walkthrough}/step-03-detail-pass.md +0 -0
  153. /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. Default picks up project-context.md if one exists.
26
- persistent_facts = [
27
- "file:{project-root}/**/project-context.md",
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([sys.executable, str(script), "--skill", str(skill_root), "--key", "workflow"])
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.** If it 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`.
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 in parallel: give it the lens `instruction` with `{skill-root}` and paths resolved absolute, the content or where to read it, any `also_consider` areas, the standing review directives, and the constraint "Return ONLY your findings — no other output." Otherwise run the lenses sequentially yourself, completing one before starting the next.
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 may run in parallel.
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 = ["file:{project-root}/**/project-context.md"]
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/llms.txt,
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,,
@@ -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
- sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
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").exists() or (current / ".git").exists():
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 None
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
- if args.project_root
79
- else find_project_root(skill_dir) or find_project_root(Path.cwd())
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:
@@ -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 { inferShimPreference, readInstalledSkillIds } = require('./shim-policy');
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 readInstalledSkillIds(bmadDir) {
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 ids;
60
+ if (!(await fs.pathExists(manifestPath))) return [];
61
61
 
62
62
  try {
63
63
  const content = await fs.readFile(manifestPath, 'utf8');
64
- const records = csv.parse(content, { columns: true, skip_empty_lines: true });
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
  };