bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,472 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF Skills Module: is a repository a skills module, and what does it ship.
6
+
7
+ skf-quick-skill wraps a library's public API, but some repositories ship
8
+ agent skills instead of code: a plain Agent Skills package (folders that
9
+ each hold a SKILL.md), and a BMad module, whose module.yaml or
10
+ module-help.csv adds menu codes. Which folder holds the skills, and which
11
+ folders are skills, has one right answer per file listing, so this helper
12
+ computes it from the listing instead of the model walking the tree, and
13
+ reads the staged SKILL.md frontmatter and module-help.csv (with Python's
14
+ yaml and csv modules) into the envelope skf-extract-public-api.py prints
15
+ for a library.
16
+
17
+ CLI:
18
+ uv run skf-skills-module.py sniff --tree-file <file> [--package-json <file>] \\
19
+ [--scope-hint <folder>] [--fetch-list <file>]
20
+ uv run skf-skills-module.py extract --sniff <file> --source-root <dir> \\
21
+ [--package <file>] [--name <name>]
22
+
23
+ sniff
24
+ Reads the repository's file listing (--tree-file: skf-github-probe.py
25
+ tree output, or any listing skf-detect-language.py's --tree-file reads)
26
+ and, with --package-json, the root package.json staged from it. The
27
+ rules:
28
+ - A skill folder holds its own SKILL.md. A module root is a folder that
29
+ directly holds module.yaml or module-help.csv and sits in no skill
30
+ folder (a skill's own assets/module.yaml makes none).
31
+ - The skill folders of a module root are all the folders below it that
32
+ hold a SKILL.md and sit in no other skill folder. The skill folders
33
+ of any other folder are its direct subfolders that hold one.
34
+ - With --scope-hint, that folder is the only candidate, and it counts
35
+ whenever it has skill folders. Otherwise the candidates are the
36
+ repository root and each top-level folder that has skill folders,
37
+ and a candidate counts only when it is a module root or the root
38
+ ships no code. The root ships no code when it has no package
39
+ manifest (package.json, pyproject.toml, setup.py, setup.cfg,
40
+ Cargo.toml, go.mod, pom.xml, build.gradle, build.gradle.kts, Gemfile
41
+ or a *.csproj), or when its only one is a package.json that declares
42
+ no `main` (or an empty one), `exports`, `bin` or `workspaces` and has
43
+ no index.js or index.ts beside it. A root package.json the call was
44
+ not given, or could not read (no --package-json, no file there, or
45
+ not a JSON object), counts as one that ships code. So a library with
46
+ a manifest and one skill/SKILL.md stays a library.
47
+ - When several candidates count, a module root wins, then the one with
48
+ the most skill folders.
49
+ Prints:
50
+ skills_module whether a candidate counts
51
+ skills_root the folder that holds the skill folders ("." for the
52
+ repository root), else null
53
+ module_root whether skills_root is a module root
54
+ skill_folders its skill folders, in path order
55
+ candidates [{"folder", "module_root", "skill_folders" (a count),
56
+ "counts"}] for every candidate looked at
57
+ suggested_scope when the repository is not a skills module: the
58
+ folder a run with no export should name as a scope to
59
+ re-run with (the module root with the most skill
60
+ folders, else the folder with the most), else null
61
+ root_files the files at the repository root
62
+ readme the root README: README.md, else another root file
63
+ named README in any letter case (a .md one first, then
64
+ by name), else null
65
+ asset_dirs the first 10 folders named scripts, bin, assets,
66
+ templates or schemas (within --scope-hint when given)
67
+ asset_dir_count how many such folders there are
68
+ skill_md_count SKILL.md files anywhere in the listing
69
+ truncated the listing was cut short
70
+ warnings [...]
71
+ --fetch-list writes the files extract reads, one path per line, for
72
+ skf-github-fetch.py --patterns-file: each skill folder's SKILL.md, then
73
+ the module-help.csv directly in the skills root when the listing has one
74
+ (an empty file when the repository is not a skills module).
75
+
76
+ extract
77
+ Reads sniff's output (--sniff) and the files --fetch-list named, staged
78
+ under --source-root laid out as in the repository, and prints
79
+ skf-extract-public-api.py's quick-mode envelope for the module:
80
+ exports one {"name", "type": "skill", "brief_description",
81
+ "source_file"} per skill folder, in sniff's order:
82
+ name from the frontmatter `name` (the folder name when
83
+ it has none), brief_description from its
84
+ `description`, whitespace runs made one space; then one
85
+ {"name", "type": "menu-code", "brief_description",
86
+ "source_file"} per module-help.csv row that has a
87
+ menu-code, in file order: the code, then the row's
88
+ description, display-name and the skill it runs, with
89
+ its action when set
90
+ usage_patterns one {"menu_code", "display_name", "description",
91
+ "skill", "action", "args", "preceded_by",
92
+ "followed_by"} per module-help.csv row (empty fields
93
+ null); a row whose skill is empty or starts with `_`
94
+ (the _meta row) gives neither an export nor a pattern
95
+ confidence "high" when every skill folder's frontmatter gave a
96
+ name and a description, else "medium"
97
+ skills, menu_codes the two counts
98
+ skills_root sniff's
99
+ --package takes skf-extract-public-api.py's output for the module's root
100
+ manifest, whose language, package_name, version, description,
101
+ dependencies and warnings the envelope keeps; without it package_name is
102
+ --name (the repository's name) and the rest is empty. A SKILL.md or
103
+ module-help.csv that is missing or cannot be read is named in
104
+ `warnings`.
105
+
106
+ Exit codes:
107
+ 0 output printed
108
+ 2 usage error: argparse, or an input file that cannot be read, a
109
+ listing that reports a failure, or a --sniff file that is not sniff's
110
+ output ({"status": "error", "message": ...} on stderr)
111
+ """
112
+
113
+ from __future__ import annotations
114
+
115
+ import argparse
116
+ import csv
117
+ import importlib.util
118
+ import io
119
+ import json
120
+ import re
121
+ import sys
122
+ from pathlib import Path
123
+
124
+ import yaml
125
+
126
+ MANIFESTS = frozenset({"package.json", "pyproject.toml", "setup.py", "setup.cfg", "Cargo.toml", "go.mod",
127
+ "pom.xml", "build.gradle", "build.gradle.kts", "Gemfile"})
128
+ MODULE_FILES = ("module.yaml", "module-help.csv")
129
+ ASSET_DIR_NAMES = frozenset({"scripts", "bin", "assets", "templates", "schemas"})
130
+ ASSET_DIR_SHOWN = 10
131
+ ROOT = "."
132
+
133
+
134
+ class UsageError(ValueError):
135
+ """An input the call cannot run with: exit 2."""
136
+
137
+
138
+ def _load(filename: str):
139
+ path = Path(__file__).resolve().parent / filename
140
+ spec = importlib.util.spec_from_file_location(filename[:-3].replace("-", "_"), path)
141
+ if spec is None or spec.loader is None:
142
+ raise ImportError(f"cannot load {path}")
143
+ module = importlib.util.module_from_spec(spec)
144
+ spec.loader.exec_module(module)
145
+ return module
146
+
147
+
148
+ # --------------------------------------------------------------------------
149
+ # sniff: the skills root from the listing
150
+ # --------------------------------------------------------------------------
151
+
152
+
153
+ def _folder(path: str) -> str:
154
+ return path.rpartition("/")[0]
155
+
156
+
157
+ def _ancestors(folder: str) -> list[str]:
158
+ """The folders above `folder`, the root excluded."""
159
+ parts = folder.split("/") if folder else []
160
+ return ["/".join(parts[:i]) for i in range(1, len(parts))]
161
+
162
+
163
+ def _skill_dirs(listing: set[str]) -> set[str]:
164
+ return {_folder(p) for p in listing if p.rpartition("/")[2] == "SKILL.md"}
165
+
166
+
167
+ def _is_module_root(folder: str, listing: set[str], skill_dirs: set[str]) -> bool:
168
+ """Directly holds module.yaml or module-help.csv, and sits in no skill folder."""
169
+ prefix = f"{folder}/" if folder else ""
170
+ holds = any(f"{prefix}{name}" in listing for name in MODULE_FILES)
171
+ in_skill = folder in skill_dirs or any(a in skill_dirs for a in _ancestors(folder))
172
+ return holds and not in_skill
173
+
174
+
175
+ def _skill_folders(folder: str, listing: set[str], skill_dirs: set[str]) -> list[str]:
176
+ prefix = f"{folder}/" if folder else ""
177
+ below = [d for d in skill_dirs if d.startswith(prefix) and d != folder]
178
+ if _is_module_root(folder, listing, skill_dirs):
179
+ return sorted(d for d in below if not any(a in skill_dirs for a in _ancestors(d)))
180
+ return sorted(d for d in below if "/" not in d[len(prefix):])
181
+
182
+
183
+ def _ships_no_code(listing: set[str], package_json: dict | None) -> bool:
184
+ root = {p for p in listing if "/" not in p}
185
+ manifests = {p for p in root if p in MANIFESTS or p.endswith(".csproj")}
186
+ if not manifests:
187
+ return True
188
+ if manifests != {"package.json"} or package_json is None:
189
+ return False
190
+ if package_json.get("main") or any(key in package_json for key in ("exports", "bin", "workspaces")):
191
+ return False
192
+ return not root & {"index.js", "index.ts"}
193
+
194
+
195
+ def _suggested_scope(listing: set[str], skill_dirs: set[str]) -> str | None:
196
+ folders = {a for p in listing for a in [*_ancestors(_folder(p)), _folder(p)] if a}
197
+ scored = [(_is_module_root(f, listing, skill_dirs), len(_skill_folders(f, listing, skill_dirs)), f)
198
+ for f in sorted(folders)]
199
+ scored = [s for s in scored if s[1]]
200
+ return max(scored, key=lambda s: s[:2])[2] if scored else None
201
+
202
+
203
+ def _readme(root_files: list[str]) -> str | None:
204
+ named = [f for f in root_files if f.lower() == "readme" or f.lower().startswith("readme.")]
205
+ if not named:
206
+ return None
207
+ return min(named, key=lambda f: (f != "README.md", f.lower() != "readme.md", not f.lower().endswith(".md"), f))
208
+
209
+
210
+ def _asset_dirs(listing: set[str], scope: str) -> list[str]:
211
+ prefix = f"{scope}/" if scope else ""
212
+ folders = {a for p in listing if p.startswith(prefix) for a in [*_ancestors(_folder(p)), _folder(p)] if a}
213
+ return sorted(f for f in folders if f.startswith(prefix) and f.rpartition("/")[2] in ASSET_DIR_NAMES)
214
+
215
+
216
+ def _shown(folder: str) -> str:
217
+ return folder or ROOT
218
+
219
+
220
+ def sniff(paths: list[str], package_json: dict | None, scope_hint: str | None, truncated: bool = False) -> dict:
221
+ """The skills-module facts of a file listing (see the module docstring)."""
222
+ listing = set(paths)
223
+ skill_dirs = _skill_dirs(listing)
224
+ scope = scope_hint.strip().strip("/") if scope_hint is not None and scope_hint.strip() not in ("", ".", "./") \
225
+ else None
226
+ candidates = []
227
+ if scope is not None:
228
+ found = _skill_folders(scope, listing, skill_dirs)
229
+ candidates.append((_is_module_root(scope, listing, skill_dirs), found, scope, bool(found)))
230
+ else:
231
+ no_code = _ships_no_code(listing, package_json)
232
+ for folder in ["", *sorted({p.split("/")[0] for p in listing if "/" in p})]:
233
+ found = _skill_folders(folder, listing, skill_dirs)
234
+ if not found:
235
+ continue
236
+ module_root = _is_module_root(folder, listing, skill_dirs)
237
+ candidates.append((module_root, found, folder, module_root or no_code))
238
+ counting = [c for c in candidates if c[3]]
239
+ best = max(counting, key=lambda c: (c[0], len(c[1]))) if counting else None
240
+ root_files = sorted(p for p in listing if "/" not in p)
241
+ assets = _asset_dirs(listing, scope or "")
242
+ warnings = []
243
+ if "package.json" in root_files and package_json is None and scope is None and \
244
+ {p for p in root_files if p in MANIFESTS or p.endswith(".csproj")} == {"package.json"}:
245
+ warnings.append("the root package.json was not read (--package-json): it counts as one that ships code")
246
+ if truncated:
247
+ warnings.append("the listing was cut short: a skill folder it lacks is not counted")
248
+ return {
249
+ "skills_module": best is not None,
250
+ "skills_root": _shown(best[2]) if best else None,
251
+ "module_root": bool(best and best[0]),
252
+ "skill_folders": list(best[1]) if best else [],
253
+ "candidates": [{"folder": _shown(c[2]), "module_root": c[0], "skill_folders": len(c[1]), "counts": c[3]}
254
+ for c in candidates],
255
+ "suggested_scope": None if best else _suggested_scope(listing, skill_dirs),
256
+ "root_files": root_files,
257
+ "readme": _readme(root_files),
258
+ "asset_dirs": assets[:ASSET_DIR_SHOWN],
259
+ "asset_dir_count": len(assets),
260
+ "skill_md_count": sum(1 for p in listing if p.rpartition("/")[2] == "SKILL.md"),
261
+ "truncated": truncated,
262
+ "warnings": warnings,
263
+ }
264
+
265
+
266
+ def fetch_list(result: dict, paths: list[str]) -> list[str]:
267
+ """The files extract reads: each skill folder's SKILL.md, then the skills root's module-help.csv."""
268
+ if not result["skills_module"]:
269
+ return []
270
+ files = [f"{folder}/SKILL.md" for folder in result["skill_folders"]]
271
+ root = result["skills_root"]
272
+ csv_path = "module-help.csv" if root == ROOT else f"{root}/module-help.csv"
273
+ if csv_path in set(paths):
274
+ files.append(csv_path)
275
+ return files
276
+
277
+
278
+ # --------------------------------------------------------------------------
279
+ # extract: the envelope from the staged files
280
+ # --------------------------------------------------------------------------
281
+
282
+
283
+ FRONTMATTER_RE = re.compile(r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.S)
284
+
285
+
286
+ def _one_line(value) -> str | None:
287
+ if not isinstance(value, str):
288
+ return None
289
+ text = " ".join(value.split())
290
+ return text or None
291
+
292
+
293
+ def frontmatter(text: str) -> dict:
294
+ """The YAML frontmatter of a SKILL.md, {} when it has none or it is not a mapping."""
295
+ m = FRONTMATTER_RE.match(text.lstrip(""))
296
+ if not m:
297
+ return {}
298
+ try:
299
+ data = yaml.safe_load(m.group(1))
300
+ except yaml.YAMLError:
301
+ return {}
302
+ return data if isinstance(data, dict) else {}
303
+
304
+
305
+ def _read_text(path: Path) -> str | None:
306
+ try:
307
+ return path.read_bytes().decode("utf-8", errors="replace")
308
+ except OSError:
309
+ return None
310
+
311
+
312
+ def _field(row: dict, name: str) -> str | None:
313
+ value = row.get(name)
314
+ return value.strip() or None if isinstance(value, str) else None
315
+
316
+
317
+ def menu_rows(text: str) -> list[dict]:
318
+ """The module-help.csv rows that run a skill: the _meta row and rows with no skill left out."""
319
+ rows = []
320
+ for row in csv.DictReader(io.StringIO(text.lstrip(""))):
321
+ skill = _field(row, "skill")
322
+ if skill is None or skill.startswith("_"):
323
+ continue
324
+ rows.append({"menu_code": _field(row, "menu-code"), "display_name": _field(row, "display-name"),
325
+ "description": _field(row, "description"), "skill": skill, "action": _field(row, "action"),
326
+ "args": _field(row, "args"), "preceded_by": _field(row, "preceded-by"),
327
+ "followed_by": _field(row, "followed-by")})
328
+ return rows
329
+
330
+
331
+ def _menu_description(row: dict) -> str:
332
+ parts = [row["description"] or "", f"({row['display_name']})" if row["display_name"] else "",
333
+ f"runs {row['skill']}" + (f" ({row['action']})" if row["action"] else "")]
334
+ return " ".join(p for p in parts if p)
335
+
336
+
337
+ def extract(sniffed: dict, source_root: Path, package: dict | None = None, name: str | None = None) -> dict:
338
+ """skf-extract-public-api.py's quick-mode envelope for a skills module."""
339
+ package = package or {}
340
+ warnings = [w for w in package.get("warnings", []) if isinstance(w, str)]
341
+ exports, complete = [], True
342
+ for folder in sniffed.get("skill_folders", []):
343
+ rel = f"{folder}/SKILL.md"
344
+ text = _read_text(source_root / rel)
345
+ if text is None:
346
+ warnings.append(f"{rel}: not staged under the source root; the skill is named after its folder")
347
+ meta = frontmatter(text or "")
348
+ skill_name = _one_line(meta.get("name"))
349
+ description = _one_line(meta.get("description"))
350
+ if text is not None and not (skill_name and description):
351
+ warnings.append(f"{rel}: the frontmatter gives no {'name' if not skill_name else 'description'}")
352
+ complete = complete and bool(skill_name and description)
353
+ exports.append({"name": skill_name or folder.rpartition("/")[2], "type": "skill",
354
+ "brief_description": description or "", "source_file": rel})
355
+ root = sniffed.get("skills_root")
356
+ usage_patterns: list[dict] = []
357
+ if root is not None:
358
+ csv_rel = "module-help.csv" if root == ROOT else f"{root}/module-help.csv"
359
+ text = _read_text(source_root / csv_rel)
360
+ if text is not None:
361
+ try:
362
+ usage_patterns = menu_rows(text)
363
+ except csv.Error as e:
364
+ warnings.append(f"{csv_rel}: cannot be read as CSV ({e})")
365
+ for row in usage_patterns:
366
+ if row["menu_code"]:
367
+ exports.append({"name": row["menu_code"], "type": "menu-code",
368
+ "brief_description": _menu_description(row), "source_file": csv_rel})
369
+ skills = sum(1 for e in exports if e["type"] == "skill")
370
+ return {
371
+ "language": package.get("language"),
372
+ "package_name": package.get("package_name") or name,
373
+ "version": package.get("version"),
374
+ "description": package.get("description"),
375
+ "exports": exports,
376
+ "dependencies": package.get("dependencies") or [],
377
+ "usage_patterns": usage_patterns,
378
+ "confidence": "high" if complete and skills else "medium",
379
+ "skills": skills,
380
+ "menu_codes": len(exports) - skills,
381
+ "skills_root": root,
382
+ "warnings": warnings,
383
+ }
384
+
385
+
386
+ # --------------------------------------------------------------------------
387
+ # CLI
388
+ # --------------------------------------------------------------------------
389
+
390
+
391
+ def _read_json(value: str, flag: str) -> dict:
392
+ try:
393
+ with open(value, encoding="utf-8-sig") as fh:
394
+ data = json.load(fh)
395
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
396
+ raise UsageError(f"cannot read {flag} {value}: {getattr(e, 'strerror', None) or e}") from e
397
+ if not isinstance(data, dict):
398
+ raise UsageError(f"{flag} {value} is not a JSON object")
399
+ return data
400
+
401
+
402
+ def _package_json(value: str | None) -> dict | None:
403
+ """The staged root package.json, or None when it is absent or unreadable (it then counts as shipping code)."""
404
+ if not value or not Path(value).is_file():
405
+ return None
406
+ try:
407
+ return _read_json(value, "--package-json")
408
+ except UsageError:
409
+ return None
410
+
411
+
412
+ def _build_parser() -> argparse.ArgumentParser:
413
+ parser = argparse.ArgumentParser(
414
+ prog="skf-skills-module",
415
+ description="Find a repository's skills root from its file listing, and list a skills module's exports.",
416
+ )
417
+ sub = parser.add_subparsers(dest="cmd", required=True)
418
+ p_sniff = sub.add_parser("sniff", help="the skills root and the listing facts")
419
+ p_sniff.add_argument("--tree-file", required=True, help="the repository's file listing")
420
+ p_sniff.add_argument("--package-json", help="the root package.json, staged")
421
+ p_sniff.add_argument("--scope-hint", help="the one folder to look at")
422
+ p_sniff.add_argument("--fetch-list", help="write the files extract reads here, one per line")
423
+ p_extract = sub.add_parser("extract", help="the envelope from the staged SKILL.md files and module-help.csv")
424
+ p_extract.add_argument("--sniff", required=True, help="sniff's output, saved")
425
+ p_extract.add_argument("--source-root", required=True, help="the folder the files were staged in")
426
+ p_extract.add_argument("--package", help="skf-extract-public-api.py's output for the root manifest")
427
+ p_extract.add_argument("--name", help="the package name when there is no --package")
428
+ return parser
429
+
430
+
431
+ def main(argv: list[str] | None = None) -> int:
432
+ args = _build_parser().parse_args(argv)
433
+ try:
434
+ if args.cmd == "sniff":
435
+ try:
436
+ paths, truncated = _load("skf-detect-language.py").read_tree_file(args.tree_file)
437
+ except ValueError as e:
438
+ raise UsageError(str(e)) from e
439
+ out = sniff(paths, _package_json(args.package_json), args.scope_hint, truncated)
440
+ if args.fetch_list:
441
+ try:
442
+ Path(args.fetch_list).write_text("".join(f"{p}\n" for p in fetch_list(out, paths)),
443
+ encoding="utf-8")
444
+ except OSError as e:
445
+ raise UsageError(f"cannot write --fetch-list {args.fetch_list}: {e.strerror or e}") from e
446
+ else:
447
+ sniffed = _read_json(args.sniff, "--sniff")
448
+ if not isinstance(sniffed.get("skill_folders"), list) or "skills_root" not in sniffed:
449
+ raise UsageError(f"--sniff {args.sniff} is not skf-skills-module.py sniff output")
450
+ package = _read_json(args.package, "--package") if args.package else None
451
+ out = extract(sniffed, Path(args.source_root), package, args.name)
452
+ except UsageError as e:
453
+ print(json.dumps({"status": "error", "message": str(e)}), file=sys.stderr)
454
+ return 2
455
+ print(json.dumps(out, indent=2))
456
+ return 0
457
+
458
+
459
+ def _force_utf8(*streams) -> None:
460
+ """Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
461
+ for stream in streams:
462
+ if hasattr(stream, "reconfigure"):
463
+ errors = getattr(stream, "errors", None)
464
+ if errors is None:
465
+ stream.reconfigure(encoding="utf-8")
466
+ else:
467
+ stream.reconfigure(encoding="utf-8", errors=errors)
468
+
469
+
470
+ if __name__ == "__main__":
471
+ _force_utf8(sys.stdout, sys.stderr)
472
+ sys.exit(main())