bmad-module-skill-forge 2.2.0 → 4.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 (437) 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 +143 -18
  18. package/docs/verifying-a-skill.md +87 -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 +3 -3
  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 +31 -59
  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 +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  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 +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  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 +21 -3
  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 +197 -55
  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 +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
  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 +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  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 +155 -14
  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 +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  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 +23 -2
  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 +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  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 +28 -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/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -0,0 +1,430 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Promote Staged: move a compiled skill from its staging folder into place, byte for byte.
6
+
7
+ skf-create-skill step 5 writes every artifact of a skill into its staging
8
+ folder, `_bmad-output/.skf-stage/{skill-name}/`, and steps 5a to 6 edit
9
+ them there (doc sources, auto-shard, doc-rot, skill-check --fix, the
10
+ description guard, the provenance fixes). Step 7 publishes that folder.
11
+ Writing each file again from the copies the model holds in context can
12
+ drop an edit a later step made on disk, or a file such as a
13
+ references/full-*.md that auto-shard created, so this helper copies the
14
+ staged bytes instead and reports what it wrote.
15
+
16
+ Subcommand:
17
+
18
+ promote --stage <dir> --package <skill_package> --forge-version <dir>
19
+ [--inventory <extraction inventory JSON> --source-root <dir>]
20
+ [--carry-manual <earlier package>]
21
+
22
+ What goes where:
23
+
24
+ <package>/ SKILL.md, context-snippet.md and metadata.json, every
25
+ file under the staged references/, and scripts/<name>
26
+ and assets/<name> for each scripts_inventory[] and
27
+ assets_inventory[] entry of --inventory, copied from
28
+ <source-root>/<source_file>
29
+ <forge-version>/ provenance-map.json, evidence-report.md and
30
+ extraction-rules.yaml
31
+
32
+ The package is built in `<package>.skf-tmp/` with skf-atomic-write.py
33
+ stage-dir and swapped into place with its commit-dir, so a reader sees the
34
+ earlier package or the new one, never a mix, and a run that compiles a
35
+ version again leaves no file of the earlier package behind. Each workspace
36
+ file goes through skf-atomic-write.py write into --forge-version, which is
37
+ created when missing; nothing else in that folder (earlier result files,
38
+ test reports) is touched. Every promoted file is read back and its SHA-256
39
+ compared with the bytes it came from.
40
+
41
+ Any other file in the staging folder is not promoted and is listed in
42
+ `ignored`, as is a file under references/ that is hidden (a name starting
43
+ with `.`) or that ends in `.skf-tmp`, a write skf-atomic-write.py did not
44
+ finish. A script or asset whose bytes no longer match the content_hash
45
+ the inventory recorded is still copied, with a warning.
46
+
47
+ Hand-written content (--carry-manual). A run that compiles a version
48
+ again replaces the earlier package of that version, so --carry-manual
49
+ names it (usually the --package folder itself) and the helper reads it
50
+ before the swap:
51
+
52
+ - every file under its scripts/[MANUAL]/ and assets/[MANUAL]/ goes into
53
+ the new package at the same path (kind "manual");
54
+ - each [MANUAL] block of its SKILL.md that holds more than whitespace
55
+ and the note create-skill seeds a marker with must be in the staged
56
+ SKILL.md: a block of the same name with the same interior once line
57
+ endings, trailing spaces and surrounding blank lines are set aside
58
+ (create-skill step 5 carries them there, so step 6 validates them).
59
+ A block is an open marker `<!-- [MANUAL:<name>] -->` and the first
60
+ `<!-- [/MANUAL:<name>] -->` after it, as skf-hash-content.py pairs
61
+ them. When one is not there, the earlier SKILL.md is written to
62
+ <forge-version>/manual-backup/SKILL-<UTC time>.md before the swap and
63
+ a warning names the block, so nothing hand-written is lost; each run
64
+ writes a new name, so a later run never replaces an earlier backup.
65
+
66
+ `manual` reports it: `from` (the folder read, or null without the flag),
67
+ `blocks` (the names found in the staged SKILL.md), `missing_blocks`,
68
+ `files` (package-relative paths) and `backup` (the copy's path, or null).
69
+
70
+ Output (stdout, exit 0):
71
+
72
+ {
73
+ "status": "ok",
74
+ "package": "<package, / separators>",
75
+ "forge_version": "<forge-version, / separators>",
76
+ "files": [{"path": "...", "kind": "deliverable|reference|script|asset|manual|workspace",
77
+ "bytes": N, "sha256": "sha256:<hex>"}, ...],
78
+ "counts": {"deliverables": 3, "references": N, "scripts": N, "assets": N,
79
+ "manual": N, "workspace": 3},
80
+ "ignored": ["<staged file not promoted>", ...],
81
+ "manual": {"from": "<folder>"|null, "blocks": ["<name>", ...],
82
+ "missing_blocks": ["<name>", ...], "files": ["scripts/[MANUAL]/...", ...],
83
+ "backup": "<forge-version>/manual-backup/SKILL-<UTC time>.md"|null},
84
+ "warnings": ["..."]
85
+ }
86
+
87
+ Exit codes:
88
+ 0 promoted and verified
89
+ 1 input error, nothing written: a staged file the package or the forge
90
+ folder needs is missing, the inventory cannot be read, a script or
91
+ asset is missing, lies outside --source-root or shares its
92
+ scripts/<name> or assets/<name> with another, --carry-manual is not
93
+ a folder or a file of it cannot be read, or a --carry-manual file
94
+ would replace a promoted file
95
+ 2 write failed (one line on stderr names it): a failed backup or package
96
+ swap leaves the earlier package in place, while a workspace file that
97
+ fails after the swap leaves the new package and the workspace files
98
+ written before it
99
+ """
100
+
101
+ from __future__ import annotations
102
+
103
+ import argparse
104
+ import hashlib
105
+ import json
106
+ import os
107
+ import re
108
+ import shutil
109
+ import subprocess
110
+ import sys
111
+ import time
112
+ from pathlib import Path, PurePosixPath
113
+
114
+ ATOMIC_WRITE = Path(__file__).resolve().parent / "skf-atomic-write.py"
115
+ DELIVERABLES = ("SKILL.md", "context-snippet.md", "metadata.json")
116
+ WORKSPACE_FILES = ("provenance-map.json", "evidence-report.md", "extraction-rules.yaml")
117
+ REFERENCES = "references"
118
+ # skf-atomic-write.py's temporary name: a write it did not finish leaves <name>.skf-tmp.
119
+ TMP_SUFFIX = ".skf-tmp"
120
+ # The inventory lists and the package folder each one lands in.
121
+ BUNDLED = (("scripts_inventory", "scripts", "script"), ("assets_inventory", "assets", "asset"))
122
+ # The package folders that hold user-authored files (docs/skill-model.md).
123
+ MANUAL_FOLDERS = ("scripts/[MANUAL]", "assets/[MANUAL]")
124
+ # The forge version folder's backup folder; skf-skill-inventory.py's
125
+ # FORGE_VERSION_FILES names it, so the ownership checks count it as SKF's.
126
+ MANUAL_BACKUP = "manual-backup"
127
+ # The note create-skill seeds a [MANUAL] marker with (compile-assembly-rules.md Section 8).
128
+ SEEDED_NOTE = b"<!-- Add custom notes here. This section is preserved during skill updates. -->"
129
+ # skf-hash-content.py's [MANUAL] markers; the open pattern never matches a close.
130
+ _MANUAL_OPEN_RE = re.compile(rb"<!--\s*\[MANUAL:([^\]]+)\]\s*-->")
131
+ _MANUAL_CLOSE_RE = re.compile(rb"<!--\s*\[/MANUAL:([^\]]+)\]\s*-->")
132
+ ATOMIC_TIMEOUT_SEC = 120
133
+
134
+
135
+ class InputError(Exception):
136
+ """Exit 1: nothing was written."""
137
+
138
+
139
+ class WriteError(Exception):
140
+ """Exit 2: a write failed."""
141
+
142
+
143
+ def _sha256(data: bytes) -> str:
144
+ return "sha256:" + hashlib.sha256(data).hexdigest()
145
+
146
+
147
+ def _posix(path: Path) -> str:
148
+ return path.as_posix()
149
+
150
+
151
+ def _atomic(args: list[str], data: bytes | None = None) -> None:
152
+ """Run skf-atomic-write.py with `args`; a non-zero exit is a WriteError."""
153
+ try:
154
+ proc = subprocess.run([sys.executable, str(ATOMIC_WRITE), *args], input=data or b"",
155
+ capture_output=True, timeout=ATOMIC_TIMEOUT_SEC, check=False)
156
+ except (OSError, subprocess.TimeoutExpired) as exc:
157
+ raise WriteError(f"skf-atomic-write.py {args[0]} did not run: {exc}") from exc
158
+ if proc.returncode != 0:
159
+ detail = proc.stderr.decode("utf-8", "replace").strip().splitlines()
160
+ raise WriteError(f"skf-atomic-write.py {args[0]} failed: {detail[-1] if detail else proc.returncode}")
161
+
162
+
163
+ def _staged_references(stage: Path) -> list[Path]:
164
+ """The staged reference files, less a hidden one and a write an interrupted run left half done."""
165
+ folder = stage / REFERENCES
166
+ if not folder.is_dir():
167
+ return []
168
+ return sorted(p for p in folder.rglob("*") if p.is_file() and not p.name.endswith(TMP_SUFFIX)
169
+ and not any(part.startswith(".") for part in p.relative_to(folder).parts))
170
+
171
+
172
+ def _bundled_files(inventory: Path | None, source_root: Path | None) -> list[tuple[str, str, Path, str | None]]:
173
+ """(kind, package-relative path, source file, recorded hash) per inventory script and asset."""
174
+ if inventory is None:
175
+ return []
176
+ try:
177
+ data = json.loads(inventory.read_text(encoding="utf-8"))
178
+ except (OSError, UnicodeDecodeError, ValueError) as exc:
179
+ raise InputError(f"cannot read the inventory {_posix(inventory)}: {exc}") from exc
180
+ if not isinstance(data, dict):
181
+ raise InputError(f"the inventory {_posix(inventory)} is not a JSON object")
182
+ entries: list[tuple[str, str, Path, str | None]] = []
183
+ seen: dict[str, str] = {}
184
+ for key, folder, kind in BUNDLED:
185
+ records = data.get(key) or []
186
+ if not isinstance(records, list):
187
+ raise InputError(f"the inventory's {key} is not a list")
188
+ for record in records:
189
+ if not isinstance(record, dict) or not isinstance(record.get("source_file"), str):
190
+ raise InputError(f"a {key} entry has no source_file: {record!r}")
191
+ if source_root is None:
192
+ raise InputError(f"the inventory lists {key} entries: pass --source-root")
193
+ rel = PurePosixPath(record["source_file"])
194
+ if rel.is_absolute() or ".." in rel.parts or not rel.parts:
195
+ raise InputError(f"{key} entry {record['source_file']!r} is not a path inside --source-root")
196
+ name = record.get("name") or rel.name
197
+ if not isinstance(name, str) or not name or "/" in name or "\\" in name:
198
+ raise InputError(f"{key} entry {record['source_file']!r} has no usable name")
199
+ target = f"{folder}/{name}"
200
+ if target in seen:
201
+ raise InputError(f"{seen[target]!r} and {record['source_file']!r} would both be {target}")
202
+ seen[target] = record["source_file"]
203
+ source = source_root.joinpath(*rel.parts)
204
+ if not source.is_file():
205
+ raise InputError(f"{key} entry {record['source_file']!r} is not a file under --source-root")
206
+ recorded = record.get("content_hash")
207
+ entries.append((kind, target, source, recorded if isinstance(recorded, str) else None))
208
+ return entries
209
+
210
+
211
+ def _manual_blocks(data: bytes) -> list[tuple[str, bytes]]:
212
+ """(name, interior) of each [MANUAL] block: an open marker paired with the
213
+ first close marker of its name after it; an unclosed one is skipped."""
214
+ closes = [(m.start(), m.group(1).strip()) for m in _MANUAL_CLOSE_RE.finditer(data)]
215
+ blocks = []
216
+ for m in _MANUAL_OPEN_RE.finditer(data):
217
+ name = m.group(1).strip()
218
+ end = next((start for start, close in closes if start >= m.end() and close == name), None)
219
+ if end is not None:
220
+ blocks.append((name.decode("utf-8", "replace"), data[m.end():end]))
221
+ return blocks
222
+
223
+
224
+ def _normalized(interior: bytes) -> bytes:
225
+ """A block's interior as the check compares it: LF line endings, no
226
+ trailing spaces, no surrounding blank lines (a CRLF checkout of the
227
+ earlier SKILL.md still holds the block an LF staged one carries)."""
228
+ lines = interior.replace(b"\r\n", b"\n").split(b"\n")
229
+ return b"\n".join(line.rstrip() for line in lines).strip()
230
+
231
+
232
+ def _hand_written(interior: bytes) -> bool:
233
+ """A block holds more than whitespace and the note create-skill seeds it with."""
234
+ return _normalized(interior) not in (b"", SEEDED_NOTE)
235
+
236
+
237
+ def _read(path: Path) -> bytes:
238
+ """The bytes of an earlier package file; one it cannot read is an InputError."""
239
+ try:
240
+ return path.read_bytes()
241
+ except OSError as exc:
242
+ raise InputError(f"cannot read {_posix(path)}: {exc}") from exc
243
+
244
+
245
+ def _manual_files(earlier: Path) -> list[tuple[str, bytes]]:
246
+ """(package-relative path, bytes) of each file under the earlier package's [MANUAL] folders."""
247
+ found = []
248
+ for folder in MANUAL_FOLDERS:
249
+ root = earlier.joinpath(*PurePosixPath(folder).parts)
250
+ if root.is_dir():
251
+ found += [(p.relative_to(earlier).as_posix(), _read(p))
252
+ for p in sorted(root.rglob("*")) if p.is_file()]
253
+ return found
254
+
255
+
256
+ def _held_blocks(earlier_skill_md: bytes, staged_skill_md: bytes) -> tuple[list[str], list[str]]:
257
+ """The names of the earlier SKILL.md's hand-written blocks the staged one
258
+ holds (same name, same normalized interior), and of those it lacks."""
259
+ staged: dict[str, set[bytes]] = {}
260
+ for name, interior in _manual_blocks(staged_skill_md):
261
+ staged.setdefault(name, set()).add(_normalized(interior))
262
+ held, lacking = [], []
263
+ for name, interior in _manual_blocks(earlier_skill_md):
264
+ if _hand_written(interior):
265
+ (held if _normalized(interior) in staged.get(name, set()) else lacking).append(name)
266
+ return held, lacking
267
+
268
+
269
+ def _backup_target(forge_version: Path) -> Path:
270
+ """manual-backup/SKILL-<UTC time>.md under the forge version folder, a
271
+ name no earlier backup holds, so a backup the user has not restored
272
+ from yet is never replaced."""
273
+ folder = forge_version / MANUAL_BACKUP
274
+ stamp = time.strftime("%Y%m%d-%H%M%S", time.gmtime())
275
+ target, n = folder / f"SKILL-{stamp}.md", 1
276
+ while os.path.lexists(target):
277
+ n += 1
278
+ target = folder / f"SKILL-{stamp}-{n}.md"
279
+ return target
280
+
281
+
282
+ def promote(stage: Path, package: Path, forge_version: Path,
283
+ inventory: Path | None = None, source_root: Path | None = None,
284
+ carry_manual: Path | None = None) -> dict:
285
+ """Promote the staging folder (see the module docstring); raises InputError or WriteError."""
286
+ if not stage.is_dir():
287
+ raise InputError(f"no staging folder at {_posix(stage)}")
288
+ missing = [name for name in (*DELIVERABLES, *WORKSPACE_FILES) if not (stage / name).is_file()]
289
+ if missing:
290
+ raise InputError(f"the staging folder {_posix(stage)} has no {', '.join(missing)}")
291
+ references = _staged_references(stage)
292
+ bundled = _bundled_files(inventory, source_root)
293
+
294
+ plan: list[tuple[str, str, bytes]] = [] # (kind, package-relative path, bytes)
295
+ for name in DELIVERABLES:
296
+ plan.append(("deliverable", name, (stage / name).read_bytes()))
297
+ for path in references:
298
+ plan.append(("reference", path.relative_to(stage).as_posix(), path.read_bytes()))
299
+ warnings: list[str] = []
300
+ for kind, rel, source, recorded in bundled:
301
+ data = source.read_bytes()
302
+ if recorded is not None and recorded != _sha256(data):
303
+ warnings.append(f"{rel}: {_posix(source)} changed since extraction recorded {recorded}")
304
+ plan.append((kind, rel, data))
305
+ workspace = [(name, (stage / name).read_bytes()) for name in WORKSPACE_FILES]
306
+
307
+ # The earlier package's hand-written content, read before the swap deletes it.
308
+ manual = {"from": None, "blocks": [], "missing_blocks": [], "files": [], "backup": None}
309
+ earlier_skill_md = b""
310
+ if carry_manual is not None:
311
+ if not carry_manual.is_dir():
312
+ raise InputError(f"--carry-manual {_posix(carry_manual)} is not a folder")
313
+ if (carry_manual / "SKILL.md").is_file():
314
+ earlier_skill_md = _read(carry_manual / "SKILL.md")
315
+ held, lacking = _held_blocks(earlier_skill_md, (stage / "SKILL.md").read_bytes())
316
+ files = _manual_files(carry_manual)
317
+ taken = {rel for _, rel, _ in plan}
318
+ for rel, data in files:
319
+ if rel in taken:
320
+ raise InputError(f"{rel} of --carry-manual would replace a promoted file")
321
+ plan.append(("manual", rel, data))
322
+ manual.update({"from": _posix(carry_manual), "blocks": held, "missing_blocks": lacking,
323
+ "files": [rel for rel, _ in files]})
324
+
325
+ promoted = {_posix(p.relative_to(stage)) for p in references} | set(DELIVERABLES) | set(WORKSPACE_FILES)
326
+ ignored = sorted(p.relative_to(stage).as_posix() for p in stage.rglob("*")
327
+ if p.is_file() and p.relative_to(stage).as_posix() not in promoted)
328
+
329
+ # A hand-written block the staged SKILL.md lacks: keep the earlier
330
+ # SKILL.md before the swap deletes it.
331
+ if manual["missing_blocks"]:
332
+ target = _backup_target(forge_version)
333
+ _atomic(["write", "--target", str(target)], earlier_skill_md)
334
+ _verified(target, "workspace", earlier_skill_md)
335
+ manual["backup"] = _posix(target)
336
+ warnings.append(f"SKILL.md: the [MANUAL] block(s) {', '.join(manual['missing_blocks'])} of "
337
+ f"{_posix(carry_manual)} are not in the new SKILL.md; the earlier SKILL.md is "
338
+ f"kept at {_posix(target)}")
339
+
340
+ # The package: build it beside its target, then swap it in.
341
+ _atomic(["stage-dir", "--target", str(package)])
342
+ building = package.with_name(package.name + ".skf-tmp")
343
+ try:
344
+ for _, rel, data in plan:
345
+ target = building.joinpath(*PurePosixPath(rel).parts)
346
+ target.parent.mkdir(parents=True, exist_ok=True)
347
+ target.write_bytes(data)
348
+ except OSError as exc:
349
+ shutil.rmtree(building, ignore_errors=True)
350
+ raise WriteError(f"cannot build {_posix(building)}: {exc}") from exc
351
+ _atomic(["commit-dir", "--target", str(package)])
352
+
353
+ # The workspace files, one atomic write each.
354
+ for name, data in workspace:
355
+ _atomic(["write", "--target", str(forge_version / name)], data)
356
+
357
+ files = []
358
+ for kind, rel, data in plan:
359
+ files.append(_verified(package.joinpath(*PurePosixPath(rel).parts), kind, data))
360
+ for name, data in workspace:
361
+ files.append(_verified(forge_version / name, "workspace", data))
362
+ counts = {"deliverables": len(DELIVERABLES), "references": len(references),
363
+ "scripts": sum(1 for kind, *_ in bundled if kind == "script"),
364
+ "assets": sum(1 for kind, *_ in bundled if kind == "asset"),
365
+ "manual": len(manual["files"]), "workspace": len(WORKSPACE_FILES)}
366
+ return {"status": "ok", "package": _posix(package), "forge_version": _posix(forge_version),
367
+ "files": files, "counts": counts, "ignored": ignored, "manual": manual, "warnings": warnings}
368
+
369
+
370
+ def _verified(path: Path, kind: str, data: bytes) -> dict:
371
+ """The promoted file's record, after checking it holds the bytes it came from."""
372
+ try:
373
+ written = path.read_bytes()
374
+ except OSError as exc:
375
+ raise WriteError(f"cannot read back {_posix(path)}: {exc}") from exc
376
+ if written != data:
377
+ raise WriteError(f"{_posix(path)} does not hold the bytes it was promoted from")
378
+ return {"path": _posix(path), "kind": kind, "bytes": len(written), "sha256": _sha256(written)}
379
+
380
+
381
+ def _build_parser() -> argparse.ArgumentParser:
382
+ parser = argparse.ArgumentParser(
383
+ prog="skf-promote-staged.py",
384
+ description=__doc__,
385
+ formatter_class=argparse.RawDescriptionHelpFormatter,
386
+ )
387
+ sub = parser.add_subparsers(dest="command", required=True)
388
+ p = sub.add_parser("promote", help="copy a staged skill into its package and forge version folders")
389
+ p.add_argument("--stage", required=True, type=Path, help="the staging folder, _bmad-output/.skf-stage/<skill>")
390
+ p.add_argument("--package", required=True, type=Path, help="the skill package folder, {skill_package}")
391
+ p.add_argument("--forge-version", required=True, type=Path, help="the forge version folder, {forge_version}")
392
+ p.add_argument("--inventory", type=Path, default=None,
393
+ help="the extraction inventory JSON whose scripts_inventory and assets_inventory are bundled")
394
+ p.add_argument("--source-root", type=Path, default=None,
395
+ help="the source tree the inventory's source_file paths are relative to")
396
+ p.add_argument("--carry-manual", type=Path, default=None,
397
+ help="the earlier package this one replaces: carry its [MANUAL] files and check "
398
+ "its [MANUAL] blocks are in the staged SKILL.md")
399
+ return parser
400
+
401
+
402
+ def main(argv: list[str] | None = None) -> int:
403
+ args = _build_parser().parse_args(argv)
404
+ try:
405
+ result = promote(args.stage, args.package, args.forge_version, args.inventory, args.source_root,
406
+ args.carry_manual)
407
+ except InputError as exc:
408
+ print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
409
+ return 1
410
+ except WriteError as exc:
411
+ print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
412
+ return 2
413
+ print(json.dumps(result, indent=2))
414
+ return 0
415
+
416
+
417
+ def _force_utf8(*streams) -> None:
418
+ """Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
419
+ for stream in streams:
420
+ if hasattr(stream, "reconfigure"):
421
+ errors = getattr(stream, "errors", None)
422
+ if errors is None:
423
+ stream.reconfigure(encoding="utf-8")
424
+ else:
425
+ stream.reconfigure(encoding="utf-8", errors=errors)
426
+
427
+
428
+ if __name__ == "__main__":
429
+ _force_utf8(sys.stdout, sys.stderr)
430
+ sys.exit(main())
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = ["pyyaml"]
4
4
  # ///
5
5
  """SKF Provenance Gap Dispatch — Major-Version Scope Reconciliation feeder.
@@ -17,8 +17,16 @@ deterministic operations before the LLM can prompt the user:
17
17
  - `promoted` for this path → already in scope (skip)
18
18
  - `skipped` for this path → user previously declined (skip)
19
19
  - `demoted-include` or `demoted-exclude` → pre-decided demotion
20
+ - `deferred-headless` for this path → a headless run met it and left
21
+ it for a person: unresolved, so the next interactive run asks
20
22
  - no matching amendment → unresolved (caller prompts user)
21
23
 
24
+ A `skipped` amendment whose reason is `headless: no user to prompt`
25
+ is the automatic skip a headless run wrote before `deferred-headless`
26
+ existed. No person declined the path, so it counts as
27
+ `deferred-headless` too (as skf-resolve-authoritative-files.py reads
28
+ the same legacy record).
29
+
22
30
  The prose previously asked the LLM to do all three per run. The
23
31
  markdown parsing in particular is fiddly (heading lookahead, table
24
32
  vs bullet vs definition list formats); keeping it in prose lets
@@ -45,7 +53,7 @@ Output JSON (stdout):
45
53
  | "unresolved",
46
54
  "prior_action": "promoted" | "skipped"
47
55
  | "demoted-include" | "demoted-exclude"
48
- | null
56
+ | "deferred-headless" | null
49
57
  }, ...
50
58
  ],
51
59
  "summary": {
@@ -59,6 +67,9 @@ Output JSON (stdout):
59
67
  - `no-candidates` → report exists but Out-of-Scope section is absent/empty
60
68
  - `candidates-found` → classified[] is populated; iterate unresolved entries
61
69
 
70
+ An `unresolved` entry with `prior_action: "deferred-headless"` is one a
71
+ headless run deferred; with `null` no run has decided it yet.
72
+
62
73
  Exit codes:
63
74
  0 — operation succeeded (any status)
64
75
  1 — user error (paths invalid, brief unparseable)
@@ -67,6 +78,7 @@ Exit codes:
67
78
  from __future__ import annotations
68
79
 
69
80
  import argparse
81
+ import importlib.util
70
82
  import json
71
83
  import re
72
84
  import sys
@@ -238,6 +250,34 @@ _PRE_DECIDED_SKIPPED = "pre-decided-skipped"
238
250
  _PRE_DECIDED_DEMOTED = "pre-decided-demoted"
239
251
  _UNRESOLVED = "unresolved"
240
252
 
253
+ _SKIPPED = "skipped"
254
+ _DEFERRED = "deferred-headless"
255
+ # The reason a headless run gave its automatic skip before deferred-headless
256
+ # existed. Such a skip was never a person's decision.
257
+ _LEGACY_HEADLESS_REASON = "headless: no user to prompt"
258
+
259
+
260
+ _RESOLVER = None
261
+
262
+
263
+ def _resolver():
264
+ """skf-resolve-authoritative-files.py from this folder, loaded once: its
265
+ parse_brief_yaml reads a brief's YAML, an SKF 1.x brief's trailing `---`
266
+ included. Raises ValueError when it cannot be loaded."""
267
+ global _RESOLVER
268
+ if _RESOLVER is None:
269
+ path = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
270
+ spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", path)
271
+ if spec is None or spec.loader is None:
272
+ raise ValueError(f"cannot load {path.name} beside {Path(__file__).name}")
273
+ module = importlib.util.module_from_spec(spec)
274
+ try:
275
+ spec.loader.exec_module(module)
276
+ except (OSError, ImportError, SyntaxError) as exc:
277
+ raise ValueError(f"cannot load {path.name} beside {Path(__file__).name}: {exc}") from exc
278
+ _RESOLVER = module
279
+ return _RESOLVER
280
+
241
281
 
242
282
  def load_brief(brief_path: Path) -> dict:
243
283
  """Parse a skill-brief.yaml file. Raises ValueError on load failure."""
@@ -245,8 +285,9 @@ def load_brief(brief_path: Path) -> dict:
245
285
  text = brief_path.read_text(encoding="utf-8")
246
286
  except OSError as exc:
247
287
  raise ValueError(f"cannot read brief at {brief_path}: {exc}") from exc
288
+ parse_brief_yaml = _resolver().parse_brief_yaml
248
289
  try:
249
- brief = yaml.safe_load(text)
290
+ brief = parse_brief_yaml(text)
250
291
  except yaml.YAMLError as exc:
251
292
  raise ValueError(f"brief at {brief_path} is not valid YAML: {exc}") from exc
252
293
  if not isinstance(brief, dict):
@@ -272,7 +313,7 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
272
313
  if not isinstance(amend, dict):
273
314
  continue
274
315
  path = amend.get("path")
275
- action = amend.get("action")
316
+ action = _amendment_action(amend)
276
317
  if isinstance(path, str) and isinstance(action, str):
277
318
  by_path.setdefault(path, []).append(action)
278
319
 
@@ -293,20 +334,35 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
293
334
  return classified
294
335
 
295
336
 
337
+ def _amendment_action(amend: dict) -> str | None:
338
+ """The action an amendment records, reading a legacy headless skip as a deferral."""
339
+ action = amend.get("action")
340
+ if not isinstance(action, str):
341
+ return None
342
+ reason = amend.get("reason")
343
+ if action == _SKIPPED and isinstance(reason, str) and reason.strip() == _LEGACY_HEADLESS_REASON:
344
+ return _DEFERRED
345
+ return action
346
+
347
+
296
348
  def _classify(prior_actions: list[str]) -> tuple[str, str | None]:
297
349
  """Pick the dominant amendment action for a single candidate path.
298
350
 
299
351
  A path may have multiple amendments (e.g. promoted then later
300
- demoted-exclude). Most recent action wins — the list is in
301
- amendment-record-insertion order, so iterate from the end.
352
+ demoted-exclude). Most recent action wins: the list is in
353
+ amendment-record-insertion order, so iterate from the end. A
354
+ deferred-headless amendment decides nothing: the path stays
355
+ unresolved for the next interactive run, which a person answers.
302
356
  """
303
357
  for action in reversed(prior_actions):
304
358
  if action == "promoted":
305
359
  return _ALREADY_IN_SCOPE, "promoted"
306
- if action == "skipped":
307
- return _PRE_DECIDED_SKIPPED, "skipped"
360
+ if action == _SKIPPED:
361
+ return _PRE_DECIDED_SKIPPED, _SKIPPED
308
362
  if action in ("demoted-include", "demoted-exclude"):
309
363
  return _PRE_DECIDED_DEMOTED, action
364
+ if action == _DEFERRED:
365
+ return _UNRESOLVED, _DEFERRED
310
366
  return _UNRESOLVED, None
311
367
 
312
368