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
@@ -7,9 +7,41 @@
7
7
  Validates SKILL.md frontmatter, context-snippet.md format, and metadata.json
8
8
  schema against agentskills.io specification. Outputs JSON validation results.
9
9
 
10
+ Two package shapes are supported via --skill-type (default: individual):
11
+ individual — the single-skill schema: SKILL.md body sections
12
+ (Overview/Description/Key Exports/Usage) and metadata.json
13
+ fields (source_repo, stats.*). This is the legacy default and
14
+ is unchanged for existing callers (quick-skill, create-skill).
15
+ stack — the capstone stack schema: skips the individual body/metadata
16
+ checks and instead validates the stack count-equalities
17
+ (library_count vs per-library reference files; integration_count
18
+ vs integration pair files; confidence_distribution sum vs
19
+ library_count), emitted under validation.stack_counts.
20
+
21
+ The --export-gate flag selects a third, self-contained mode used by
22
+ skf-export-skill's publishing gate (load-skill.md §2 + package.md §1-3). It is
23
+ additive and orthogonal to --skill-type: it does NOT touch the individual/stack
24
+ code path above, so existing callers keep byte-identical output. Under the flag
25
+ the script emits the deterministic verdict the export prompt previously derived
26
+ by hand each run:
27
+ - metadata.json required-field presence for the full agentskills.io set
28
+ (name, version, skill_type, source_authority, exports, generation_date,
29
+ confidence_tier) as high-severity issues under validation.metadata.issues;
30
+ - enum-membership checks (skill_type, source_authority, confidence_tier) as
31
+ high-severity issues under validation.metadata.enum_issues;
32
+ - SKILL.md Section 7b (Scripts & Assets) cross-reference against on-disk
33
+ scripts/ and assets/ files, under validation.crossref_7b.{missing,orphans}
34
+ (a §7b-named file absent on disk is high; an on-disk file not named in §7b
35
+ is a low orphan warning);
36
+ - a deterministic export_status ∈ READY / WARNINGS / NOT_READY alongside the
37
+ existing PASS/FAIL result (NOT_READY on any high issue, WARNINGS when only
38
+ medium/low issues remain, READY when clean).
39
+
10
40
  CLI: python3 skf-validate-output.py <skill-package-dir>
11
41
  python3 skf-validate-output.py <skill-package-dir> --generated-by quick-skill
12
42
  python3 skf-validate-output.py <skill-package-dir> --skip-frontmatter
43
+ python3 skf-validate-output.py <skill-package-dir> --skill-type stack
44
+ python3 skf-validate-output.py <skill-package-dir> --export-gate
13
45
  """
14
46
 
15
47
  from __future__ import annotations
@@ -156,13 +188,270 @@ def validate_metadata_json(data, generated_by=None):
156
188
  return issues
157
189
 
158
190
 
159
- def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False):
191
+ # --- Export-gate mode (skf-export-skill publishing gate) -------------------
192
+ #
193
+ # These helpers back the --export-gate flag only. They are additive: nothing
194
+ # above (individual / stack modes) calls them, so existing callers of
195
+ # validate_skill_package() keep byte-identical output.
196
+
197
+ _EXPORT_GATE_ENUMS = {
198
+ "skill_type": ("single", "stack"),
199
+ "source_authority": ("official", "internal", "community"),
200
+ "confidence_tier": ("Quick", "Forge", "Forge+", "Deep"),
201
+ }
202
+
203
+ # Matches a scripts/… or assets/… path token (e.g. `scripts/run.py`). The
204
+ # leading filename char must be alphanumeric/dot/underscore so a bare
205
+ # `scripts/` (the §7b directory note with nothing after the slash) is not
206
+ # captured as a file reference.
207
+ _SECTION_7B_PATH_RE = re.compile(r"(?:scripts|assets)/[A-Za-z0-9._][A-Za-z0-9._/\-]*")
208
+
209
+
210
+ def validate_metadata_export_gate(data):
211
+ """agentskills.io export-gate metadata validation.
212
+
213
+ Returns (required_issues, enum_issues). Required-field presence for the full
214
+ agentskills.io set is high-severity; enum-membership mismatches are
215
+ high-severity. An empty `exports` array is a low warning (matching
216
+ load-skill.md §2's "warn if empty — graceful handling"), not a hard halt.
217
+ """
218
+ required_issues = []
219
+ enum_issues = []
220
+
221
+ # String required fields — high-severity presence checks.
222
+ for field in ("name", "version", "skill_type", "source_authority",
223
+ "generation_date", "confidence_tier"):
224
+ val = data.get(field)
225
+ if not val or not isinstance(val, str):
226
+ required_issues.append({
227
+ "severity": "high",
228
+ "field": field,
229
+ "message": f"{field} missing or not a non-empty string",
230
+ })
231
+
232
+ # exports — must be present as an array (high if absent/not-a-list).
233
+ exports = data.get("exports")
234
+ if not isinstance(exports, list):
235
+ required_issues.append({
236
+ "severity": "high",
237
+ "field": "exports",
238
+ "message": "exports missing or not an array",
239
+ })
240
+ elif len(exports) == 0:
241
+ required_issues.append({
242
+ "severity": "low",
243
+ "field": "exports",
244
+ "message": "exports array is empty",
245
+ })
246
+
247
+ # Enum membership — only when the value is present as a non-empty string
248
+ # (otherwise the required-field check above already fired for it).
249
+ for field, allowed in _EXPORT_GATE_ENUMS.items():
250
+ val = data.get(field)
251
+ if isinstance(val, str) and val and val not in allowed:
252
+ enum_issues.append({
253
+ "severity": "high",
254
+ "field": field,
255
+ "message": f"{field} '{val}' not in {list(allowed)}",
256
+ })
257
+
258
+ return required_issues, enum_issues
259
+
260
+
261
+ def _extract_section_7b_refs(skill_md_text):
262
+ """Extract scripts/… and assets/… paths named in SKILL.md's Section 7b
263
+ (Scripts & Assets). Returns a set of posix path strings.
264
+
265
+ Section 7b is optional (create-skill emits it only when scripts or assets
266
+ are detected), so an absent section yields an empty set. The section is
267
+ located by its heading — a markdown heading whose text mentions both
268
+ "script" and "asset" (the "Scripts & Assets" title) — and the region runs
269
+ until the next heading of the same or higher level.
270
+ """
271
+ if not skill_md_text:
272
+ return set()
273
+
274
+ lines = skill_md_text.split("\n")
275
+ start = None
276
+ heading_level = None
277
+ for i, line in enumerate(lines):
278
+ m = re.match(r"^(#{1,6})\s+(.*)$", line)
279
+ if m:
280
+ text = m.group(2).lower()
281
+ if "script" in text and "asset" in text:
282
+ start = i + 1
283
+ heading_level = len(m.group(1))
284
+ break
285
+
286
+ if start is None:
287
+ return set()
288
+
289
+ end = len(lines)
290
+ for j in range(start, len(lines)):
291
+ m = re.match(r"^(#{1,6})\s+", lines[j])
292
+ if m and len(m.group(1)) <= heading_level:
293
+ end = j
294
+ break
295
+
296
+ region = "\n".join(lines[start:end])
297
+ refs = set()
298
+ for match in _SECTION_7B_PATH_RE.findall(region):
299
+ refs.add(match.rstrip("./-"))
300
+ return refs
301
+
302
+
303
+ def crossref_section_7b(skill_md_text, pkg_dir):
304
+ """Cross-reference SKILL.md Section 7b against on-disk scripts/ and assets/.
305
+
306
+ Returns (missing, orphans) — both sorted lists of posix relative paths
307
+ (e.g. "scripts/run.py"). `missing` = paths named under Section 7b that do
308
+ not exist on disk (a broken manifest — the caller treats these as high).
309
+ `orphans` = scripts/assets files present on disk but not named under
310
+ Section 7b (a low warning). Deterministic: same inputs → same sorted lists.
311
+ """
312
+ pkg_dir = Path(pkg_dir)
313
+ referenced = _extract_section_7b_refs(skill_md_text)
314
+
315
+ on_disk = set()
316
+ for sub in ("scripts", "assets"):
317
+ d = pkg_dir / sub
318
+ if d.is_dir():
319
+ for p in d.rglob("*"):
320
+ if p.is_file():
321
+ on_disk.add(p.relative_to(pkg_dir).as_posix())
322
+
323
+ missing = sorted(referenced - on_disk)
324
+ orphans = sorted(on_disk - referenced)
325
+ return missing, orphans
326
+
327
+
328
+ def validate_stack_counts(skill_dir, meta):
329
+ """Validate stack-package count equalities against on-disk reference files.
330
+
331
+ Deterministic integer arithmetic the validate.md prompt previously did by
332
+ hand each run:
333
+ - library_count (metadata) == number of per-library reference files
334
+ (references/*.md, excluding the integrations/ subdir and stack-catalog.md)
335
+ - integration_count (metadata) == number of integration pair files
336
+ (references/integrations/*.md)
337
+ - sum(confidence_distribution over t1, t1_low, t2, t3) == library_count
338
+
339
+ Returns (issues, observed) where issues use the same {severity, field,
340
+ message} shape as the other validators and observed carries the exact
341
+ counts so the prompt can echo numbers without recomputing them.
342
+ """
343
+ skill_dir = Path(skill_dir)
344
+ references = skill_dir / "references"
345
+
346
+ # Per-library reference files: top-level references/*.md.
347
+ # glob("*.md") is non-recursive, so references/integrations/*.md is
348
+ # excluded automatically; stack-catalog.md is excluded by name.
349
+ ref_file_count = 0
350
+ if references.is_dir():
351
+ for p in sorted(references.glob("*.md")):
352
+ if p.name == "stack-catalog.md":
353
+ continue
354
+ ref_file_count += 1
355
+
356
+ # Integration pair files: references/integrations/*.md.
357
+ integrations_dir = references / "integrations"
358
+ pair_file_count = 0
359
+ if integrations_dir.is_dir():
360
+ pair_file_count = sum(1 for _ in integrations_dir.glob("*.md"))
361
+
362
+ library_count_meta = meta.get("library_count")
363
+ integration_count_meta = meta.get("integration_count")
364
+
365
+ dist = meta.get("confidence_distribution")
366
+ confidence_sum = None
367
+ if isinstance(dist, dict):
368
+ confidence_sum = 0
369
+ for key in ("t1", "t1_low", "t2", "t3"):
370
+ val = dist.get(key, 0)
371
+ if isinstance(val, (int, float)) and not isinstance(val, bool):
372
+ confidence_sum += val
373
+
374
+ observed = {
375
+ "library_count_meta": library_count_meta,
376
+ "ref_file_count": ref_file_count,
377
+ "integration_count_meta": integration_count_meta,
378
+ "pair_file_count": pair_file_count,
379
+ "confidence_sum": confidence_sum,
380
+ }
381
+
382
+ def _is_int(v):
383
+ return isinstance(v, int) and not isinstance(v, bool)
384
+
385
+ issues = []
386
+
387
+ # library_count vs per-library reference file count
388
+ if _is_int(library_count_meta):
389
+ if library_count_meta != ref_file_count:
390
+ issues.append({
391
+ "severity": "medium",
392
+ "field": "library_count",
393
+ "message": f"library_count ({library_count_meta}) does not match per-library reference file count ({ref_file_count})",
394
+ })
395
+ else:
396
+ issues.append({
397
+ "severity": "medium",
398
+ "field": "library_count",
399
+ "message": "library_count missing or not an integer",
400
+ })
401
+
402
+ # integration_count vs integration pair file count
403
+ if _is_int(integration_count_meta):
404
+ if integration_count_meta != pair_file_count:
405
+ issues.append({
406
+ "severity": "medium",
407
+ "field": "integration_count",
408
+ "message": f"integration_count ({integration_count_meta}) does not match integration pair file count ({pair_file_count})",
409
+ })
410
+ else:
411
+ issues.append({
412
+ "severity": "medium",
413
+ "field": "integration_count",
414
+ "message": "integration_count missing or not an integer",
415
+ })
416
+
417
+ # confidence_distribution sum vs library_count
418
+ if confidence_sum is None:
419
+ issues.append({
420
+ "severity": "medium",
421
+ "field": "confidence_distribution",
422
+ "message": "confidence_distribution missing or not an object with t1/t1_low/t2/t3 keys",
423
+ })
424
+ elif _is_int(library_count_meta) and confidence_sum != library_count_meta:
425
+ issues.append({
426
+ "severity": "medium",
427
+ "field": "confidence_distribution",
428
+ "message": f"confidence_distribution sum ({confidence_sum}) does not match library_count ({library_count_meta})",
429
+ })
430
+
431
+ return issues, observed
432
+
433
+
434
+ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False, skill_type="individual", export_gate=False):
160
435
  """Validate a complete skill package directory.
161
436
 
162
437
  When `skip_frontmatter` is True, the SKILL.md frontmatter pass is omitted —
163
438
  intended for callers that already validated frontmatter via skill-check or
164
439
  skf-validate-frontmatter.py and only want body / snippet / metadata checks.
440
+
441
+ `skill_type` selects the package schema (default "individual", unchanged for
442
+ existing callers). "stack" skips the individual-skill body-structure and
443
+ metadata-schema passes (which assume the single-skill shape) and instead
444
+ runs validate_stack_counts, emitting validation.stack_counts.{issues,observed}.
445
+ Frontmatter and context-snippet checks are shape-agnostic and run for both.
446
+
447
+ When `export_gate` is True, a self-contained export-gate validation runs
448
+ instead (see _validate_export_gate). This branch is additive: the
449
+ individual/stack code below is untouched, so callers that never pass
450
+ export_gate=True get byte-identical output.
165
451
  """
452
+ if export_gate:
453
+ return _validate_export_gate(skill_dir)
454
+
166
455
  skill_dir = Path(skill_dir)
167
456
  skill_name = skill_dir.name
168
457
 
@@ -195,8 +484,15 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
195
484
  else:
196
485
  fm_issues = validate_frontmatter(content, skill_name)
197
486
  fm_section = fm_issues
198
- body_issues = validate_body_structure(content)
199
- result["validation"]["skill_md"] = {"frontmatter": fm_section, "body": body_issues}
487
+ if skill_type == "stack":
488
+ # Individual-skill body sections (Overview/Description/Key Exports/
489
+ # Usage) do not apply to a stack capstone — checked in validate.md §4.
490
+ body_section = {"skipped": "stack-type: individual body-structure check not applicable"}
491
+ body_issues = []
492
+ else:
493
+ body_issues = validate_body_structure(content)
494
+ body_section = body_issues
495
+ result["validation"]["skill_md"] = {"frontmatter": fm_section, "body": body_section}
200
496
  for issue in fm_issues + body_issues:
201
497
  result["summary"]["total_issues"] += 1
202
498
  result["summary"]["by_severity"][issue["severity"]] += 1
@@ -223,17 +519,31 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
223
519
  try:
224
520
  with open(meta_path, encoding="utf-8") as f:
225
521
  meta = json.load(f)
226
- meta_issues = validate_metadata_json(meta, generated_by)
227
- result["validation"]["metadata"] = {"issues": meta_issues}
228
- for issue in meta_issues:
229
- result["summary"]["total_issues"] += 1
230
- result["summary"]["by_severity"][issue["severity"]] += 1
522
+ if skill_type == "stack":
523
+ # Individual-skill metadata schema (source_repo, stats.*) does not
524
+ # apply to a stack capstone; validate the stack count-equalities.
525
+ result["validation"]["metadata"] = {"skipped": "stack-type: individual metadata schema not checked (see validation.stack_counts)"}
526
+ stack_issues, observed = validate_stack_counts(skill_dir, meta)
527
+ result["validation"]["stack_counts"] = {"issues": stack_issues, "observed": observed}
528
+ for issue in stack_issues:
529
+ result["summary"]["total_issues"] += 1
530
+ result["summary"]["by_severity"][issue["severity"]] += 1
531
+ else:
532
+ meta_issues = validate_metadata_json(meta, generated_by)
533
+ result["validation"]["metadata"] = {"issues": meta_issues}
534
+ for issue in meta_issues:
535
+ result["summary"]["total_issues"] += 1
536
+ result["summary"]["by_severity"][issue["severity"]] += 1
231
537
  except json.JSONDecodeError as e:
232
538
  result["validation"]["metadata"] = {"error": f"JSON parse error: {e}"}
233
539
  result["summary"]["total_issues"] += 1
234
540
  result["summary"]["by_severity"]["high"] += 1
541
+ if skill_type == "stack":
542
+ result["validation"]["stack_counts"] = {"skipped": f"metadata.json parse error: {e}"}
235
543
  else:
236
544
  result["validation"]["metadata"] = {"skipped": "metadata.json not found"}
545
+ if skill_type == "stack":
546
+ result["validation"]["stack_counts"] = {"skipped": "metadata.json not found"}
237
547
 
238
548
  # Overall pass/fail
239
549
  result["result"] = "PASS" if result["summary"]["by_severity"]["high"] == 0 else "FAIL"
@@ -241,16 +551,125 @@ def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False)
241
551
  return result
242
552
 
243
553
 
554
+ def _validate_export_gate(skill_dir):
555
+ """Export-gate validation for skf-export-skill's publishing gate.
556
+
557
+ Self-contained: does not run the individual/stack passes. Emits the
558
+ deterministic verdict load-skill.md §2 and package.md §1-3 previously
559
+ derived in-prompt — SKILL.md presence/non-emptiness, metadata.json valid
560
+ JSON + required-field presence + enum membership, and the SKILL.md
561
+ Section 7b <-> on-disk scripts/assets cross-reference — plus a deterministic
562
+ export_status ∈ READY / WARNINGS / NOT_READY.
563
+ """
564
+ skill_dir = Path(skill_dir)
565
+ skill_name = skill_dir.name
566
+
567
+ result = {
568
+ "status": "ok",
569
+ "mode": "export-gate",
570
+ "skill_dir": str(skill_dir),
571
+ "skill_name": skill_name,
572
+ "files_found": {},
573
+ "validation": {},
574
+ "summary": {"total_issues": 0, "by_severity": {"high": 0, "medium": 0, "low": 0}},
575
+ }
576
+
577
+ def _record(issues):
578
+ for issue in issues:
579
+ result["summary"]["total_issues"] += 1
580
+ result["summary"]["by_severity"][issue["severity"]] += 1
581
+
582
+ skill_md_path = skill_dir / "SKILL.md"
583
+ meta_path = skill_dir / "metadata.json"
584
+ result["files_found"]["SKILL.md"] = skill_md_path.exists()
585
+ result["files_found"]["metadata.json"] = meta_path.exists()
586
+
587
+ # 1. SKILL.md must exist and be non-empty.
588
+ skill_md_text = ""
589
+ skill_md_issues = []
590
+ if not skill_md_path.exists():
591
+ skill_md_issues.append({"severity": "high", "field": "SKILL.md", "message": "SKILL.md not found"})
592
+ else:
593
+ skill_md_text = skill_md_path.read_text(encoding="utf-8")
594
+ if not skill_md_text.strip():
595
+ skill_md_issues.append({"severity": "high", "field": "SKILL.md", "message": "SKILL.md is empty"})
596
+ result["validation"]["skill_md"] = {"issues": skill_md_issues}
597
+ _record(skill_md_issues)
598
+
599
+ # 2. metadata.json must exist, parse as JSON, and satisfy the required-field
600
+ # presence + enum-membership contract.
601
+ if not meta_path.exists():
602
+ meta_issues = [{"severity": "high", "field": "metadata.json", "message": "metadata.json not found"}]
603
+ result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
604
+ _record(meta_issues)
605
+ else:
606
+ try:
607
+ with open(meta_path, encoding="utf-8") as f:
608
+ meta = json.load(f)
609
+ except json.JSONDecodeError as e:
610
+ meta_issues = [{"severity": "high", "field": "metadata.json", "message": f"JSON parse error: {e}"}]
611
+ result["validation"]["metadata"] = {"issues": meta_issues, "enum_issues": []}
612
+ _record(meta_issues)
613
+ else:
614
+ required_issues, enum_issues = validate_metadata_export_gate(meta)
615
+ result["validation"]["metadata"] = {"issues": required_issues, "enum_issues": enum_issues}
616
+ _record(required_issues)
617
+ _record(enum_issues)
618
+
619
+ # 3. SKILL.md Section 7b <-> on-disk scripts/assets cross-reference.
620
+ missing, orphans = crossref_section_7b(skill_md_text, skill_dir)
621
+ crossref_issues = []
622
+ for path in missing:
623
+ crossref_issues.append({
624
+ "severity": "high",
625
+ "field": "crossref_7b",
626
+ "message": f"SKILL.md Section 7b references '{path}' but it is absent on disk",
627
+ })
628
+ for path in orphans:
629
+ crossref_issues.append({
630
+ "severity": "low",
631
+ "field": "crossref_7b",
632
+ "message": f"'{path}' present on disk but not referenced in SKILL.md Section 7b (orphan)",
633
+ })
634
+ result["validation"]["crossref_7b"] = {
635
+ "missing": missing,
636
+ "orphans": orphans,
637
+ "issues": crossref_issues,
638
+ }
639
+ _record(crossref_issues)
640
+
641
+ # Deterministic export_status + PASS/FAIL result.
642
+ by_sev = result["summary"]["by_severity"]
643
+ if by_sev["high"] > 0:
644
+ result["export_status"] = "NOT_READY"
645
+ elif by_sev["medium"] > 0 or by_sev["low"] > 0:
646
+ result["export_status"] = "WARNINGS"
647
+ else:
648
+ result["export_status"] = "READY"
649
+ result["result"] = "PASS" if by_sev["high"] == 0 else "FAIL"
650
+
651
+ return result
652
+
653
+
244
654
  if __name__ == "__main__":
245
655
  if len(sys.argv) < 2:
246
656
  print(
247
657
  "Usage: python3 skf-validate-output.py <skill-package-dir> "
248
- "[--generated-by <generator>] [--skip-frontmatter]",
658
+ "[--generated-by <generator>] [--skip-frontmatter] "
659
+ "[--skill-type {individual,stack}] [--export-gate]",
249
660
  file=sys.stderr,
250
661
  )
251
662
  sys.exit(1)
252
663
 
253
664
  pkg_dir = sys.argv[1]
665
+
666
+ # --export-gate is a distinct, self-contained mode (skf-export-skill's
667
+ # publishing gate). It ignores the individual/stack flags below.
668
+ if "--export-gate" in sys.argv:
669
+ result = validate_skill_package(pkg_dir, export_gate=True)
670
+ print(json.dumps(result, indent=2))
671
+ sys.exit(0 if result["result"] == "PASS" else 1)
672
+
254
673
  gen_by = None
255
674
  if "--generated-by" in sys.argv:
256
675
  idx = sys.argv.index("--generated-by")
@@ -259,6 +678,20 @@ if __name__ == "__main__":
259
678
 
260
679
  skip_fm = "--skip-frontmatter" in sys.argv
261
680
 
262
- result = validate_skill_package(pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm)
681
+ skill_type = "individual"
682
+ if "--skill-type" in sys.argv:
683
+ idx = sys.argv.index("--skill-type")
684
+ if idx + 1 < len(sys.argv):
685
+ skill_type = sys.argv[idx + 1]
686
+ if skill_type not in ("individual", "stack"):
687
+ print(
688
+ f"Error: --skill-type must be 'individual' or 'stack', got: {skill_type}",
689
+ file=sys.stderr,
690
+ )
691
+ sys.exit(2)
692
+
693
+ result = validate_skill_package(
694
+ pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm, skill_type=skill_type
695
+ )
263
696
  print(json.dumps(result, indent=2))
264
697
  sys.exit(0 if result["result"] == "PASS" else 1)
@@ -5,7 +5,7 @@
5
5
  """SKF Validate Pins — resolve and validate version pins for a GitHub repository.
6
6
 
7
7
  Shared pin validation script consumed by skf-analyze-source (AN auto-scope,
8
- deepwiki --pin) and campaign workflows. Validates that a user-supplied --pin
8
+ forge-auto --pin) and campaign workflows. Validates that a user-supplied --pin
9
9
  resolves to an existing tag or branch, or auto-resolves the latest release tag
10
10
  when no --pin is provided.
11
11
 
@@ -17,7 +17,7 @@ Tag matching priority mirrors source-resolution-protocols.md:
17
17
  4. Crate prefix: {name}/v{pin}, {name}/{pin}, {name}-v{pin}
18
18
 
19
19
  CLI:
20
- uv run python src/shared/scripts/skf-validate-pins.py \\
20
+ uv run src/shared/scripts/skf-validate-pins.py \\
21
21
  --repo-url <url> [--pin <version>] [--format tag|branch|any]
22
22
 
23
23
  Input:
@@ -51,7 +51,7 @@ import sys
51
51
  from typing import Any, Dict, List, Optional, Tuple
52
52
 
53
53
  _GITHUB_URL_RE = re.compile(
54
- r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s.]+?)(?:\.git)?/?$",
54
+ r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s]+?)(?:\.git)?/?$",
55
55
  re.IGNORECASE,
56
56
  )
57
57