bmad-module-skill-forge 1.9.0 → 2.0.1

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 (281) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/README.md +10 -5
  4. package/docs/_data/pinned.yaml +2 -2
  5. package/docs/agents.md +11 -2
  6. package/docs/architecture.md +5 -4
  7. package/docs/bmad-synergy.md +30 -7
  8. package/docs/campaign.md +172 -0
  9. package/docs/examples.md +7 -3
  10. package/docs/forge-auto.md +90 -0
  11. package/docs/getting-started.md +9 -0
  12. package/docs/how-it-works.md +6 -4
  13. package/docs/index.md +4 -3
  14. package/docs/skill-model.md +1 -1
  15. package/docs/troubleshooting.md +17 -1
  16. package/docs/verifying-a-skill.md +2 -2
  17. package/docs/why-skf.md +1 -1
  18. package/docs/workflows.md +64 -24
  19. package/package.json +2 -2
  20. package/src/knowledge/skill-lifecycle.md +1 -1
  21. package/src/module-help.csv +4 -3
  22. package/src/shared/data/language-corpora.json +32 -0
  23. package/src/shared/references/pipeline-contracts.md +6 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  26. package/src/shared/scripts/skf-count-tokens.py +222 -0
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  28. package/src/shared/scripts/skf-detect-docs.py +240 -4
  29. package/src/shared/scripts/skf-detect-language.py +82 -3
  30. package/src/shared/scripts/skf-detect-tools.py +105 -2
  31. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  32. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  33. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  34. package/src/shared/scripts/skf-find-cycles.py +246 -0
  35. package/src/shared/scripts/skf-hash-content.py +487 -2
  36. package/src/shared/scripts/skf-language-corpora.py +100 -0
  37. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  39. package/src/shared/scripts/skf-preapply.py +1 -1
  40. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  41. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  42. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  43. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  44. package/src/shared/scripts/skf-shape-detect.py +576 -25
  45. package/src/shared/scripts/skf-shard-body.py +461 -0
  46. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  47. package/src/shared/scripts/skf-structural-diff.py +307 -67
  48. package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
  49. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  51. package/src/shared/scripts/skf-validate-output.py +443 -10
  52. package/src/shared/scripts/skf-validate-pins.py +3 -3
  53. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  54. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  55. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  56. package/src/skf-analyze-source/SKILL.md +17 -32
  57. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  58. package/src/skf-analyze-source/customize.toml +2 -0
  59. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  60. package/src/skf-analyze-source/references/continue.md +8 -13
  61. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  62. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  63. package/src/skf-analyze-source/references/health-check.md +1 -2
  64. package/src/skf-analyze-source/references/identify-units.md +25 -36
  65. package/src/skf-analyze-source/references/init.md +12 -25
  66. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  67. package/src/skf-analyze-source/references/recommend.md +4 -14
  68. package/src/skf-analyze-source/references/scan-project.md +3 -11
  69. package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
  70. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  71. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  72. package/src/skf-audit-skill/SKILL.md +9 -6
  73. package/src/skf-audit-skill/customize.toml +2 -0
  74. package/src/skf-audit-skill/references/init.md +58 -32
  75. package/src/skf-audit-skill/references/re-index.md +15 -34
  76. package/src/skf-audit-skill/references/report.md +11 -58
  77. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  78. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  79. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  80. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  81. package/src/skf-brief-skill/SKILL.md +7 -36
  82. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  83. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  84. package/src/skf-brief-skill/customize.toml +14 -0
  85. package/src/skf-brief-skill/references/analyze-target.md +13 -18
  86. package/src/skf-brief-skill/references/confirm-brief.md +2 -15
  87. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  88. package/src/skf-brief-skill/references/gather-intent.md +27 -24
  89. package/src/skf-brief-skill/references/headless-args.md +1 -1
  90. package/src/skf-brief-skill/references/health-check.md +1 -22
  91. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  92. package/src/skf-brief-skill/references/scope-definition.md +6 -15
  93. package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
  94. package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
  95. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  96. package/src/skf-brief-skill/references/write-brief.md +19 -13
  97. package/src/skf-campaign/SKILL.md +121 -0
  98. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  99. package/src/skf-campaign/customize.toml +75 -0
  100. package/src/skf-campaign/manifest.yaml +15 -0
  101. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  102. package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
  103. package/src/skf-campaign/references/health-check.md +35 -0
  104. package/src/skf-campaign/references/step-01-setup.md +121 -0
  105. package/src/skf-campaign/references/step-02-strategy.md +102 -0
  106. package/src/skf-campaign/references/step-03-pins.md +56 -0
  107. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  108. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  109. package/src/skf-campaign/references/step-06-batch.md +88 -0
  110. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  111. package/src/skf-campaign/references/step-08-verify.md +75 -0
  112. package/src/skf-campaign/references/step-09-refine.md +83 -0
  113. package/src/skf-campaign/references/step-10-export.md +106 -0
  114. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  115. package/src/skf-campaign/references/step-resume.md +115 -0
  116. package/src/skf-campaign/scripts/.gitkeep +0 -0
  117. package/src/skf-campaign/scripts/campaign-deps.py +244 -0
  118. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  119. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  120. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  121. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  122. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  123. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  124. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  125. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  126. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  127. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  128. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  129. package/src/skf-create-skill/SKILL.md +15 -4
  130. package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
  131. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  132. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  133. package/src/skf-create-skill/customize.toml +13 -0
  134. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  135. package/src/skf-create-skill/references/compile.md +37 -47
  136. package/src/skf-create-skill/references/component-extraction.md +8 -35
  137. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  138. package/src/skf-create-skill/references/enrich.md +4 -17
  139. package/src/skf-create-skill/references/extract.md +20 -27
  140. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  141. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  142. package/src/skf-create-skill/references/generate-artifacts.md +20 -25
  143. package/src/skf-create-skill/references/health-check.md +2 -2
  144. package/src/skf-create-skill/references/load-brief.md +16 -18
  145. package/src/skf-create-skill/references/report.md +27 -14
  146. package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
  147. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  148. package/src/skf-create-skill/references/step-doc-rot.md +33 -15
  149. package/src/skf-create-skill/references/step-doc-sources.md +13 -4
  150. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  151. package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
  152. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  153. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  154. package/src/skf-create-skill/references/validate.md +69 -29
  155. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  156. package/src/skf-create-stack-skill/SKILL.md +15 -9
  157. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  158. package/src/skf-create-stack-skill/customize.toml +10 -0
  159. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  160. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  161. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  162. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  163. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  164. package/src/skf-create-stack-skill/references/init.md +9 -81
  165. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  166. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  167. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  168. package/src/skf-create-stack-skill/references/report.md +27 -62
  169. package/src/skf-create-stack-skill/references/validate.md +35 -31
  170. package/src/skf-drop-skill/SKILL.md +20 -23
  171. package/src/skf-drop-skill/customize.toml +10 -0
  172. package/src/skf-drop-skill/references/execute.md +67 -36
  173. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  174. package/src/skf-drop-skill/references/health-check.md +1 -13
  175. package/src/skf-drop-skill/references/report.md +44 -10
  176. package/src/skf-drop-skill/references/select.md +93 -48
  177. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  178. package/src/skf-export-skill/SKILL.md +17 -14
  179. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  180. package/src/skf-export-skill/customize.toml +10 -6
  181. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  182. package/src/skf-export-skill/references/load-skill.md +44 -42
  183. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  184. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  185. package/src/skf-export-skill/references/package.md +27 -33
  186. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  187. package/src/skf-export-skill/references/result-envelope.md +28 -0
  188. package/src/skf-export-skill/references/summary.md +11 -5
  189. package/src/skf-export-skill/references/token-report.md +42 -24
  190. package/src/skf-export-skill/references/update-context.md +32 -33
  191. package/src/skf-forger/SKILL.md +32 -39
  192. package/src/skf-forger/references/pipeline-mode.md +34 -0
  193. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  194. package/src/skf-quick-skill/SKILL.md +10 -60
  195. package/src/skf-quick-skill/customize.toml +9 -0
  196. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  197. package/src/skf-quick-skill/references/compile.md +15 -52
  198. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  199. package/src/skf-quick-skill/references/finalize.md +12 -6
  200. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  201. package/src/skf-quick-skill/references/health-check.md +3 -4
  202. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  203. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  204. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  205. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  206. package/src/skf-refine-architecture/SKILL.md +16 -8
  207. package/src/skf-refine-architecture/customize.toml +9 -0
  208. package/src/skf-refine-architecture/references/compile.md +10 -8
  209. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  210. package/src/skf-refine-architecture/references/health-check.md +1 -1
  211. package/src/skf-refine-architecture/references/improvements.md +9 -25
  212. package/src/skf-refine-architecture/references/init.md +9 -16
  213. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  214. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  215. package/src/skf-refine-architecture/references/report.md +14 -11
  216. package/src/skf-rename-skill/SKILL.md +11 -21
  217. package/src/skf-rename-skill/customize.toml +10 -0
  218. package/src/skf-rename-skill/references/execute.md +112 -82
  219. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  220. package/src/skf-rename-skill/references/report.md +16 -8
  221. package/src/skf-rename-skill/references/select.md +69 -42
  222. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  223. package/src/skf-setup/SKILL.md +21 -13
  224. package/src/skf-setup/customize.toml +10 -0
  225. package/src/skf-setup/references/auto-index.md +11 -10
  226. package/src/skf-setup/references/ccc-index.md +14 -15
  227. package/src/skf-setup/references/detect-and-tier.md +10 -8
  228. package/src/skf-setup/references/report.md +26 -32
  229. package/src/skf-setup/references/tier-rules.md +2 -32
  230. package/src/skf-setup/references/write-config.md +9 -10
  231. package/src/skf-test-skill/SKILL.md +10 -8
  232. package/src/skf-test-skill/customize.toml +2 -0
  233. package/src/skf-test-skill/references/coherence-check.md +19 -31
  234. package/src/skf-test-skill/references/coverage-check.md +111 -67
  235. package/src/skf-test-skill/references/detect-mode.md +10 -41
  236. package/src/skf-test-skill/references/external-validators.md +11 -19
  237. package/src/skf-test-skill/references/init.md +39 -21
  238. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  239. package/src/skf-test-skill/references/report.md +60 -40
  240. package/src/skf-test-skill/references/score.md +32 -64
  241. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  242. package/src/skf-test-skill/references/source-access-protocol.md +15 -5
  243. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  244. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  245. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  246. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  247. package/src/skf-test-skill/scripts/compute-score.py +135 -4
  248. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  249. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  250. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  251. package/src/skf-update-skill/SKILL.md +17 -5
  252. package/src/skf-update-skill/customize.toml +11 -6
  253. package/src/skf-update-skill/references/detect-changes.md +48 -36
  254. package/src/skf-update-skill/references/health-check.md +5 -11
  255. package/src/skf-update-skill/references/init.md +40 -32
  256. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  257. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  258. package/src/skf-update-skill/references/merge.md +13 -57
  259. package/src/skf-update-skill/references/re-extract.md +27 -25
  260. package/src/skf-update-skill/references/report.md +14 -7
  261. package/src/skf-update-skill/references/validate.md +32 -30
  262. package/src/skf-update-skill/references/write.md +86 -51
  263. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  264. package/src/skf-verify-stack/SKILL.md +11 -21
  265. package/src/skf-verify-stack/customize.toml +12 -3
  266. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  267. package/src/skf-verify-stack/references/coverage.md +21 -10
  268. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  269. package/src/skf-verify-stack/references/health-check.md +1 -1
  270. package/src/skf-verify-stack/references/init.md +20 -25
  271. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  272. package/src/skf-verify-stack/references/integrations.md +44 -49
  273. package/src/skf-verify-stack/references/report.md +28 -34
  274. package/src/skf-verify-stack/references/requirements.md +3 -3
  275. package/src/skf-verify-stack/references/synthesize.md +36 -39
  276. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  277. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  278. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  279. package/tools/cli/lib/ui.js +3 -2
  280. package/docs/deepwiki.md +0 -89
  281. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """Deterministic schema validation of the §1 subagent inventory JSON.
6
+
7
+ Structural validator for the SKF test-skill coverage step (coverage-check.md
8
+ §1a "Schema validation" block). test-skill is a quality gate, so it must not
9
+ trust subagent output blindly: before any downstream step consumes the
10
+ documented inventory, the shape is validated. That validation is pure
11
+ structural checking — strip wrapping fences, parse JSON, assert required keys
12
+ and types, assert each export entry is a dict with a non-empty `name` and a
13
+ `kind` drawn from a fixed enum, and assert each cross-check mismatch carries its
14
+ five fields — with exactly one correct pass/fail per input and no
15
+ interpretation of meaning. It is therefore script work, not prompt work; the
16
+ prompt reads this verdict and owns the HALT decision plus the separate
17
+ grep ground-truth spot-check.
18
+
19
+ Distinct from reconcile-coverage.py (coverage numerator arithmetic) and
20
+ compute-score.py (weight tables): this script only validates the *shape* of the
21
+ subagent inventory and, on success, echoes the fence-stripped parsed inventory
22
+ back so the prompt consumes it directly instead of re-parsing the raw response.
23
+
24
+ CLI usage (mirrors reconcile-coverage.py):
25
+ uv run validate-inventory.py '<raw response>' # positional
26
+ uv run validate-inventory.py --json-input '<raw response>' # explicit flag
27
+ echo '<raw response>' | uv run validate-inventory.py --stdin # piped input
28
+
29
+ Input: the subagent's RAW response text (JSON, optionally wrapped in a markdown
30
+ code fence — a leading line of three backticks with an optional language tag and
31
+ a trailing line of three backticks are stripped before parsing).
32
+
33
+ Output (stdout, one object):
34
+ {
35
+ "valid": <bool>, # true only when parse + every schema check pass
36
+ "violations": [<str>, ...], # human-readable failures; empty when valid
37
+ "rejectedCount": <int>, # count of malformed exports[] entries
38
+ "exportsCount": <int>, # len(exports) when parsed; 0 otherwise
39
+ "inventory": {...} | null # fence-stripped parsed inventory when valid; null otherwise
40
+ }
41
+
42
+ Exit codes:
43
+ 0 — inventory valid
44
+ 1 — no input provided (usage error)
45
+ 2 — inventory invalid (parse failure or schema violation; result JSON emitted)
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ import argparse
51
+ import json
52
+ import re
53
+ import sys
54
+
55
+ # Fixed enum for exports[].kind — the constructs SKF documents across languages
56
+ # and skill types: JS/TS (function/class/type/constant/hook/interface/method),
57
+ # Rust public-API items (struct/enum/trait/macro, alongside the shared
58
+ # type/constant/function), and stack-composition scaffolds (adapter). Mirrors the
59
+ # enum documented in coverage-check.md §1a.
60
+ VALID_KINDS = (
61
+ "function",
62
+ "class",
63
+ "type",
64
+ "constant",
65
+ "hook",
66
+ "interface",
67
+ "method",
68
+ "struct",
69
+ "enum",
70
+ "trait",
71
+ "macro",
72
+ "adapter",
73
+ )
74
+
75
+ REQUIRED_MISMATCH_FIELDS = (
76
+ "export",
77
+ "skill_md_line",
78
+ "reference_file",
79
+ "reference_line",
80
+ "issue",
81
+ )
82
+
83
+ _FENCE_OPEN = re.compile(r"^```[A-Za-z0-9_-]*$")
84
+ _FENCE_CLOSE = re.compile(r"^```$")
85
+
86
+
87
+ def strip_fences(text):
88
+ """Remove a wrapping markdown code fence, matching coverage-check.md §1a step 1.
89
+
90
+ When the first non-empty line is three backticks (optionally with a language
91
+ tag) and the last non-empty line is three backticks, drop those two lines.
92
+ Otherwise return the text unchanged.
93
+ """
94
+ lines = text.split("\n")
95
+ non_empty_idx = [i for i, ln in enumerate(lines) if ln.strip()]
96
+ if len(non_empty_idx) < 2:
97
+ return text
98
+ first, last = non_empty_idx[0], non_empty_idx[-1]
99
+ if _FENCE_OPEN.match(lines[first].strip()) and _FENCE_CLOSE.match(lines[last].strip()):
100
+ kept = [ln for i, ln in enumerate(lines) if i != first and i != last]
101
+ return "\n".join(kept)
102
+ return text
103
+
104
+
105
+ def validate_inventory(raw):
106
+ """Pure structural validation of the subagent inventory response.
107
+
108
+ `raw` is the raw response text. Returns the result dict documented in the
109
+ module docstring. Deterministic: identical input yields an identical verdict.
110
+ """
111
+ violations = []
112
+ inner = strip_fences(raw)
113
+ try:
114
+ data = json.loads(inner)
115
+ except json.JSONDecodeError as exc:
116
+ return {
117
+ "valid": False,
118
+ "violations": [f"subagent response not valid JSON: {exc.msg}"],
119
+ "rejectedCount": 0,
120
+ "exportsCount": 0,
121
+ "inventory": None,
122
+ }
123
+
124
+ if not isinstance(data, dict):
125
+ return {
126
+ "valid": False,
127
+ "violations": ["subagent response is not a JSON object"],
128
+ "rejectedCount": 0,
129
+ "exportsCount": 0,
130
+ "inventory": None,
131
+ }
132
+
133
+ exports = data.get("exports")
134
+ if not isinstance(exports, list):
135
+ violations.append(
136
+ "missing/typo: `exports` must be present and a list"
137
+ )
138
+ exports = []
139
+
140
+ cross = data.get("cross_check_mismatches")
141
+ if not isinstance(cross, list):
142
+ violations.append(
143
+ "missing/typo: `cross_check_mismatches` must be present and a list (may be empty)"
144
+ )
145
+ cross = []
146
+
147
+ # Per-entry export validation — count rejections.
148
+ rejected = 0
149
+ for idx, entry in enumerate(exports):
150
+ if not isinstance(entry, dict):
151
+ rejected += 1
152
+ continue
153
+ name = entry.get("name")
154
+ kind = entry.get("kind")
155
+ if not (isinstance(name, str) and name):
156
+ rejected += 1
157
+ continue
158
+ if kind not in VALID_KINDS:
159
+ rejected += 1
160
+ continue
161
+ if rejected > 0:
162
+ subject = "entry does" if rejected == 1 else "entries do"
163
+ violations.append(
164
+ f"{rejected} exports[] {subject} not match schema "
165
+ f"(each needs a non-empty string `name` and a `kind` in "
166
+ f"{{{', '.join(VALID_KINDS)}}})"
167
+ )
168
+
169
+ # Non-empty cross-check mismatch entries must carry all five fields.
170
+ for idx, entry in enumerate(cross):
171
+ if not isinstance(entry, dict):
172
+ violations.append(f"cross_check_mismatches[{idx}] is not an object")
173
+ continue
174
+ missing = [f for f in REQUIRED_MISMATCH_FIELDS if f not in entry]
175
+ if missing:
176
+ violations.append(
177
+ f"cross_check_mismatches[{idx}] missing field(s): {', '.join(missing)}"
178
+ )
179
+
180
+ valid = not violations
181
+ return {
182
+ "valid": valid,
183
+ "violations": violations,
184
+ "rejectedCount": rejected,
185
+ "exportsCount": len(exports),
186
+ "inventory": data if valid else None,
187
+ }
188
+
189
+
190
+ # --- CLI --------------------------------------------------------------------
191
+
192
+
193
+ def _build_parser():
194
+ parser = argparse.ArgumentParser(
195
+ prog="validate-inventory",
196
+ description=(
197
+ "Deterministic schema validation of the §1 subagent inventory JSON "
198
+ "(coverage-check.md §1a). Strips wrapping fences, parses, and asserts "
199
+ "the required-keys/types/enum/mismatch-field contract, returning "
200
+ "{valid, violations, rejectedCount, exportsCount, inventory}."
201
+ ),
202
+ formatter_class=argparse.RawDescriptionHelpFormatter,
203
+ )
204
+ src = parser.add_mutually_exclusive_group()
205
+ src.add_argument(
206
+ "raw_input",
207
+ nargs="?",
208
+ help="Raw subagent response as a positional argument.",
209
+ )
210
+ src.add_argument(
211
+ "--json-input",
212
+ dest="raw_input_flag",
213
+ help="Raw subagent response passed via flag (overrides positional).",
214
+ )
215
+ src.add_argument(
216
+ "--stdin",
217
+ action="store_true",
218
+ help="Read the raw subagent response from stdin.",
219
+ )
220
+ return parser
221
+
222
+
223
+ def _resolve_input(args):
224
+ if args.stdin:
225
+ return sys.stdin.read()
226
+ if args.raw_input_flag is not None:
227
+ return args.raw_input_flag
228
+ if args.raw_input is not None:
229
+ return args.raw_input
230
+ return ""
231
+
232
+
233
+ def main(argv=None):
234
+ parser = _build_parser()
235
+ args = parser.parse_args(argv)
236
+ raw = _resolve_input(args)
237
+ if not raw.strip():
238
+ parser.print_usage(file=sys.stderr)
239
+ print(
240
+ "error: no input provided (positional arg, --json-input, or --stdin)",
241
+ file=sys.stderr,
242
+ )
243
+ return 1
244
+
245
+ result = validate_inventory(raw)
246
+ print(json.dumps(result, indent=2))
247
+ return 0 if result["valid"] else 2
248
+
249
+
250
+ if __name__ == "__main__":
251
+ raise SystemExit(main())
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """Deterministic numerator ground-truth verifier.
7
+
8
+ Grep-and-count helper for the SKF test-skill workflow (coverage-check.md §4b
9
+ "Numerator ground-truth"). It fires only on the inflation signature
10
+ (`stats.exports_documented == effective_denominator` exactly): the numerator
11
+ equals the denominator, the tell of a documented count padded to force 100%
12
+ coverage. On that signature the declared count is not trusted — every declared
13
+ export name is greped against the skill's documentation surface and only the
14
+ names that actually appear count toward coverage.
15
+
16
+ Distinct from reconcile-coverage.py (whose scalar/stack branches grep the §1
17
+ inventory names and return only a residual missing *count*): this script greps
18
+ the full declared metadata/provenance set and enumerates the *absent* names so
19
+ §4b can list them in a High-severity gap. Both scripts share the same grep
20
+ semantics (SKILL.md ∪ sorted references/*.md, UTF-8, case-sensitive substring
21
+ containment — mirroring `grep "{name}"`) so their numerators agree.
22
+
23
+ Input schema (one JSON object):
24
+ {
25
+ "declaredNames": ["foo", "Bar::baz", ...], # metadata.exports[] / provenance declared set
26
+ "skillPackagePath": "/path/to/skill" # SKILL.md ∪ references/*.md
27
+ }
28
+
29
+ Output (stdout, one object):
30
+ {
31
+ "declared": <int>, # de-duplicated declared name count
32
+ "verified": <int>, # declared names present in the doc surface
33
+ "present": [names], # sorted
34
+ "absent": [names], # sorted (declared − verified) — the gap list
35
+ "inflated": <bool> # verified < declared (numerator was padded)
36
+ }
37
+ or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
38
+
39
+ `verified` is the numerator §4b uses for Export Coverage (it overrides the
40
+ declared count when `inflated` is true).
41
+
42
+ CLI usage (mirrors reconcile-coverage.py):
43
+ uv run verify-declared-numerator.py '<JSON>' # positional
44
+ uv run verify-declared-numerator.py --json-input '<JSON>' # explicit flag
45
+ cat input.json | uv run verify-declared-numerator.py --stdin # piped input
46
+
47
+ Exit codes:
48
+ 0 — verification emitted successfully
49
+ 1 — no input / input could not be parsed as JSON
50
+ 2 — input parsed but schema/semantics invalid (error object emitted as JSON)
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import argparse
56
+ import json
57
+ import sys
58
+ from pathlib import Path
59
+
60
+
61
+ def make_error(message):
62
+ return {"error": message, "code": "INVALID_INPUT"}
63
+
64
+
65
+ def load_doc_text(skill_package_path):
66
+ """Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
67
+
68
+ Deterministic: references are read in sorted order. Reads as UTF-8 so
69
+ non-ASCII exports do not mojibake on Windows (cp1252 default). Mirrors
70
+ reconcile-coverage.py.load_doc_text so the two numerators agree.
71
+ """
72
+ root = Path(skill_package_path)
73
+ parts = []
74
+ skill_md = root / "SKILL.md"
75
+ if skill_md.is_file():
76
+ parts.append(skill_md.read_text(encoding="utf-8"))
77
+ refs_dir = root / "references"
78
+ if refs_dir.is_dir():
79
+ for ref in sorted(refs_dir.glob("*.md")):
80
+ parts.append(ref.read_text(encoding="utf-8"))
81
+ return "\n".join(parts)
82
+
83
+
84
+ def _validate(inp):
85
+ if inp is None or not isinstance(inp, dict):
86
+ return "Input must be a JSON object"
87
+ names = inp.get("declaredNames")
88
+ if not isinstance(names, list) or not names:
89
+ return "declaredNames must be a non-empty list of strings"
90
+ if not inp.get("skillPackagePath"):
91
+ return "skillPackagePath is required to grep SKILL.md ∪ references"
92
+ return None
93
+
94
+
95
+ def verify(inp, doc_text=None):
96
+ """Pure verification. `doc_text` may be injected (tests); else loaded from disk."""
97
+ err = _validate(inp)
98
+ if err:
99
+ return make_error(err)
100
+
101
+ declared_set = sorted({n for n in inp["declaredNames"] if isinstance(n, str) and n})
102
+ if doc_text is None:
103
+ doc_text = load_doc_text(inp["skillPackagePath"])
104
+
105
+ present = sorted(n for n in declared_set if n in doc_text)
106
+ absent = sorted(n for n in declared_set if n not in doc_text)
107
+
108
+ return {
109
+ "declared": len(declared_set),
110
+ "verified": len(present),
111
+ "present": present,
112
+ "absent": absent,
113
+ "inflated": len(present) < len(declared_set),
114
+ }
115
+
116
+
117
+ # --- CLI --------------------------------------------------------------------
118
+
119
+
120
+ def _build_parser():
121
+ parser = argparse.ArgumentParser(
122
+ prog="verify-declared-numerator",
123
+ description=(
124
+ "Deterministic numerator ground-truth verifier (coverage-check.md "
125
+ "§4b). Greps the full declared export set against SKILL.md ∪ "
126
+ "references/*.md and enumerates the present/absent names so a padded "
127
+ "numerator is caught and the verified count replaces it."
128
+ ),
129
+ formatter_class=argparse.RawDescriptionHelpFormatter,
130
+ epilog=(
131
+ "Example:\n"
132
+ " uv run verify-declared-numerator.py "
133
+ "'{\"declaredNames\":[\"foo\",\"bar\"],"
134
+ "\"skillPackagePath\":\"/path/to/skill\"}'"
135
+ ),
136
+ )
137
+ src = parser.add_mutually_exclusive_group()
138
+ src.add_argument(
139
+ "json_input",
140
+ nargs="?",
141
+ help="JSON object as a positional argument (single-quote it on the shell).",
142
+ )
143
+ src.add_argument(
144
+ "--json-input",
145
+ dest="json_input_flag",
146
+ help="JSON object passed via flag (overrides positional).",
147
+ )
148
+ src.add_argument(
149
+ "--stdin",
150
+ action="store_true",
151
+ help="Read the JSON object from stdin.",
152
+ )
153
+ return parser
154
+
155
+
156
+ def _resolve_input(args):
157
+ if args.stdin:
158
+ return sys.stdin.read()
159
+ if args.json_input_flag is not None:
160
+ return args.json_input_flag
161
+ if args.json_input is not None:
162
+ return args.json_input
163
+ return ""
164
+
165
+
166
+ def main(argv=None):
167
+ parser = _build_parser()
168
+ args = parser.parse_args(argv)
169
+ raw = _resolve_input(args)
170
+ if not raw.strip():
171
+ parser.print_usage(file=sys.stderr)
172
+ print(
173
+ "error: no input provided (positional arg, --json-input, or --stdin)",
174
+ file=sys.stderr,
175
+ )
176
+ return 1
177
+
178
+ try:
179
+ data = json.loads(raw)
180
+ except json.JSONDecodeError as exc:
181
+ print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
182
+ return 1
183
+
184
+ result = verify(data)
185
+ print(json.dumps(result, indent=2))
186
+ if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
187
+ return 2
188
+ return 0
189
+
190
+
191
+ if __name__ == "__main__":
192
+ raise SystemExit(main())
@@ -12,6 +12,7 @@ Surgically updates existing skills when source code changes, preserving all [MAN
12
12
  ## Conventions
13
13
 
14
14
  - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
15
+ - **Module-level path exception:** bare paths beginning with `knowledge/` or `shared/` resolve from the SKF module root (`{project-root}/_bmad/skf/` installed, `src/` in dev), not the skill root — stage files reference `knowledge/version-paths.md` and `knowledge/tool-resolution.md`, and the terminal step chains to `shared/health-check.md`.
15
16
  - `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
17
  - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
17
18
  - `{project-root}`-prefixed paths resolve from the project working directory.
@@ -50,11 +51,11 @@ These rules apply to every step in this workflow:
50
51
  | Aspect | Detail |
51
52
  |--------|--------|
52
53
  | **Inputs** | skill_name [required] |
53
- | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--from-test-report` (gap-driven mode); `--allow-workspace-drift` (gap-driven only — bypass §0.a pinning guard); `--detect-only` (run detect-changes only, exit before re-extract; envelope `status="detect-only"`); `--dry-run` (run detect-changes + re-extract, exit before merge/write; envelope `status="dry-run"` describes what would change). If both `--detect-only` and `--dry-run` are passed, `--detect-only` wins. |
54
+ | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--from-test-report` (gap-driven mode); `--allow-workspace-drift` (gap-driven only — bypass §0.a pinning guard); `--allow-degraded` (headless only — pre-authorize the lossy degraded full re-extraction when the provenance map is missing, instead of halting `blocked`; init.md §4); `--detect-only` (run detect-changes only, exit before re-extract; envelope `status="detect-only"`); `--dry-run` (run detect-changes + re-extract, exit before merge/write; envelope `status="dry-run"` describes what would change). If both `--detect-only` and `--dry-run` are passed, `--detect-only` wins. |
54
55
  | **Gates** | step 1: Confirm Gate [C] | step 4: Confirm Gate [C if clean merge, HALT if conflicts] |
55
- | **Outputs** | Updated SKILL.md, updated provenance-map.json, evidence-report.md (none when `--detect-only` or `--dry-run` is set — those modes are read-only inspection paths) |
56
- | **Concurrency** | Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at `{forge_data_folder}/{skill_name}/.skf-update.lock` before any artifact read; live-PID collisions halt with `status: "halted-for-concurrent-run"`. Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) and by every documented halt path. Read-only modes (`--detect-only`, `--dry-run`) skip the lock entirely — they're safe alongside a concurrent real update. |
57
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Each auto-resolved gate appends a `{gate, default_action, taken_action, reason, evidence?}` entry to `headless_decisions[]`, surfaced in step 7's `SKF_UPDATE_RESULT_JSON` envelope so non-interactive runs can be audited post-hoc. Pipeline branches on the envelope's top-level `status` field (`success`, `no-changes`, `detect-only`, `dry-run`, or one of the documented `halted-for-*` codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. Schema: `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. |
56
+ | **Outputs** | Updated SKILL.md, metadata.json, provenance-map.json, evidence-report.md (none when `--detect-only` or `--dry-run` is set — those modes are read-only inspection paths) |
57
+ | **Concurrency** | Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at `{forge_data_folder}/{skill_name}/.skf-update.lock` before any artifact read; live-PID collisions halt with `status: "halted-for-concurrent-run"`. Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) on the normal path and by the two init-stage headless halts (§4/§6); mid-workflow halts leave it for the next run's stale-lock self-heal (see init.md §1b Release contract). Read-only modes (`--detect-only`, `--dry-run`) skip the lock entirely — they're safe alongside a concurrent real update. |
58
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Each auto-resolved gate appends a `{gate, default_action, taken_action, reason, evidence?}` entry to `headless_decisions[]`, surfaced in step 7's `SKF_UPDATE_RESULT_JSON` envelope so non-interactive runs can be audited post-hoc. A HALT reached in headless mode emits its own `SKF_UPDATE_RESULT_JSON` at the halt site (the site's `status` code plus an `error: {phase, path?, reason}` object) and exits — halts do not fall through to step 7. Pipeline branches on the envelope's top-level `status` field (`success`, `no-changes`, `detect-only`, `dry-run`, or one of the documented `halted-for-*`/`blocked` codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. The status enum is defined once in `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. |
58
59
 
59
60
  ## On Activation
60
61
 
@@ -64,4 +65,15 @@ These rules apply to every step in this workflow:
64
65
 
65
66
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
66
67
 
67
- 3. Load, read the full file, and then execute `references/init.md` to begin the workflow.
68
+ 3. **Resolve workflow customization.** Run:
69
+
70
+ ```bash
71
+ python3 {project-root}/_bmad/scripts/resolve_customization.py \
72
+ --skill {skill-root} --key workflow
73
+ ```
74
+
75
+ This merges the three layers per `bmad-customize` rules (scalars override, arrays append): `{skill-root}/customize.toml` (bundled defaults), `_bmad/custom/<skill-name>.toml` under `{project-root}` (team overrides), and `_bmad/custom/<skill-name>.user.toml` under `{project-root}` (personal overrides). If the script is missing or fails, read `{skill-root}/customize.toml` directly.
76
+
77
+ Apply the resolved values so no surface is a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts — the bundled default loads any `project-context.md` under `{project-root}`); resolve `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string, and stash it in workflow context (`references/report.md` §5b invokes it after the result contract is written; empty string = the hook is a no-op). After activation completes, execute each entry in `workflow.activation_steps_append` in order before `init.md` runs.
78
+
79
+ 4. Load, read the full file, and then execute `references/init.md` to begin the workflow.
@@ -1,6 +1,8 @@
1
1
  # DO NOT EDIT -- overwritten on every update.
2
2
  #
3
3
  # Workflow customization surface for skf-update-skill.
4
+ # Team overrides: _bmad/custom/skf-update-skill.toml (under {project-root})
5
+ # Personal overrides: _bmad/custom/skf-update-skill.user.toml (under {project-root})
4
6
 
5
7
  [workflow]
6
8
 
@@ -34,11 +36,14 @@ persistent_facts = [
34
36
  "file:{project-root}/**/project-context.md",
35
37
  ]
36
38
 
37
- # --- Optional gate thresholds ---
39
+ # Pipeline-integration hook invoked after report.md §5b writes the result
40
+ # contract (the per-run record plus update-skill-result-latest.json). The
41
+ # command is called as:
42
+ # <on_complete> --result-path=<{forge_version}/update-skill-result-latest.json>
43
+ # Useful for Slack notifications, dashboard ingest, CI hooks, or chaining a
44
+ # downstream skill (audit, export, test). Failures are logged to the run's
45
+ # warnings[] but never fail the workflow.
38
46
  #
39
- # Override the default thresholds used by detect-changes and merge. Empty
40
- # string or commented = use the bundled default (50% deletion ratio,
41
- # 80% rename similarity).
47
+ # Empty string = no-op (default).
42
48
 
43
- deletion_ratio_threshold = ""
44
- rename_similarity_threshold = ""
49
+ on_complete = ""