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,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)
@@ -238,6 +249,12 @@ _PRE_DECIDED_SKIPPED = "pre-decided-skipped"
238
249
  _PRE_DECIDED_DEMOTED = "pre-decided-demoted"
239
250
  _UNRESOLVED = "unresolved"
240
251
 
252
+ _SKIPPED = "skipped"
253
+ _DEFERRED = "deferred-headless"
254
+ # The reason a headless run gave its automatic skip before deferred-headless
255
+ # existed. Such a skip was never a person's decision.
256
+ _LEGACY_HEADLESS_REASON = "headless: no user to prompt"
257
+
241
258
 
242
259
  def load_brief(brief_path: Path) -> dict:
243
260
  """Parse a skill-brief.yaml file. Raises ValueError on load failure."""
@@ -272,7 +289,7 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
272
289
  if not isinstance(amend, dict):
273
290
  continue
274
291
  path = amend.get("path")
275
- action = amend.get("action")
292
+ action = _amendment_action(amend)
276
293
  if isinstance(path, str) and isinstance(action, str):
277
294
  by_path.setdefault(path, []).append(action)
278
295
 
@@ -293,20 +310,35 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
293
310
  return classified
294
311
 
295
312
 
313
+ def _amendment_action(amend: dict) -> str | None:
314
+ """The action an amendment records, reading a legacy headless skip as a deferral."""
315
+ action = amend.get("action")
316
+ if not isinstance(action, str):
317
+ return None
318
+ reason = amend.get("reason")
319
+ if action == _SKIPPED and isinstance(reason, str) and reason.strip() == _LEGACY_HEADLESS_REASON:
320
+ return _DEFERRED
321
+ return action
322
+
323
+
296
324
  def _classify(prior_actions: list[str]) -> tuple[str, str | None]:
297
325
  """Pick the dominant amendment action for a single candidate path.
298
326
 
299
327
  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.
328
+ demoted-exclude). Most recent action wins: the list is in
329
+ amendment-record-insertion order, so iterate from the end. A
330
+ deferred-headless amendment decides nothing: the path stays
331
+ unresolved for the next interactive run, which a person answers.
302
332
  """
303
333
  for action in reversed(prior_actions):
304
334
  if action == "promoted":
305
335
  return _ALREADY_IN_SCOPE, "promoted"
306
- if action == "skipped":
307
- return _PRE_DECIDED_SKIPPED, "skipped"
336
+ if action == _SKIPPED:
337
+ return _PRE_DECIDED_SKIPPED, _SKIPPED
308
338
  if action in ("demoted-include", "demoted-exclude"):
309
339
  return _PRE_DECIDED_DEMOTED, action
340
+ if action == _DEFERRED:
341
+ return _UNRESOLVED, _DEFERRED
310
342
  return _UNRESOLVED, None
311
343
 
312
344