bmad-module-skill-forge 1.4.0 → 1.5.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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +0 -8
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/docs/_internal/STABILITY.md +1 -1
  5. package/docs/architecture.md +2 -2
  6. package/docs/examples.md +1 -1
  7. package/docs/workflows.md +5 -5
  8. package/package.json +2 -2
  9. package/src/README.md +1 -1
  10. package/src/knowledge/ccc-bridge.md +12 -12
  11. package/src/knowledge/qmd-registry.md +5 -5
  12. package/src/shared/health-check.md +5 -5
  13. package/src/shared/references/description-guard-protocol.md +100 -0
  14. package/src/shared/references/output-contract-schema.md +1 -1
  15. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
  16. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
  17. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
  18. package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
  19. package/src/shared/scripts/skf-build-change-manifest.py +420 -0
  20. package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
  21. package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
  22. package/src/shared/scripts/skf-description-guard.py +359 -0
  23. package/src/shared/scripts/skf-detect-language.py +2 -2
  24. package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
  25. package/src/shared/scripts/skf-detect-tools.py +97 -8
  26. package/src/shared/scripts/skf-detect-workspaces.py +1 -1
  27. package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
  29. package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
  30. package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
  31. package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
  32. package/src/shared/scripts/skf-hash-content.py +284 -0
  33. package/src/shared/scripts/skf-load-provenance.py +295 -0
  34. package/src/shared/scripts/skf-merge-ccc-exclusions.py +45 -1
  35. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  36. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  37. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  38. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  39. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  40. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  41. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  42. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  43. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  44. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  45. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  46. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  47. package/src/skf-analyze-source/SKILL.md +69 -16
  48. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  49. package/src/skf-analyze-source/customize.toml +55 -0
  50. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  51. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  52. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  53. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +44 -14
  54. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +27 -6
  55. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  56. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  57. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +12 -10
  58. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  59. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  60. package/src/skf-audit-skill/SKILL.md +66 -15
  61. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  62. package/src/skf-audit-skill/customize.toml +49 -0
  63. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  64. package/src/skf-audit-skill/references/init.md +305 -0
  65. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  66. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  67. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  68. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  69. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  70. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +29 -16
  71. package/src/skf-brief-skill/SKILL.md +48 -18
  72. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  73. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  74. package/src/skf-brief-skill/customize.toml +44 -0
  75. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +5 -5
  76. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  77. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  78. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +24 -24
  79. package/src/skf-brief-skill/references/headless-args.md +9 -9
  80. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  81. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  82. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  83. package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
  84. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +19 -16
  85. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  86. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +11 -10
  87. package/src/skf-create-skill/SKILL.md +25 -19
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  89. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  90. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  91. package/src/skf-create-skill/customize.toml +35 -0
  92. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  93. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  94. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  96. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  97. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  98. package/src/skf-create-skill/references/extraction-patterns.md +8 -8
  99. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +17 -9
  100. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  101. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  102. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  103. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  104. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  105. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  106. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  107. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +46 -26
  108. package/src/skf-create-stack-skill/SKILL.md +76 -17
  109. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  110. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  111. package/src/skf-create-stack-skill/customize.toml +46 -0
  112. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  113. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  114. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +32 -11
  115. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +28 -28
  116. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  117. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  118. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  119. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  120. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +40 -17
  122. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  123. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  124. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  125. package/src/skf-drop-skill/SKILL.md +84 -12
  126. package/src/skf-drop-skill/customize.toml +57 -0
  127. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  128. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  129. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  130. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  131. package/src/skf-export-skill/SKILL.md +85 -16
  132. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  133. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  134. package/src/skf-export-skill/customize.toml +49 -0
  135. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +18 -12
  136. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  137. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +21 -51
  138. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  139. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  140. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  141. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  142. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  143. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +74 -0
  144. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +30 -6
  145. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  146. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +116 -91
  147. package/src/skf-quick-skill/SKILL.md +63 -123
  148. package/src/skf-quick-skill/customize.toml +44 -0
  149. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  150. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  151. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  152. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  153. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  154. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  155. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  156. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  157. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  158. package/src/skf-refine-architecture/SKILL.md +86 -16
  159. package/src/skf-refine-architecture/customize.toml +49 -0
  160. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  161. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  162. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  163. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  164. package/src/skf-refine-architecture/references/init.md +144 -0
  165. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  166. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  167. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  168. package/src/skf-rename-skill/SKILL.md +82 -12
  169. package/src/skf-rename-skill/customize.toml +52 -0
  170. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  171. package/src/skf-rename-skill/references/health-check.md +30 -0
  172. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  173. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  174. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  175. package/src/skf-setup/SKILL.md +35 -35
  176. package/src/skf-setup/customize.toml +33 -0
  177. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  178. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +14 -22
  179. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  180. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  181. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  182. package/src/skf-setup/references/tier-rules.md +1 -1
  183. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  184. package/src/skf-test-skill/SKILL.md +77 -15
  185. package/src/skf-test-skill/customize.toml +54 -0
  186. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +28 -71
  187. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  188. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  189. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  190. package/src/skf-test-skill/references/health-check.md +14 -0
  191. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  192. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  193. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  194. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  195. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  196. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  197. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  198. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  199. package/src/skf-update-skill/SKILL.md +23 -14
  200. package/src/skf-update-skill/customize.toml +44 -0
  201. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  202. package/src/skf-update-skill/references/health-check.md +32 -0
  203. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  204. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  205. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  206. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  207. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  208. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  209. package/src/skf-update-skill/references/report.md +214 -0
  210. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  211. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  212. package/src/skf-verify-stack/SKILL.md +89 -17
  213. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  214. package/src/skf-verify-stack/customize.toml +50 -0
  215. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  216. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +7 -7
  217. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  218. package/src/skf-verify-stack/references/init.md +166 -0
  219. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  220. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +12 -12
  221. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +22 -41
  222. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +7 -7
  223. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +8 -8
  224. package/tools/validate-docs-drift.js +1 -1
  225. package/src/skf-audit-skill/steps-c/step-01-init.md +0 -221
  226. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  227. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  228. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  229. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  230. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  231. package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
@@ -6,8 +6,8 @@
6
6
 
7
7
  Validates and normalizes the headless argument set passed to skf-brief-skill
8
8
  before the workflow's interactive sequence runs. Catches malformed inputs at
9
- point of capture instead of letting them surface 5+ minutes later in step-02
10
- or step-05.
9
+ point of capture instead of letting them surface 5+ minutes later in step 2
10
+ or step 5.
11
11
 
12
12
  CLI:
13
13
  uv run skf-validate-brief-inputs.py --json '{...}'
@@ -77,7 +77,7 @@ KNOWN_FIELDS = {
77
77
  "exclude",
78
78
  "force",
79
79
  }
80
- # `preset` is intentionally NOT in KNOWN_FIELDS — it is consumed at the step-01 §8 GATE
80
+ # `preset` is intentionally NOT in KNOWN_FIELDS — it is consumed at the step 1 §8 GATE
81
81
  # (the LLM merges the named preset YAML into the args dict and drops the `preset` key
82
82
  # before calling the validator). If the key leaks through, the validator's existing
83
83
  # unknown-field handling emits `"unrecognized field 'preset' — passed through unchanged"`
@@ -218,9 +218,9 @@ def validate(inp: dict[str, Any]) -> dict[str, Any]:
218
218
  normalized: dict[str, Any] = dict(inp)
219
219
  normalized.setdefault("source_type", "source")
220
220
  # `source_authority` is intentionally NOT setdefault'd here for source-type targets:
221
- # step-01 §3.3's headless detection branch runs `gh api user` and may resolve to
221
+ # step 1 §3.3's headless detection branch runs `gh api user` and may resolve to
222
222
  # `official` for repos owned by the authenticated user. Stamping a default at
223
- # validator time would pre-empt the detection because step-01 §8 GATE treats the
223
+ # validator time would pre-empt the detection because step 1 §8 GATE treats the
224
224
  # `normalized` object as the source of truth. Absence is the signal "run detection."
225
225
  # The docs-only branch below still forces `community` since detection cannot apply.
226
226
  normalized.setdefault("scripts_intent", "detect")
@@ -0,0 +1,348 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pyyaml", "jsonschema>=4.0"]
4
+ # ///
5
+ """SKF Validate Brief Schema — schema check for a skill-brief.yaml on disk.
6
+
7
+ Loads `skill-brief.yaml` from a path (or from stdin/inline-YAML), validates
8
+ it against `src/shared/scripts/schemas/skill-brief.v1.json`, and applies the
9
+ two conditional rules from `skf-create-skill/references/load-brief.md §3`
10
+ that the JSON schema doesn't express today:
11
+
12
+ - When `source_type == "docs-only"`: `doc_urls` must have ≥1 entry, and
13
+ `source_authority` must be `community` (the prose says it is "forced
14
+ to community" — we surface a warning when it disagrees rather than
15
+ silently rewriting).
16
+ - `version` must be present AND non-empty AND not whitespace-only. The
17
+ schema rejects empty strings via its semver pattern, but a string that
18
+ only contains whitespace would slip past a naive presence check; we
19
+ catch that case explicitly so the downstream directory-resolution code
20
+ doesn't produce `{name}//` paths.
21
+
22
+ The script produces skill-friendly error messages — translating raw
23
+ jsonschema diagnostics into the "Brief validation failed: ..." form the
24
+ calling stage prose already uses. Calling stages can read `errors[]`
25
+ directly and forward the `message` field to the user.
26
+
27
+ CLI:
28
+ uv run skf-validate-brief-schema.py <path/to/skill-brief.yaml>
29
+ cat brief.yaml | uv run skf-validate-brief-schema.py -
30
+ uv run skf-validate-brief-schema.py --yaml '<inline yaml>'
31
+
32
+ Output (JSON on stdout):
33
+ {
34
+ "valid": bool,
35
+ "errors": [{"field": "...", "message": "..."}, ...],
36
+ "warnings": [{"field": "...", "message": "..."}, ...],
37
+ "halt_reason": "brief-missing" | "brief-malformed" | "brief-invalid" | null,
38
+ "brief": { ...parsed YAML if loadable, else null... }
39
+ }
40
+
41
+ Exit codes:
42
+ 0 — valid (errors empty)
43
+ 1 — invalid (errors non-empty) OR file/yaml load failed
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import json
50
+ import sys
51
+ from pathlib import Path
52
+ from typing import Any
53
+
54
+ import yaml
55
+ from jsonschema import Draft202012Validator
56
+
57
+
58
+ REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
59
+ SCHEMA_PATH = (
60
+ REPO_ROOT / "src" / "shared" / "scripts" / "schemas" / "skill-brief.v1.json"
61
+ )
62
+
63
+
64
+ # --------------------------------------------------------------------------
65
+ # Loading
66
+ # --------------------------------------------------------------------------
67
+
68
+
69
+ def load_schema() -> dict:
70
+ """Read the skill-brief schema from its canonical path."""
71
+ return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
72
+
73
+
74
+ def load_brief_text(text: str) -> tuple[dict | None, str | None]:
75
+ """Parse YAML text. Returns (brief, error_message)."""
76
+ try:
77
+ brief = yaml.safe_load(text)
78
+ except yaml.YAMLError as exc:
79
+ return None, f"Brief is not valid YAML: {exc}"
80
+ if brief is None:
81
+ return None, "Brief is empty"
82
+ if not isinstance(brief, dict):
83
+ return None, f"Brief must be a YAML mapping; got {type(brief).__name__}"
84
+ return brief, None
85
+
86
+
87
+ # --------------------------------------------------------------------------
88
+ # Error translation
89
+ # --------------------------------------------------------------------------
90
+
91
+
92
+ def _field_path(error_path: tuple) -> str:
93
+ """Render a jsonschema error path as `a.b[0].c`."""
94
+ parts: list[str] = []
95
+ for p in error_path:
96
+ if isinstance(p, int):
97
+ parts.append(f"[{p}]")
98
+ else:
99
+ parts.append(f".{p}" if parts else str(p))
100
+ return "".join(parts) or "(root)"
101
+
102
+
103
+ def _translate_jsonschema_error(err) -> dict:
104
+ """Produce a skill-friendly error record from a jsonschema ValidationError."""
105
+ field = _field_path(err.absolute_path)
106
+ validator = err.validator
107
+ inst = err.instance
108
+
109
+ if validator == "required":
110
+ missing = err.message.split("'")[1] if "'" in err.message else "(unknown)"
111
+ return {
112
+ "field": missing,
113
+ "message": (
114
+ f"Brief validation failed: missing required field `{missing}`. "
115
+ f"Update your skill-brief.yaml and re-run."
116
+ ),
117
+ }
118
+ if validator == "pattern":
119
+ return {
120
+ "field": field,
121
+ "message": (
122
+ f"Brief validation failed: `{field}` field `{inst}` does not "
123
+ f"match required pattern `{err.validator_value}`. "
124
+ f"Update your skill-brief.yaml and re-run."
125
+ ),
126
+ }
127
+ if validator == "enum":
128
+ return {
129
+ "field": field,
130
+ "message": (
131
+ f"Brief validation failed: `{field}` field `{inst}` is not one of "
132
+ f"{err.validator_value}. Update your skill-brief.yaml and re-run."
133
+ ),
134
+ }
135
+ if validator == "type":
136
+ expected = err.validator_value
137
+ actual = type(inst).__name__
138
+ return {
139
+ "field": field,
140
+ "message": (
141
+ f"Brief validation failed: `{field}` field has type `{actual}`, "
142
+ f"expected `{expected}`. Update your skill-brief.yaml and re-run."
143
+ ),
144
+ }
145
+ if validator == "minLength":
146
+ return {
147
+ "field": field,
148
+ "message": (
149
+ f"Brief validation failed: `{field}` field must be non-empty. "
150
+ f"Update your skill-brief.yaml and re-run."
151
+ ),
152
+ }
153
+ if validator == "minItems":
154
+ return {
155
+ "field": field,
156
+ "message": (
157
+ f"Brief validation failed: `{field}` field must contain at least "
158
+ f"{err.validator_value} item(s). Update your skill-brief.yaml and re-run."
159
+ ),
160
+ }
161
+ # Fallback — preserve raw message but with consistent prefix
162
+ return {
163
+ "field": field,
164
+ "message": f"Brief validation failed: `{field}` — {err.message}",
165
+ }
166
+
167
+
168
+ # --------------------------------------------------------------------------
169
+ # Conditional rules from §3 prose
170
+ # --------------------------------------------------------------------------
171
+
172
+
173
+ def _docs_only_rules(brief: dict) -> tuple[list[dict], list[dict]]:
174
+ """Apply the docs-only conditional rules.
175
+
176
+ Returns (errors, warnings).
177
+ """
178
+ errors: list[dict] = []
179
+ warnings: list[dict] = []
180
+ if brief.get("source_type") != "docs-only":
181
+ return errors, warnings
182
+
183
+ doc_urls = brief.get("doc_urls")
184
+ if not isinstance(doc_urls, list) or len(doc_urls) == 0:
185
+ errors.append(
186
+ {
187
+ "field": "doc_urls",
188
+ "message": (
189
+ "Brief validation failed: `doc_urls` must have at least one "
190
+ "entry when `source_type` is `docs-only`. "
191
+ "Update your skill-brief.yaml and re-run."
192
+ ),
193
+ }
194
+ )
195
+
196
+ source_authority = brief.get("source_authority")
197
+ if source_authority not in (None, "community"):
198
+ warnings.append(
199
+ {
200
+ "field": "source_authority",
201
+ "message": (
202
+ f"`source_authority` is `{source_authority}` but will be treated "
203
+ f"as `community` because `source_type` is `docs-only`."
204
+ ),
205
+ }
206
+ )
207
+ return errors, warnings
208
+
209
+
210
+ def _version_non_empty_rule(brief: dict) -> list[dict]:
211
+ """The §3 prose calls out version-whitespace-only as a hard error."""
212
+ version = brief.get("version")
213
+ if isinstance(version, str) and version.strip() == "" and version != "":
214
+ # purely whitespace — schema's minLength check on patterns won't catch
215
+ # leading/trailing whitespace that strips to empty
216
+ return [
217
+ {
218
+ "field": "version",
219
+ "message": (
220
+ "Brief validation failed: `version` field is required and "
221
+ "must be non-empty. Update your skill-brief.yaml and re-run."
222
+ ),
223
+ }
224
+ ]
225
+ return []
226
+
227
+
228
+ # --------------------------------------------------------------------------
229
+ # Main validate
230
+ # --------------------------------------------------------------------------
231
+
232
+
233
+ def validate_brief(brief: dict) -> dict:
234
+ """Validate a parsed brief against the schema + conditional rules."""
235
+ schema = load_schema()
236
+ validator = Draft202012Validator(schema)
237
+
238
+ errors: list[dict] = []
239
+ warnings: list[dict] = []
240
+
241
+ for err in sorted(validator.iter_errors(brief), key=lambda e: e.absolute_path):
242
+ errors.append(_translate_jsonschema_error(err))
243
+
244
+ cond_errors, cond_warnings = _docs_only_rules(brief)
245
+ errors.extend(cond_errors)
246
+ warnings.extend(cond_warnings)
247
+
248
+ errors.extend(_version_non_empty_rule(brief))
249
+
250
+ return {
251
+ "valid": not errors,
252
+ "errors": errors,
253
+ "warnings": warnings,
254
+ }
255
+
256
+
257
+ # --------------------------------------------------------------------------
258
+ # CLI
259
+ # --------------------------------------------------------------------------
260
+
261
+
262
+ def _emit(envelope: dict) -> None:
263
+ json.dump(envelope, sys.stdout, indent=2)
264
+ sys.stdout.write("\n")
265
+
266
+
267
+ def _envelope_load_error(reason: str, message: str) -> dict:
268
+ return {
269
+ "valid": False,
270
+ "errors": [{"field": "(file)", "message": message}],
271
+ "warnings": [],
272
+ "halt_reason": reason,
273
+ "brief": None,
274
+ }
275
+
276
+
277
+ def _envelope_invalid(brief: dict, errors: list[dict], warnings: list[dict]) -> dict:
278
+ return {
279
+ "valid": False,
280
+ "errors": errors,
281
+ "warnings": warnings,
282
+ "halt_reason": "brief-invalid",
283
+ "brief": brief,
284
+ }
285
+
286
+
287
+ def _envelope_valid(brief: dict, warnings: list[dict]) -> dict:
288
+ return {
289
+ "valid": True,
290
+ "errors": [],
291
+ "warnings": warnings,
292
+ "halt_reason": None,
293
+ "brief": brief,
294
+ }
295
+
296
+
297
+ def main(argv: list[str] | None = None) -> int:
298
+ parser = argparse.ArgumentParser(
299
+ prog="skf-validate-brief-schema",
300
+ description="Validate a skill-brief.yaml against the SKF brief schema.",
301
+ )
302
+ src = parser.add_mutually_exclusive_group(required=True)
303
+ src.add_argument(
304
+ "path",
305
+ nargs="?",
306
+ help="path to skill-brief.yaml; pass `-` to read from stdin",
307
+ )
308
+ src.add_argument(
309
+ "--yaml",
310
+ dest="inline_yaml",
311
+ help="inline YAML text to validate (alternative to a path)",
312
+ )
313
+ args = parser.parse_args(argv)
314
+
315
+ if args.inline_yaml is not None:
316
+ text = args.inline_yaml
317
+ elif args.path == "-":
318
+ text = sys.stdin.read()
319
+ else:
320
+ brief_path = Path(args.path)
321
+ if not brief_path.is_file():
322
+ _emit(
323
+ _envelope_load_error(
324
+ "brief-missing",
325
+ f"Brief not found at `{brief_path}`. Run [BS] Brief Skill to "
326
+ "create one, or use [QS] Quick Skill for brief-less generation.",
327
+ )
328
+ )
329
+ return 1
330
+ text = brief_path.read_text(encoding="utf-8")
331
+
332
+ brief, load_err = load_brief_text(text)
333
+ if load_err is not None:
334
+ _emit(_envelope_load_error("brief-malformed", load_err))
335
+ return 1
336
+ assert brief is not None
337
+
338
+ result = validate_brief(brief)
339
+ if not result["valid"]:
340
+ _emit(_envelope_invalid(brief, result["errors"], result["warnings"]))
341
+ return 1
342
+
343
+ _emit(_envelope_valid(brief, result["warnings"]))
344
+ return 0
345
+
346
+
347
+ if __name__ == "__main__":
348
+ raise SystemExit(main())
@@ -6,7 +6,7 @@
6
6
 
7
7
  Replaces the prose-driven YAML emission, version-precedence resolution,
8
8
  conditional optional-field rendering, and non-atomic file write currently
9
- inlined in `src/skf-brief-skill/steps-c/step-05-write-brief.md` §3-§4.
9
+ inlined in `src/skf-brief-skill/references/write-brief.md` §3-§4.
10
10
 
11
11
  Each of those operations is purely deterministic: there is no LLM
12
12
  judgement required to render the YAML, decide which optional fields
@@ -56,7 +56,7 @@ Context payload shape (consumed by `write`):
56
56
 
57
57
  Version precedence (resolved into the rendered YAML's `version` field):
58
58
  1. version_resolved if explicitly supplied (caller already ran the
59
- precedence rule). Used by step-05 when it has confirmed values.
59
+ precedence rule). Used by step 5 when it has confirmed values.
60
60
  2. Otherwise: target_version if non-null.
61
61
  3. Otherwise: detected_version if non-null.
62
62
  4. Otherwise: "1.0.0".
@@ -263,6 +263,36 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
263
263
  if not isinstance(scope["notes"], str):
264
264
  _die("scope.notes must be a string (use empty string when no notes)", field="scope.notes")
265
265
 
266
+ # scope.rationale — optional authoring-time scope-type decision record.
267
+ # Absent/None → field is simply not present (same null-drop path as
268
+ # doc_urls). When present it must be a complete six-subkey object.
269
+ rationale = scope.get("rationale")
270
+ if rationale is not None:
271
+ if not isinstance(rationale, dict):
272
+ _die("scope.rationale must be an object when present", field="scope.rationale")
273
+ _RATIONALE_STR_KEYS = ("recommended", "chosen", "heuristic", "reason", "recorded")
274
+ for rk in (*_RATIONALE_STR_KEYS, "accepted_recommendation"):
275
+ if rk not in rationale:
276
+ _die(f"scope.rationale.{rk} is required", field=f"scope.rationale.{rk}")
277
+ for rk in _RATIONALE_STR_KEYS:
278
+ if not isinstance(rationale[rk], str) or not rationale[rk]:
279
+ _die(
280
+ f"scope.rationale.{rk} must be a non-empty string",
281
+ field=f"scope.rationale.{rk}",
282
+ )
283
+ if not isinstance(rationale["accepted_recommendation"], bool):
284
+ _die(
285
+ "scope.rationale.accepted_recommendation must be a boolean",
286
+ field="scope.rationale.accepted_recommendation",
287
+ )
288
+ for rk in ("recommended", "chosen"):
289
+ if rationale[rk] not in VALID_SCOPE_TYPES:
290
+ _die(
291
+ f"scope.rationale.{rk} must be one of {sorted(VALID_SCOPE_TYPES)}. "
292
+ f"Got: {rationale[rk]!r}",
293
+ field=f"scope.rationale.{rk}",
294
+ )
295
+
266
296
  # target_version semver shape (when present)
267
297
  tv = ctx.get("target_version")
268
298
  if tv is not None:
@@ -308,6 +338,21 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
308
338
  },
309
339
  }
310
340
 
341
+ # Conditional: scope.rationale — canonical position is after `notes` and
342
+ # before `amendments` (amendments is appended post-authoring by other
343
+ # workflows and is never emitted here, so appending after notes yields the
344
+ # canonical order). Absent → key omitted, matching the legacy-brief default.
345
+ scope_rationale = ctx["scope"].get("rationale")
346
+ if scope_rationale is not None:
347
+ brief["scope"]["rationale"] = {
348
+ "recommended": scope_rationale["recommended"],
349
+ "chosen": scope_rationale["chosen"],
350
+ "accepted_recommendation": scope_rationale["accepted_recommendation"],
351
+ "heuristic": scope_rationale["heuristic"],
352
+ "reason": scope_rationale["reason"],
353
+ "recorded": scope_rationale["recorded"],
354
+ }
355
+
311
356
  # Conditional: target_version (must equal version)
312
357
  tv = ctx.get("target_version")
313
358
  if tv is not None:
@@ -346,7 +391,7 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
346
391
  def render_yaml(brief: dict[str, Any]) -> str:
347
392
  """Dump the brief dict as YAML in canonical key order with a leading document marker.
348
393
 
349
- The step-05 §3 template shows leading and trailing `---` markers, but those were
394
+ The step 5 §3 template shows leading and trailing `---` markers, but those were
350
395
  wrapping the example YAML for documentation purposes — actual on-disk YAML uses
351
396
  only the leading `---` (or none). A trailing `---` would start a second empty
352
397
  document and break callers that use `yaml.safe_load` (which expects a single
@@ -388,7 +433,13 @@ def atomic_write(target: Path, content: str) -> int:
388
433
  # Top-level keys that get folded into the nested `scope` sub-object — every
389
434
  # other top-level key passes through unchanged so future additions to the
390
435
  # schema don't require a translator update.
391
- _FLAT_SCOPE_KEYS = ("scope_type", "scope_include", "scope_exclude", "scope_notes")
436
+ _FLAT_SCOPE_KEYS = (
437
+ "scope_type",
438
+ "scope_include",
439
+ "scope_exclude",
440
+ "scope_notes",
441
+ "scope_rationale",
442
+ )
392
443
 
393
444
 
394
445
  def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
@@ -437,6 +488,11 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
437
488
  scope["exclude"] = flat["scope_exclude"]
438
489
  if "scope_notes" in flat and flat["scope_notes"] is not None:
439
490
  scope["notes"] = flat["scope_notes"]
491
+ # Optional authoring-time rationale. Null/absent → key dropped (same
492
+ # null-drop semantics as doc_urls); when present it carries the full
493
+ # six-subkey object validated by validate_context.
494
+ if "scope_rationale" in flat and flat["scope_rationale"] is not None:
495
+ scope["rationale"] = flat["scope_rationale"]
440
496
  nested["scope"] = scope
441
497
  return nested
442
498
 
@@ -495,7 +551,7 @@ def main() -> int:
495
551
  "scope_type/scope_include/scope_exclude/scope_notes top-level keys, "
496
552
  "optional fields nullable) instead of the nested shape. Eliminates "
497
553
  "the conditional-omit logic the LLM currently walks at the §3 "
498
- "assembly site in step-05."
554
+ "assembly site in step 5."
499
555
  ),
500
556
  )
501
557
 
@@ -9,6 +9,14 @@ description: Discover what to skill in a large repo and produce recommended skil
9
9
 
10
10
  Analyzes a large repo or multi-service project to identify discrete skillable units, map exports and integration points, and produce recommended skill-brief.yaml files as the primary entry point for brownfield onboarding. The analysis must be thorough enough to produce actionable briefs, but scoped enough to avoid overwhelming the user with false positives. Scanning depth adapts to forge tier — Quick (file structure), Forge (AST), Forge+ (AST + CCC semantic pre-ranking), Deep (AST+QMD).
11
11
 
12
+ ## Conventions
13
+
14
+ - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
15
+ - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
16
+ - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
17
+ - `{project-root}`-prefixed paths resolve from the project working directory.
18
+ - `{skill-name}` resolves to the skill directory's basename.
19
+
12
20
  ## Role
13
21
 
14
22
  You are a source code analyst and decomposition architect collaborating with a developer onboarding an existing project. You bring expertise in codebase analysis, service boundary detection, and skill scoping, while the user brings their domain knowledge. Work together as equals.
@@ -17,35 +25,56 @@ You are a source code analyst and decomposition architect collaborating with a d
17
25
 
18
26
  These rules apply to every step in this workflow:
19
27
 
20
- - Read each step file completely before taking any action
21
- - Follow the mandatory sequence in each step exactly — do not skip, reorder, or optimize
22
28
  - Only load one step file at a time — never preload future steps
23
- - Update `stepsCompleted` in output file frontmatter before loading next step
24
- - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
25
- - Always communicate in `{communication_language}`
29
+ - Always communicate in `{communication_language}` (the language for user-facing prose). Written artifact text — the per-unit recommendation `description` and `scope.notes` persisted into `skill-brief.yaml` — is in `{document_output_language}`; per-step rules call this out where it applies. The two values may be the same.
26
30
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
27
31
 
28
32
  ## Stages
29
33
 
30
34
  | # | Step | File | Auto-proceed |
31
35
  |---|------|------|--------------|
32
- | 1 | Initialize | steps-c/step-01-init.md | Yes |
33
- | 1b | Continue (session resume) | steps-c/step-01b-continue.md | Yes |
34
- | 2 | Scan Project | steps-c/step-02-scan-project.md | No (confirm) |
35
- | 3 | Identify Units | steps-c/step-03-identify-units.md | No (confirm) |
36
- | 4 | Map & Detect | steps-c/step-04-map-and-detect.md | Yes |
37
- | 5 | Recommend | steps-c/step-05-recommend.md | No (confirm) |
38
- | 6 | Generate Briefs | steps-c/step-06-generate-briefs.md | Yes |
39
- | 7 | Workflow Health Check | steps-c/step-07-health-check.md | Yes |
36
+ | 1 | Initialize | references/init.md | Yes |
37
+ | 1b | Continue (session resume) | references/continue.md | Yes |
38
+ | 2 | Scan Project | references/scan-project.md | No (confirm) |
39
+ | 3 | Identify Units | references/identify-units.md | No (confirm) |
40
+ | 4 | Map & Detect | references/map-and-detect.md | Yes |
41
+ | 5 | Recommend | references/recommend.md | No (confirm) |
42
+ | 6 | Generate Briefs | references/generate-briefs.md | Yes |
43
+ | 7 | Workflow Health Check | references/health-check.md | Yes |
40
44
 
41
45
  ## Invocation Contract
42
46
 
43
47
  | Aspect | Detail |
44
48
  |--------|--------|
45
49
  | **Inputs** | project_path [required], scope_hint [optional] |
46
- | **Gates** | step-02: Confirm Gate [C] | step-03: Confirm Gate [C] | step-05: Confirm Gate [C] |
47
- | **Outputs** | analysis-report.md, skill-brief.yaml files (one per recommended unit) |
50
+ | **Headless inputs** | `--project-path <path>` (skip Step 1 project-path prompt), `--scope-hint <text>` (skip Step 1 scope-hint prompt), `--intent-hint <text>` (pre-supply analysis intent; drives recommendation ranking in Step 5) |
51
+ | **Headless flag** | `--headless` / `-H` flips every confirm gate to auto-proceed |
52
+ | **Gates** | step 2: Confirm Gate [C] | step 3: Confirm Gate [C] | step 5: Confirm Gate [C] |
53
+ | **Outputs** | analysis-report.md, skill-brief.yaml files (one per recommended unit); final `SKF_ANALYZE_RESULT_JSON` line on stdout when `{headless_mode}` is true |
48
54
  | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
55
+ | **Exit codes** | See "Exit Codes" below |
56
+
57
+ ## Exit Codes
58
+
59
+ Every HARD HALT in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text:
60
+
61
+ | Code | Meaning | Raised by |
62
+ | ---- | -------------------- | ------------------------------------------------------------------------------------------ |
63
+ | 0 | success | step 7 (terminal — health check completion) |
64
+ | 2 | input-missing | step 1 §2-3 — required config absent (config.yaml not loadable, project path empty/invalid in headless mode) |
65
+ | 3 | resolution-failure | step 1 §2 (`forge-tier.yaml` missing at `{sidecar_path}/forge-tier.yaml`); step 1 §3 (project path does not exist or remote URL inaccessible) |
66
+ | 4 | write-failure | step 1 §6 (analysis report write failed); step 6 §5 (skill-brief.yaml write failed); step 6 §9 (result contract write failed) |
67
+ | 6 | user-cancelled | any interactive menu in steps 2/3/5/6 (user selected `[X]` Cancel and exit) |
68
+
69
+ ## Result Contract (Headless)
70
+
71
+ When `{headless_mode}` is true, step 6 emits a single-line JSON envelope on **stdout** before chaining to step 7, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`:
72
+
73
+ ```
74
+ SKF_ANALYZE_RESULT_JSON: {"status":"success|error","report_path":"…|null","brief_paths":["…"],"unit_counts":{"confirmed":N,"skipped":N,"maybe":N},"exit_code":0,"halt_reason":null}
75
+ ```
76
+
77
+ `status` is `"success"` on the terminal happy path, `"error"` on any HALT. `halt_reason` is one of: `null` (success), `"input-missing"`, `"forge-tier-missing"`, `"path-invalid"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches the table above. `brief_paths` is an array of absolute paths to every generated `skill-brief.yaml` (empty array if none were generated). `unit_counts` reports confirmed/skipped/maybe counts from step 5's user decisions.
49
78
 
50
79
  ## On Activation
51
80
 
@@ -54,4 +83,28 @@ These rules apply to every step in this workflow:
54
83
 
55
84
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
56
85
 
57
- 3. Load, read the full file, and then execute `./steps-c/step-01-init.md` to begin the workflow.
86
+ 3. **Resolve workflow customization.** Run:
87
+
88
+ ```bash
89
+ python3 {project-root}/_bmad/scripts/resolve_customization.py \
90
+ --skill {skill-root} --key workflow
91
+ ```
92
+
93
+ The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append):
94
+
95
+ - `{skill-root}/customize.toml` — bundled defaults
96
+ - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
97
+ - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
98
+
99
+ If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly — the bundled defaults are an empty string for each path scalar.
100
+
101
+ Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each scalar, if the merged value is empty or absent, use the bundled default:
102
+
103
+ - `{unitDetectionHeuristicsPath}` ← `workflow.unit_detection_heuristics_path` if non-empty, else `references/unit-detection-heuristics.md`
104
+ - `{briefSchemaPath}` ← `workflow.brief_schema_path` if non-empty, else `assets/skill-brief-schema.md`
105
+ - `{analysisReportTemplatePath}` ← `workflow.analysis_report_template_path` if non-empty, else `templates/analysis-report-template.md`
106
+ - `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string (no-op — workflow skips the hook invocation)
107
+
108
+ Stash all four as workflow-context variables. Stage files reference `{unitDetectionHeuristicsPath}` / `{briefSchemaPath}` / `{analysisReportTemplatePath}` / `{onCompleteCommand}` directly — no conditional at the usage site. Empty-string overrides cleanly fall through to the bundled default; non-empty values let orgs swap in house-style copies (or wire in pipeline hooks) without forking the skill.
109
+
110
+ 4. Load, read the full file, and then execute `references/init.md` to begin the workflow.
@@ -49,7 +49,7 @@ If the source is a remote GitHub repo, use `gh api repos/{owner}/{repo}/contents
49
49
 
50
50
  If detection succeeds, use the detected version. If it fails or returns a non-semver value, fall back to `"1.0.0"`.
51
51
 
52
- The create-skill workflow (step-03-extract) also performs version reconciliation at extraction time — if the source version has changed since the brief was created, the extraction step warns and uses the source version.
52
+ The create-skill workflow (extract) also performs version reconciliation at extraction time — if the source version has changed since the brief was created, the extraction step warns and uses the source version.
53
53
 
54
54
  **Target version override:** When `target_version` is present in the brief, it takes precedence over auto-detection. Auto-detection still runs for informational purposes (displayed as "Detected version" alongside the user-specified "Target version"), but the `target_version` value is used as the brief's `version` field. This is particularly useful for docs-only skills (where no package manifest exists) and when the user wants to compile a skill for a specific older version.
55
55