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,461 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # dependencies = []
5
+ # ///
6
+ """SKF Shard Body — deterministic auto-shard of an oversized SKILL.md body.
7
+
8
+ Replaces the in-prompt line-counting / boundary-detection / size-sort /
9
+ file-write / blockquote-replacement surgery that `skf-create-skill`'s
10
+ `references/step-auto-shard.md` §1–§5 and `references/validate.md` §4
11
+ otherwise perform by hand (LLM line-counting is the least reliable
12
+ deterministic op — it silently ships an over-budget body or wrongly HALTs).
13
+
14
+ What it does (single positional invocation):
15
+
16
+ 1. Split frontmatter (leading `---` … `---`) from body. Frontmatter is
17
+ NEVER modified.
18
+ 2. Count body lines between the frontmatter close and EOF, excluding
19
+ trailing blank lines → `body_lines_before`.
20
+ 3. If `body_lines_before` <= `--budget` (default 400): emit
21
+ `action: "skip"`, write nothing.
22
+ 4. Otherwise enumerate Tier-2 sections — level-2 headings whose text
23
+ starts with `Full` (`## Full API Reference`, `## Full Type
24
+ Definitions`, …). Each section spans its heading through the line
25
+ before the next level-1/level-2 heading (or EOF).
26
+ 5. Sort candidates by line count descending (largest first, stable) and
27
+ greedily extract the largest, re-counting after each, stopping the
28
+ moment the body fits under budget. If selective extraction of every
29
+ Tier-2 section still cannot fit, extract them all and report
30
+ `under_budget: false` (the caller then trims Tier-1 by editing
31
+ judgment — that fallback stays in the prompt).
32
+ 6. Each extracted section is written to `references/{kebab(heading)}.md`
33
+ (heading preserved) via `skf-atomic-write.py`, and the section in
34
+ SKILL.md is replaced by a `> See [Title](references/…md)` blockquote.
35
+ The rewritten SKILL.md is itself written via `skf-atomic-write.py`.
36
+ 7. Verify every Tier-1 heading that was inline before extraction is still
37
+ inline (`tier1_preserved`, with `tier1_missing[]`), and every emitted
38
+ blockquote resolves to a written reference file (`xref_ok`).
39
+
40
+ Deterministic: same input + budget → same JSON and same on-disk result.
41
+
42
+ Output JSON (stdout, or `-o`):
43
+ {
44
+ "action": "skip" | "shard",
45
+ "body_lines_before": <int>,
46
+ "body_lines_after": <int>,
47
+ "sections_extracted": [
48
+ {"heading": "Full API Reference",
49
+ "file": "references/full-api-reference.md",
50
+ "lines": <int>}
51
+ ],
52
+ "tier1_preserved": <bool>,
53
+ "tier1_missing": ["Overview", ...],
54
+ "xref_ok": <bool>,
55
+ "under_budget": <bool>,
56
+ "dry_run": <bool>,
57
+ "budget": <int>
58
+ }
59
+
60
+ Heading parsing reuses `skf-scan-skill-md-structure.py`'s `_HEADING_RE`
61
+ (single source of truth for what counts as a heading), imported from the
62
+ adjacent script; a local copy of the same pattern is the fallback if the
63
+ sibling is unavailable.
64
+
65
+ Exit codes:
66
+ 0 — analysis succeeded (skip, shard, or a tier1-violation report the
67
+ caller HALTs on). Emitting a valid report is success; the caller
68
+ gates on `tier1_preserved` / `xref_ok`.
69
+ 1 — user error (file not found, unreadable, bad frontmatter).
70
+ 2 — operation failure (a reference/SKILL.md write could not complete).
71
+ """
72
+
73
+ from __future__ import annotations
74
+
75
+ import argparse
76
+ import importlib.util
77
+ import json
78
+ import re
79
+ import subprocess
80
+ import sys
81
+ from pathlib import Path
82
+
83
+
84
+ # --------------------------------------------------------------------------
85
+ # Heading regex — reuse the sibling scanner's single source of truth
86
+ # --------------------------------------------------------------------------
87
+
88
+
89
+ def _load_heading_re() -> "re.Pattern[str]":
90
+ """Import `_HEADING_RE` from the adjacent structure-scanner script.
91
+
92
+ Both scripts install into the same directory (dev `src/shared/scripts/`
93
+ and installed `_bmad/skf/shared/scripts/`), so the sibling is always
94
+ adjacent. Fall back to an identical literal if the import fails, so the
95
+ script still runs standalone (graceful degradation).
96
+ """
97
+ sibling = Path(__file__).resolve().parent / "skf-scan-skill-md-structure.py"
98
+ try:
99
+ spec = importlib.util.spec_from_file_location(
100
+ "skf_scan_skill_md_structure", sibling
101
+ )
102
+ if spec is None or spec.loader is None:
103
+ raise ImportError("no import spec")
104
+ mod = importlib.util.module_from_spec(spec)
105
+ spec.loader.exec_module(mod)
106
+ return mod._HEADING_RE # type: ignore[attr-defined]
107
+ except Exception:
108
+ return re.compile(r"^\s*(#+)\s+(.*?)\s*$")
109
+
110
+
111
+ _HEADING_RE = _load_heading_re()
112
+
113
+
114
+ # --------------------------------------------------------------------------
115
+ # Canonical Tier-1 headings (mirror step-auto-shard.md §3). Any that were
116
+ # inline before extraction must remain inline after.
117
+ # --------------------------------------------------------------------------
118
+
119
+
120
+ TIER1_HEADINGS: list[str] = [
121
+ "Overview",
122
+ "Quick Start",
123
+ "Common Workflows",
124
+ "Key API Summary",
125
+ "Component Catalog",
126
+ "Migration & Deprecation Warnings",
127
+ "Key Types",
128
+ "Architecture at a Glance",
129
+ "CLI",
130
+ "Scripts & Assets",
131
+ "Manual Sections",
132
+ ]
133
+
134
+
135
+ # --------------------------------------------------------------------------
136
+ # Parsing helpers
137
+ # --------------------------------------------------------------------------
138
+
139
+
140
+ class Section:
141
+ """A Tier-2 (`## Full …`) section as a half-open line range [start, end)."""
142
+
143
+ __slots__ = ("start", "end", "heading", "kebab")
144
+
145
+ def __init__(self, start: int, end: int, heading: str) -> None:
146
+ self.start = start
147
+ self.end = end
148
+ self.heading = heading
149
+ self.kebab = kebab_case(heading)
150
+
151
+ @property
152
+ def lines(self) -> int:
153
+ return self.end - self.start
154
+
155
+ @property
156
+ def blockquote(self) -> str:
157
+ return f"> See [{self.heading}](references/{self.kebab}.md)"
158
+
159
+ @property
160
+ def ref_rel(self) -> str:
161
+ return f"references/{self.kebab}.md"
162
+
163
+
164
+ def kebab_case(text: str) -> str:
165
+ """Lowercase, collapse non-alphanumeric runs to single hyphens, trim."""
166
+ return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
167
+
168
+
169
+ def split_frontmatter(text: str) -> tuple[list[str], list[str]]:
170
+ """Return (frontmatter_lines, body_lines).
171
+
172
+ Frontmatter is a leading `---` … `---` block. `frontmatter_lines`
173
+ includes both fences. When no frontmatter is present, it is empty and
174
+ the whole file is the body.
175
+ """
176
+ lines = text.splitlines()
177
+ if lines and lines[0].strip() == "---":
178
+ for i in range(1, len(lines)):
179
+ if lines[i].strip() == "---":
180
+ return lines[: i + 1], lines[i + 1 :]
181
+ return [], lines
182
+
183
+
184
+ def count_body_lines(body: list[str]) -> int:
185
+ """Number of body lines excluding trailing blank lines."""
186
+ end = len(body)
187
+ while end > 0 and body[end - 1].strip() == "":
188
+ end -= 1
189
+ return end
190
+
191
+
192
+ def _headings(body: list[str]) -> list[tuple[int, int, str]]:
193
+ """Return [(line_index, level, heading_text)] for every heading line."""
194
+ out: list[tuple[int, int, str]] = []
195
+ for idx, line in enumerate(body):
196
+ m = _HEADING_RE.match(line)
197
+ if m:
198
+ out.append((idx, len(m.group(1)), m.group(2).strip()))
199
+ return out
200
+
201
+
202
+ def find_full_sections(body: list[str]) -> list[Section]:
203
+ """Enumerate Tier-2 `## Full …` sections in document order.
204
+
205
+ A section spans its `## Full …` heading through the line before the next
206
+ level-1 or level-2 heading (or EOF), so nested `###` sub-headings stay
207
+ with their section.
208
+ """
209
+ heads = _headings(body)
210
+ sections: list[Section] = []
211
+ for hi, (idx, level, htext) in enumerate(heads):
212
+ if level != 2:
213
+ continue
214
+ if not (htext == "Full" or htext.startswith("Full ")):
215
+ continue
216
+ end = len(body)
217
+ for nidx, nlevel, _ in heads[hi + 1 :]:
218
+ if nlevel <= 2:
219
+ end = nidx
220
+ break
221
+ sections.append(Section(idx, end, htext))
222
+ return sections
223
+
224
+
225
+ def present_tier1(body: list[str]) -> set[str]:
226
+ """Set of canonical Tier-1 headings present as level-2 headings."""
227
+ canonical = set(TIER1_HEADINGS)
228
+ found: set[str] = set()
229
+ for _, level, htext in _headings(body):
230
+ if level == 2 and htext in canonical:
231
+ found.add(htext)
232
+ return found
233
+
234
+
235
+ def rebuild_body(body: list[str], selected: list[Section]) -> list[str]:
236
+ """Replace each selected section's line range with its blockquote."""
237
+ by_start = {s.start: s for s in selected}
238
+ out: list[str] = []
239
+ i = 0
240
+ n = len(body)
241
+ while i < n:
242
+ sec = by_start.get(i)
243
+ if sec is not None:
244
+ out.append(sec.blockquote)
245
+ i = sec.end
246
+ else:
247
+ out.append(body[i])
248
+ i += 1
249
+ return out
250
+
251
+
252
+ def select_sections(
253
+ body: list[str], sections: list[Section], budget: int
254
+ ) -> tuple[list[Section], list[str], int]:
255
+ """Greedily extract largest-first until the body fits, or all are taken.
256
+
257
+ Returns (selected, new_body, body_lines_after). `sorted(reverse=True)` is
258
+ stable, so equal-size sections keep document order — deterministic.
259
+ """
260
+ candidates = sorted(sections, key=lambda s: s.lines, reverse=True)
261
+ selected: list[Section] = []
262
+ new_body = body
263
+ after = count_body_lines(body)
264
+ for sec in candidates:
265
+ selected.append(sec)
266
+ new_body = rebuild_body(body, selected)
267
+ after = count_body_lines(new_body)
268
+ if after <= budget:
269
+ break
270
+ return selected, new_body, after
271
+
272
+
273
+ # --------------------------------------------------------------------------
274
+ # Writing (routed through skf-atomic-write.py)
275
+ # --------------------------------------------------------------------------
276
+
277
+
278
+ def _atomic_write(target: Path, content: str) -> None:
279
+ """Write `content` to `target` atomically via the shared atomic-write helper."""
280
+ helper = Path(__file__).resolve().parent / "skf-atomic-write.py"
281
+ proc = subprocess.run(
282
+ [sys.executable, str(helper), "write", "--target", str(target)],
283
+ input=content.encode("utf-8"),
284
+ capture_output=True,
285
+ )
286
+ if proc.returncode != 0:
287
+ detail = proc.stderr.decode("utf-8", "replace").strip() or proc.stdout.decode(
288
+ "utf-8", "replace"
289
+ ).strip()
290
+ raise OSError(f"atomic write failed for {target}: {detail}")
291
+
292
+
293
+ def _section_content(body: list[str], sec: Section) -> str:
294
+ """Section text (heading preserved), trailing blanks trimmed, one newline."""
295
+ chunk = body[sec.start : sec.end]
296
+ end = len(chunk)
297
+ while end > 0 and chunk[end - 1].strip() == "":
298
+ end -= 1
299
+ return "\n".join(chunk[:end]) + "\n"
300
+
301
+
302
+ # --------------------------------------------------------------------------
303
+ # CLI
304
+ # --------------------------------------------------------------------------
305
+
306
+
307
+ def _emit(payload: dict, out_path: str | None) -> None:
308
+ text = json.dumps(payload, indent=2) + "\n"
309
+ if out_path:
310
+ Path(out_path).write_text(text, encoding="utf-8")
311
+ else:
312
+ sys.stdout.write(text)
313
+
314
+
315
+ def _cmd_shard(args: argparse.Namespace) -> int:
316
+ skill_md = Path(args.skill_md)
317
+ if not skill_md.is_file():
318
+ print(f"error: file not found: {skill_md}", file=sys.stderr)
319
+ return 1
320
+ try:
321
+ text = skill_md.read_text(encoding="utf-8")
322
+ except OSError as exc:
323
+ print(f"error: cannot read {skill_md}: {exc}", file=sys.stderr)
324
+ return 1
325
+
326
+ budget: int = args.budget
327
+ dry_run: bool = args.dry_run
328
+
329
+ frontmatter, body = split_frontmatter(text)
330
+ body_lines_before = count_body_lines(body)
331
+ tier1_before = present_tier1(body)
332
+
333
+ if args.verbose:
334
+ print(
335
+ f"skf-shard-body: body_lines_before={body_lines_before} budget={budget}",
336
+ file=sys.stderr,
337
+ )
338
+
339
+ # --- Skip path: already within budget -----------------------------------
340
+ if body_lines_before <= budget:
341
+ _emit(
342
+ {
343
+ "action": "skip",
344
+ "body_lines_before": body_lines_before,
345
+ "body_lines_after": body_lines_before,
346
+ "sections_extracted": [],
347
+ "tier1_preserved": True,
348
+ "tier1_missing": [],
349
+ "xref_ok": True,
350
+ "under_budget": True,
351
+ "dry_run": dry_run,
352
+ "budget": budget,
353
+ },
354
+ args.output,
355
+ )
356
+ return 0
357
+
358
+ # --- Shard path ---------------------------------------------------------
359
+ sections = find_full_sections(body)
360
+ selected, new_body, body_lines_after = select_sections(body, sections, budget)
361
+
362
+ tier1_after = present_tier1(new_body)
363
+ tier1_missing = [
364
+ h for h in TIER1_HEADINGS if h in tier1_before and h not in tier1_after
365
+ ]
366
+ tier1_preserved = not tier1_missing
367
+ under_budget = body_lines_after <= budget
368
+
369
+ references_dir = skill_md.parent / "references"
370
+
371
+ # Compute the plan fully before touching disk. Only write when we have a
372
+ # real extraction AND Tier-1 survived — never leave a half-mutated tree.
373
+ should_write = (not dry_run) and bool(selected) and tier1_preserved
374
+ if should_write:
375
+ try:
376
+ for sec in selected:
377
+ _atomic_write(
378
+ references_dir / f"{sec.kebab}.md", _section_content(body, sec)
379
+ )
380
+ new_text = "\n".join(frontmatter + new_body) + "\n"
381
+ _atomic_write(skill_md, new_text)
382
+ except OSError as exc:
383
+ print(f"error: {exc}", file=sys.stderr)
384
+ return 2
385
+
386
+ blockquotes_ok = all(sec.blockquote in new_body for sec in selected)
387
+ if should_write:
388
+ xref_ok = blockquotes_ok and all(
389
+ (references_dir / f"{sec.kebab}.md").is_file() for sec in selected
390
+ )
391
+ else:
392
+ # dry-run, nothing-to-extract, or aborted tier1 violation
393
+ xref_ok = blockquotes_ok and tier1_preserved
394
+
395
+ _emit(
396
+ {
397
+ "action": "shard",
398
+ "body_lines_before": body_lines_before,
399
+ "body_lines_after": body_lines_after,
400
+ "sections_extracted": [
401
+ {"heading": sec.heading, "file": sec.ref_rel, "lines": sec.lines}
402
+ for sec in selected
403
+ ],
404
+ "tier1_preserved": tier1_preserved,
405
+ "tier1_missing": tier1_missing,
406
+ "xref_ok": xref_ok,
407
+ "under_budget": under_budget,
408
+ "dry_run": dry_run,
409
+ "budget": budget,
410
+ },
411
+ args.output,
412
+ )
413
+ return 0
414
+
415
+
416
+ def _build_parser() -> argparse.ArgumentParser:
417
+ parser = argparse.ArgumentParser(
418
+ prog="skf-shard-body",
419
+ description=(
420
+ "Deterministically shard an oversized SKILL.md body: extract the "
421
+ "largest `## Full …` Tier-2 sections to references/ until the body "
422
+ "fits under --budget, replacing each with a cross-reference "
423
+ "blockquote, and emit a JSON report the auto-shard / validate "
424
+ "steps consume instead of counting and splitting by hand."
425
+ ),
426
+ )
427
+ parser.add_argument("skill_md", help="path to the staging SKILL.md file")
428
+ parser.add_argument(
429
+ "--budget",
430
+ type=int,
431
+ default=400,
432
+ help="body line budget below which no shard occurs (default 400)",
433
+ )
434
+ parser.add_argument(
435
+ "--dry-run",
436
+ action="store_true",
437
+ help="compute and report the plan without writing any files",
438
+ )
439
+ parser.add_argument(
440
+ "-o",
441
+ "--output",
442
+ default=None,
443
+ help="write JSON report to this path (default stdout)",
444
+ )
445
+ parser.add_argument(
446
+ "--verbose",
447
+ action="store_true",
448
+ help="emit progress diagnostics to stderr",
449
+ )
450
+ parser.set_defaults(func=_cmd_shard)
451
+ return parser
452
+
453
+
454
+ def main(argv: list[str] | None = None) -> int:
455
+ parser = _build_parser()
456
+ args = parser.parse_args(argv)
457
+ return args.func(args)
458
+
459
+
460
+ if __name__ == "__main__":
461
+ raise SystemExit(main())
@@ -10,12 +10,21 @@ versions via symlinks, and outputs a JSON inventory. Reused by 9+ skills.
10
10
  CLI: python3 skf-skill-inventory.py <skills-output-folder>
11
11
  python3 skf-skill-inventory.py <skills-output-folder> --skill <name>
12
12
  python3 skf-skill-inventory.py <skills-output-folder> --manifest-only
13
+ python3 skf-skill-inventory.py <skills-output-folder> --match-target <url-or-name>
14
+
15
+ The --match-target mode deterministically computes coexistence matches: it
16
+ normalizes scheme / trailing .git / trailing slash, derives the expected kebab
17
+ skill name, compares case-insensitively, and emits a top-level `matches[]`
18
+ array. This replaces the equivalent normalize/derive/compare that a consuming
19
+ prompt would otherwise perform by hand (identical (target, inventory) always
20
+ yields the same match set).
13
21
  """
14
22
 
15
23
  from __future__ import annotations
16
24
 
17
25
  import json
18
26
  import os
27
+ import re
19
28
  import sys
20
29
  from pathlib import Path
21
30
 
@@ -115,7 +124,115 @@ def scan_skill_group(skill_group_dir, skill_name):
115
124
  return entry
116
125
 
117
126
 
118
- def scan_inventory(skills_folder, skill_filter=None, manifest_only=False):
127
+ def normalize_url(value):
128
+ """Normalize a URL / path / source_repo for case-insensitive comparison.
129
+
130
+ Lowercase; strip an ``http://`` / ``https://`` scheme; strip a trailing
131
+ ``.git`` suffix and any trailing slashes. Deterministic and idempotent, so
132
+ ``https://github.com/Foo/Bar.git/`` and ``github.com/foo/bar`` normalize to
133
+ the same value. Mirrors step-auto-scope §0c's URL-match normalization.
134
+ """
135
+ if not value:
136
+ return ""
137
+ s = str(value).strip().lower()
138
+ for scheme in ("https://", "http://"):
139
+ if s.startswith(scheme):
140
+ s = s[len(scheme):]
141
+ break
142
+ s = s.rstrip("/")
143
+ if s.endswith(".git"):
144
+ s = s[:-4]
145
+ s = s.rstrip("/")
146
+ return s
147
+
148
+
149
+ def _kebab(segment):
150
+ """Kebab-case, lowercase a single name segment (§6 skill-name rule).
151
+
152
+ Lowercase, drop a trailing ``.git``, collapse every run of non-alphanumeric
153
+ characters (``.``, ``_``, spaces, ...) to a single hyphen, and strip leading
154
+ / trailing hyphens. ``docs.example.com`` -> ``docs-example-com`` (mirrors
155
+ §0a's dot-to-hyphen), ``Bar`` -> ``bar``, ``my_project`` -> ``my-project``.
156
+ """
157
+ s = str(segment).strip().lower()
158
+ if s.endswith(".git"):
159
+ s = s[:-4]
160
+ s = re.sub(r"[^a-z0-9]+", "-", s)
161
+ return s.strip("-")
162
+
163
+
164
+ def derive_name(target):
165
+ """Derive the expected skill name from a target URL / path / bare name.
166
+
167
+ Mirrors step-auto-scope §6 (repo/package name = the last non-empty path
168
+ segment, kebab-lowercased) and §0a (a bare doc hostname has its dots turned
169
+ into hyphens). Both cases funnel through :func:`_kebab`, so
170
+ ``github.com/x/bar-baz`` -> ``bar-baz`` and ``docs.example.com`` ->
171
+ ``docs-example-com``. Returns "" when no segment can be derived.
172
+ """
173
+ if not target:
174
+ return ""
175
+ s = str(target).strip().lower()
176
+ for scheme in ("https://", "http://"):
177
+ if s.startswith(scheme):
178
+ s = s[len(scheme):]
179
+ break
180
+ s = s.rstrip("/")
181
+ if s.endswith(".git"):
182
+ s = s[:-4]
183
+ s = s.rstrip("/")
184
+ segments = [seg for seg in s.split("/") if seg]
185
+ if not segments:
186
+ return ""
187
+ return _kebab(segments[-1])
188
+
189
+
190
+ def compute_matches(skills, target):
191
+ """Return the deterministic coexistence match set for ``target``.
192
+
193
+ For each inventory skill, a hit fires when either the normalized
194
+ ``metadata.source_repo`` equals the normalized target (URL match) or the
195
+ derived expected name equals the skill's name, case-insensitively (name
196
+ match). Each match entry is
197
+ ``{name, active_version, source_repo, active_path, match_reason}`` where
198
+ ``match_reason`` is ``"url"``, ``"name"``, or ``"both"``.
199
+ """
200
+ norm_target = normalize_url(target)
201
+ derived = derive_name(target)
202
+ matches = []
203
+ for entry in skills:
204
+ source_repo = None
205
+ meta = entry.get("metadata")
206
+ if meta:
207
+ source_repo = meta.get("source_repo")
208
+ url_match = (
209
+ bool(source_repo)
210
+ and norm_target != ""
211
+ and normalize_url(source_repo) == norm_target
212
+ )
213
+ name_match = (
214
+ bool(derived)
215
+ and str(entry.get("name") or "").strip().lower() == derived
216
+ )
217
+ if not (url_match or name_match):
218
+ continue
219
+ if url_match and name_match:
220
+ reason = "both"
221
+ elif url_match:
222
+ reason = "url"
223
+ else:
224
+ reason = "name"
225
+ matches.append({
226
+ "name": entry.get("name"),
227
+ "active_version": entry.get("active_version"),
228
+ "source_repo": source_repo,
229
+ "active_path": entry.get("active_path"),
230
+ "match_reason": reason,
231
+ })
232
+ return matches
233
+
234
+
235
+ def scan_inventory(skills_folder, skill_filter=None, manifest_only=False, match_target=None):
119
236
  """Scan the skills output folder and produce an inventory."""
120
237
  skills_dir = Path(skills_folder)
121
238
 
@@ -188,23 +305,42 @@ def scan_inventory(skills_folder, skill_filter=None, manifest_only=False):
188
305
  result["summary"]["with_metadata"] = sum(1 for s in result["skills"] if s["metadata"])
189
306
  result["summary"]["with_provenance"] = sum(1 for s in result["skills"] if s["has_provenance_map"])
190
307
 
308
+ # Coexistence matching (opt-in via --match-target; additive top-level key).
309
+ if match_target is not None:
310
+ result["matches"] = compute_matches(result["skills"], match_target)
311
+
191
312
  return result
192
313
 
193
314
 
194
315
  if __name__ == "__main__":
195
316
  if len(sys.argv) < 2:
196
- print("Usage: python3 skf-skill-inventory.py <skills-output-folder> [--skill <name>] [--manifest-only]", file=sys.stderr)
317
+ print(
318
+ "Usage: python3 skf-skill-inventory.py <skills-output-folder> "
319
+ "[--skill <name>] [--manifest-only] [--match-target <url-or-name>]",
320
+ file=sys.stderr,
321
+ )
197
322
  sys.exit(1)
198
323
 
199
324
  folder = sys.argv[1]
200
325
  skill = None
201
326
  manifest_only = "--manifest-only" in sys.argv
327
+ match_target = None
202
328
 
203
329
  if "--skill" in sys.argv:
204
330
  idx = sys.argv.index("--skill")
205
331
  if idx + 1 < len(sys.argv):
206
332
  skill = sys.argv[idx + 1]
207
333
 
208
- result = scan_inventory(folder, skill_filter=skill, manifest_only=manifest_only)
334
+ if "--match-target" in sys.argv:
335
+ idx = sys.argv.index("--match-target")
336
+ if idx + 1 < len(sys.argv):
337
+ match_target = sys.argv[idx + 1]
338
+
339
+ result = scan_inventory(
340
+ folder,
341
+ skill_filter=skill,
342
+ manifest_only=manifest_only,
343
+ match_target=match_target,
344
+ )
209
345
  print(json.dumps(result, indent=2))
210
346
  sys.exit(0 if result["status"] == "ok" else 1)